Allow edit of Model based on a set permission as well as Model's property value in Laravel

Viewed 144

I have a Location Model, which contains two properties: ID and Name.

To edit this Model, I have set up this route:

Route::get('administration/location/{location}/edit', 'LocationController@edit')->name('location.edit');

I set up very simple permissions: In the AuthServiceProvider I am checking in the boot method the following

Gate::before(function ($user, $permission) {
    if ($user->permissions->pluck('name')->contains($permission)) {
        return true;
    }
});

Where permission is a Model that contains an ID and a name, mapped via a permission_user table.

I have these permissions set up:

  • edit_los_angeles
  • edit_new_york
  • edit_boston
  • plenty_of_other_permissions_not_related_to_location

After all this rambling, my actual question:

How can I tie these permissions to the edit the location?

The problem that I am facing is, that a given user is not allowed to edit all locations but may only be allowed to edit one location. Only the user with permission edit_los_angeles would be allowed to edit the Location with the name Los Angeles.
So I cannot group this into one permission like edit_location and add this to my route ->middleware('can:edit_location').

Instead, I would need something like this, I guess:

Route::get('administration/location/{location}/edit', 'LocationController@edit')->name('location.edit')->middleware('can:edit_los_angeles');
Route::get('administration/location/{location}/edit', 'LocationController@edit')->name('location.edit')->middleware('can:edit_new_york');
Route::get('administration/location/{location}/edit', 'LocationController@edit')->name('location.edit')->middleware('can:edit_boston');

...obviously this would not work.

What would be your approach to tackle this dilemma? :-)
Maybe I am doing something completely wrong and there is a better Laravel-Way of doing this?

Thank you very much for your help in advance!

I am using Laravel 6.0 :-)

2 Answers

If you have a requirement based upon the location relationship, then you will need to capture that relationship in the data. A good starting point to this would be to add a pivot table specific for these editing permissions. Consider a table, location_permissions, with a user_id and a location_id. You could then modify or add permission middleware to do a check for a record in this table once you have a specific user and location.

Edit: to answer the question about implementation of middleware,

The crux of the implementation would likely be solved by defining a relationship on the user model to location via this new pivot table.

I would recommend then adding an additional method which consumes the new locations relationship to the model along the lines of

public function canEditLocation(Location $location): bool { 

   return $this->locations
               ->where('location_id', '=', $location->id)
               ->count() > 0; 
    }

And the actual middleware something along these lines:

public function handle($request, Closure $next, $location)
    {
        if (! $request->user()->canEditLocation($location)) {
            \\handle failed permission as appropriate here.
        }

        return $next($request);
    }

My middleware parameters knowledge is rusty, but I believe that is correct as defined at https://laravel.com/docs/master/middleware#middleware-parameters

Two assumption for my approach to work, use model binding in the controller (you should do that no matter what). Secondly there needs to be a relation between location and the permission it needs, something similar to the slug you suggested.

Your controller function would look something like this. Adding a FormRequest is a good approach for doing this logic.

class LocationController {
     public function edit(EditLocationRequest $request, Location $location) { // implicit model binding
         ...
     }
}

For ease of use, i would also make a policy.

class LocationPolicy
{
    public function edit(User $user, Location $location) {
        return $user->permissions->pluck('name')
            ->contains($location->permission_slug); // assuming we have a binding
    }
}

Remember to register policy in the AuthServiceProvider.php.

protected $policies = [
    Location::class => LocationPolicy::class,
];

Now in your form request consume the policy in the authorize method. From here you are in a request context, you can access user on $this->user() and you can access all models that are model binding on their name for example $this->location.

class EditLocationRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('edit', $this->location);
    }
}

Now you should be able to only have a single route definition.

Route::get('administration/location/{location}/edit', 'LocationController@edit')->name('location.edit');

EDIT

Withouth the form request if you use the trait AuthorizesRequests you can do the following. This will throw an AuthorizationException of it fails.

use AuthorizesRequests;

public function edit() {
    $this->authorize('edit', $location);
}
Related