Well, good point! Most of the ACL systems out here such as romanbican/roles, kodeine/laravel-acl or Sentinel are packed with tons of amazing features... which most of the time I'm not using! :D
That's why I thought to build a minimal Laravel roles and permissions manager that provides a very simple RBAC implementation. Each user can be assigned a single Role, while permissions for each Role are stored in a single config file. With the package are provided a very intuitive and well documented API, a Trait to check permissions directly on the Eloquent User Model and two Middlewares to easily protect routes and Controllers.
However, as your application grown, you might need a more complex ACL system, that's why the package comes with a couple of Contracts that you can leverage to improve or replace functionalities at need. You can see Rooles not only as a fully working RBAC but also as a starting point to develop your own custom roles and permission manager.
Run the following from your terminal from within the path containing the Laravel composer.json
file:
$ composer require micc83/rooles
Open config/app.php
and add the following line at the end of the providers array:
Rooles\RoolesServiceProvider::class
Run the following command from your terminal to publish the migration file (it will simply add a role
column to the default Users table), the config file and a default blade template for the 403-Forbidden view (It will not be published if one has already been created):
$ php artisan vendor:publish
In order to be able to use route and Controllers middlewares (so to be able to filter who's able to access a given route or Controller method) open App/Http/Kernel.php
and add the following lines at the end of the $routeMiddleware
array:
'perms' => \Rooles\PermsMiddleware::class,
'role' => \Rooles\RoleMiddleware::class,
As Rooles works on top of the default Auth system of Laravel and with the Eloquent User Model you must add the Rooles\Traits\UserRole
trait to the User Class located in App/User.php
as follow:
use \Rooles\Traits\UserRole;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
{
use Authenticatable, CanResetPassword, UserRole;
// ...
}
Important note on Laravel version >= 1.1.11 From this version on Laravel implements its own permission manager trough the
Authorizable
trait/contract so, in order to have the User model work with Rooles, you must remove any reference to both theAuthorizable
trait and interface from the Eloquent user model.
Only a single Role can be assigned to each User. You can hardcode the role inside the User Eloquent model adding the role attribute as follow:
protected $attributes = [
'role' => 'admin'
];
Or run the provided migration to add the role
column to the Users Table so to be able to change Users role at runtime:
$user = User::find(1);
$user->role = 'admin';
$user->save();
All the permissions for any given role are set in the config/rooles.php
file as follow:
<?php return [
'roles' => [
'default' => []
'admin' => [
'name' => 'Administrator',
'grant' => '*'
],
'editor' => [
'grant' => [
'posts.*',
'users.*.read',
'users.*.ban',
'comments.*',
'profile.*'
],
'deny' => [
'users.admin.ban',
'posts.delete',
'comments.delete'
]
]
]
];
As you can see the format used is:
[
'roles' => [
'role_id' => [
'name' => 'role_name',
'grant' => 'string_or_array_of_granted_permissions',
'deny' => 'string_or_array_of_denied_permissions',
]
]
]
The default
role is applied to any user which has no role applied and provides no permissions unless differently stated in the config file.
The name
property is optional and allows to set a name differing from the provided Role ID.
You can also create roles and handle permissions manually. Here's an example:
app()->make(\Rooles\Contracts\RoleRepository::class)
->getOrCreate('customer')
->assignName('Client')
->grant(['cart.*', 'products.buy'])
->deny('cart.discount');
There are four main concept to remember when creating a permissions strategy for Rooles:
- Every role always start with no permissions;
- The wildcard character * is used to define a whole subset of available permissions. For example if we take in consideration the grant
users.*.ban
, that means that editors can ban any group of users (users.reader
,users.author
etc... ) but notusers.admin
as the permission has been denied in the deny array. - When you grant or deny a permission, if not already set, a wildcard will be automatically appended so
customers
is the same ascustomers.*
. That also means that any child permission of the given one will be granted or denied, for example:$role->grant('comments'); // Same as writing comments.* $role->can('comments.write'); // true $role->can('comments.pingbacks.write') // true
- When you apply both grants and denies in order to figure out which rule will 'win' you'll have to think in terms of specificity. The more specific rule will always win. Let's see an example:
$role->grant('comments.write.*') // Same as writing comments.write ->deny('*.write'); $role->can('comments.write'); // true $role->can('users.write') // false
As you probably guessed from the example specificity is calculated on the position of the wildcards and length of the permission. As you move the wildcard to the right you gain in specificity.
From within your Controller methods or wherever you feel comfortable you can check for a given user permissions as follow:
$user = User::find(1);
if ($user->can('comments.post')){
// Do something...
}
The same to check the logged in user permissions:
public function index(Illuminate\Contracts\Auth\Guard $auth) {
if ( $auth->user->can('users.list') ){
// Do something...
}
}
The API exposes a convenient method to negate a permissions assertion:
if ( $user->cannot('users.list') ) redirect()->to('dashboard');
You can evaluate multiple assertions passing an array through:
if ( $user->can(['users.list', 'users.read']) ) // Do something when the user has both the permissions (AND)
There are also two convenient operator to use with the can/cannot assertions:
if ( $user->can('users.list&users.read') ) // Do something when the user has both the permissions (& > AND)
if ( $user->can('users.list|users.read') ) // Do something when the user has one of the requested permissions (| > OR)
Multiple operators can ben be joined together but mind that AND operators have always priority over OR operators.
You can make a more general assertion checking for the user role ID (case insensitive):
if ( $user->role->is('admin') ) echo 'Hello Boss';
Or check if the user role ID is in a given range (still case insensitive):
if ( $user->role->isIn(['lamer', 'trool']) ) echo 'Hello Looser';
You can also get the User role name (the ID will be returned if no name is provided), using one of the following syntax:
// If in a string context:
echo $user->role;
// Otherwise:
if ($user->role->name() === 'Admin') // Do something
If you need to make some comparisons, like for example in a Select input field you better use the ID instead of the name. Example:
{!! Form::select('role', ['editor' => 'Editor', 'admin' => 'Administrator'], $user->role->id()) !!}
Remember that role ID is automatically converted to lowercase with UTF8 support.
Rooles provides two Middlewares to protect both routes and Controllers.
To protect routes by User Role you can use the role Middleware:
Route::get('admin/users/', [
'middleware' => [
'auth',
'role:admin|editor', // Give access to both admins and editors
],
function () {
return view('admin.users.index');
}
]);
In order to check for user permissions on a route you can use the perms Middleware as follow:
Route::get('admin/users/', [
'middleware' => [
'auth',
'perms:users.list|users.edit', // Give access to users with users.list OR users.edit permissions
]
function () {
return view('admin.users.index');
}
]);
In both case you'll have probably noticed that I'm calling the Auth middleware as the user must be logged in in order to check its Role and permissions.
Most of the times you'll be probably being dealing with routes groups, in that case you can simply:
// Route Group
Route::group([
'middleware' => [
'auth',
'role:admin|editor' // Give access to both admins and editors
]
], function () {
Route::resource('users', 'UserController');
Route::resource('posts', 'PostController');
});
Middlewares can also be used in Controllers as follow:
class UserController extends Controller
{
/**
* @var UserRepository
*/
private $users;
/**
* @param UserRepo $users
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
$this->middleware('perms:users', ['except' => 'show']);
}
Here we are saying that in order to access any controller method we must have a role that provides all the users
permissions but we don't need any permission to show user profiles. You can find a better documentation on Controller Middlewares on the official Laravel website.
Rooles middlewares handles error responses differently depending on the nature of the request. For Ajax requests they will respond with a JSON Object and a 403
status code as follow:
{
"error" : {
"code" : 403,
"message" : "Forbidden"
}
}
So that you can intercept it in JavaScript as follow:
if ('error' in response) console.log(response.error.message);
For normal requests in case of missing authorizations a Rooles\ForbiddenHttpException
is thrown, which by default (when debug is disabled) will result in the previously published 403 error page with a 403
status code. The page itself can be customized editing the resources/views/errors/403.blade.php
template.
Otherwise if you'd rather not to show a view but instead implement some custom behaviour you can play with the render method in app/Exceptions/Handler.php
as follow:
public function render($request, Exception $e)
{
if ($e instanceof \Rooles\ForbiddenHttpException) {
return redirect('/')->withErrors(['You don\'t have the needed permissions to perform this action!']);
}
return parent::render($request, $e);
}
This way when an Forbiddeng error is thrown you'll be redirected to the given page with an error flash message. To show the message you can add the following to your blade template:
@if ($errors->has())
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
{{ $error }}
@endforeach
</div>
@endif
I firmly believe that even the best coded application in the world is bound to failure when missing a good documentation. That's why I humbly ask to open an issue whenever you'll think something is missing or could be improved.
I'd be glad if you'd like to contribute to the project however I'd ask not to implement new features but to improve the few existing ones (improve patterns, algorythms etc). Each PR must follow PSR-2 coding standards and pass all the existing tests (or add furthers when needed).