Permissions in your Laravel API
Introduction
Welcome to part six. If you have not read any of the previous parts we do highly recommend that you go back and read those, starting here.
A link to the github repository used, will be provided at the end of this article.
Laravel Policies
Laravel provides a way to authorize actions of users against given resources. For example, even though we authenticate as user, and this user can perform action A
, it is not given that this user can also perform action B
and C
. And vice versa if a user can perform action B
and C
it is not given that the user can perform action A
.
Laravel helps you by offering its authorization features to provide an easy, organized way of managing these types of authorization checks.
Laravel provides us two ways of authorizing actions, and those two are gates
and policies
. These two can be thought of like routes and controllers. Since we will be using policies
we will not go into details about gates
. You can view gates
as a simple, closure-based approach to authorize.
Policies works like controllers, in that essence that they group logic around a particular model or resource.
You should use policies
whenever you wish to authorize an action for a particular model or resource.
Our application is based around user actions, so since we have an App\Models\User
model we can have a corresponding App\Policies\UserPolicy
to authorize user actions.
You can generate a policy by running the Artisan command in your terminal of choice, for making a policy, as this: php artisan make:policy UserPolicy
.
After the policy class has been generated, it needs to be registered. By registering policies we can inform Laravel which policy to use when authorizing actions against a given model type.
The App\Providers\AuthServiceProvider
, which is included with a fresh Laravel application, contains a policies property which maps your Eloquent models to their corresponding policies
Registering a policy will instruct Laravel which policy to utilize when authorizing actions against a given Eloquent model, and in our case it is done as following:
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use App\Models\User;
use App\Policies\UserPolicy;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
User::class => UserPolicy::class,
];
...
}
Laravel can automatically discover policies as long as the model and policy follow the standard Laravel naming conventions, instead of manually registering model policies.
Policies must specifically be in a Policies
directory either at the directory that contains your models, or above it.
So, for example, the models may be placed in the app/Models
directory while the policies may be placed in the app/Policies
directory. In such a situation Laravel will firstly check for policies in app/Models/Policies
then app/Policies
. On top of that, the policy name must match the model name and have a Policy suffix. In our case the policy is placed inside app/Policies
.
Authorizing actions using policies via the UserForm class
The User model that is included with a Laravel application includes two methods which are very helpful for authorizing actions. These two methods are the can
and cannot
.
Both the can
and cannot
methods receives the name of the action you wish to authorize and the relevant model.
If you have registered a policy for a given model, the can
method will automatically call that appropriate policy and return the boolean result, and if no policy is registered, the can
method will try and attempt a call to the closure-based Gate
that matches the given action name. In our case we will make our own policy for that.
What is spatie/laravel-permission and getting started with Spatie
Spatie
is a package that allows you to manage user permissions and roles in a database.
You can install the Spatie
package by entering the following inside your prefered terminal that points to your project folder. The package we are going to install is the package that can be used with Laravel v.6.0 >
.
composer require spatie/laravel-permission
This installation package will publish a config/permission.php
file, make sure to either remove or rename that file if you already have it before installation. This is per Spatie’s own advice.
Follow the installation process as described here.
After following the installation process as described by Spatie themselves, we now need to add the Spatie\Permission\Traits\HasRoles
trait to our User model.
...
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use ..., HasRoles;
...
}
This package makes it so, that users can be associated with permissions and roles. Every role is associated with multiple permissions. A Role
and a Permission
are regular Eloquent
models. They require a name
.
Seeding the role(s), permission(s) and assigning them
Under the path Database\Seeders
we have created a new file that will be our seeder, PermissionsDemoSeeder
:
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;
use App\Models\User;
class PermissionsDemoSeeder extends Seeder
{
/**
* Create the initial roles and permissions.
*
* @return void
*/
public function run()
{
app()[PermissionRegistrar::class]->forgetCachedPermissions();
$createPermissionOnUser = Permission::create(['name' => 'Create a new user']);
$updatePermissionOnUser = Permission::create(['name' => 'Update an existing user']);
$readPermissionOnUser = Permission::create(['name' => 'Get an existing user or users']);
$deletePermissionOnUser = Permission::create(['name' => 'Delete or remove a user or users']);
$allroundRole = Role::create(['name' => 'canCRUDonUser']);
$allroundRole->givePermissionTo([
$createPermissionOnUser,
$updatePermissionOnUser,
$readPermissionOnUser,
$deletePermissionOnUser
]);
$adminUser = User::factory()->create([
'name' => 'Admin',
'email' => 'admin@example.com',
'password' => bcrypt("asd")
]);
$adminUser->assignRole($allroundRole);
$adminUser->givePermissionTo([
$createPermissionOnUser,
$updatePermissionOnUser,
$readPermissionOnUser,
$deletePermissionOnUser
]);
$nonAdminUser = User::factory()->create([
'name' => 'Non-Admin',
'email' => 'nonAdmin@example.com',
'password' => bcrypt("asd")
]);
}
}
In this file we have also created two users, one being and admin which is assigned a role with permissions, and the other being an ordinary user without roles and permissions. These will be used for test purposes, so do not pay them much attention, but rather focus on the idea behind them.
In our case, when seeding this file we also empty our entire database tables, therefore when seeding we make sure we have these two users to work with. The Artisan command we run is the below:
php artisan migrate:fresh --seed --seeder=PermissionsDemoSeeder
Next step towards connecting/binding the spatie permissions and Laravel policies
As mentioned earlier, we use Policies
whenever we wish to authorize an action for a particular model or resource, in our case a model, User
.
For a single authorization “rule” we could define either a Gate
or a Policy
, the choice between one or the other only defines where the rule will be stored in our code. For Laravel it will be the same thing in the end because in the AuthServiceProvider.php
we will “register” the policies:
By having our UserPolicy
class, and registered that correct, we have then instructed Laravel to utilize that policy when authorizing an action against our User
model.
Since Laravel provides Policies
to improve code readability, it is then an “easier” solution to work with Policies
, because if an app contains a lot of authorization gates then one can, and might as well get lost in the AuthServiceProvider.php
file.
Since our User
model already contains two helpful methods, can
and cannot
, as a part of Laravel application, we can in our case use the can
method.
This can
method will receive the name of our action, which we have defined inside our UserPolicy
class.
UserPolicy
:
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function __construct()
{
//
}
public function create(User $user)
{
return $user->hasRole('canCRUDonUser');
}
public function update(User $user)
{
return $user->hasRole('canCRUDonUser');
}
}
The functions create
and update
follows the Laravel naming conventions, and these two receives our User
object which we want to authorize against. When working with spatie
permissions and roles, these includes helper methods, such as the one we have used, hasRole
. The hasRole
method checks whether the $user
has the role named canCRUDonUser
, and returns a boolean. This role, canCRUDonUser
, we have created with our PermissionsDemoSeeder
and stored inside the roles
database table.
Inside UserForm
we modify our authorize
method, to check whether the user in action, has access to those given roles, or some of them.
UserForm
:
...
class UserForm extends FormRequest {
public function authorize()
{
if($this->isMethod('post'))
return $this->user()->can('create', User::class);
return $this->user()->can('update', User::class);
}
...
}
The authorize
method is usually used to authorize the actual request basing on some policy we, or you, would like to respect. In our case we are trying to make a request to either create
or update
a User
model. We use the authorize
method to check that the specific user, who is either trying to create or update, has the permissions to do so.
We first check which type of request that is in action, and then use the can
method on our user
in action, and see if this user “can” perform either of these two actions, create
and update
, on the User
model. The authorized role that gets returned, depends on the form request action.
The can
method, in simple terms, receives the name of the action we wish to authorize and the relevant model. This method will automatically call the appropriate policy, UserPolicy
, and returns the boolean result of the action that is in question.
Testing
In this article, no tests have been made, to check the results of the new functionalities implemented and modified. But do not worry about that, we will do that in the next article, so stay tuned for that.
Changes and implementations along the way
- Inside
UserController
new annotations have been added to verify our OpenAPI, that now uses authorization against authenticated users.
...class UserController extends Controller
{
/**
* ...
* @OA\Response(response=403, description="Forbidden"),
* ...
*/
public function index(Request $request)
{
return User::all();
} /**
* ...
* @OA\Response(response=403, description="Forbidden"),
* ...
*/
public function store(Form $request)
{
return $request->saveOrUpdate();
} /**
* ...
* @OA\Response(response=403, description="Forbidden"),
* ...
*/
public function update(Form $request, int $id)
{
return $request->saveOrUpdate($id);
}
}
Github repository for this article can be found here.