In Laravel, roles and permissions have been one of the most confusing topics over the years. Mostly, because there is no documentation about it: the same things "hide" under other terms in the framework, like "gates", "policies", "guards", etc. In this article, I will try to explain them all in "human language".
Gate is the same as Permission
One of the biggest confusions, in my opinion, is the term "gate". I think developers would have avoided a lot of confusion if they were called what they are.
Gates are Permissions, just called by another word.
What are the typical actions we need to perform with permissions?
- Define the permission, ex. "manage_users"
- Check the permission on the front-end, ex. show/hide the button
- Check the permission on the back-end, ex. can/can't update the data
So yeah, replace the word "permission" with "gate", and you understand it all.
A simple Laravel example would be this:
app/Providers/AppServiceProvider.php:
1use App\Models\User; 2use Illuminate\Support\Facades\Gate; 3 4class AppServiceProvider extends ServiceProvider 5{ 6 public function boot() 7 { 8 // Should return TRUE or FALSE 9 Gate::define('manage_users', function(User $user) {10 return $user->is_admin == 1;11 });12 }13}
resources/views/navigation.blade.php:
1<ul> 2 <li> 3 <a href="{{ route('projects.index') }}">Projects</a> 4 </li> 5 @can('manage_users') 6 <li> 7 <a href="{{ route('users.index') }}">Users</a> 8 </li> 9 @endcan10</ul>
routes/web.php:
1Route::resource('users', UserController::class)->middleware('can:manage_users');
Now, I know that, technically, Gate may mean more than one permission. So, instead of "manage_users", you could define something like "admin_area". But in most examples I've seen, Gate is a synonym for Permission.
Also, in some cases, the permissions are called "abilities", like in the Bouncer package. It also means the same thing - ability/permission for some action. We'll get to the packages, later in this article.
Various Ways to Check Gate Permission
Another source of confusion is how/where to check the Gate. It's so flexible that you may find very different examples. Let's run through them:
Option 1. Routes: middleware('can:xxxxxx')
This is the example from above. Directly on the route/group, you may assign the middleware:
1Route::post('users', [UserController::class, 'store'])2 ->middleware('can:create_users');
Option 2. Controller: can() / cannot()
In the first lines of the Controller method, we can see something like this, with methods can()
or cannot()
, identical to the Blade directives:
1public function store(Request $request)2{3 if (!$request->user()->can('create_users'))4 abort(403);5 }6}
The opposite is cannot()
:
1public function store(Request $request)2{3 if ($request->user()->cannot('create_users'))4 abort(403);5 }6}
Or, if you don't have a $request
variable, you can use auth()
helper:
1public function create()2{3 if (!auth()->user()->can('create_users'))4 abort(403);5 }6}
Option 3. Gate::allows() or Gate::denies()
Another way is to use a Gate facade:
1public function store(Request $request)2{3 if (!Gate::allows('create_users')) {4 abort(403);5 }6}
Or, the opposite way:
1public function store(Request $request)2{3 if (Gate::denies('create_users')) {4 abort(403);5 }6}
Or, a shorter way to abort, with helpers:
1public function store(Request $request)2{3 abort_if(Gate::denies('create_users'), 403);4}
Option 4. Controller: authorize()
Even shorter option, and my favorite one, is to use authorize()
in Controllers. In case of failure, it would return a 403 page, automatically.
1public function store(Request $request)2{3 $this->authorize('create_users');4}
Option 5. Form Request class:
I've noticed that many developers generate Form Request classes just to define the validation rules, totally ignoring the first method of that class, which is authorize()
.
You can use it to check the gates as well. This way, you're achieving a separation of concerns, which is a good practice for solid code, so the Controller doesn't take care of the validation, because it's done in its dedicated Form Request class.
1public function store(StoreUserRequest $request)2{3 // No check is needed in the Controller method4}
And then, in the Form Request:
1class StoreProjectRequest extends FormRequest 2{ 3 public function authorize() 4 { 5 return Gate::allows('create_users'); 6 } 7 8 public function rules() 9 {10 return [11 // ...12 ];13 }14}
Policy: Model-Based Set of Permissions
If your permissions can be assigned to an Eloquent model, in a typical CRUD Controller, you can build a Policy class around them.
If we run this command:
1php artisan make:policy ProductPolicy --model=Product
It will generate the file app/Policies/UserPolicy.php, with the default methods that have a comment to explain their purpose:
1use App\Models\Product; 2use App\Models\User; 3 4class ProductPolicy 5{ 6 use HandlesAuthorization; 7 8 /** 9 * Determine whether the user can view any models.10 */11 public function viewAny(User $user)12 {13 //14 }15 16 /**17 * Determine whether the user can view the model.18 */19 public function view(User $user, Product $product)20 {21 //22 }23 24 /**25 * Determine whether the user can create models.26 */27 public function create(User $user)28 {29 //30 }31 32 /**33 * Determine whether the user can update the model.34 */35 public function update(User $user, Product $product)36 {37 //38 }39 40 /**41 * Determine whether the user can delete the model.42 */43 public function delete(User $user, Product $product)44 {45 //46 }47 48 /**49 * Determine whether the user can restore the model.50 */51 public function restore(User $user, Product $product)52 {53 //54 }55 56 /**57 * Determine whether the user can permanently delete the model.58 */59 public function forceDelete(User $user, Product $product)60 {61 //62 }63}
In each of those methods, you define the condition for the true/false return. So, if we follow the same examples as Gates before, we can do this:
1class ProductPolicy2{3 public function create(User $user)4 {5 return $user->is_admin == 1;6 }
Then, you can check the Policy in a very similar way as Gates:
1public function store(Request $request)2{3 $this->authorize('create', Product::class);4}
So, you specify the method name and the class name of the Policy.
In other words, Policies are just another way to group the permissions, instead of Gates. If your actions are mostly around CRUDs of Models, then Policies are probably a more convenient and better-structured option than Gates.
Role: Universal Set of Permissions
Let's discuss another confusion: in Laravel docs, you won't find any section about User Roles. The reason is simple: the term "roles" is artificially made up, to group the permission under some kind of name, like "administrator" or "editor".
From the framework point of view, there are no "roles", only gates/policies that you can group by in whatever way you want.
In other words, a role is an entity OUTSIDE of the Laravel framework, so we need to build the role structure ourselves. It may be a part of the overall auth confusion, but it makes perfect sense because we should control how roles are defined:
- Is it one role or multiple roles?
- Can a user have one role or multiple roles?
- Who can manage roles in the system?
- etc.
So, the Role functionality is another layer of your Laravel application. This is where we get to the Laravel packages that may help. But we can also create the roles without any package:
- Create "roles" DB table and Role Eloquent Model
- Add a relationship from User to Role: one-to-many or many-to-many
- Seed the default Roles and assign them to the existing Users
- Assign a default Role at the registration
- Change Gates/Policies to check the Role instead
The last bit is the most crucial.
So, instead of:
1class ProductPolicy2{3 public function create(User $user)4 {5 return $user->is_admin == 1;6 }
You would do something like:
1class ProductPolicy2{3 public function create(User $user)4 {5 return $user->role_id == Role::ADMIN;6 }
Again, here you have a few options to check the roles. In the example above, we assume there's a belongsTo
relationship from User to Role, and also there are constants in the Role model like ADMIN = 1
, like EDITOR = 2
, just to avoid querying the database too much.
But if you prefer to be flexible, you can query the database every time:
1class ProductPolicy2{3 public function create(User $user)4 {5 return $user->role->name == 'Administrator';6 }
But remember to eager load "role" relationship, otherwise, you can easily run into an N+1 query problem here.
Making it Flexible: Permissions Saved in DB
In my personal experience, the usual model of building it all together is this:
- All permissions and roles are saved in the database, managed with some admin panel;
- Relationships: roles many-to-many permissions, User belongs to Role (or many-to-many roles);
- Then, in AppServiceProvider you make a
foreach
loop from all permissions from DB, and run aGate::define()
statement for each of them, returning true/false based on the role; - And finally, you check the permissions with
@can('permission_name')
and$this->authorize('permission_name')
, like in the examples above.
1$roles = Role::with('permissions')->get(); 2$permissionsArray = []; 3foreach ($roles as $role) { 4 foreach ($role->permissions as $permissions) { 5 $permissionsArray[$permissions->title][] = $role->id; 6 } 7} 8 9// Every permission may have multiple roles assigned10foreach ($permissionsArray as $title => $roles) {11 Gate::define($title, function ($user) use ($roles) {12 // We check if we have the needed roles among current user's roles13 return count(array_intersect($user->roles->pluck('id')->toArray(), $roles)) > 0;14 });15}
In other words, we don't check any access by roles. Role is just an "artificial" layer, a set of permissions that is transformed into Gates during the application lifecycle.
Looks complicated? No worries, this is where we come to packages that can help.
Packages To Manage Roles/Permissions
The most popular packages for this are Spatie Laravel Permission and Bouncer, I have a separate long article about them. The article is very old, but the market leaders are still the same, because of their stability.
What those packages do is help you to abstract the permission management into a human-friendly language, with methods that you can easily remember and use.
Look at this beautiful syntax from Spatie permission:
1$user->givePermissionTo('edit articles');2$user->assignRole('writer');3$role->givePermissionTo('edit articles');4$user->can('edit articles');
Bouncer is maybe a bit less intuitive but still very good:
1Bouncer::allow($user)->to('create', Post::class);2Bouncer::allow('admin')->to('ban-users');3Bouncer::assign('admin')->to($user);
You can read more about how to use those packages in the links to their Github, or my article above.
So, these packages are the final "layer" of authentication/authorization that we cover here in this article, I hope now you get the full picture and will be able to pick what strategy to use.
P.S. Wait, What About Guards?
Oh, those. They cause so much confusion over the years. Many developers thought that Guards are Roles, and started creating separate DB tables like "administrators", and then assigning those as Guards. Partly, because in the documentation you may find code snippets like Auth::guard('admin')->attempt($credentials))
I even submitted a Pull Request to the docs with a warning to avoid this misunderstanding.
In the official documentation, you may find this paragraph:
At its core, Laravel's authentication facilities are made up of "guards" and "providers". Guards define how users are authenticated for each request. For example, Laravel ships with a session guard which maintains state using session storage and cookies.
So, guards are a more global concept than roles. One example of a guard is "session", later in the documentation, you may see a JWT guard example. In other words, a guard is a full authentication mechanism, and for the majority of Laravel projects, you won't ever need to change the guard or even know how they work. Guards are outside of this roles/permissions topic.
0 comments:
Post a Comment
Thanks