Role and Permission Management in Laravel 12 using Policies & Gates (Step-by-Step)

Author

Kritim Yantra

Jun 03, 2025

Role and Permission Management in Laravel 12 using Policies & Gates (Step-by-Step)

Controlling who can access what is essential for any web application. Whether you're building a blog, admin panel, or a SaaS platform—Role-Based Access Control (RBAC) ensures users only do what they're allowed to.

Laravel 12 makes this easy with two built-in tools:

  • Gates – for general access rules
  • Policies – for model-specific rules

In this guide, you’ll learn how to implement a full role and permission system using Laravel’s Gates and Policies. We’ll keep it simple, clean, and practical 💡


🧠 Gates vs Policies – What's the Difference?

Before we start coding, let’s understand these tools:

Feature Gates Policies
Type Closure-based Class-based
Best For General checks (e.g. "access-dashboard") Model-specific checks (e.g. "edit-post")

Use Gates: When the action is general, like accessing an admin panel
Use Policies: When the action depends on a specific model, like editing a post


🛠️ Step 1: Set Up Database Tables

We’ll need tables for roles, permissions, and their relationships.

// 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();
});

// Pivot table to link roles and permissions
Schema::create('role_permission', function (Blueprint $table) {
    $table->foreignId('role_id')->constrained()->cascadeOnDelete();
    $table->foreignId('permission_id')->constrained()->cascadeOnDelete();
    $table->primary(['role_id', 'permission_id']);
});

// Add role to users
Schema::table('users', function (Blueprint $table) {
    $table->foreignId('role_id')->constrained()->default(3); // default = 'user'
});

🧩 Step 2: Define Relationships in Models

// app/Models/Role.php
class Role extends Model {
    public function users() {
        return $this->hasMany(User::class);
    }

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

// app/Models/Permission.php
class Permission extends Model {
    public function roles() {
        return $this->belongsToMany(Role::class);
    }
}

// app/Models/User.php
class User extends Authenticatable {
    public function role() {
        return $this->belongsTo(Role::class);
    }

    public function hasPermission($name) {
        return $this->role->permissions->contains('name', $name);
    }
}

🔐 Step 3: Working with Gates

📌 Define Gates in AuthServiceProvider

use Illuminate\Support\Facades\Gate;

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

    // Dynamic Gates from DB
    foreach (Permission::all() as $permission) {
        Gate::define($permission->name, function ($user) use ($permission) {
            return $user->hasPermission($permission->name);
        });
    }

    // Or define manually
    Gate::define('access-admin-dashboard', fn($user) => $user->role->name === 'admin');
}

✅ Using Gates

// In controller
if (Gate::allows('edit-settings')) {
    // Allow editing
}

// In Blade
@can('edit-settings')
    <a href="/settings/edit">Edit Settings</a>
@endcan

🧭 Step 4: Using Policies for Models

🔧 Create a Policy

php artisan make:policy PostPolicy --model=Post

✏️ Define Permission Logic

// app/Policies/PostPolicy.php
class PostPolicy {
    public function view(User $user, Post $post) {
        return $user->hasPermission('view-posts') || 
               ($post->user_id == $user->id && $user->hasPermission('view-own-posts'));
    }

    public function create(User $user) {
        return $user->hasPermission('create-posts');
    }

    public function update(User $user, Post $post) {
        return $user->hasPermission('update-any-posts') || 
               ($post->user_id == $user->id && $user->hasPermission('update-own-posts'));
    }

    public function delete(User $user, Post $post) {
        return $user->hasPermission('delete-any-posts') || 
               ($post->user_id == $user->id && $user->hasPermission('delete-own-posts'));
    }
}

🗂️ Register the Policy

protected $policies = [
    Post::class => PostPolicy::class,
];

🧪 Use Policies in Controllers & Views

// Controller
$this->authorize('update', $post);

// Blade
@can('update', $post)
    <a href="{{ route('posts.edit', $post) }}">Edit</a>
@endcan

️ Advanced Usage

✅ Use Middleware

Route::post('/post', function () {
    // Create post logic
})->middleware('can:create,App\Models\Post');

✅ Blade Helpers

@can('create', App\Models\Post::class)
    <a href="{{ route('posts.create') }}">New Post</a>
@endcan

@cannot('delete', $post)
    You can't delete this post.
@endcannot

🚀 Boost Performance: Cache Permissions

// User model
public function getPermissionsAttribute() {
    return Cache::remember("user.{$this->id}.permissions", now()->addDay(), function () {
        return $this->role->permissions->pluck('name')->toArray();
    });
}

public function hasPermission($name) {
    return in_array($name, $this->permissions);
}

🧪 Testing Authorization

// tests/Feature/PostPolicyTest.php

public function test_admin_can_update_any_post() {
    $admin = User::factory()->create(['role_id' => Role::where('name', 'admin')->first()->id]);
    $post = Post::factory()->create();
    $this->assertTrue($admin->can('update', $post));
}

public function test_user_cannot_delete_others_posts() {
    $user1 = User::factory()->create();
    $user2 = User::factory()->create();
    $post = Post::factory()->create(['user_id' => $user2->id]);
    $this->assertFalse($user1->can('delete', $post));
}

🛠️ Bonus: Manage Permissions via Artisan

// app/Console/Commands/CreatePermission.php
protected $signature = 'permission:create {name} {description?}';

public function handle() {
    $permission = Permission::create([
        'name' => $this->argument('name'),
        'description' => $this->argument('description') ?? '',
    ]);
    $this->info("Permission {$permission->name} created!");
}

🧭 Best Practices

  • ✅ Use "verb-noun" naming for permissions (edit-user, delete-post)
  • ✅ Prefer policies over gates for model actions
  • ✅ Keep logic inside policies, not scattered in controllers
  • ✅ Cache permissions to reduce DB hits
  • ✅ Regularly test all permission flows

️ Common Mistakes to Avoid

  • ❌ Using gates for model-specific logic
  • ❌ Forgetting to register policies
  • ❌ Not caching permissions = performance hit
  • ❌ Hardcoding roles instead of using permissions
  • ❌ Not testing edge cases

✅ Conclusion

Laravel makes it easy to build secure, maintainable role and permission systems using Gates and Policies. By following the steps above, you can:

  • Grant fine-grained access
  • Keep your code clean
  • Easily expand as your app grows

🔐 Authorization isn’t just a feature—it’s a security backbone. So take time to plan, test, and document your permissions well.

Happy coding! 🚀

Tags

Comments

No comments yet. Be the first to comment!

Please log in to post a comment:

Sign in with Google

Related Posts