Role-based permission on Laravel

I am trying to perform role-based permission control in a Laravel application. I want to check what actions some users can perform, but I can’t understand how to implement gates and policies in my model (the permission description is in the database and the booleans are associated with the table in which the resource identifiers are stored).

This is a database model in which im uses:

Diagram

I would like to know if laravel gates is useful in my case, and how I can implement it, if not, how to make basic middleware that provides permission control to protect routes (or controllers).

In the table resource, I have uuid that identifies the resources, alias is the name of the resource and has dot notation values ​​for the actions or resource context (for example, "mysystem.users.create", "mysystem". Role.delete ',' mysystem.users .images.view '). Policy tables have a "allow" boolean field that describes user permissions.

Thanks in advance.

+8
php laravel
source share
4 answers

This is how I implement role-based permissions in Laravel using policies.

Users can have multiple roles. Roles have associated permissions. Each resolution allows a specific action in a particular model.

Migrations

Role table

class CreateRolesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('roles', function (Blueprint $table) { $table->increments('id'); $table->string('name')->unique(); $table->string('label'); $table->text('description'); $table->timestamps(); }); } // rest of migration file 

Permission table

 class CreatePermissionsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('permissions', function (Blueprint $table) { $table->increments('id'); $table->string('name')->unique(); $table->string('label'); $table->text('description'); $table->timestamps(); }); } // rest of migration file 

Permission Role Summary

 class CreatePermissionRolePivotTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('permission_role', function (Blueprint $table) { $table->integer('permission_id')->unsigned()->index(); $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade'); $table->integer('role_id')->unsigned()->index(); $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); $table->primary(['permission_id', 'role_id']); }); } // rest of migration file 

User Roles Summary Table

 class CreateRoleUserPivotTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('role_user', function (Blueprint $table) { $table->integer('role_id')->unsigned()->index(); $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade'); $table->integer('user_id')->unsigned()->index(); $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); $table->primary(['role_id', 'user_id']); }); } // rest of migration file 

models

user

 public function roles() { return $this->belongsToMany(Role::class); } public function assignRole(Role $role) { return $this->roles()->save($role); } public function hasRole($role) { if (is_string($role)) { return $this->roles->contains('name', $role); } return !! $role->intersect($this->roles)->count(); } 

Role

 class Role extends Model { protected $guarded = ['id']; protected $fillable = array('name', 'label', 'description'); public function permissions() { return $this->belongsToMany(Permission::class); } public function givePermissionTo(Permission $permission) { return $this->permissions()->save($permission); } /** * Determine if the user may perform the given permission. * * @param Permission $permission * @return boolean */ public function hasPermission(Permission $permission, User $user) { return $this->hasRole($permission->roles); } /** * Determine if the role has the given permission. * * @param mixed $permission * @return boolean */ public function inRole($permission) { if (is_string($permission)) { return $this->permissions->contains('name', $permission); } return !! $permission->intersect($this->permissions)->count(); } } 

resolution

 class Permission extends Model { protected $guarded = ['id']; protected $fillable = array('name', 'label', 'description'); public function roles() { return $this->belongsToMany(Role::class); } /** * Determine if the permission belongs to the role. * * @param mixed $role * @return boolean */ public function inRole($role) { if (is_string($role)) { return $this->roles->contains('name', $role); } return !! $role->intersect($this->roles)->count(); } } 

policies

A policy is required for each model. Here is an example policy for an item model. A policy defines “rules” for four actions: view, create, update, delete.

 class ItemPolicy { use HandlesAuthorization; /** * Determine whether the user can view the item. * * @param \App\User $user * @return mixed */ public function view(User $user) { $permission = Permission::where('name', 'items-view')->first(); return $user->hasRole($permission->roles); } /** * Determine whether the user can create items. * * @param \App\User $user * @return mixed */ public function create(User $user) { $permission = Permission::where('name', 'items-create')->first(); return $user->hasRole($permission->roles); } /** * Determine whether the user can update the item. * * @param \App\User $user * @return mixed */ public function update(User $user) { $permission = Permission::where('name', 'items-update')->first(); return $user->hasRole($permission->roles); } /** * Determine whether the user can delete the item. * * @param \App\User $user * @return mixed */ public function delete(User $user) { $permission = Permission::where('name', 'items-delete')->first(); return $user->hasRole($permission->roles); } } 

Register each policy with AuthServiceProvider.php

 use App\Item; use App\Policies\ItemPolicy; class AuthServiceProvider extends ServiceProvider { /** * The policy mappings for the application. * * @var array */ protected $policies = [ Item::class => ItemPolicy::class, ]; // rest of file 

Controllers

In each controller, refer to the corresponding authorization action from the policy.

For example, in the method index ItemController :

 public function index() { $this->authorize('view', Item::class); $items = Item::orderBy('name', 'asc')->get(); return view('items', ['items' => $items]); } 

Views

In your views, you can check if the user plays a role:

 @if (Auth::user()->hasRole('item-administrator')) // do stuff @endif 

or if special permission is required:

 @can('create', App\User::class) // do stuff @endcan 
+9
source share

Answer your question : how to create basic middleware that takes care of permission to protect routes (or controllers)?

Just an example:
Here is a simple middleware for your routes
Adminmin

 namespace App\Http\Middleware; use Illuminate\Support\Facades\Auth; use Closure; class AdminRole { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { if(Auth::user()->role->name!=="admin"){ //Check your users' role or permission, in my case only admin role for routes return redirect('/access-denied'); } return $next($request); } } 

After defining this middleware, update the kernel.php file as

 protected $routeMiddleware = [ .............. 'admin' =>\App\Http\Middleware\AdminRole::class, ................... ]; 

And use this path for middleware:
There is another way to use route middleware, but the following example:

 Route::group(['middleware' => ['auth','admin']], function () { Route::get('/', ' AdminController@index ')->name('admin'); }); 

Note. There are several tools and libraries for roles and permissions on laravel, but above is an example of creating a basic middle tier.

+1
source share

Since the laravel model didn’t match my database like that, I did almost everything again. This is a functional draft in which some functions are missing, the code is not optimized and may be a bit dirty, but here it is:

proyect / app / Components / Contracts / Gate.php This interface is used to create a singleton in AuthServiceProvider.

 <?php namespace App\Components\Contracts; interface Gate { public function check($resources, $arguments = []); public function authorize($resource, $arguments = []); } 

proyect / app / Components / Security / Gate.php This file loads permissions from the database. This can be greatly improved :(

 <?php namespace App\Components\Security; use App\Components\Contracts\Gate as GateContract; use App\Models\Security\Resource; use App\Models\Security\User; use Illuminate\Auth\Access\HandlesAuthorization; use Illuminate\Contracts\Container\Container; use Illuminate\Support\Arr; use Illuminate\Support\Str; class Gate implements GateContract { use HandlesAuthorization; protected $container; protected $userResolver; protected $policies = []; public function __construct(Container $container, callable $userResolver) { $this->container = $container; $this->userResolver = $userResolver; } public function permissionsForUser(User $user) { $result = User::with(['roles.resources', 'groups.resources', 'policies'])->where('id', $user->id)->first(); $list = []; //role-specific ... the order is important role < group < user permissions foreach ($result->roles as $role) { foreach ($role->resources as $permission) { if (isset($list[$permission->uuid])) { if ($list[$permission->uuid]['on'] == User::ROLE_POLICY) { if ($permission->pivot->allow == false) { $list[$permission->uuid]['allow'] = false; } } else { $list[$permission->uuid]['allow'] = $permission->pivot->allow ? true : false; $list[$permission->uuid]['on'] = User::ROLE_POLICY; $list[$permission->uuid]['id'] = $role->id; } } else { $list[$permission->uuid] = [ 'allow' => ($permission->pivot->allow ? true : false), 'on' => User::ROLE_POLICY, 'id' => $role->id]; } } } // group-specific foreach ($result->groups as $group) { foreach ($group->resources as $permission) { if (isset($list[$permission->uuid])) { if ($list[$permission->uuid]['on'] == User::GROUP_POLICY) { if ($permission->pivot->allow == false) { $list[$permission->uuid]['allow'] = false; } } else { $list[$permission->uuid]['allow'] = $permission->pivot->allow ? true : false; $list[$permission->uuid]['on'] = User::GROUP_POLICY; $list[$permission->uuid]['id'] = $group->id; } } else { $list[$permission->uuid] = [ 'allow' => ($permission->pivot->allow ? true : false), 'on' => User::GROUP_POLICY, 'id' => $group->id]; } } } // user-specific policies foreach ($result->policies as $permission) { if (isset($list[$permission->uuid])) { if ($list[$permission->uuid]['on'] == User::USER_POLICY) { if ($permission->pivot->allow == false) { $list[$permission->uuid]['allow'] = false; } } else { $list[$permission->uuid]['allow'] = $permission->pivot->allow ? true : false; $list[$permission->uuid]['on'] = User::USER_POLICY; $list[$permission->uuid]['id'] = $result->id; } } else { $list[$permission->uuid] = [ 'allow' => ($permission->pivot->allow ? true : false), 'on' => User::USER_POLICY, 'id' => $result->id, ]; } } return $list; } public function check($resources, $arguments = []) { $user = $this->resolveUser(); return collect($resources)->every(function ($resource) use ($user, $arguments) { return $this->raw($user, $resource, $arguments); }); } protected function raw(User $user, $resource, $arguments = []) { $list = $user->getPermissionList(); if (!Resource::isUUID($resource)) { if (empty($resource = Resource::byAlias($resource))) { return false; } } if (empty($list[$resource->uuid]['allow'])) { return false; } else { return $list[$resource->uuid]['allow']; } } public function authorize($resource, $arguments = []) { $theUser = $this->resolveUser(); return $this->raw($this->resolveUser(), $resource, $arguments) ? $this->allow() : $this->deny(); } protected function resolveUser() { return call_user_func($this->userResolver); } } 

proyect / app / Traits / Security / AuthorizesRequests.php This file is added to the controller. Allows the use of $this->authorize('stuff'); in the controller when adding.

 <?php namespace App\Traits\Security; use App\Components\Contracts\Gate; trait AuthorizesRequests { public function authorize($ability, $arguments = []) { list($ability, $arguments) = $this->parseAbilityAndArguments($ability, $arguments); return app(Gate::class)->authorize($ability, $arguments); } } 

proyect / app / Providers / AuthServiceProvider.php This file is the same as on proyect/vendor/laravel/framework/src/Illuminate/Auth/AuthServiceProvider.php , but I changed some parts to add a new class. Here are the important methods:

 <?php namespace App\Providers; use App\Components\Contracts\Gate as GateContract; use App\Components\Security\Gate; use Illuminate\Auth\AuthManager; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Support\ServiceProvider; class AuthServiceProvider extends ServiceProvider { /* function register() ... */ /* other methods () */ protected function registerAccessGate() { $this->app->singleton(GateContract::class, function ($app) { return new Gate($app, function () use ($app) { return call_user_func($app['auth']->userResolver()); }); }); } /* ... */ } 

proyect / app / Http / Middleware / AuthorizeRequest.php This file is used to add the 'can' middleware to routes, for example: Route::get('users/', 'Security\ UserController@index ')->name('users.index')->middleware('can:inet.user.list') ;

 <?php namespace App\Http\Middleware; use App\Components\Contracts\Gate; use Closure; use Illuminate\Contracts\Auth\Factory as Auth; class AuthorizeRequest { protected $auth; protected $gate; public function __construct(Auth $auth, Gate $gate) { $this->auth = $auth; $this->gate = $gate; } public function handle($request, Closure $next, $resource, ...$params) { $this->auth->authenticate(); $this->gate->authorize($resource, $params); return $next($request); } } 

but you have to overwrite the default value in proyect/app/Http/Kernel.php :

 /* ... */ protected $routeMiddleware = [ 'can' => \App\Http\Middleware\AuthorizeRequest::class, /* ... */ ]; 

To use @can('inet.user.list') in the blade template, you must add these lines to proyect/app/Providers/AppServiceProvider.php :

 class AppServiceProvider extends ServiceProvider { public function boot() Blade::if ('can', function ($resource, ...$params) { return app(\App\Components\Contracts\Gate::class)->check($resource, $params); }); } /* ... */ 

User model in proyect / app / Models / Security / User.php

 <?php namespace App\Models\Security; use App\Components\Contracts\Gate as GateContract; use App\Models\Security\Group; use App\Models\Security\Resource; use App\Models\Security\Role; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Facades\Hash; class User extends Authenticatable { use SoftDeletes; use Notifiable; public $table = 'user'; const CREATED_AT = 'created_at'; const UPDATED_AT = 'updated_at'; // tipos de politicas const GROUP_POLICY = 'group_policy'; const ROLE_POLICY = 'role_policy'; const USER_POLICY = 'user_policy'; protected $dates = ['deleted_at']; public $fillable = [ ]; public function policies() { return $this->belongsToMany(Resource::class, 'user_policy', 'user_id', 'resource_id') ->whereNull('user_policy.deleted_at') ->withPivot('allow') ->withTimestamps(); } public function groups() { return $this->belongsToMany(Group::class, 'user_group', 'user_id', 'group_id') ->whereNull('user_group.deleted_at') ->withTimestamps(); } public function roles() { return $this->belongsToMany(Role::class, 'user_role', 'user_id', 'role_id') ->whereNull('user_role.deleted_at') ->withTimestamps(); } public function getPermissionList() { return app(GateContract::class)->permissionsForUser($this); } } 

Group model in proyect / app / Models / Security / Group.php . This is the same as for a role, change only names

 <?php namespace App\Models\Security; use App\Models\Security\Resource; use App\Models\Security\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Group extends Model { use SoftDeletes; public $table = 'group'; const CREATED_AT = 'created_at'; const UPDATED_AT = 'updated_at'; protected $dates = ['deleted_at']; public $fillable = [ 'name', ]; public static $rules = [ ]; public function users() { return $this->hasMany(User::class); } public function resources() { return $this->belongsToMany(Resource::class, 'group_policy', 'group_id', 'resource_id') ->whereNull('group_policy.deleted_at') ->withPivot('allow') ->withTimestamps(); } } 

Resource Model proyect / app / Models / Security / Resource.php

 <?php namespace App\Models\Security; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Resource extends Model { use SoftDeletes; public $table = 'resource'; const CREATED_AT = 'created_at'; const UPDATED_AT = 'updated_at'; protected $dates = ['deleted_at']; public $fillable = [ 'alias', 'uuid', 'type', ]; public static $rules = [ ]; public static function isUUID($value) { $UUIDv4 = '/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i'; return preg_match($UUIDv4, $value); } public static function byAlias($value) { return Resource::where('alias', $value)->first(); } } 

There are many things that I have not put here, but this is what I still have

0
source share

The problem that I encounter when trying to combine permissions from the database with politicians is that it is about ownership of the record.

Ultimately, in our code, we would like to check access to the resource using only permission. This is because as the list of roles grows, we do not need to constantly add checks for these roles to the code base.

If we have a user table, we might want the “administrator” (role) to update all user records, but the “primary” user could only update his own user records. We would like to be able to control this access ONLY using a database.

However, if you have the permission "update_user", do you give it to both roles? If you do not transfer it to the primary user role, then the request will not reach the ownership verification policy.

Therefore, you cannot revoke access for the basic user to update his record only from the database. Also, the value "update_user" in the permissions table now implies the ability to update ANY user.

DECISION?

Add additional permissions in case the user owns the entry.

Thus, you can have rights to "update_user" and "update_own_user" .

The user 'admin' will have the first permission, and the user 'basic' - the second.

Then in the policy we first check the permission of "update_user", and if it is not there, we check "update_own_user".

If the permission "update_own_user" is present, we check ownership. Otherwise, we return false.

The solution will work, but it seems ugly to have "own" permissions in the database.

0
source share

All Articles