Implementation of Access Control List System in Laravel.
The system handles authorizations of certain actions based on roles and permissions. Permissions are assigned to roles, and roles are assigned to users. If a user's role has the matching permission, then the user is authorized to perform the given action; else the user is forbidden. The permissions can be arbitrarily defined by the application developers.
The system can handle resource-based permissions, that is: a permission is associated with a resource/model/entity in the database. Thus, it is possible to, for example, define authorization for a user to edit all articles or just the articles he has created by creating permissions for those articles in particular. This is better than adding a 'user_id' column to the articles table.
- Role-based access control list
- Per-role permission: assign permission to a role
- Per-user permission: assign permission to a user
- Per-model permission: associate a unique model in DB with a permission
- Automated Laravel Policies for controllers using per-model permissions
- Clean interface with permission-name guessing
- Arbitrary permissions
- Arbitrary roles
A demo app is available on github at uwla/lacl-demo to illustrate usage.
Why should I use this package instead of popular ones, such as spatie permissions?
This package provides some functionality that spatie's and other permission-management packages do not provide, such as per-model permission and Resource Policy automation. At the same time, their packages provide functionality that this package does not provide, such as searching permissions based on wildcards or support for team permissions. Please, read the full README to understand better what this package does and what it does not. If you should use this package or not will depend on the specific needs for your application; it is up to you as developer to figure it out.
Why this package?
I had specific needs that led me to develop this package and I was not aware of another package that would fit my needs at the time I started developing this package.
Install using composer:
composer require uwla/lacl
Publish the ACL table migrations:
php artisan vendor:publish --provider="Uwla\Lacl\AclServiceProvider"
Run the migrations:
php artisan migrate
Convention used here:
User
refers toApp\Models\User
Role
refers toUwla\Lacl\Models\Role
Permission
refers toUwla\Lacl\Models\Permission
Collection
refers toIlluminate\Database\Eloquent\Collection
Add Traits to the application's user class:
<?php
use Uwla\Lacl\Traits\HasRole;
use Uwla\Lacl\Contracts\HasRoleContract;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements HasRoleContract
{
use HasRole;
//
}
Add role or multiple roles to user (the roles must already exist):
<?php
// single role
$user->addRole(Role::first()); // using Eloquent model
$user->addRole('administrator'); // using string name
// multiple roles
$user->addRole(Role::all()); // using Eloquent Collection
$user->addRole(['editor', 'manager', 'senior']); // using string names
Role the role or multiple roles to user (revoking previous ones):
<?php
// single role
$user->setRole(Role::first()); // using Eloquent model
$user->setRole('administrator'); // using string name
// multiple roles
$user->setRole(Role::all()); // using Eloquent Collection
$user->setRole(['editor', 'manager', 'senior']); // using string names
Get the roles of the user (returns Collection<Role>
or array<string>
):
<?php
$user->getRoles(); // get Eloquent models
$user->getRoleNames(); // get string names
Delete role (returns void
):
<?php
// single role
$user->delRole($role); // using Eloquent model
$user->delRole('editor'); // using string name
// multiple roles
$user->delRole($roles); // using Eloquent Collection
$user->delRole(['editor', 'manager']); // using string names
// all roles
$user->delAllRoles();
Has role (returns bool
):
<?php
// check whether the user has a role
$user->hasRole($role); // using Eloquent model
$user->hasRole('editor'); // using string name
// check whether the user has all of the given multiple roles
$user->hasRoles($roles); // using Eloquent Collection
$user->hasRoles(['editor', 'manager']); // using string names
// check whether the user has at least one of the given roles
$user->hasAnyRole($roles); // using Eloquent Collection
$user->hasAnyRole(['editor', 'manager']); // using string names
Count how many roles the user has (returns int
):
<?php
$user->countRoles();
Get multiple users along with their roles:
<?php
$users = User::withRoles($users);
// access the roles via the 'roles' property
$roles_of_first_user = $users[0]->roles
Get multiple users along with the names of their roles:
<?php
$users = User::withRoleNames($users);
// access the role names via the 'roles' property
$role_names_of_first_user = $users[0]->roles;
Here, we will assign permissions to a role, but they can also be assigned directly to a user or any model by using the traits and contracts as follow:
<?php
use Uwla\Lacl\Traits\HasPermission;
use Uwla\Lacl\Contracts\HasPermissionContract;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements HasPermissionContract
{
use HasPermission;
//
}
Add a permission or multiple permissions to a role (the permissions must already exist):
<?php
// single role
$role->addPermission(Permission::first()); // using Eloquent model
$role->addPermission('manage client emails'); // using string name
// multiple permissions
$role->addPermission(Permission::all()); // using Eloquent Collection
$role->addPermission(['user.view', 'user.create', 'user.delete']); // using string names
Set the permission or multiple permissions to role (revoking previous ones):
<?php
// single permission
$role->setPermission(Permission::first()); // using Eloquent model
$role->setPermission('manage client emails'); // using string name
// multiple permissions
$role->setPermissions(Permission::all()); // using Eloquent Collection
$role->setPermissions(['user.view', 'user.create', 'user.delete']); // using string names
Get the permissions of the role (return a Collection
of Permission
or string
):
<?php
$role->getPermissions(); // get Eloquent models
$role->getPermissionNames(); // get string names
Delete permission (returns void
):
<?php
// single permission
$role->delPermission($permission); // using Eloquent model
$role->delPermission('view emails'); // using string name
// multiple permissions
$role->delPermission($permissions); // using Eloquent Collection
$role->delPermission(['user.view', 'user.del']); // using string names
// all permissions
$role->delAllPermissions();
Has permission (returns bool
):
<?php
// check whether the role has a permission
$role->hasPermission($permission); // using Eloquent model
$role->hasPermission('user.view'); // using string name
// check whether the role has all of the given permissions
$role->hasPermissions($permissions); // using Eloquent Collection
$role->hasPermissions(['user.view', 'user.del']); // using string names
// check whether the role has at least one of the given permissions
$role->hasAnyPermissions($permissions); // using Eloquent Collection
$role->hasAnyPermissions(['user.view', 'user.del']); // using string names
Count how many permissions the role has (returns int
):
<?php
$role->countPermissions();
Get multiple roles along with their permissions:
<?php
$roles = Role::withPermissions($roles);
// access the permissions via the 'permission' property
$permissions_of_first_role = $roles[0]->permissions
Get multiple roles along with the name of their permissions:
<?php
$roles = Role::withPermissionNames($roles);
// access the permission names via the 'permission' property
$permission_names_of_first_role = $roles[0]->permissions
Create an arbitrary permission:
<?php
$permission = Permission::create([
'name' => $customName,
'description' => $optionalDescription,
]);
// shorter way
$permission = Permission::createOne('View confidential documents');
// or many at once
$permissions = Permission::createMany([
'view documents', 'edit documents', 'upload files',
]);
Create a permission for a given model:
<?php
$article = Article::first();
$permission = Permission::create([
'name' => 'article.edit', // can be any name, but standards help automation
'model' => Article::class,
'model_id' => $article->id;
]);
// now you could do something like
$user->add($permission);
$user->hasPermission('article.edit', Article::class, $article->id); // true
Get a permission by name:
<?php
// standard Eloquent way
$permission = Permission::where('name', 'view documents')->first();
// shorter way
$permission = Permission::getByName('view documents');
// or many at once
$permissions = Permission::getByName([
'view documents', 'edit documents', 'upload files'
]);
Get all roles associated with a permission:
<?php
$roles = $permission->getRoles();
// or, get the role names
$roleNames = $permission->getRoleNames();
Get all model instances for the given model class which have the specific permission:
<?php
$permission = Permission::getByName('vip content');
// we get all users with the 'VIP' permission.
// the first parameter is the class of the model.
// the second parameter is the name of the id column of the model.
$users = $permission->getModels(User::class, 'id');
// it can be used on users, roles, or any model such as a Team.
// a team could have permissions associated with the team members.
$teams = $permissions->getModels(Team::class, 'id');
It is worth noting that you can perform any operations on Permission
that are
supported by Eloquent models, such as deleting, updating, fetching, etc.
The Trait Permissionable
provides an interface for managing CRUD permissions
associated with a given model. In the following examples, we will use the
Article
model to illustrate how would we manage per-article permissions.
First, make sure the class do use the trait.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Uwla\Lacl\Traits\Permissionable;
class Article extends Model
{
use Permissionable;
}
OBS: If your model needs to use both Permissionable
and HasRole
traits,
then you will be better off using the PermissionableHasRole
trait, which is
basically a mix of the both that solves conflict declarations between them. That
could be the case for the User
class, which could have roles and, at the same
time, be a permissionable model.
Here is a summary of the auxiliary methods provided by Permissionable
:
Name | Description |
---|---|
createViewPermission |
Create permission for viewing the model. |
createUpdatePermission |
Create permission for updating the model. |
createDeletePermission |
Create permission for deleting the model. |
createCrudPermissions |
Create permissions above. |
getViewPermission |
Get the permission for viewing the model. |
getUpdatePermission |
Get the permission for updating the model. |
getDeletePermission |
Get the permission for deleting the model. |
getCrudPermissions |
Get the permissions above. |
deleteViewPermission |
Delete the permission for viewing the model. |
deleteUpdatePermission |
Delete the permission for updating the model. |
deleteDeletePermission |
Delete the permission for deleting the model. |
deleteCrudPermissions |
Delete the permissions above. |
grantViewPermission |
Grant the permission for viewing the model to the given user/role. |
grantUpdatePermission |
Grant the permission for updating the model to the given user/role. |
grantDeletePermission |
Grant the permission for deleting the model to the given user/role. |
grantCrudPermissions |
Grant the permissions above to the given user/role. |
revokeViewPermission |
Revoke the permission for viewing the model from the given user/role. |
revokeUpdatePermission |
Revoke the permission for updating the model from the given user/role. |
revokeDeletePermission |
Revoke the permission for deleting the model from the given user/role. |
revokeCrudPermissions |
Revoke the permissions above from the given user/role. |
As you can see, the per-model permissions are 'view', 'update', and 'delete'. This is because the most generic actions a user can perform on a single model is to view it, update it, or delete it. It also facilitates automation and integration with Laravel Policies.
The create-permission helpers will either fetch from or insert into the database the associated permission, depending on whether it already exists or not. The get-permissions helpers assume the permission exists in DB, and then try to fetch. The delete-permission helpers will try to delete the permissions in DB, but does not assume they already exist. The grant permission helpers will assign the permissions to the user or to the given user/role, which assumes the permissions already exist (if they don't exist, an Error will be thrown). The revoke-permission helpers try to revoke the permission from the user/role; it assumes the permissions exist but it does not assume the user/role has access to those permissions.
Create crud permission (or fetch them, if already exist) for the article:
<?php
$article = Article::find(1);
$viewPermission = $article->createViewPermission();
$updatePermission = $article->createUpdatePermission();
$deletePermission = $article->createDeletePermission();
// or, more simply
$crudPermissions = $article->createCrudPermissions();
Get the permissions, assuming they were already created before:
<?php
$article = Article::find(1);
$viewPermission = $article->getViewPermission();
$updatePermission = $article->getUpdatePermission();
$deletePermission = $article->getDeletePermission();
// or, more simply
$crudPermissions = $article->getCrudPermissions();
Delete the permissions (they may exist or not).
<?php
$article = Article::find(1);
$article->deleteViewPermission();
$article->deleteUpdatePermission();
$article->deleteDeletePermission();
// or, more simply
$article->deleteCrudPermissions();
Grant the permissions to the user:
<?php
// you can fetch the permissions manually and then grant it to the user or role
$viewPermission = $article->getViewPermission();
$role->addPermission($viewPermission); // assign to a role
$user->addPermission($viewPermission); // assign to a specific user
$crudPermissions = $article->getCrudPermissions();
$user->addPermissions($crudPermissions);
$role->addPermissions($crudPermissions);
// but it is easier to grant them via the model
$article->grantViewPermission($role);
$article->grantViewPermission($user);
// grant all crud permissions to the given user/role
$article->grantCrudPermissions($user);
$article->grantCrudPermissions($role);
Revoking permissions is done in the same way:
<?php
// you could fetch the permissions manually, then revoke from the user or role
$viewPermission = $article->getViewPermission();
$role->delPermission($viewPermission); // revoke from a role
$user->delPermission($viewPermission); // revoke from a specific user
$crudPermissions = $article->getCrudPermissions();
$user->delPermissions($crudPermissions);
$role->delPermissions($crudPermissions);
// it is easier to revoke them via the model
$article->revokeViewPermission($role);
$article->revokeViewPermission($user);
// revoke all crud permissions to the given user/role
$article->revokeCrudPermissions($user);
$article->revokeCrudPermissions($role);
For now, to check if the user has a permission to view/update/delete the model, you could do the following:
<?php
if ($user->hasPermission($article->getViewPermission())
{
// user can view the article
return new Response(['data' => $article]);
}
if ($user->hasPermission($article->getDeletePermission())
{
// user can delete the article
$article->delete();
return new Response(['success' => true]);
}
Also, it is important to remember that the user permissions are all permissions assigned specifically to him plus the permissions assigned to any role he has. Therefore, if the user does not have a direct permission to view the article, but one of his roles has the permission, then the user will also have that permission.
To delete all per-model permissions associated with a model, you can use the
deleteThisModelPermissions
method that comes with the Permissionable
trait.
<?php
$model->deleteThisModelPermissions();
If you want that behavior to be triggered automatically before deleting an
Eloquent model, you can add that to the boot
method of your model:
<?php
/*
* Register callback to delete permissions associated with this model when it
* gets deleted.
*
* @return void
*/
protected static function boot() {
parent::boot();
static::deleted(function($model) {
Permission::where([
'model' => $model::class,
'model_id' => $model->id,
])->delete();
});
}
Just keep in mind that mass deletions do not trigger the static:deleted
because when you use Eloquent Models for mass deletion it will not fetch the
models first and later deleted them one by one. It will, instead, send a
deletion query to the database, so it does instantiate any model.
There is a shorter, cleaner way (aka, syntax sugar) to deal with permissions:
<?php
// create a permission to send emails
Permission::create(['name' => 'sendEmails']);
// shorter way to add, check, and del single permission:
$user->addPermissionToSendEmails();
$user->hasPermissionToSendEmails(); // true
$user->delPermissionToSendEmails();
$user->hasPermissionToSendEmails(); // false
Whenever you call a method that it is undefined, it will trigger PHP's language
construct __call
method, which allows us programmers to define custom
behavior to handle undefined methods. In this case, the HasPermission
trait
handles custom behavior in the following way:
- If the method name does not start with
hasPermissionTo
,addPermissionTo
, ordelPermissionTo
, then it will call theparent::__call
to handle it. - The remaining of the method name (in this case, it is
SendEmails
) is passed to a method calledguessPermissionName
, which you are encouraged to overwrite to fit your needs. - By default,
guessPermissionName
will just lower case the first letter of the remaining method name. - It will then call one of the following
hasPermission
,addPermission
,delPermission
, depending on the method name. - If an argument is passed, it is assumed to be a class that uses the
Permissionable
trait, provided by this package. The permission to be create/checked/delete will be a model-based permission
The default convention is that the permission name is prefixed with the
<model>.
, where <model>
is the lowercase name of the model's class. This is
the current convention, but it will be customizable in the near future.
Here is an example with models:
<?php
$user->addPermissionToView($article);
$user->hasPermissionToView($article); // true
$user->delPermissionToView($article);
$user->addPermissionToDeleteForever($article);
$user->hasPermissionToView($article); // false, since we deleted it
$user->hasPermissionToDeleteForever($article); // true
// of course, this works for roles too
$role->addPermissionToUpdate($article);
Notice that before adding a permission, the permission should already exist. If the permission does not exist, you should create it.
Here is how to fetch the models of a specific type that the user or a role has access to:
<?php
// per user
$articles = $user->getModels(Article::class);
// per role
$articles = $role->getModels(Article::class);
This will fetch all Article
models such that there is a per-model permission
associated with them and the user or role has access to at least one of such
per-model permissions.
You can specify the name of the permission too:
<?php
// get all articles this user can view
$articles = $user->getModels(Article::class, 'view');
// get all articles this user can edit
$articles = $user->getModels(Article::class, 'update');
// get all the users this role can delete
$users = $role->getModels(User::class, 'delete');
// get all products this user is able to cancel the delivery of
$products = $user->getModels(Product::class, 'cancelDelivery');
That way, you have granular control to fetch the models each user or role has permission to access, filtering by a particular action (aka, permission name).
Generic model permissions are permissions to access all instances of a model. This is different from per-model permission, which handles access to a particular instance of a model.
Those generic model permissions are create
, viewAny
, updateAny
, and
deleteAny
. They are designed to follow the standards of Laravel Policies. Of
course, you can redefine those and use the custom names you want, but it is more
convenient to stick to those conventions because we can automate tasks instead
of manually defining custom names.
The interface for generic model permissions are the same as for the per-model permission, the only difference is that the methods are static.
<?php
// so, instead of
$article->createCrudPermissions();
$article->deleteUpdatePermission();
$article->grantDeletePermission($user);
// we basically do:
Article::createCrudPermissions();
Article::deleteUpdateAnyPermission();
Article::grantDeleteAnyPermission($user);
In the second example above, the user would be able to delete all articles
since he was granted permission to deleteAny
any article model.
Everything that was explained about per-model permissions applies to generic model permissions: creation, fetching, deletion, granting, revoking, dynamic names, etc. There are only three differences:
- The permissions must be created, fetched, and deleted using static methods.
- The permissions grant access to all models, not just one.
- The permission names end with the
Any
, such asupdateAny
, except for thecreate
permission.
Actually, there is also more two exceptions. First, to delete all generic model permissions:
<?php
Article::deleteGenericModelPermissions();
Delete all model permissions, both generic model permissions and per-model permissions (be careful with this one, since it will delete all of them):
<?php
Article::deleteAllModelPermissions();
This package provides the ResourcePolicy
trait to automate Laravel Policies
using a standard convention for creating permissions.
The convention is:
- To create a model, the user must have the
{model}.create
permission. - To view all models, the user must have the
{model}.viewAny
permission. - To view a specific model, the user must have either the
{model}.viewAny
permission or the{model}.view
per-model permission for the specific model. - To update a specific model, the user must have either the
{model}.updateAny
permission or the{model}.update
per-model permission for the specific model. - To delete a specific model, the user must have either the
{model}.deleteAny
permission or the{model}.delete
per-model permission for the specific model. - To force-delete a specific model, the user must have either the
{model}.forceDeleteAny
permission or the{model}.forceDelete
per-model permission for the specific model. - To restore a specific model, the user must have either the
{model}.restoreAny
permission or the{model}.restore
per-model permission for the specific model.
Where {model}
is the lowercase name of the model's class name. For example, if
it is the App\Models\User
, it would be user
; if it is App\Models\Product
,
it would be product
.
Here is how you would use it for a ArticlePolicy
:
<?php
namespace App\Policies;
use App\Models\Article;
use Uwla\Lacl\Traits\ResourcePolicy;
use Uwla\Lacl\Contracts\ResourcePolicy as ResourcePolicyContract;
class ArticlePolicy implements ResourcePolicyContract
{
use ResourcePolicy;
public function getResourceModel()
{
return Article::class;
}
}
Then, in the ArticleController
:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreArticleRequest;
use App\Http\Requests\UpdateArticleRequest;
use App\Models\Article;
use Illuminate\Http\Response;
class ArticleController extends Controller
{
public function __construct()
{
$this->authorizeResource(Article::class, 'article');
}
public function index(): Response
{
return new Response(Article::all());
}
public function store(StoreArticleRequest $request): Response
{
$article = Article::create($request->all());
return new Response($article);
}
public function show(Article $article): Response
{
return new Response($article);
}
public function update(UpdateArticleRequest $request, Article $article): Response
{
$article->update($request->all());
return new Response($article);
}
public function destroy(Article $article): Response
{
return new Response($article);
}
}
The Laravel Policies are triggered before the request is handled to the
controller. Since we are using the ResourcePolicy
, before the request is sent
to the ArticleController
, our application will check if the user has the
permission to perform the action associated with the method. The goal here is
to have an automated process of Access Control, freeing the developer from
having to manually check if the user has the permission to perform the common
CRUD operations.
It is possible to override the models which are used by the traits HasRole
,
HasPermission
, Permissionable
, PermissionableHasRole
.
To do so, just override the protected static methods Permission
and Role
:
<?php
protected static function Permission()
{
return CustomPermission::class;
}
protected static function Role()
{
return CustomRole::class;
}
Contributions are welcome. Fork the repository, make your changes, then make a pull request. Check development for all development-related instructions.
If you any need help, feel free to open an issue on this package's github repo.
MIT.