Skip to content

Laravel Permissions Implementation

Introduction

Laravel provides policies to define authorization logic for your application. These policies establish the rules governing which users are authorized to perform specific actions on your application's models. In this guide, we'll discuss the steps required to implement fine-grained permissions in a Laravel application using a database schema, a trait, and a policy.

Prerequisites

Before proceeding, ensure that you have a Laravel application set up and running. You should also be familiar with Laravel migrations, models, and policies.

Database Schema

To implement permissions in a Laravel application, create a database schema that includes tables for roles, permissions, and their relationships. The following example demonstrates how to create these tables using Laravel migrations:

bash
php artisan make:model Role -m
php
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('roles');
    }
};
bash
php artisan make:model Permission -m
php
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('permissions', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('permissions');
    }
};
bash
php artisan make:model RolePermission -m
php
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('role_permissions', function (Blueprint $table) {
            $table->id();
            $table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
            $table->foreignId('permission_id')->constrained('permissions')->onDelete('cascade');
            $table->softDeletes();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('role_permissions');
    }
};
bash
php artisan make:model UserRole -m
php
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('user_roles', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
            $table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('user_roles');
    }
};

In this example, we create tables for roles, permissions, and their relationships. The roles table includes an ID, a name, a description, and timestamps. The permissions table consists of an ID, a name, a description, and timestamps. The role_permissions table contains an ID, a role_id, a permission_id, and timestamps. The role_id and permission_id columns are foreign keys that reference the id columns in the roles and permissions tables, respectively.

Model Schemas

To provide type hints and improve code readability when interacting with the Role, Permission, and User models, we created corresponding <Model>Schema classes. These classes define constants that represent the column names in the corresponding database tables.

For example, here's the PermissionSchema class:

php
<?php

namespace App\Schemas;

class PermissionSchema
{
    const id = 'id';
    const name = 'name';
    const description = 'description';
    const created_at = 'created_at';
    const updated_at = 'updated_at';
    const deleted_at = 'deleted_at';
}

In the above example, we're defining constants for each of the columns in the permissions table. We can then use these constants to provide type hints and improve code readability when interacting with the Permission model and its associated data.

We will later use these schemas to define the name of the columns when interacting with models. For instance, in the HasPermissions trait we will create later, we used the PermissionSchema::name constant to reference the name column in the permissions table:

php
use App\Schemas\PermissionSchema;

// ...

public function hasPermission(string $permission): bool
{
    return $this->roles->some(function ($role) use ($permission) {
        return $role->permissions->contains(PermissionSchema::name, $permission);
    });
}

By using PermissionSchema::name instead of the raw string "name", we're making our code more readable and less prone to errors. Additionally, if we were to ever change the name of the name column in the permissions table, we could simply update the PermissionSchema::name constant to reflect the new name, and all references to that constant in our code would automatically update as well.

By using model schemas in this way, we can make our code more maintainable and easier to work with over time.

Models

To implement fine-grained permissions in a Laravel application, create four models: Permission, Role, RolePermission, and UserRole.These models interact with the database tables that store information about roles, permissions, and user/role relationships.

Here's the Role model:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property string $name
 * @property string|null $description
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 */
class Role extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'user_roles')->withTimestamps();
    }

    public function permissions(): BelongsToMany
    {
        return $this->belongsToMany(Permission::class, 'role_permissions')->withTimestamps();
    }
}

In the Role model example above, we use the BelongsToMany relationship to define the relationship between the Role and User models, as well as the relationship between the Role and Permission models.

Here's the Permission model:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property string $name
 * @property string|null $description
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 */
class Permission extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'role_permissions')->withTimestamps();
    }
}

In the Permission model example above, we use the BelongsToMany relationship to define the relationship between the Permission and Role models.

Here's the RolePermission model:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property int $role_id
 * @property int $permission_id
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon|null $deleted_at
 */
class RolePermission extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function role(): BelongsTo
    {
        return $this->belongsTo(Role::class);
    }

    public function permission(): BelongsTo
    {
        return $this->belongsTo(Permission::class);
    }
}

In the RolePermission model example above, we use the BelongsTo relationship to define the relationship between the RolePermission and Role models, as well as the relationship between the RolePermission and Permission models.

Here's the UserRole model:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
 * @property int $id
 * @property int $user_id
 * @property int $role_id
 * @property \Illuminate\Support\Carbon $created_at
 * @property \Illuminate\Support\Carbon $updated_at
 * @property \Illuminate\Support\Carbon $deleted_at
 */
class UserRole extends Model
{
    use HasFactory, SoftDeletes;

    protected $guarded = ['id'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function role(): BelongsTo
    {
        return $this->belongsTo(Role::class);
    }
}

In the UserRole model example above, we use the BelongsTo relationship to define the relationship between the UserRole and User models, as well as the relationship between the UserRole and Role models.

Finally, here's the User model:

php
<?php

namespace App\Models;

use App\Traits\HasPermissions;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

/**
 * User model representing a user in the system.
 *
 * @property int $id
 * @property string $name
 * @property string $email
 * @property \DateTime|null $email_verified_at
 * @property string $password
 * @property string|null $remember_token
 * @property int|null $contact_id
 * @property \DateTime $created_at
 * @property \DateTime $updated_at
 */
class User extends Authenticatable
{
    use HasFactory, HasPermissions, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, 'user_roles')->withTimestamps();
    }
}

In the User model example above, we use the BelongsToMany relationship to define the relationship between the User and Role models. Additionally, we use various traits and properties to handle authentication, notifications, and permissions.

Permission Checking Trait

To simplify permission checks in a policy, create a trait that adds a hasPermission() method to the class that uses it. The following example demonstrates how to create this trait:

php
<?php

namespace App\Traits;

use App\Schemas\PermissionSchema;

trait HasPermissions
{
    public function hasPermission(string $permission): bool
    {
        return $this->roles->some(function ($role) use ($permission) {
            return $role->permissions->contains(PermissionSchema::name, $permission);
        });
    }
}

This method streamlines the process of checking if a user has a specific permission.

The hasPermission method takes a string parameter, $permission, and returns a boolean value. It checks if the current user has the specified permission by examining all roles associated with the user.

If none of the roles possess the required $permission, the some function returns false, indicating that the user does not have the necessary permission.

By incorporating this trait into your User model, you can easily verify if a user has a specific permission.

Implementing Fine-Grained Permissions with Policies

To apply fine-grained permissions using the User model, create a policy class for each model you want to control access to. In this example, we'll create a PostPolicy to manage access to the Post model.

First, create the PostPolicy class:

php
namespace App\Policies;

use App\Models\User;
use App\Models\Post;
use Illuminate\Auth\Access\HandlesAuthorization;

class PostPolicy
{
    use HandlesAuthorization;

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

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

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

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

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

Note that we utilize the hasPermission trait added to the User model.

Next, register the policy in the AuthServiceProvider. Here's an example of registering the PostPolicy:

php
namespace App\Providers;

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

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

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

In this example, we register the PostPolicy for the Post model. This instructs Laravel to use the PostPolicy for controlling access to Post model instances.

Now, you can use this policy in your controllers to authorize actions. Here's an example of using the PostPolicy in a controller:

php
namespace App\Http\Controllers;

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

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

        return view('posts.edit', compact('post'));
    }

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

        // Update the post...
    }
}

In this example, we use the authorize() method to verify if the authenticated user has permission to update the given post, employing the update method of the PostPolicy. If the user is authorized, we allow them to edit or update the post. If not, we return an HTTP 403 Forbidden error.

By using policies to control access to your models, you ensure that only authorized users can perform sensitive actions in your application.