Language:

Search

Implementing Roles and Permissions in Laravel

  • Share this:
Implementing Roles and Permissions in Laravel
Updated for Laravel 6.0

Introduction

In this article, we'll learn how to implement & setup roles and permissions in Laravel. There are alot of packages which handle this stuff for you by just pulling them in via composer, setting them up and you're good to go. But what I feel is these packages often comes with too much juice which I don't really need.

What if you only need to setup a real simple roles and permissions setup for your project. with these packages you'll have less options to customize as per your needs. Some people often refer this as re-inventing the wheel, but as a matter of fact. Its not.

We'll try to cover almost every little thing for setting up roles and permissions. So Let's straight dive in and see what are we covering in this article.

- Database Structure & Migrations

- Relationships between Models

- Custom Directives for views

- Assigning roles and permissions to users

- Setting up Roles & Permissions middleware

There's so much to cover in this article, Let's start by a fresh Laravel install.

Setting Up

Open up your terminal and create a new Laravel project by typing in the following command

$ laravel new roles-permissions

DYI Config

Setup and config your database with your project and head over to the next step.

Making up our Authentication scaffolding

Lets start by making our authentication scaffolding:

$ php artisan make:auth

Models & Migrations

Start of by creating the required models and migrations for this project.

In the terminal, type in:

$ php artisan make:model Permission -m
$ php artisan make:model Role -m

As you may know, -m flag will create a migration file for the model. Now you'll have two new migration files waiting for you to add new fields. Run the following command and migrate your database.

$ php artisan migrate

Edit the Permission migration file

Now, for the permissions table, we only need two fields, an id, a slug and a name. Let's add these in our migration file and our schema should look like

Schema::create('permissions', function (Blueprint $table) {
    $table->increments('id');
    $table->string('slug'); //edit-posts
    $table->string('name'); // edit posts
    $table->timestamps();
});

Same for the Role migration file

We'll have the same set of fields for the roles table as well.

    Schema::create('roles', function (Blueprint $table) {
        $table->increments('id');
        $table->string('slug'); //web-developer
        $table->string('name'); //Web Developer
        $table->timestamps();
    });

Adding pivot tables

As our understanding, we need to have the pivot tables for the following set of rules.

- User can have Permission

For this first pivot table, we'll create a new migration file for the table users_permissions

$ php artisan make:migration create_users_permissions_table --create=users_permissions

For this pivot table between users and permissions, our schema should look like

Schema::create('users_permissions', function (Blueprint $table) {
    $table->integer('user_id')->unsigned();
    $table->integer('permission_id')->unsigned();

 //FOREIGN KEY CONSTRAINTS
    $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
    $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade');
 
//SETTING THE PRIMARY KEYS
    $table->primary(['user_id','permission_id']);
});

Here, we've set the foreign key constraints in order to delete the corresponding records if a user or permission is deleted.  Primary keys for this table are user_id and permission_id.

- User can have Role

Now let's create a pivot table for users_roles.

$ php artisan make:migration create_users_roles_table --create=users_roles

The fields inside this table will pretty much the same as in users_permissions table. Our schema for this table will look like:

Schema::create('users_roles', function (Blueprint $table) {
   $table->integer('user_id')->unsigned();
   $table->integer('role_id')->unsigned();

 //FOREIGN KEY CONSTRAINTS
   $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
   $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');

 //SETTING THE PRIMARY KEYS
   $table->primary(['user_id','role_id']);
});

- Under a particular Role, User may have specific Permission

For example, a user may have the permission for post a topic, and an admin may have the permission to edit or delete a topic. In this case, let's setup a new table for roles_permissions to handle this complexity.

$ php artisan make:migration create_roles_permissions_table --create=roles_permissions

The Schema will be like:

Schema::create('roles_permissions', function (Blueprint $table) {
 $table->integer('role_id')->unsigned();
 $table->integer('permission_id')->unsigned();

 //FOREIGN KEY CONSTRAINTS
 $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
 $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade');

 //SETTING THE PRIMARY KEYS
 $table->primary(['role_id','permission_id']);
});

If this doesn't start to make any sense to you just yet, well wait for us to build up the relationships between our tables. Things will get clear I promise.

Let's migrate every thing now.

$ php artisan migrate

Setting up the relationships

We'll start by creating the relationships between roles and permissions table. In our Role.php

//Role.php
public function permissions() {
   return $this->belongsToMany(Permission::class,'roles_permissions');
}

Same goes for Permission.php in reverse.

//Permission.php
public function roles() {
   return $this->belongsToMany(Role::class,'roles_permissions');
}

Now, in terms of User. A user has many roles. A user may have many permissions. But potentially a Role has many users and permission has many users. So we need to setup many to many relations in our User model. But what we'll do is we'll create a new Trait for this logic so that if we add another model in our project in the future, we can just pull it in.

 

Creating a Trait

Inside of our app directory, let's create a new directory and name it as Permissions and create a new file namely HasPermissionsTrait.php

<?php
namespace App\Permissions;

use App\Permission;
use App\Role;

trait HasPermissionsTrait {

   public function roles() {
      return $this->belongsToMany(Role::class,'users_roles');

   }


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

   }
}

A nice little trait has been setup to handle user relations. Back in our User model, just import this trait and we're good to go.

User hasRole

Now, we'll create a new function inside our HasPermissionsTrait.php and name it as hasRole as following

public function hasRole( ... $roles ) {
   foreach ($roles as $role) {
      if ($this->roles->contains('slug', $role)) {
         return true;
      }
   }
   return false;
}

Here, we're iterating through the roles and checking by the slug field, if that specific role exists. You can check or debug this by using:

$user = $request->user(); //getting the current logged in user
dd($user->hasRole('admin','editor')); // and so on

Checking Permissions

Now we need to build the ability to give a user some permissions. But wait, here we have a couple of conditions to tackle with:

- User may have individual Permission for some actions.

Back inside of HasPermissionsTrait.php, we will add some new methods for user permissions:

protected function hasPermissionTo($permission) {
   return $this->hasPermission($permission);
}

protected function hasPermission($permission) {
   return (bool) $this->permissions->where('slug', $permission->slug)->count();
}

We'll be utilizing the Laravel's "can" directive to check if the User have Permission. and instead of using $user->hasPermissionTo(), we'll use $user->can() To do so, we need to create a new PermissionsServiceProvider for authorization

$ php artisan make:provider PermissionsServiceProvider

Register your service provider and head over to the boot method to provide us a Gateway to use can() method.

//PermissionsServiceProvider.php 
public function boot()
 {
     Permission::get()->map(function($permission){
Gate::define($permission->slug, function($user) use ($permission){
   return $user->hasPermissionTo($permission);
});
     });
 }

Here, what we're doing is, mapping through all permissions, defining that permission slug (in our case) and finally checking if the user has permission. You can learn more about Laravel's Gate facade at Laravel's documentation. You can test it out as:

dd($user->can('permission-slug'));

- User may have Permission through a Role

To achieve this condition, in our HasPermissionsTrait.

//HasPermissionsTrait.php
public function hasPermissionThroughRole($permission) {
   foreach ($permission->roles as $role){
      if($this->roles->contains($role)) {
         return true;
      }
   }
   return false;
}

Here, we're iterating through each permission associated with a role, remember we've a many to many relationship setup between roles and permissions table.

Now the hasPermissionTo() method will check between these two conditions.

//HasPermissionsTrait.php
public function hasPermissionTo($permission) {
   return $this->hasPermissionThroughRole($permission) || $this->hasPermission($permission);
}

Giving Permissions

Now let's say, we want to give a set of permissions to a logged in user, here's how we can achieve this

//HasPermissionsTrait.php
public function givePermissionsTo(... $permissions) {
   $permissions = $this->getAllPermissions($permissions);
   dd($permissions);
   if($permissions === null) {
      return $this;
   }
   $this->permissions()->saveMany($permissions);
   return $this;
}

Deleting Permissions

For deleting or removing permissions from the user scope, we can use the detach method.

//HasPermissionsTrait.php
public function deletePermissions( ... $permissions ) {
   $permissions = $this->getAllPermissions($permissions);
   $this->permissions()->detach($permissions);
   return $this;
}

This is pretty straight forward, same things will be applied for roles as well. So give it a try and leave us a comment on how you did it.

Add the Seeders

In this part, we'll create seeders for permissions, roles & users and will verify our set of methods accordingly. So let's start by creating seeders.

$ php artisan make:seeder PermissionTableSeeder
$ php artisan make:seeder RoleTableSeeder
$ php artisan make:seeder UserTableSeeder

Let's start by editing adding a couple of records in UserTableSeeder.php

//UserTableSeeder.php
$dev_role = Role::where('slug','developer')->first();
$manager_role = Role::where('slug', 'manager')->first();
$dev_perm = Permission::where('slug','create-tasks')->first();
$manager_perm = Permission::where('slug','edit-users')->first();

$developer = new User();
$developer->name = 'Usama Muneer';
$developer->email = '[email protected]';
$developer->password = bcrypt('secret');
$developer->save();
$developer->roles()->attach($dev_role);
$developer->permissions()->attach($dev_perm);


$manager = new User();
$manager->name = 'Asad Butt';
$manager->email = '[email protected]';
$manager->password = bcrypt('secret');
$manager->save();
$manager->roles()->attach($manager_role);
$manager->permissions()->attach($manager_perm);

Here, we're assigning the roles and permissions to our newly created users.

Head over to the next file, i.e RoleTableSeeder.php

$dev_permission = Permission::where('slug','create-tasks')->first();
$manager_permission = Permission::where('slug', 'edit-users')->first();

//RoleTableSeeder.php
$dev_role = new Role();
$dev_role->slug = 'developer';
$dev_role->name = 'Front-end Developer';
$dev_role->save();
$dev_role->permissions()->attach($dev_permission);

$manager_role = new Role();
$manager_role->slug = 'manager';
$manager_role->name = 'Assistant Manager';
$manager_role->save();
$manager_role->permissions()->attach($manager_permission);

Here, we're attaching the permissions to the roles, remember we've ManyToMany relationship between roles and permissions.

Last one is PermissionTableSeeder.php

//PermissionTableSeeder.php
$dev_role = Role::where('slug','developer')->first();
$manager_role = Role::where('slug', 'manager')->first();

$createTasks = new Permission();
$createTasks->slug = 'create-tasks';
$createTasks->name = 'Create Tasks';
$createTasks->save();
$createTasks->roles()->attach($dev_role);

$editUsers = new Permission();
$editUsers->slug = 'edit-users';
$editUsers->name = 'Edit Users';
$editUsers->save();
$editUsers->roles()->attach($manager_role);

Now that, all of our seeders are ready to go, lets run our migration with the --seed flag.

$ php artisan migrate:refresh --seed

To test this out in your routes files, we can die and dump on:

$user = $request->user();
dd($user->hasRole('developer')); //will return true, if user has role
dd($user->givePermissionsTo('create-tasks')); // will return permission, if not null
dd($user->can('create-tasks')); // will return true, if user has permission

Setting up the Custom Blade Directives

We can utilize the can directive inside of our blade files, which is Laravel's one of pre-defined method, which can be used for authorizing permissions for users,

Here we'll create a role directive to be available in our blade files for checking the roles for users.

Inside of the boot method in PermissionsServiceProvider.php

//PermissionsServiceProvider.php
  Blade::directive('role', function ($role){
   return "<?php if(auth()->check() && auth()->user()->hasRole({$role})) :";
  });
  Blade::directive('endrole', function ($role){
   return "<?php endif; ?>";
  });

Inside of our view files, we can use it like:

@role('admin')

<h1>Hello from the admin</h1>

@endrole

Setup the Middleware

In order to protect our routes, we can setup the middleware to do so.

$ php artisan make:middleware RoleMiddleware

Add the middleware into your kernel & setup the handle method as follows

public function handle($request, Closure $next, $role, $permission = null)
 {
   if(!$request->user()->hasRole($role)) {
     abort(404);
  }
  if($permission !== null && !$request->user()->can($permission)) {
      abort(404);
  }
     return $next($request);
 }

Right in our routes, we can do something like this

Route::group(['middleware' => 'role:admin'], function() {
   Route::get('/admin', function() {
      return 'Welcome Admin';
   });
});

Simple enough?


 

Final words

Github repo has been updated for Laravel 6.0.

Here's the working Github rep0 for this Laravel install. clone or download the files if you get stuck anywhere in between. Moreover try out these little tricks yourself and setup roles and permissions for your next Laravel application. Good luck. If you have any queries regarding this article, you may leave a comment below or you can also ask us on Twitter.

Usama Muneer

Usama Muneer

A web enthusiastic, self-motivated & detail-oriented professional Full-Stack Web Developer from Karachi, Pakistan with experience in developing applications using JavaScript, WordPress & Laravel specifically. Loves to write on different web technologies with an equally useful skill to make some sense out of it.