Laravel 11 - Roles and permissions system

In this article:

  • Migrations for Roles and Permissions
  • Models for Role and Permission
  • Middleware for Role and Permission Checks
    • Registering Middleware in Route Files
  • Applying Role and Permission Checks in Controllers
  • Blade Directives for Role and Permission Checks
  • Using Traits for Reusable Authorization Logic
  • Gates and Policies
    • Introduction
    • Implementing Gates
      • Define Gates in AuthServiceProvider
      • Using Gates in Controllers
      • Using Gates in Blade Templates
    • Implementing Policies
      • Create a Policy
      • Register the Policy
      • Using Policies in Controllers
      • Using Policies in Blade Templates
      • Global Policies with the „GOD” Role
  • Attach or detach a role to / from a user

 

Migrations for Roles and Permissions

Create a migration file for the roles and permissions system:

php artisan make:migration create_roles_permissions_tables

update the code

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateRolesPermissionsTables extends Migration
{
    public function up()
    {
        // Roles Table
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->string('description')->nullable();
            $table->timestamps();
        });

        // Permissions Table
        Schema::create('permissions', function (Blueprint $table) {
            $table->id();
            $table->string('name')->unique();
            $table->string('description')->nullable();
            $table->timestamps();
        });

        // Role-User Pivot Table
        Schema::create('role_user', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->foreignId('role_id')->constrained()->onDelete('cascade');
            $table->timestamps();
        });

        // Permission-Role Pivot Table
        Schema::create('permission_role', function (Blueprint $table) {
            $table->id();
            $table->foreignId('role_id')->constrained()->onDelete('cascade');
            $table->foreignId('permission_id')->constrained()->onDelete('cascade');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('permission_role');
        Schema::dropIfExists('role_user');
        Schema::dropIfExists('permissions');
        Schema::dropIfExists('roles');
    }
}

and run the migration

php artisan migrate

Models for Role and Permission

Create the Role and Permission models:

php artisan make:model Role
php artisan make:model Permission

app/Models/Role.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'description'];

    public function users()
    {
        return $this->belongsToMany(User::class);
    }

    public function permissions()
    {
        return $this->belongsToMany(Permission::class);
    }
}

app/Models/Permission.php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Permission extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'description'];

    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

Extend the app/Models/User.php model to include methods for checking roles and permissions:

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    // ...

    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }

    public function hasRole($roles)
    {
        if ($this->isGod()) {
            return true;
        }

        if (is_array($roles)) {
            return $this->roles()->whereIn('name', $roles)->exists();
        }

        return $this->roles()->where('name', $roles)->exists();
    }

    public function hasPermission($permission)
    {
        if ($this->isGod()) {
            return true;
        }

        return $this->roles()->whereHas('permissions', function ($query) use ($permission) {
            $query->where('name', $permission);
        })->exists();
    }

    public function hasAnyPermission(array $permissions)
    {
        if ($this->isGod()) {
            return true;
        }

        return $this->roles()->whereHas('permissions', function ($query) use ($permissions) {
            $query->whereIn('name', $permissions);
        })->exists();
    }

    public function isGod()
    {
        return $this->roles()->where('name', 'GOD')->exists();
    }
}

Middleware for Role and Permission Checks

Create middleware to enforce role and permission checks

php artisan make:middleware RoleMiddleware
php artisan make:middleware PermissionMiddleware

app/Http/Middleware/RoleMiddleware.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class RoleMiddleware
{
    public function handle(Request $request, Closure $next, $role)
    {
        if (!auth()->user()->hasRole($role)) {
            abort(403, 'Unauthorized');
        }

        return $next($request);
    }
}

app/Http/Middleware/PermissionMiddleware.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class PermissionMiddleware
{
    public function handle(Request $request, Closure $next, $permission)
    {
        if (!auth()->user()->hasPermission($permission)) {
            abort(403, 'Unauthorized');
        }

        return $next($request);
    }
}

Registering Middleware in Route Files

use App\Http\Middleware\RoleMiddleware;
use App\Http\Middleware\PermissionMiddleware;

Route::middleware([RoleMiddleware::class.':admin'])->group(function () {
    Route::get('/admin', [AdminController::class, 'index']);
});

Route::middleware([PermissionMiddleware::class.':manage-users'])->group(function () {
    Route::get('/manage-users', [UserController::class, 'manage']);
});

Applying Role and Permission Checks in Controllers

In your controllers, you can use the hasRole, hasPermission, and hasAnyPermission methods for authorization:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class AdminController extends Controller
{
    public function index()
    {
        $user = auth()->user();

        if (!$user->hasRole('admin')) {
            abort(403, 'You do not have access to this section.');
        }

        return view('admin.dashboard');
    }

    public function manageUsers()
    {
        $user = auth()->user();

        if (!$user->hasPermission('manage-users')) {
            abort(403, 'You do not have permission to manage users.');
        }

        return view('users.manage');
    }
}

Blade Directives for Role and Permission Checks

app/Providers/AppServiceProvider.php

use Illuminate\Support\Facades\Blade;

public function boot()
{
    // Directive for roles
    Blade::if('role', function ($role) {
        return auth()->check() && auth()->user()->hasRole($role);
    });

    // Directive for permissions
    Blade::if('permission', function ($permission) {
        return auth()->check() && auth()->user()->hasPermission($permission);
    });
}

Use these directives in your Blade views:

@role('admin') ... @endrole
@permission('manage-users') ... @endpermission

Using Traits for Reusable Authorization Logic

php artisan make:trait Authorizable

app/Traits/Authorizable.php

namespace App\Traits;

trait Authorizable
{
    public function authorizeRole($role)
    {
        if (!auth()->user()->hasRole($role)) {
            abort(403, 'Unauthorized action.');
        }
    }

    public function authorizePermission($permission)
    {
        if (!auth()->user()->hasPermission($permission)) {
            abort(403, 'Unauthorized action.');
        }
    }

    public function authorizeAnyPermission(array $permissions)
    {
        if (!auth()->user()->hasAnyPermission($permissions)) {
            abort(403, 'Unauthorized action.');
        }
    }
}

Using the Trait in a Controller

namespace App\Http\Controllers;

use App\Traits\Authorizable;

class PostController extends Controller
{
    use Authorizable;

    public function create()
    {
        $this->authorizePermission('create-posts');

        return view('posts.create');
    }

    public function delete()
    {
        $this->authorizeAnyPermission(['delete-posts', 'admin-posts']);

        // Proceed with deleting a post
        // ...
    }
}

Gates and Policies

Extending your roles and permissions system in Laravel 11 using Gates and Policies can provide a more granular and organized way to manage authorization logic, particularly when dealing with complex authorization requirements. Gates and Policies are built into Laravel and provide a structured way to control access to various parts of your application based on user roles, permissions, and other conditions.

Introduction to Gates and Policies

  • Gates: Gates are a simple closure-based approach to authorization. They are best suited for scenarios where you need to authorize an action without necessarily tying it to a specific model.
  • Policies: Policies are class-based authorization and are ideal when you need to authorize actions on specific models, such as managing posts, users, or any other resource.

Implementing Gates

Define Gates in AuthServiceProvider

Open the App\Providers\AuthServiceProvider and define your gates within the boot method:

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use App\Models\User;

class AuthServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // Define a Gate for managing users
        Gate::define('manage-users', function (User $user) {
            return $user->hasPermission('manage-users');
        });

        // Define a Gate for viewing posts
        Gate::define('view-posts', function (User $user) {
            return $user->hasPermission('view-posts');
        });

        // Example: Allow all actions for the "GOD" role
        Gate::before(function (User $user) {
            if ($user->isGod()) {
                return true;
            }
        });
    }
}

Using Gates in Controllers

You can now use these gates within your controllers to authorize actions:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;

class UserController extends Controller
{
    public function manage()
    {
        if (Gate::denies('manage-users')) {
            abort(403, 'You do not have permission to manage users.');
        }

        // Proceed with the user management logic
        return view('users.manage');
    }
}

Using Gates in Blade Templates

@can('manage-users') ... @endcan
@can('view-posts') ... @endcan

Implementing Policies

Policies are a more structured way of handling authorization, especially when you want to tie permissions to specific models.

Create a Policy

php artisan make:policy PostPolicy

App\Policies\PostPolicy.php

namespace App\Policies;

use App\Models\User;
use App\Models\Post;

class PostPolicy
{
    public function view(User $user, Post $post)
    {
        // Any user with 'view-posts' permission can view posts
        return $user->hasPermission('view-posts');
    }

    public function create(User $user)
    {
        // Only users with the 'create-posts' permission can create posts
        return $user->hasPermission('create-posts');
    }

    public function update(User $user, Post $post)
    {
        // Only users with the 'edit-posts' permission can update posts
        return $user->hasPermission('edit-posts');
    }

    public function delete(User $user, Post $post)
    {
        // Only users with the 'delete-posts' permission can delete posts
        return $user->hasPermission('delete-posts');
    }
}

Register the Policy

You need to register your policy in the App\Providers\AuthServiceProvider:

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Models\Post;
use App\Policies\PostPolicy;

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Post::class => PostPolicy::class,
    ];

    public function boot()
    {
        $this->registerPolicies();

        // Global "GOD" role check
        Gate::before(function (User $user) {
            if ($user->isGod()) {
                return true;
            }
        });
    }
}

Using Policies in Controllers

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function update(Request $request, Post $post)
    {
        $this->authorize('update', $post);

        // Proceed with updating the post
        // ...
    }

    public function delete(Request $request, Post $post)
    {
        $this->authorize('delete', $post);

        // Proceed with deleting the post
        // ...
    }
}

Using Policies in Blade Templates

@can('update', $post) ... @endcan
@can('delete', $post) ... @endcan

Global Policies with the „GOD” Role

Gate::before(function (User $user) {
    if ($user->isGod()) {
        return true;
    }
});

Attach or detach a role to / from a user

$user->roles()->attach($request->input('role_id'));
$user->roles()->detach($request->input('role_id'));