We really like the laravel-permissions package and use it on most projects.
One nice thing is that it lets you use the existing Laravel can
middleware to check if a user has a certain permission.
In a routes file, we can wrap a whole group of routes in a permission-based middleware check like this:
// routes/web.php
Route::middleware('can:manage-articles')->group(function () {
Route::get('/articles', 'ArticleController@index');
Route::get('/articles/{article}', 'ArticleController@show');
// more article routes...
});
Normally, we'd have a Permission
enum and use that instead of a string, but I'm keeping the example simple and easier to read.
In this example, if the user doesn't have access to the manage-articles
permission, they shouldn't be able to access any of these routes. And we certainly don't need to do any more granular checks about "can they edit this specific Article model". We just deny early.
But something potentially unexpected happens if a user with no permission to this group hits a route like /articles/123
, where 123
is a non-existent article. They'll get a 404 error instead of the expected 403 error, because of route model binding.
By default, Laravel assigns the SubstituteBindings
middleware a higher priority than the Authorize
middleware. So if a model is not found, it will return a 404 before the Authorize
middleware even has a chance to return a 403.
Before talking about a solution, why is it ordered this way? Well, consider a more traditional policy-based authorization check. Some of those methods require a bound model in order to run.
For example, a typical edit
method in a policy might look like this:
public function edit(User $user, Article $article)
{
return $user->id === $article->user_id;
}
So if the model isn't bound, the policy method can't run. And if the policy method can't run, the Authorize
middleware can't do its job. So it makes sense for SubstituteBindings
to run first.
In our particular example, we don't need a bound model, but if we just flip the order of the middleware, we will break other parts of our authorization logic in policies that do rely on a bound model.
So what's the solution? In this case, we can use the package-specific PermissionMiddleware
instead, and we can also make sure it has a higher priority than the SubstituteBindings
middleware.
Hope this helps,
Joel
P.S. If you like diving deep on a topic and understanding how Laravel really works, check out Mastering Laravel Validation Rules.