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 artisan make:model Role -m
<?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');
}
};
php artisan make:model Permission -m
<?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');
}
};
php artisan make:model RolePermission -m
<?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');
}
};
php artisan make:model UserRole -m
<?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
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:
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
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
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
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
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
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
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:
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
:
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:
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.