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

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.

Related

Laravel policy not running on update/delete model

I'm trying to make this policy stuff work, and i have followed the documentation. But it doesn't seem like the policy code is not even run.
I have Role model. And i created RolePolicy. The thing i want to do in the policy is to ensure that the role with the ID of 1 never-ever gets updated or deleted.
My RolePolicy looks like this:
<?php
namespace App\Policies;
use App\Models\Role;
use Illuminate\Support\Facades\Response;
class RolePolicy
{
/**
* Determine whether the user can update the model.
*
* #param \App\User $user
* #param \App\Models\Role $role
* #return mixed
*/
public function update(Role $role)
{
return $role->id === 1
? Response::deny('Cannot change super-admin role')
: Response::allow();
}
/**
* Determine whether the user can delete the model.
*
* #param \App\User $user
* #param \App\Models\Role $role
* #return mixed
*/
public function delete(Role $role)
{
return $role->id === 1
? Response::deny('Cannot delete super-admin role')
: Response::allow();
}
}
I even tried to do a dd() inside both delete and update method in the policy, but when i try to delete/update the model with the ID of 1, nothing happens. The dd wont run, nor will the response in the current code above.
I have registered the policy in the AuthServiceProvider where i also have this gate to give the super-admin all the permissions.
<?php
namespace App\Providers;
use App\Models\Role;
use App\Policies\RolePolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
Role::class => RolePolicy::class
];
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
// Implicitly grant "Super Admin" role all permissions
// This works in the app by using gate-related functions like auth()->user->can() and #can()
Gate::before(function($user, $ability) {
return $user->hasRole('super-admin') ? true : null;
});
}
}
Here is also my RoleController method for updating the Role model:
/**
* Edit role
*
* #param Edit $request
* #param Role $role
* #return void
*/
public function postEdit(Edit $request, Role $role)
{
# Validation checks happens in the FormRequest
# Session flash also happens in FormRequest
# Update model
$role->update([
'name' => $request->name
]);
# Sync permissions
$permissions = Permission::whereIn('name', $request->input('permissions', []))->get();
$role->syncPermissions($permissions);
return redirect(route('dashboard.roles.edit.get', ['role' => $role->id]))->with('success', 'Changes saved');
}
Does the gate i use to give all permissions have anything to do with the policy not running? Or what am i doing wrong here?
Thanks in advance if anyone can point me in the right direction.
The User model that is included with your Laravel application includes two helpful methods for authorizing actions: can and cant. The can method receives the action you wish to authorize and the relevant model. For example, let's determine if a user is authorized to update a given Role model:
if ($user->can('update', $role)) {
//
}
If a policy is registered for the given model, the can method will automatically call the appropriate policy and return the boolean result. If no policy is registered for the model, the can method will attempt to call the Closure based Gate matching the given action name.
Via Controller Helpers
In addition to helpful methods provided to the User model, Laravel provides a helpful authorize method to any of your controllers which extend the App\Http\Controllers\Controller base class. Like the can method, this method accepts the name of the action you wish to authorize and the relevant model. If the action is not authorized, the authorize method will throw an Illuminate\Auth\Access\AuthorizationException, which the default Laravel exception handler will convert to an HTTP response with a 403 status code:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Role;
use Illuminate\Http\Request;
class RoleController extends Controller
{
/**
* Update the given role.
*
* #param Request $request
* #param role $role
* #return Response
* #throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Role $role)
{
$this->authorize('update', $role);
// The current user can update the role...
}
}
The Gate::before method in the AuthServiceProvider was the problem. Removed this and rewrote the permissions, policies and some gates to get the error messages from the policies.
Decided to give the role super-admin the permission * and check for this with $user->can() and middleware .....->middlware('can:*') and everything is working now.

Laravel Nova only access from specific guard

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.

Middleware before Model injection

I have a design doubt I would like to share.
I have a model in Laravel with an Observer at retrieved:
class MailingObserver
{
public function retrieved($mailing)
{
// we retrieve HTML content from disk file
$mailing->setAttribute('content', \Illuminate\Support\Facades\Storage::disk('mailings')->get("{$mailing->id}-{$mailing->slug}.html"));
$mailing->syncOriginal();
}
}
which retrieve an attribute stored in a plain text instead of database.
The site is a multibrand platform so disk('mailings') is different per each logged user. This configuration is loaded in a middleware according to the the current logged user.
Up to here all is fine.
Now the "problem". I have a Controller which injects the entity Mailing:
class MailingCrudController extends CrudController
{
/**
* Sends the mailing
* #param Request $request
* #param \App\Mailing $mailing
*/
public function send(Request $request, \App\Mailing $mailing)
{
// WHATEVER
}
}
When the model is injected the retrieved Observer method is fired but the Middleware wasn't still executed so mailings disk is still not set up.
I don't know how to change this order: first execute middleare, then the model injection.
One approach
I tried in AppServiceProvider to add:
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$middleware = new \App\Http\Middleware\CheckBrandHost();
$middleware->setBrandInformation(request());
$middleware->loadBrandConfig(request()->get('brand.code_name'));
}
Would you approve this solution? What problems can cause it to me? Is it the proper way to do it?
Thanks all!

laravel model - how to dynamically set a column value in the Model?

When a User is created, e.g. with the following line in a controller:
$user = $this->create($request->all());
This will insert a user record with the form values (name, email, password etc).
But if we want to set a "hidden" user fields/colums on the model and DB table, e.g. a special unique generated token (user.token), We dont want to do this in every controller.
If laravel had a service layer, it could be done here, but better would be to do it in the model itself.
e.g. by catching a beforeSave callback, and having some code generate the token and set the corresponding field before it gets written to the DB. I see that the model has saved() event/observers, but this looks like it happens after the save, and I dont want to put this logic in an external class, It belongs in the model, and the documenation doesnt say if the observer can modify the model (by setting columns in this case)
Any suggestions?
It is possible to define event listeners directly within your model. E.g. add a boot method to your User model:
/**
* Define model event callbacks.
*
* #return void
*/
public static function boot()
{
parent::boot();
static::saving(function ($model) {
$model->token = str_random(60);
});
}
Alternative, more verbose implementation:
/**
* Define model event callbacks.
*
* #return void
*/
public static function boot()
{
parent::boot();
static::saving(function ($model) {
if (method_exists($model, 'beforeSave')) $model->beforeSave();
});
}
/**
* Before save event listener.
*
* #return void
*/
public function beforeSave()
{
$this->token = str_random(60);
}

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