Given Ncube
Posted on August 22, 2022
Laravel's authorization module gives you the skeleton to implement robust authorization logic in your app, with two choices: Gates and Policies.
Spatie Laravel-Permission offers a way to implement roles and permissions in your app.
Both are fully documented with examples, but when you try to apply it, everything just gets messy.
Here's how I implement authorization in my Laravel projects.
Background and preconditions
For this technique/pattern or whatever you want to call it to work. I follow a set of conventions to make it easier.
1. Models
I don't use modules/services/repositories or any other shenanigans coming up every day. All my models are in the default app/models
directory, and they all extend Eloquent's Model
.
2. Routes
For routing whenever possible I use resource routes. For example, say I'm building an e-commerce store.
Route::resource('products', ProductController::class);
3. Controllers
My controllers are straight defaults from Laravel and all live in the app/controllers
directory. I might namespace them depending on what they are for. And again, I use resourceful controllers.
Just stick to the defaults.
The logic behind how I authorize goes like this:
For every app, there's are going to be models, CRUD and users. Each CRUD action performed on a model has to be authorized by a single permission. For example, to create a product, a user should have permission to create products
.
Laravel offers a nice way to do just that, with polices.
Policies authorize the whole CRUD operations and pairs nicely with resourceful controllers, as you'll see by the end of this article.
Each method should return a boolean to see if a user can perform a certain CRUD action, We then just check if the user has permission to that and return that boolean. Easy-peasy.
Now, without enough theory, let's see how this is done.
Requirements
To implement this, you are going to need the following
A Laravel app, best if it's a complete app that just needs authorization
spatie/laravel-permissions
To handle roles and permissions
Setup
Let's install the required packages. In your terminal, install spatie/laravel-permissions
with composer.
composer require spatie/laravel-permissions
Follow the documentation to set up the package.
Now we are ready to do the authorization. But first we need to…
Open the Gates for super admin
For every app, there has to be a super admin
or root user with all the rights and can do anything. This is the first thing I would implement. To do this, we need to use Gate::before()
method to allow a user to do anything as long as they have the super admin
role.
In your AuthServiceProvider
add this to the boot method after registering polices.
Gate::before(function ($user, $ability) {
return $user->hasRole('super admin') ? true : null;
});
This will make sure that every time $user->can()
is called, it returns true as long as the user has super admin
role.
Generate roles and permissions
Now that's done, let's go ahead and create the super admin role. But first we need more preparation. See, normally you'd add authorization after your application is finished, and you know everything works as it's supposed to. At least, that's how I'd do it.
To make sure we can just plug and play this into production, let's create a seeder.
php artisan make:migration RolesAndPermissions
In the seeder I will use the following code
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use ReflectionClass;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use Symfony\Component\Finder\SplFileInfo;
class RolesPermissions extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$roles = collect([
'super admin',
]);
$models = collect(File::allFiles(app_path()))
->map(function (SplFileInfo $info) {
$path = $info->getRelativePathname();
$class = sprintf('\%s%s',
app()->getNamespace(),
Str::replace('/', '\\', Str::beforeLast($path, '.')));
return $class;
})
->filter(function (string $class) {
try {
$reflection = new ReflectionClass($class);
} catch (\ReflectionException $throwable) {
return false;
}
return $reflection->isSubclassOf(Model::class) &&
!$reflection >isAbstract();
})
->map(function ($model) {
return Str::lower(Str::plural(Str::afterLast($model, '\\')));
})
->map(function ($model) {
return [
"create {$model}",
"update {$model}",
"delete {$model}",
"restore {$model}",
"view {$model}",
"force delete {$model}",
];
});
$roles->each(function ($role) use ($models) {
$role = Role::create(['name' => $role]);
});
}
}
Okay, let me explain…
To authorize this, we just require permissions for each CRUD action per model. For example, there is a permission to create users
or update users
which we can use as $user->can('update users')
Notice there is only one role super admin
defined. All other roles will be created and assigned permissions via a GUI by the super admin.
In the first part of the code, I get all the classes in the app
directory as their fully qualified class names, e.g., \App\Models\User
.
Next, we use the Reflection API
to make sure the class is insatiable, and it extends Eloquent's base Model
.
Next, I map that and return just a plural lower case model name like users
.
Map that again to return an array of permission for each model like create users
, view users
, update users
, or delete users
for example.
Then we just go on to create the default roles which is just super admin
, but you're welcome to add more if you want like editor
, seo consultant
, intern
or something.
Let's go ahead, seed the database
php artisan db:seed RolesAndPermissions
Now let's create the policies for each of our models we have.
Defining the policies
Let's take for example we are building an e-commerce store, we're going to generate the policy for the Product model
php artisan make:policy ProductPolicy --model=Product
This will generate a boilerplate policy in your app/policies
directory. This is how I would define my policy.
namespace App\Policies;
use App\Models\Product;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class ProductPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $user
*/
public function viewAny(User $user): bool
{
return $user->can('view all products');
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $user
* @param \App\Models\Product $product
*/
public function view(User $user, Product $product): bool
{
return $user->can('view products');
}
/**
* Determine whether the user can create models.
*
* @param \App\Models\User $user
*/
public function create(User $user): bool
{
return $user->can('create products');
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $user
* @param \App\Models\Product $product
*/
public function update(User $user, Product $product): bool
{
return $user->can('update products');
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\Product $product
*/
public function delete(User $user, Product $product): bool
{
return $user->can('delete products');
}
/**
* Determine whether the user can restore the model.
*
* @param \App\Models\User $user
* @param \App\Models\Product $product
*/
public function restore(User $user, Product $product): bool
{
return $user->can('restore products');
}
/**
* Determine whether the user can permanently delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\Product $product
*/
public function forceDelete(User $user, Product $product): bool
{
return $user->can('force delete products');
}
}
Take for example the update()
method, I just check if the user can update products
. This will return true if the user has permission to update products
otherwise it returns false.
Authorizing controllers
For each CRUD authorization method, we just check if the user has permission to perform that action. Since we are using resourceful controllers. Our product controller can look like this.
/**
* Create the controller instance.
*
* @return void
*/
public function __construct()
{
$this->authorizeResource(Product::class, 'product');
}
Calling the authorizeResource()
method will authorize all the resourceful routes using that Product Policy we just created, simple and powerful.
Just like that you have a robust authorization logic with strong role based access control. At this point I would typically give a set of permissions to a role and give that role to a user.
And with that the user gets the permission. I would use a GUI so that I can change permissions any time.
Authorization in just one artisan command
So as I was writing this article, I realized that I've been re implementing this over and over again. I decided to create a package that helps you generate all that in just one command.
How to use this package
Getting started is simple. You just need to install the package via composer
composer require flixtechs-labs/laravel-authorizer
Next run the following command to setup the package
php artisan authorizer:setup
After that generate your supercharged policy with
php artisan authorizer:policies:generate Product --model=Product
Go to the documentation to see how this package can speed up your development time.
Final thoughts
Handling authorization in Laravel can be quite overwhelming especially when you're starting out. I hope this article gives you a great starting to start using authorization in your Laravel apps.
Posted on August 22, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 20, 2024