DEV Community

Cover image for Managing permissions in Laravel applications using Spatie/laravel-permission
Elvis Ansima
Elvis Ansima

Posted on • Edited on

Managing permissions in Laravel applications using Spatie/laravel-permission

In this post, I'll share my approach to managing roles and permissions in Laravel apps.

I will be using Spatie(laravel-permission), which is an optional package that can be used to implement RBAC in your apps. For getting started, please check their official documentation at https://spatie.be/docs/laravel-permission/v6/introduction.

In summary we will discuss on how to:

  • Manage roles/permissions both in development and production environments
  • Protect routes

Why Spatie?

RBAC can be tedious because we end up doing more than we thought. It's okay to bring your own implementation, but trust me it will not an easy path.

A manual implementation can look like this (These SQLs should be turned into models/migrations):

-- Permission table
CREATE TABLE IF NOT EXISTS Permissions (
    ID INT PRIMARY KEY AUTO_INCREMENT,
    permission_name VARCHAR(255)
);

-- Role table
CREATE TABLE IF NOT EXISTS Roles (
    ID INT PRIMARY KEY AUTO_INCREMENT,
    role_name VARCHAR(255)
);

-- Table to associate roles with their permissions
CREATE TABLE IF NOT EXISTS Role_Permissions (
    Role_ID INT,
    Permission_ID INT,
    FOREIGN KEY (Role_ID) REFERENCES Roles(ID),
    FOREIGN KEY (Permission_ID) REFERENCES Permissions(ID),
    PRIMARY KEY (Role_ID, Permission_ID)
);

-- Users Table
CREATE TABLE IF NOT EXISTS Users (
    ID INT PRIMARY KEY AUTO_INCREMENT,
    Name VARCHAR(255)
);

-- Table to associate users with roles
CREATE TABLE IF NOT EXISTS User_Role (
    User_ID INT,
    Role_ID INT,
    FOREIGN KEY (User_ID) REFERENCES Users(ID),
    FOREIGN KEY (Role_ID) REFERENCES Roles(ID),
    PRIMARY KEY (User_ID, Role_ID)
);

Enter fullscreen mode Exit fullscreen mode

Following example illustrate how to retrieve permissions for a user with id 1 :

SELECT Users.ID, Users.Name, Roles.role_name, Permissions.permission_name
FROM Users
JOIN User_Role ON Users.ID = User_Role.User_ID
JOIN Roles ON User_Role.Role_ID = Roles.ID
JOIN Role_Permissions ON Roles.ID = Role_Permissions.Role_ID
JOIN Permissions ON Role_Permissions.Permission_ID = Permissions.ID
WHERE Users.id=1;
Enter fullscreen mode Exit fullscreen mode

The above approach can work and you can implement a middleware (https://laravel.com/docs/11.x/middleware) to check if a user has required permission(s) before accessing a resource. This can be tedious and error-prone if not done right. Spatie/laravel-permission exists to save us time and ensure we have a high-quality implementation in place.

Managing permissions

I assume you have already read the Spatie official documentation, if not please read : https://spatie.be/docs/laravel-permission/v6/introduction.

Now you want to ensure that you have the flexibility to add new roles/permissions as your application grows and this may change from time to time.

From the docs, these are the lines we can use to create a perm and link to a role

$writers_role = \Spatie\Permission\Models\Role::create(['name' => 'writer']);
$edit_article_permission = \Spatie\Permission\Models\Permission::create(['name' => 'edit articles']);

$writers_role->givePermissionTo($edit_article_permission ); //give 'edit articles' permission to writers
Enter fullscreen mode Exit fullscreen mode

Simple! But where should you run this code to get permissions in your DB? There are many options. For example, you can use Laravel Tinker, or even run this code once and delete it, etc... But how to make the process easy as the app keeps growing? 🤔🤔

I have 2 suggestions that usually work for me :

Use a migration file with no call to Schema related functions

It's a hack, but it works well. Firstly, this ensures that we will never create this permission twice, and as migrations are well handled by Laravel, we do not have to worry about calling the same code twice. We can create the permission in the up method and delete it in the down method.

The migration can look like this

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

return new class extends Migration
{
    public function up(): void
    {
        $writers_role = Role::findOrCreate('publishers');
        $edit_article_permission = Permission::findOrCreate('edit articles');
        $writers_role->givePermissionTo($edit_article_permission); //give 'edit articles' permission to writers
    }

    public function down(): void
    {
        //here you can revert above created roles or permissions but you must ensure no user is still using any of them
    }
};

Enter fullscreen mode Exit fullscreen mode

This is flexible because you can run migrations in production environment without breaking anything.

Create a specific artisan command

In Laravel it is not complicated to create a console command.
Spatie/laravel-permission comes with some predefined artisan commands, but they are not suitable for production as they are manual in nature, find more info about them here

We will create our own command.
Go to your terminal and run:

php artisan make:command InitPerms
Enter fullscreen mode Exit fullscreen mode

This will create a file at app/Console/Commands/InitPerms.php

Paste inside InitPerms.php the following code:

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

class InitPerms extends Command
{

    protected $signature = 'init:perms';
    protected $description = 'Create permissions';
    public function handle()
    {
        $writers_role = Role::findOrCreate('writer');
        $edit_article_permission = Permission::findOrCreate('edit articles');
        $writers_role->givePermissionTo($edit_article_permission); //give 'edit articles' permission to writers
        $this->info("Done!");
    }
}
Enter fullscreen mode Exit fullscreen mode

The permissions can then be created within your application by running php artisan init:perms from your terminal.
This approach is good because you can run the command as many times as you like, modify the code, add more permissions whenever you really want.

Tip
If you have the env file for your production environment (typically for a small scale application) and you can access your database remotely you can run php artisan init:perms --env=production to execute the permission logic in your production DB (notice the env flag)

This approach is also good because you can add this command to your deployment pipeline, for example, and the permissions will be updated each time you trigger a new deployment.

Protect your routes

Implementing routes guard using spatie/laravel-permission can be done in a sec!
Simply calling the can() method is enough to check if a specific user has a give permission.
eg.

<?php
$user = Auth::user(); //get currently authenticated user
$hasPermToDeleteUsers = $user->can('delete users'); //return a true if user has permission
Enter fullscreen mode Exit fullscreen mode

Spatie/laravel-permission create middleware that you can use in your route to protect them from unauthorized access
They have three middleware classes: \Spatie\Permission\Middleware\RoleMiddleware (for checking if user has given role),
\Spatie\Permission\Middleware\PermissionMiddleware (for checking if user has specific permission) ,
\Spatie\Permission\Middleware\RoleOrPermissionMiddleware (for checking if user has specific role or permission)

Here is an example using the permission middleware :

Route::middleware([
    'auth:sanctum',
    config('jetstream.auth_session'),
    '\Spatie\Permission\Middleware\PermissionMiddleware::class:edit articles|delete articles', //notice here the middleware
    'verified',
])->group(function () {
    Route::get('/dashboard', function () {
        return Inertia::render('Dashboard');
    })->name('dashboard');
});
Enter fullscreen mode Exit fullscreen mode

You can also register a your middleware alias to avoid using FQCN and improve code readability : https://laravel.com/docs/11.x/middleware#middleware-aliases

Conclusion

You can choose any of the above methods and your permissions will be well maintained over time across all your environments.
Some people also recommend writing these permissions in a seeder, but I personally don't like this as seeding itself in sensitive environments is dangerous!
If you have an application that heavily manipulates permissions, you can also create a UI where people can manage other users' permissions and roles.

Remember that there are only best practices, not perfect code. Have you found any of the above techniques useful in your particular situation? Please share in the comments!

Top comments (0)