Liviu Hariton
PHP developer
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'));