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:
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
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 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:
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:
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:
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
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.