Laravel Nova only access from specific guard - laravel

In NovaServiceProvider there is:
protected function gate()
{
Gate::define('viewNova', function ($user) {
return in_array($user->email, [
'example#example.com',
]);
});
}
But what I would like to do is only allow people from the admins guard that I've setup in config/auth to access Nova. All users from the web guard should ideally get a 404 when they access any Nova URL.
This question for Telescope seems to be similar, but I can't seem to figure out where I should define this, and how to generate a 404 for the web guard.
A question that is probably related: what does viewNova in the gate method actually mean?
Can I define that specific action for a specific guard in config/auth? (I think I've seen this somewhere but can't seem to find it)?
There doesn't seem to be a Policy written for Nova?

Checkout vendor/laravel/nova/src/NovaApplicationServiceProvider.php. It has a method called authorization:
/**
* Configure the Nova authorization services.
*
* #return void
*/
protected function authorization()
{
$this->gate();
Nova::auth(function ($request) {
return app()->environment('local') ||
Gate::check('viewNova', [$request->user()]);
});
}
If the environment was local, it allows everyone to access the panel, but if the environment was something else, it checks for the definition on viewNova method and it passes the $request->user() to it.
In the same file, there's gate() method which defined viewNova:
/**
* Register the Nova gate.
*
* This gate determines who can access Nova in non-local environments.
*
* #return void
*/
protected function gate()
{
Gate::define('viewNova', function ($user) {
return in_array($user->email, [
//
]);
});
}
Basically, this method does nothing. You can implement it in app/Providers/NovaServiceProvider.php (which is the default implementation you see in the file and you've mentioned). In your case, you could implement it this way:
/**
* Register the Nova gate.
*
* This gate determines who can access Nova in non-local environments.
*
* #return void
*/
protected function gate()
{
Gate::define('viewNova', function ($user) {
Auth::guard('admin')->check();
});
}
It returns true if the currently authenticated user is in admin guard. Hope I could answer all your questions.

Related

Laravel Lighthouse Graphql multiple guards

I am trying to make my custom guard to work against a GraphQL schema with Lighthouse but its not working.
The problem is that my custom guard is not reached by graphql query.
The schema.graphql: (removed some code for brevity)
extend type Query #guard(with: ["api", "partner"])
{
GetHighscores(): [Highscore!]!
}
Note the #guard directive, it has api and partner. I want they run in that order (btw does it have order?).
Now the definition of the partner guard
AuthServiceProvider#boot
Config::set('auth.guards.partner', [
'driver' => 'partner',
]);
Auth::viaRequest('partner', function (Request $request) {
dd('MUST GET HERE');
});
When I run the query using an authenticated bearer token, the guard partner is not executed. But if I set the partner guard before api then it gets executed. What am I missing here?
edit
Using CURL, I have requested a protected laravel route and it works:
Route::middleware(['auth:api', 'auth:partner'])->get('/partners', function (Request $request) {
print_r($request->user()->toArray());
});
it printed the dd() message, which is right.
So why Lighthouse could not reached my custom guard?
I solved my issue moving the check for the API guard inside the partner guard:
Auth::viaRequest('partner', function (Request $request) {
$user = auth()->user();
if ($user &&
$user->partner()->exists() === true &&
auth()->guard('api')->check() === true
) {
return $user;
}
});
in the graphql schema:
extend type Query #guard(with: "partner")
{
GetHighscores(): [Highscore!]!
}
I still thinking that Lighthouse should be take care of multiple guards like it was an "AND"... anyways, its working now.
The lighthouse #guard directive behaves like a OR directive.
If the first guard passes, the second one is never checked.
What you could to is add the #guard directive twice. I think that should work.
extend type Query #guard(with: ["api"]) #guard(with: ["partner"])
{
GetHighscores(): [Highscore!]!
}
This is the code from the directive:
/**
* Determine if the user is logged in to any of the given guards.
*
* #param array<string> $guards
*
* #throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate(array $guards): void
{
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
$this->auth->shouldUse($guard);
return;
}
}
$this->unauthenticated($guards);
}
My use case may be different as I wanted authorization not multiple kinds of authentication, I have users that should be allowed to authenticate but need to do some verification mutations and limited querying and be admitted to use the rest of the API by an administrator.
Was thinking to just copy the guard directive and make it work as an and?
But I ended up creating a FieldMiddleware directive as I did not want other checks to need to do a separate authentication or copy same auth guard everywhere as that could be subject to change, I just wanted to check some attributes from the model were okay for a user to continue to use that query or mutation.
Although it does seem like #can and policy or gate would be more appropriate if not authenticating but checking some role/permission/state, I just wanted something I could use that was clear cut as I haven't made policy for absolutely every model yet a lot of them are just querying any data, using Policy would also apply for my back-end system which has different checks for admin already.
sail artisan lighthouse:directive AllowedInApp --field
1 FieldMiddleware
So the field Middleware can wrap and do checks I need and throw if not allowed.
<?php
namespace App\GraphQL\Directives;
use App\Models\User;
use Closure;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Exceptions\AuthorizationException;
use Nuwave\Lighthouse\Schema\Directives\BaseDirective;
use Nuwave\Lighthouse\Schema\Values\FieldValue;
use Nuwave\Lighthouse\Support\Contracts\FieldMiddleware;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;
final class AllowedInAppDirective extends BaseDirective implements FieldMiddleware
{
public static function definition(): string
{
return /** #lang GraphQL */ <<<'GRAPHQL'
directive #allowedInApp on FIELD_DEFINITION
GRAPHQL;
}
/**
* Wrap around the final field resolver.
*
* #param \Nuwave\Lighthouse\Schema\Values\FieldValue $fieldValue
* #param \Closure $next
* #return \Nuwave\Lighthouse\Schema\Values\FieldValue
*/
public function handleField(FieldValue $fieldValue, Closure $next)
{
$previousResolver = $fieldValue->getResolver();
$fieldValue->setResolver(function ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) use ($previousResolver) {
/***
* #var User
*/
$user = $context->user();
if ($user->isVerifiedAndAdmitted === false) {
throw new AuthorizationException(
"You are not authorized to access {$this->nodeName()}"
);
}
return $previousResolver($root, $args, $context, $resolveInfo);
});
return $next($fieldValue);
}
}
In my user model I added an attribute that did the check I wanted, only have users but it is possible to have multiple kinds of authenticatable model actually.
/**
* Get verified status returns true if both mobile and email are verified and the user is admitted.
*
* #return \Illuminate\Database\Eloquent\Casts\Attribute
*/
public function isVerifiedAndAdmitted(): Attribute
{
return Attribute::make(
get: fn($value, $attributes) => isset($attributes['mobile_verified_at'])
&& isset($attributes['email_verified_at'])
&& $attributes['admitted_at'] !== null
&& now()->greaterThanOrEqualTo($attributes['admitted_at'])
);
}
From what I saw about the guard directive in lighthouse source.
The built in Guard Directive just returns when one defined guard or default guard succeeds.
src/Auth/GuardDirective.php
Untested, not sure its the right thing to use, maybe policy or gate is better.
/**
* Determine if the user is logged in to all of the given guards.
*
* #param array<string|null> $guards
*
* #throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate(array $guards): void
{
$canPass = true;
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
// #phpstan-ignore-next-line passing null works fine here
$this->auth->shouldUse($guard);
} else {
$canPass = false;
break;
}
}
if ($canPass) {
return;
}
$this->unauthenticated($guards);
}
```

Loses Auth :: user () when declaring an Oberserver in AppServiceProvider

I'm trying to use a local scope in one of my models but for this I need to check the user permission, so I try to get the autenticated user by Auth::user().
But it givens me a null because I have an Observer declared for this model, and if I comment the declaration of the Observer the Auth::user() method give me a user authenticated.
There is a correct way or place to declare the Observer and in the model I can get the authenticated user, because I need to use Observers and get in boot method the authenticated user?
Laravel Framework 6.5.2
AppServiceProvider Don't work auth in model
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Conciliador::observe(ConciliadorObserver::class);
Proposta::observe(PropostaObserver::class);
}
AppServiceProvider work auth in model
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
//Conciliador::observe(ConciliadorObserver::class);
//Proposta::observe(PropostaObserver::class);
}
Model does not have user logged in when Observer is declared in AppServiceProvider
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
$user = Auth::user();
dd($user); // null if Observer is declared in AppServiceProvider
if($user && $user->tipo == 'admin-gerenciador'){
$conciliadores = $user->conciliadores->pluck('id')->toArray();
static::addGlobalScope('Conciliadores', function (Builder $builder) {
$builder->whereIn('id',$conciliadores);
});
}
}
Don't call Auth::user() anywhere. It may trigger authentication side-effects. It is highly recommended to do that only in controllers and middleware.
But you can safely call Auth::user() if you check by Auth::hasUser() in advance; it checks if the user is ALREADY authenticated.
So your code goes like:
/**
* The "booting" method of the model.
*/
protected static function boot(): void
{
static::addGlobalScope('Conciliadores', function (Builder $query) {
if (Auth::hasUser() && Auth::user()->tipo === 'admin-gerenciador') {
$query->whereKey(Auth::user()->conciliadores->modelKeys());
}
});
}
It's very simple solution. Even middleware is unnecessary.
EDIT
This scope is always available, but actually apply conditions only if the user is already authenticated.
You shouldn't be doing this in your model's boot method like that. The boot method is only called once for the model, not for every model instance. The first time the model is used boot gets called, which would be when you are adding the observer for it in the Service Provider in your case; which would be way before the request is dispatched to a route and through the middleware stack. (There is no session at this point, so no authenticated user.)
You probably want to add your global scope to your model via a middleware.

Laravel conditional routing based on database value

I'm developing an admin panel with laravel. I'm putting a preference for an end user to choose if the site is down (for maintenance or similar purpose).
The preference will be stored as boolean in the database. Based on this value frontend will be routed to a custom view if the site is down.
(Site will be hosted on a shared host, no SSL. Using artisan commands are not an option.)
Currently, I can get "site_is_down" value from the database at boot time with a custom method in AppServiceProvider.php's register() method.
But I'm not sure how can I route calls based on this value in the routes file.
I have two named route groups (Frontend and Backend) and standard Auth::routes() in routes/web.php. Only frontend routes should be conditionally routed. Backend and Auth should be excluded. (So the user can access Backend panel).
I'm trying to achieve something like this:
(I know this is not proper syntax, I'm trying to explain my mind)
<?php
if (config('global.site_is_down') === true) {
//Route all frontend route group to maintenance view ->except(Backend and auth)
} else {
//Route all as normal
}
Create a middleware:
<?php
namespace App\Http\Middleware;
use Closure;
class CheckMaintainaceMode
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if (config('global.site_is_down')/*or what ever logic you need*/) {
return redirect('mainainance-mode-url');
}
return $next($request);
}
}
then use this middleware in frontend routes
Route::get('/frontend', function () {
//
})->middleware('CheckMaintainaceMode');
or
Route::group(['middleware' => ['CheckMaintainaceMode']], function () {
//
});

laravel 5.5. how to response a redirect from a action, not the route action

I want to check the auth before run the action. if the user deny, I will response the 403 status.
I use the method function in the __construct to check .
the code is following.
The User Controller:
public function __construct()
{
if (!app()->runningInConsole()) {
$beforeMethod = \Route::getCurrentRoute()->getActionMethod()."before";
if (method_exists($this, $beforeMethod)) {
return $this->$beforeMethod();
}
}
}
/**
* Edit interface.
*
* #param $id
* #return Content
*/
public function edit($id)
{
return "success";
}
/**
* Check user Auth
*
* #return \Illuminate\Http\RedirectResponse
*/
public function editBefore()
{
$id = \request()->route('user');
if (Gate::denies('edit', User::find($id))) {
return redirect("login");
}
}
the above code, don't return to the login.
what code should I use to achieve my purpose? Thanks!
You're really close to what I would do. I would use Policies.
You can add these to the routes or route groups so that you can check the policy before it hits the controller method whatsoever.
Create a new policy class that looks something like:
class UserPolicy
{
public function canEdit(User $user) // this user is the logged in user
{
// Here you return true or false depending on whether they can edit or not.
return $user->isAllowedToEdit();
}
}
Then you make sure the policy is added to AuthServiceProvider.php:
public function boot(GateContract $gate)
{
$gate->define('user.edit', UserPolicy::class.'#canEdit');
// Additional policies
}
Then make sure to add can to your $routeMiddleware in the Kernel.php:
protected $routeMiddleware = [
// other middleware
'can' => \Illuminate\Auth\Middleware\Authorize::class,
];
Finally, you can add it to your route making sure that you use the same name you defined in the AuthServiceProvider:
Route::get('/user/edit')
->uses('UserController#edit')
->middleware('can:user.edit');
I personally like doing it this way because it cleans up the controllers and lets the policies do all the work "behind the scenes". Even better, you can then use that same users.edit policy for multiple routes.

How to prevent access to Model by id with JWTAuth and Laravel?

I'm building some API endpoints with Laravel, and I'm using JWTAuth as the token provider for authorizing requests.
I've gotten the setup to protect a group of API routes that works correctly using:
Route::group(['prefix' => '/v1', 'middleware' => 'jwt.auth'], function() {
Route::resource('messages', 'MessagesController');
});
The Message model belongs to a User
I'm attempting to perform some requests using that relationship while keeping the request from providing data that didn't belong to the user:
Get a list of the logged in user's messages
Get an individual message of the logged in user
The main question I have is how to prevent the user from accessing a Message that does not belong to them. I have this in my controller:
public function show($message_id)
{
$message = Message::findOrFail($message_id);
return $message;
}
That obviously will return the Message regardless of whether or not it belongs to that user. Any suggestions on how to improve that to restrict accessing other user's messages?
For the listing of all messages, I was able to do the following in the controller:
public function index()
{
$user_id = Auth::User()->id;
$messages = Message::where('user_id', $user_id)->paginate(10);
return $messages;
}
This works, but I'm not sure this is the best way to do it. Maybe it is, but some feedback would be appreciated. I'm confused as to whether or not the middleware should be handling what the user has access to or if it should be part of the eloquent query?
Your question is,
how to prevent the user from accessing a Message that does not belong
to them.
Thats falls under Authorization
You can use Gates to Authorize users and I think it's the best approach here. Fist of all you'd need a Policy object.
<?php
namespace App\Policies;
use App\User;
use App\Message;
class MessagePolicy
{
/**
* Determine if the given Message can be viewed by the user.
*
* #param \App\User $user
* #param \App\Message $Message
* #return bool
*/
public function view(User $user, Message $Message)
{
return $user->id === $Message->user_id;
// Here this User's id should match with the user_id of the Massage
}
}
You can even generate the boilerplate like this,
php artisan make:policy MessagePolicy --model=Message
Then you can register it in your AuthServiceProvider like this,
<?php
namespace App\Providers;
use App\Message;
use App\Policies\MessagePolicy;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
Message::class => MessagePolicy::class,
];
/**
* Register any application authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
Gate::define('view-message', 'MessagePolicy#view');
}
}
And in your Controller you can use it like this,
public function show($message_id)
{
if (Gate::allows('view-message', Message::findOrFail($message_id)) {
return $message;
}
}
Note: Please use this code snippet as a reference or a starting point since I haven't tested it properly. But underlying concept is correct. Use this as pseudocode. If something wrong found, please update it here :)

Resources