Laravel Roles and Permissions: Gates and Policies Explained

Published on by

Laravel Roles and Permissions: Gates and Policies Explained image

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:

use App\Models\User;
use Illuminate\Support\Facades\Gate;
 
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
// Should return TRUE or FALSE
Gate::define('manage_users', function(User $user) {
return $user->is_admin == 1;
});
}
}

resources/views/navigation.blade.php:

<ul>
<li>
<a href="{{ route('projects.index') }}">Projects</a>
</li>
@can('manage_users')
<li>
<a href="{{ route('users.index') }}">Users</a>
</li>
@endcan
</ul>

routes/web.php:

Route::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:

Route::post('users', [UserController::class, 'store'])
->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:

public function store(Request $request)
{
if (!$request->user()->can('create_users'))
abort(403);
}
}

The opposite is cannot():

public function store(Request $request)
{
if ($request->user()->cannot('create_users'))
abort(403);
}
}

Or, if you don't have a $request variable, you can use auth() helper:

public function create()
{
if (!auth()->user()->can('create_users'))
abort(403);
}
}

Option 3. Gate::allows() or Gate::denies()

Another way is to use a Gate facade:

public function store(Request $request)
{
if (!Gate::allows('create_users')) {
abort(403);
}
}

Or, the opposite way:

public function store(Request $request)
{
if (Gate::denies('create_users')) {
abort(403);
}
}

Or, a shorter way to abort, with helpers:

public function store(Request $request)
{
abort_if(Gate::denies('create_users'), 403);
}

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.

public function store(Request $request)
{
$this->authorize('create_users');
}

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.

public function store(StoreUserRequest $request)
{
// No check is needed in the Controller method
}

And then, in the Form Request:

class StoreProjectRequest extends FormRequest
{
public function authorize()
{
return Gate::allows('create_users');
}
 
public function rules()
{
return [
// ...
];
}
}

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:

php 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:

use App\Models\Product;
use App\Models\User;
 
class ProductPolicy
{
use HandlesAuthorization;
 
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user)
{
//
}
 
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Product $product)
{
//
}
 
/**
* Determine whether the user can create models.
*/
public function create(User $user)
{
//
}
 
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Product $product)
{
//
}
 
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Product $product)
{
//
}
 
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Product $product)
{
//
}
 
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Product $product)
{
//
}
}

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:

class ProductPolicy
{
public function create(User $user)
{
return $user->is_admin == 1;
}

Then, you can check the Policy in a very similar way as Gates:

public function store(Request $request)
{
$this->authorize('create', Product::class);
}

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:

  1. Create "roles" DB table and Role Eloquent Model
  2. Add a relationship from User to Role: one-to-many or many-to-many
  3. Seed the default Roles and assign them to the existing Users
  4. Assign a default Role at the registration
  5. Change Gates/Policies to check the Role instead

The last bit is the most crucial.

So, instead of:

class ProductPolicy
{
public function create(User $user)
{
return $user->is_admin == 1;
}

You would do something like:

class ProductPolicy
{
public function create(User $user)
{
return $user->role_id == Role::ADMIN;
}

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:

class ProductPolicy
{
public function create(User $user)
{
return $user->role->name == 'Administrator';
}

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 a Gate::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.
$roles = Role::with('permissions')->get();
$permissionsArray = [];
foreach ($roles as $role) {
foreach ($role->permissions as $permissions) {
$permissionsArray[$permissions->title][] = $role->id;
}
}
 
// Every permission may have multiple roles assigned
foreach ($permissionsArray as $title => $roles) {
Gate::define($title, function ($user) use ($roles) {
// We check if we have the needed roles among current user's roles
return count(array_intersect($user->roles->pluck('id')->toArray(), $roles)) > 0;
});
}

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:

$user->givePermissionTo('edit articles');
$user->assignRole('writer');
$role->givePermissionTo('edit articles');
$user->can('edit articles');

Bouncer is maybe a bit less intuitive but still very good:

Bouncer::allow($user)->to('create', Post::class);
Bouncer::allow('admin')->to('ban-users');
Bouncer::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.

PovilasKorop photo

Creator of Courses and Tutorials at Laravel Daily

Cube

Laravel Newsletter

Join 40k+ other developers and never miss out on new tips, tutorials, and more.

Laravel Forge logo

Laravel Forge

Easily create and manage your servers and deploy your Laravel applications in seconds.

Laravel Forge
Tinkerwell logo

Tinkerwell

The must-have code runner for Laravel developers. Tinker with AI, autocompletion and instant feedback on local and production environments.

Tinkerwell
No Compromises logo

No Compromises

Joel and Aaron, the two seasoned devs from the No Compromises podcast, are now available to hire for your Laravel project. ⬧ Flat rate of $7500/mo. ⬧ No lengthy sales process. ⬧ No contracts. ⬧ 100% money back guarantee.

No Compromises
Kirschbaum logo

Kirschbaum

Providing innovation and stability to ensure your web application succeeds.

Kirschbaum
Shift logo

Shift

Running an old Laravel version? Instant, automated Laravel upgrades and code modernization to keep your applications fresh.

Shift
Bacancy logo

Bacancy

Supercharge your project with a seasoned Laravel developer with 4-6 years of experience for just $2500/month. Get 160 hours of dedicated expertise & a risk-free 15-day trial. Schedule a call now!

Bacancy
Lucky Media logo

Lucky Media

Get Lucky Now - the ideal choice for Laravel Development, with over a decade of experience!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel E-Commerce

E-Commerce for Laravel. An open-source package that brings the power of modern headless e-commerce functionality to Laravel.

Lunar: Laravel E-Commerce
LaraJobs logo

LaraJobs

The official Laravel job board

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS Starter Kit

SaaSykit is a Multi-tenant Laravel SaaS Starter Kit that comes with all features required to run a modern SaaS. Payments, Beautiful Checkout, Admin Panel, User dashboard, Auth, Ready Components, Stats, Blog, Docs and more.

SaaSykit: Laravel SaaS Starter Kit
Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate logo

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate

Build your SaaS application in hours. Out-of-the-box multi-tenancy and seamless Stripe integration. Supports subscriptions and one-time purchases, allowing you to focus on building and creating without repetitive setup tasks.

Supercharge Your SaaS Development with FilamentFlow: The Ultimate Laravel Filament Boilerplate
Rector logo

Rector

Your partner for seamless Laravel upgrades, cutting costs, and accelerating innovation for successful companies

Rector
MongoDB logo

MongoDB

Enhance your PHP applications with the powerful integration of MongoDB and Laravel, empowering developers to build applications with ease and efficiency. Support transactional, search, analytics and mobile use cases while using the familiar Eloquent APIs. Discover how MongoDB's flexible, modern database can transform your Laravel applications.

MongoDB

The latest

View all →
Asymmetric Property Visibility in PHP 8.4 image

Asymmetric Property Visibility in PHP 8.4

Read article
Access Laravel Pulse Data as a JSON API image

Access Laravel Pulse Data as a JSON API

Read article
Laravel Forge adds Statamic Integration image

Laravel Forge adds Statamic Integration

Read article
Transform Data into Type-safe DTOs with this PHP Package image

Transform Data into Type-safe DTOs with this PHP Package

Read article
PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell image

PHPxWorld - The resurgence of PHP meet-ups with Chris Morrell

Read article
Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3 image

Herd Executable Support and Pest 3 Mutation Testing in PhpStorm 2024.3

Read article