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:

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

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

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

class CreatePermissionsTable extends Migration
{
    public function up()
    {
        Schema::create('permissions', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }

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

class CreateRolePermissionsTable extends Migration
{
    public function up()
    {
        Schema::create('role_permissions', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('role_id');
            $table->unsignedBigInteger('permission_id');
            $table->timestamps();

            $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
            $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('role_permissions');
    }
}

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 three models: Role, Permission, and User. These models interact with the database tables that store information about roles, permissions, and user/role relationships.

Here's an example of the Role model:

php
namespace App\Models;

use App\Schemas\RolePermissionSchema;
use App\Schemas\UserRoleSchema;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Role extends Model
{
    use HasFactory;

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

    public function permissions(): BelongsToMany
    {
        return $this->belongsToMany(Permission::class, RolePermissionSchema::table)->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 an example of the Permission model:

php
namespace App\Models;

use App\Schemas\RolePermissionSchema;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Permission extends Model
{
    use HasFactory;

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

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

Finally, here's an example of the User model:

php
namespace App\Models;

use App\Schemas\UserRoleSchema;
use App\Traits\HasPermissions;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, HasPermissions;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, UserRoleSchema::table)->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.