Supercharge Your Laravel App with Spatie Roles and Permissions .

Code Axion The Security Breach
11 min readApr 9, 2023

In This Article I have Described How You Can Handle Authorization With Spatie Permission Package And Give Permissions To Different Roles And Restrict Certain Areas And Features With Laravel Authorization Methods

And Believe me its really easy peasy

Lets do the Installation and stuffs

Install the Spatie Permission Package with these commands :

//Install the package
composer require spatie/laravel-permission

//Register the provider in the config/app.php
'providers' => [
// ...
Spatie\Permission\PermissionServiceProvider::class,
];

//This will generate the necessary migrations for the package and the config file
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"


php artisan optimize:clear
# or
php artisan config:clear


php artisan migrate // will migrate the neccessary tables required for this package

User Model Should Have HasRole Trait:


use Illuminate\Foundation\Auth\User as Authenticatable;
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
use HasRoles;

// ...
}

Now we are gonna create some permissions in the permissions table .

Imp: Make Sure To First Add The “ MODULE Name “ With The DOT NOTATION And The OPERATION Name Like This :

Module Name: categories
Operation Name: create

Final: categories.create

This is gonna help to display the “add permissions to roles ” settings category wise like this :

Now lets create the RoleController with the “ php artisan make:controller RoleController “ command. We don’t need to create the model for roles or permissions as we already have it in our package and we can use them with “use Spatie\Permission\Models\Role ”. Lets create some methods for our Role controller .

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use Spatie\Permission\Models\Role; <--- ROLE MODEL
use Spatie\Permission\Models\Permission; <---- PERMISSION MODEL

class RoleController extends Controller
{
public function allRoles()
{

}

public function create()
{

}

public function store(Request $request)
{

}

public function edit($id)
{

}

public function update(Request $request, $id)
{

}

public function delete($id)
{

}

}

Lets take a look at create roles page and how to display it like this :

Create method :

public function create()
{
\DB::statement("SET SQL_MODE=''");;
$role_permission = Permission::select('name','id')->groupBy('name')->get();

$custom_permission = array();

foreach($role_permission as $per){

$key = substr($per->name, 0, strpos($per->name, "."));

if(str_starts_with($per->name, $key)){

$custom_permission[$key][] = $per;
}

}

return view('admin.roles.create')->with('permissions',$custom_permission);
}

Take a look at how we are gonna create this array:

  1. $role_permission = Permission::select('name','id')->groupBy('name')->get();:This selects only the name and id columns from the permissions table, and groups the results by the name column and finally, it fetches all the results :

Example :-

2. $custom_permission = array();: This initializes an empty array called $custom_permission which will be used to store custom permissions later in the code.

   foreach($role_permission as $per){

$key = substr($per->name, 0, strpos($per->name, "."));

if(str_starts_with($per->name, $key)){
$custom_permission[$key][] = $per;
}

}

3. $key = substr($per->name, 0, strpos($per->name, "."));: This line extracts the substring from the beginning of the name field of the current $per item, up to the first occurrence of a period (".") character. This is done using the substr() and strpos() functions in PHP, and the resulting substring is stored as the $key .

4. if(str_starts_with($per->name, $key)){ ... }: It then checks if the ‘name’ column string starts with the extracted $key using the str_starts_with() function. eg:- like this :

if categories.create starts with categories which is true 
(categories == categories.create) ==> true
Some defination of str_starts_with.

str_starts_with($string, $substring)
Parameters:

$string: This parameter refers to the string whose starting string needs to be checked.
$substring: This parameter refers to the string that needs to be checked.

Return Type: If the string begins with the substring then str_starts_with() will return TRUE otherwise it will return FALSE.

5. If it does, it proceeds to add the record to the $custom_permission array using $key as the array key which is the categories and the $per as the object.

So it will look like this :

This is used as a grouping mechanism to group permissions with similar prefixes together in the $custom_permission array.

We can access the $custom_permission array and can use it to display custom permissions grouped by their prefixes.

Now lets display this in view

create.blade.php:

<div class="ml-4 mt-16 w-9/12">
<form action="{{route('roles.store')}}" method="POST">
@csrf

<h1 class="text-3xl mt-4 mb-8"> Create Role </h1>

<div class="mb-6">
<label for="text" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Role Name</label>
<input type="text" value="{{old('name')}}" name="name" id="email" class="bg-gray-50 w-80 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 " placeholder="User, Editor, Author ... " >

@foreach ($errors->get('name') as $error)
<p class="text-red-600">{{$error}}</p>
@endforeach
</div>

<table class="permissionTable border rounded-md bg-white overflow-hidden shadow-lg my-4 p-4">
<th class="px-4 py-4">
{{__('Section')}}
</th>

<th class="px-4 py-4">
<label>
<input class="grand_selectall" type="checkbox">
{{__('Select All') }}
</label>
</th>

<th class="px-4 py-4">
{{__("Available permissions")}}
</th>



<tbody>
@foreach($permissions as $key => $group)
<tr class="py-8">
<td class="p-6">
<b>{{ ucfirst($key) }}</b>
</td>
<td class="p-6" width="30%">
<label>
<input class="selectall" type="checkbox">
{{__('Select All') }}
</label>
</td>
<td class="p-6">

@forelse($group as $permission)

<label style="width: 30%" class="">
<input name="permissions[]" class="permissioncheckbox" class="rounded-md border" type="checkbox" value="{{ $permission->id }}">
{{$permission->name}} &nbsp;&nbsp;
</label>

@empty
{{ __("No permission in this group !") }}
@endforelse

</td>

</tr>
@endforeach
</tbody>
</table>


<button type="submit" class="text-white bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 shadow-lg shadow-blue-500/50 dark:shadow-lg dark:shadow-blue-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 ">
Create Role
</button>

</form>
</div>

Now in order to get the select all feature working paste this code inside script tag in create.blade.php:



$(".permissionTable").on('click', '.selectall', function () {

if ($(this).is(':checked')) {
$(this).closest('tr').find('[type=checkbox]').prop('checked', true);

} else {
$(this).closest('tr').find('[type=checkbox]').prop('checked', false);

}

calcu_allchkbox();

});

$(".permissionTable").on('click', '.grand_selectall', function () {
if ($(this).is(':checked')) {
$('.selectall').prop('checked', true);
$('.permissioncheckbox').prop('checked', true);
} else {
$('.selectall').prop('checked', false);
$('.permissioncheckbox').prop('checked', false);
}
});

$(function () {

calcu_allchkbox();
selectall();

});

function selectall(){

$('.selectall').each(function (i) {

var allchecked = new Array();

$(this).closest('tr').find('.permissioncheckbox').each(function (index) {
if ($(this).is(":checked")) {
allchecked.push(1);
} else {
allchecked.push(0);
}
});

if ($.inArray(0, allchecked) != -1) {
$(this).prop('checked', false);
} else {
$(this).prop('checked', true);
}

});
}

function calcu_allchkbox(){

var allchecked = new Array();

$('.selectall').each(function (i) {


$(this).closest('tr').find('.permissioncheckbox').each(function (index) {
if ($(this).is(":checked")) {
allchecked.push(1);
} else {
allchecked.push(0);
}
});


});

if ($.inArray(0, allchecked) != -1) {
$('.grand_selectall').prop('checked', false);
} else {
$('.grand_selectall').prop('checked', true);
}

}



$('.permissionTable').on('click', '.permissioncheckbox', function () {

var allchecked = new Array;

$(this).closest('tr').find('.permissioncheckbox').each(function (index) {
if ($(this).is(":checked")) {
allchecked.push(1);
} else {
allchecked.push(0);
}
});

if ($.inArray(0, allchecked) != -1) {
$(this).closest('tr').find('.selectall').prop('checked', false);
} else {
$(this).closest('tr').find('.selectall').prop('checked', true);

}

calcu_allchkbox();

});

Store method :

Now we will create the role method and we will attach the permissions to the roles using givePermissionTo() method .

public function store(Request $request)
{
$request->validate([

'name' => 'required',
]);

$role = Role::create([
'name' => $request->name,
]);

if($request->permissions){

foreach ($request->permissions as $key => $value) {
$role->givePermissionTo($value);
}
}

return redirect()->route('roles.all');
}

Edit Method:

 public function edit($id)
{

$role = Role::with('permissions')->find($id);

\DB::statement("SET SQL_MODE=''");
$role_permission = Permission::select('name','id')->groupBy('name')->get();


$custom_permission = array();

foreach($role_permission as $per){

$key = substr($per->name, 0, strpos($per->name, "."));

if(str_starts_with($per->name, $key)){
$custom_permission[$key][] = $per;
}

}

return view('admin.roles.edit',compact('role'))->with('permissions',$custom_permission);
}

edit.blade.php:

   <div class="ml-4 mt-16 w-9/12">
<form action="{{route('roles.store')}}" method="POST">
@csrf

<h1 class="text-3xl mt-4 mb-8"> Update Role </h1>

<div class="mb-6">
<label for="text" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Role Name</label>
<input type="text" value="{{old('name',$role->name ?? '')}}" name="name" id="email" class="bg-gray-50 w-80 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 " placeholder="Wedding, Kitty kat, parties, lol deaths haha" >

@foreach ($errors->get('name') as $error)
<p class="text-red-600">{{$error}}</p>
@endforeach
</div>

<table class="permissionTable border rounded-md bg-white overflow-hidden shadow-lg my-4 p-4">
<th class="px-4 py-4">
{{__('Section')}}
</th>

<th class="px-4 py-4">
<label>
<input class="grand_selectall" type="checkbox">
{{__('Select All') }}
</label>
</th>

<th class="px-4 py-4">
{{__("Available permissions")}}
</th>



<tbody>
@foreach($permissions as $key => $group)
<tr class="py-8">
<td class="p-6">
<b>{{ ucfirst($key) }}</b>
</td>
<td class="p-6" width="30%">
<label>
<input class="selectall" type="checkbox">
{{__('Select All') }}
</label>
</td>
<td class="p-6">

@forelse($group as $permission)

<label style="width: 30%" class="">
<input {{ $role->permissions->contains('id',$permission->id) ? "checked" : "" }} name="permissions[]" class="permissioncheckbox" class="rounded-md border" type="checkbox" value="{{ $permission->id }}">
{{$permission->name}} &nbsp;&nbsp;
</label>

@empty
{{ __("No permission in this group !") }}
@endforelse

</td>

</tr>
@endforeach
</tbody>
</table>


<button type="submit" class="text-white bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 hover:bg-gradient-to-br focus:ring-4 focus:outline-none focus:ring-blue-300 dark:focus:ring-blue-800 shadow-lg shadow-blue-500/50 dark:shadow-lg dark:shadow-blue-800/80 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 ">
Update Role
</button>
</form>
</div>

Update method :

Multiple permissions can be synced to a role using syncPermissions method:

public function update(Request $request, $id)
{

$role = Role::where('id',$id)->first();

$request->validate([
'name' => 'required'
]);

$role->update([
"name" => $request->name
]);

$role->syncPermissions($request->permissions);


return redirect()->route('admin.roles.all')->with('success','Roles Updated Successfully');
}

Delete method :

public function delete($id)
{
$role = Role::where('id',$id)->first();

if(isset($role)){

$role->permissions()->detach();
$role->delete();

return redirect()->route('roles.all')->with('success','Roles Deleted Successfully');

}
}

index.blade.php:

<div class="flex flex-col mt-6">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div class="overflow-hidden border border-gray-200 dark:border-gray-700 md:rounded-lg">
<table id="eventstable" class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr>
<th scope="col" class="py-3.5 px-4 text-sm font-normal text-left rtl:text-right text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-x-3">
<input type="checkbox" class="text-blue-500 border-gray-300 rounded dark:bg-gray-900 dark:ring-offset-gray-900 dark:border-gray-700">
<span>Id</span>
</div>
</th>

<th scope="col" class="px-12 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-500 dark:text-gray-400">
<button class="flex items-center gap-x-2">
<span>Role Name</span>
</button>
</th>

<th scope="col" class="px-4 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-500 dark:text-gray-400">
<button class="flex items-center gap-x-2">
<span>Permissions</span>
</button>
</th>


<th scope="col" class="relative py-3.5 px-4">
<span class="sr-only">Edit</span>
</th>

</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200 dark:divide-gray-700 dark:bg-gray-900">

@foreach ($roles as $role)

<tr>
<td class="px-4 py-4 text-sm font-medium text-gray-700 whitespace-nowrap">
<div class="inline-flex items-center gap-x-3">

<div class="flex items-center gap-x-2">
<div>
<h2 class="font-medium text-gray-800 dark:text-white ">{{$loop->iteration}}</h2>
</div>
</div>
</div>
</td>
<td class="px-4 py-4 text-sm text-gray-500 dark:text-gray-300 whitespace-nowrap">{{$role->name}}</td>
<td class="px-4 py-4 text-sm text-gray-500 dark:text-gray-300 whitespace-nowrap">
<div class="flex items-center flex-wrap gap-2">
@foreach ($role->permissions as $permission)
<div class="rounded-full bg-indigo-400 px-2 py-0.5 text-indigo-200 font-semibold">{{$permission->name}}</div>
@endforeach
</div>
</td>


<td class="px-4 py-4 text-sm whitespace-nowrap">
<div class="flex items-center gap-x-6">

<a href="{{route('roles.edit',$role->id)}}" class="block text-gray-500 transition-colors duration-200 dark:hover:text-yellow-500 dark:text-gray-300 hover:text-yellow-500 focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</svg>
</a>

<form method="POST" action="{{route('roles.delete',$role->id)}}">
@csrf
@method('DELETE')

<button type="submit" class="text-gray-500 transition-colors duration-200 dark:hover:text-red-500 dark:text-gray-300 hover:text-red-500 focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg>
</button>
</form>
</div>
</td>
</tr>

@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>

Now in order to make this work we can attach roles to the users using assignRole method :

$user->assignRole('Seller');

So lets say if you create a role and attach the permissions of creating the categories and viewing the categories .

Now in Spatie permissions package all the permissions will have gates assigned to them so you can do this to prevent users to access certain areas

Lets say if you want that only the role with the permissions of the categories create can only view the create category page/sections so you can do it like this :

In Blade

@can('categories.create')
<div class="ml-auto px-3 py-1 text-blue-600 bg-blue-100 rounded-md">
<a href="{{route('categories.create')}}">Category Create</a>
</div>
@endcan

//=============== OR FOR CHECKING MULTIPLE ABILITIES/PERMISSIONS ========== //

@canany(['categories.create', 'categories.delete'])
<div class="actions">
@can('categories.edit')
<button>Edit</button>
@endcan
@can('categories.delete')
<button>Delete</button>
@endcan
</div>
@endcanany

In Controller:

public function createCategory()
{
$this->authorize('categories.create');

================= OR =================

if (Gate::allows('categories.create')) {
// User is authorized, perform the action
} else {
abort(403); // Or redirect, or return an error response, depending on your needs
}

return view('admin.categories.create');
}

When Using the Middleware:

Route::group(['middleware' => ['can:categories.create']], function () {
//
});

While storing/updating the data with Form Request Class:

Requests/StoreCategories.php

 public function authorize(): bool
{
switch ($this->method()) {
case 'POST':
return $this->user()->can('categories.create');

break;

case 'PUT':
return $this->user()->can('categories.edit');

break;
}

return true;
}

Defining Super Admin:

If you want a “Super Admin” role to respond true to all permissions, without needing to assign all those permissions to a role, you can use Laravel's Gate::before() method. For example:

use Illuminate\Support\Facades\Gate;

class AuthServiceProvider extends ServiceProvider
{
public function boot()
{
$this->registerPolicies();
// Implicitly grant "Super Admin" role all permissions
// This works in the app by using gate-related functions like auth()->user->can() and @can()
Gate::before(function ($user, $ability) {
return $user->hasRole('Super Admin') ? true : null;
});
}
}

NOTE: Gate::before rules need to return null rather than false, else it will interfere with normal policy operation

Best Practices from Spatie Permissions Package :

Roles are best to only assign to Users in order to “group” people by “sets of permissions”.

Permissions are best assigned to roles. The more granular/detailed your permission-names (such as separate permissions like “view document” and “edit document”), the easier it is to control access in your application.

Users should rarely be given “direct” permissions. Best if Users inherit permissions via the Roles that they’re assigned to.

When designed this way, all the sections of your application can check for specific permissions needed to access certain features or perform certain actions AND this way you can always use the native Laravel @can and can() directives everywhere in your app, which allows Laravel's Gate layer to do all the heavy lifting.

I hope you thoroughly enjoyed reading this article.

--

--

Code Axion The Security Breach

A Developer Who Enjoys Code Refactoring, Optimizing, and Improving The performance of Applications