Spatie User Permissions - Belongs To Many Companies, solution feedback - php

I have a situation where a user can belong to many teams/companies and within that team/company they can have different roles and permissions depending on which one they are signed into. I have come up with the following solution and would love some feedback!
Note: Currently I am only using the model_has_roles table with Spatie permissions and always use $user->can('Permission') to check permissions.
Our company model has the following relationships and method
class Company extends Model
{
public function owner(): HasOne
{
return $this->hasOne(User::class, 'id', 'user_id');
}
public function users(): BelongsToMany
{
return $this->belongsToMany(
User::class, 'company_users', 'company_id', 'user_id'
)->using(CompanyUser::class);
}
public function addTeamMember(User $user)
{
$this->users()->detach($user);
$this->users()->attach($user);
}
}
We modify the pivot model to have the Spatie HasRoles trait. This allows us to assign a role to the CompanyUser as opposed to the Auth User. You also need to specify the default guard or Spatie permissions squarks.
class CompanyUser extends Pivot
{
use HasRoles;
protected $guard_name = 'web';
}
On the user model, I have created the HasCompanies Trait. This provides the relationships and provides a method for assigning the roles to the new company user. Additionally, it overwrites the gate can() method.
A user can belong to many companies, but can only have one active company at a time (i.e. the one they are viewing). We define this with the current_company_id column.
It is also important to ensure the pivot table ID is pulled across (which it will not be as standard) as this is now what we are using in the Spatie model_has_roles table.
trait HasCompanies
{
public function companies(): HasMany
{
return $this->hasMany(Company::class);
}
public function currentCompany(): HasOne
{
return $this->hasOne(Company::class, 'id', 'current_company_id');
}
public function teams(): BelongsToMany
{
return $this->belongsToMany(
Company::class, 'company_users', 'user_id', 'company_id'
)->using(CompanyUser::class)->withPivot('id');
}
public function switchCompanies(Company $company): void
{
$this->current_company_id = $company->id;
$this->save();
}
private function companyWithPivot(Company $company)
{
return $this->teams()->where('companies.id', $company->id)->first();
}
public function assignRolesForCompany(Company $company, ...$roles)
{
if($company = $this->companyWithPivot($company)){
/** #var CompanyUser $companyUser */
$companyUser = $company->pivot;
$companyUser->assignRole($roles);
return;
}
throw new Exception('Roles could not be assigned to company user');
}
public function hasRoleForCurrentCompany(string $roles, Company $company = null, string $guard = null): bool
{
if(! $company){
if(! $company = $this->currentCompany){
throw new Exception('Cannot check role for current company because it has not been set');
}
}
if($company = $this->companyWithPivot($company)){
/** #var CompanyUser $companyUser */
$companyUser = $company->pivot;
return $companyUser->hasRole($roles, $guard);
}
return false;
}
public function can($ability, $arguments = []): bool
{
if(isset($this->current_company_id)){
/** #var CompanyUser $companyUser */
$companyUser = $this->teams()->where('companies.id', $this->current_company_id)->first()->pivot;
if($companyUser->hasPermissionTo($ability)){
return true;
}
// Still run through the gate as this will check for gate bypass
return app(Gate::class)->forUser($this)->check('N/A', []);
}
return app(Gate::class)->forUser($this)->check($ability, $arguments);
}
}
Now we can do something like this:
Create the role & permission
/** #var Role $ownerRoll */
$ownerRoll = Role::create(['name' => 'Owner']);
/** #var Permission $permission */
$permission = Permission::create([
'name' => 'Create Company',
'guard_name' => 'web',
]);
$ownerRoll->givePermissionTo($permission);
Create a new company with an owning user and then switch this company to that owner's active company.
public function store(CompanyStoreRequest $request)
{
DB::transaction(function () use($request) {
/** #var User $owner */
$owner = User::findOrFail($request->user_id);
/** #var Company $company */
$company = $owner->companies()->create($request->validated());
$company->addTeamMember($owner);
$owner->assignRolesForCompany($company, 'Owner');
$owner->switchCompanies($company);
});
return redirect()->back();
}
So this all works, my main concerns are that:
We are overwriting the can method. There may be other authorization methods/gate functions that are not caught.
We have 2 sets of model_permissions. The Auth user and the company user. I think I need to build in some checks to ensure that only the correct kinds of users can be assigned to the roles. At this stage, all administrator users would have permissions assigned to their auth user, while any users who own a company should only have permissions on the company user model

Related

How to deploy relationship in pivot of three model in laravel?

I'm developing a role and permissions based on laravel framework.
I have 3 models :
Weblog
User
Permissions
This is pivot table
user_id , weblog_id , permission_id
Now, a user can have a weblog with permission id 1,2 and another weblog with permission 1,2,3,4
How can I deploy relationships? and how can I check user permissions when managing a weblog. (middleware and ...)
With the fact that Permission are specific to Weblog
Say the pivot table is called permission_user_weblog
class User extends Model
{
public function weblogs()
{
return $this->belongsToMany(Weblog::class, 'permission_user_weblog');
}
public function permissionsFor(int $weblogId)
{
$permissionIds = null;
$this->weblogs()
->where('id', $weblogId)
->with('permissions')
->get()
->each(function($weblog) use(&$permissionIds) {
$permissionIds = $weblog->permissions->pluck('id');
});
return $permissionIds;
}
}
class Weblog extends Model
{
public function users()
{
return $this->belongsToMany(User::class, 'permission_user_weblog');
}
public function permissions()
{
return $this->belongsToMany(Permission::class, 'permission_user_weblog');
}
}
class Permission extends Model
{
public function weblogs()
{
return $this->belongsToMany(Weblog::class, 'permission_user_weblog');
}
}
Then you can check anywhere for whether logged in user has specific permission for a specific weblog
public function update(Request $request, $weblogId)
{
$user = auth()->user();
$permissions = $user->permissionsFor($weblogId);
//Check whether the logged in user has permission identified by id 1 or 4 for weblog
$can = !! $permissions->intersect([1,4])->count();
//Do rest of processing
}
your Weblog,User,Permission has ManyToMany Relation, its a kind of odd but if you want to have this kind of relation its not a problem.
just consider each pair a ManyToMany. and every one of those can have a hasMany to Pivot (i named it Access) too (based on your needs).
User model:
class User extends Model{
/**
* retrive weblogs
*
* #return BelongsToMany weblogs
*/
public function weblogs()
{
return $this->belongsToMany(App\WebLog::class,'accesses_table')
->withPivot("permission_id")
->using(App\Access::class);
}
/**
* retrive permissions
*
* #return BelongsToMany permissions
*/
public function permissions()
{
return $this->belongsToMany(App\Permission::class,'accesses_table')
->withPivot("weblog_id")
->using(App\Access::class);
}
/**
* retrive access
*
* #return hasMany [description]
*/
public function accesses()
{
return $this->hasMany(App\Access::class, "user_id");
}
}
Weblog model:
class Weblog extends Model{
/**
* retrive users
*
* #return BelongsToMany users
*/
public function users()
{
return $this->belongsToMany(App\User::class,'accesses_table')
->withPivot("permission_id")
->using(App\Access::class);
}
/**
* retrive permissions
*
* #return BelongsToMany permissions
*/
public function permissions()
{
return $this->belongsToMany(App\Permission::class,'accesses_table')
->withPivot("user_id")
->using(App\Access::class);
}
/**
* retrive access
*
* #return hasMany [description]
*/
public function accesses()
{
return $this->hasMany(App\Access::class, "weblog_id");
}
}
Permission model:
class Permission extends Model{
/**
* retrieve users
*
* #return BelongsToMany users
*/
public function users()
{
return $this->belongsToMany(App\User::class,'accesses_table')
->withPivot("weblog_id")
->using(App\Access::class);
}
/**
* retrieve weblogs
*
* #return BelongsToMany weblogs
*/
public function weblogs()
{
return $this->belongsToMany(App\Weblog::class,'accesses_table')
->withPivot("user_id")
->using(App\Access::class);
}
/**
* retrive access
*
* #return hasMany [description]
*/
public function accesses()
{
return $this->hasMany(App\Access::class, "permission_id");
}
}
and you can have a model for your pivot, which i named it Access :
Illuminate\Database\Eloquent\Relations\Pivot;
class Access extends Pivot
{
public $incrementing = true;
public function user()
{
return $this->belongsTo(App\User::class);
}
public function weblog()
{
return $this->belongsTo(App\Weblog::class);
}
public function permission()
{
return $this->belongsTo(App\Permission::class);
}
}

Laravel: relations on same table

In my application I have a users table, in this table there is a field called managedByUsername which is the username of that particular user's manager.
To get your employees specifically you could perform a query as follows:
$employees = User::where('managedByUsername', auth()->user->username)->get()
To get your manager, on the User model you could have the relation;
public function mananager()
{
return $this->belongsTo(User::class, 'username', 'managedByUsername');
}
However, I can't think of how you would do this the other way around?
Perhaps
public function employees()
{
return $this->hasMany(User::class, 'username', 'managedByUsername');
}
But this obviously wouldn't work.
I have also tried the following:
/**
* Get the manager for this user
*
* #return void
*/
public function mananager()
{
return $this->belongsTo(User::class, 'managedByUsername', 'username');
}
/**
* Get the manager for this user
*
* #return void
*/
public function employees()
{
return $this->hasMany(User::class, 'managedByUsername', 'username');
}
The best approach to solve this problem would be to use the id of the user as the foreign key for manager.
So replace managedByUsername field with manager_id.
Then, you can write your Eloquent relations as:
public function mananager()
{
return $this->belongsTo(User::class, 'manager_id');
}
public function employees()
{
return $this->hasMany(User::class, 'manager_id');
}
Hope this helps to solve your problem.

Role based permission to Laravel

I am trying to do a role based permission control in a Laravel application. I want to check what actions can some user do, but i can't figure out how to implement gates and policies in my model (the permission description is in the database and are booleans asociated to a table that stores the resource's ids).
This is the database model that im using:
I would like to know if laravel gates is useful in my case, and how can i implement it, if not, how to make a basic middleware that take care of permission control to protect routes (or controllers).
In the table resource i have a uuid that identifies the resources, the alias is the name of the resource and has dot notation values of actions or context of the resource (eg. 'mysystem.users.create', 'mysystem.roles.delete', 'mysystem.users.images.view'). The policy tables has a boolean 'allow' field that describes the permission of users.
Thanks in advance.
This is the way that I implement role based permissions in Laravel using Policies.
Users can have multiple roles.
Roles have associated permissions.
Each permission allows a specific action on a specific model.
Migrations
Roles 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
Permissions 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 Pivot Table
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
Role User Pivot 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();
}
}
Permission
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 a model item. The policy defines the 'rules' for the 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 in 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 authorisation action from the policy.
For example, in the index method of 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 has a specific role:
#if (Auth::user()->hasRole('item-administrator'))
// do stuff
#endif
or if a specific permission is required:
#can('create', App\User::class)
// do stuff
#endcan
Answer for your Question:how to make a basic middleware that take care of permission control to protect routes (or controllers)?.
Just an Example:
Here is the simple role middleware for your routes
AdminRole
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 your kernel.php file as
protected $routeMiddleware = [
..............
'admin' =>\App\Http\Middleware\AdminRole::class,
...................
];
And to use this route middleware:
There are different way to use route middleware but following is one example
Route::group(['middleware' => ['auth','admin']], function () {
Route::get('/', 'AdminController#index')->name('admin');
});
Note: There are some tools and libraries for roles and permission on laravel but above is the example of creating basic route middle-ware.
Because the laravel model did not fit my database so much, I did almost everything again. This is a functional draft in which some functions are missing, the code is not optimized and it may be a bit dirty, but here it is:
proyect/app/Components/Contracts/Gate.php This interface is used to create 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 the permissions from the database. This could be improved a lot :(
<?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 controller. Allows to use $this->authorize('stuff'); in a controller when is added.
<?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 that can be found on proyect/vendor/laravel/framework/src/Illuminate/Auth/AuthServiceProvider.php, but i changed some parts to add new classe. 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 allow add the 'can' middleware to routes, eg: 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 must 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 a blade template you have to add this 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 at 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 at proyect/app/Models/Security/Group.php THis is the same than 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 a lot of things that I have not put here, but this is what I have so far
The problem i find with trying to combine permissions from a db with policies is when it comes to the ownership of a record.
Ultimately in our code we would like to check access to a resource using permission only. This is because as the list of roles grows we don't want to have to keep adding checks for these roles to the codebase.
If we have a users table we may want 'admin' (role) to be able to update all user records but a 'basic' user to only be able to update their own user record. We would like to be able to control this access SOLELY using the database.
However, if you have an 'update_user' permission then do you give it to both roles?
If you don't give it to the basic user role then the request won't get as far as the policy to check ownership.
Hence, you cannot revoke access for a basic user to update their record from the db alone.
Also the meaning of 'update_user' in the permissions table now implies the ability to update ANY user.
SOLUTION?
Add extra permissions to cater for the case where a user owns the record.
So you could have permissions to 'update_user' AND 'update_own_user'.
The 'admin' user would have the first permission whilst the 'basic' user would have the second one.
Then in the policy we check for the 'update_user' permission first and if it's not present we check for the 'update_own_user'.
If the 'update_own_user' permission is present then we check ownership. Otherwise we return false.
The solution will work but it seems ugly to have to have manage 'own' permissions in the db.

Laravel 5.2 ACL how to have multiple permissions with the same name and avoid having one role per user

Okay, so I'm trying to implement an ACL using Laravel on an Intranet and I'm having some problems with permissions growing rapidly out of control. So first off, here's what I've got:
My five tables defining my users, my roles and my permissions like this:
tblIntranetUser
UserID
Name
FirstName
Username
tblIntranetRoles
RoleID
RoleName
Description
tblIntranetPermissions
PermissionID
PermissionName
Description
tblIntranetRoles_Permissions
RoleID
PermissionID
tblIntranetUsers_Roles
UserID
RoleID
And also I have the AuthServiceProvider as well as the Permission and Role models:
class Permission extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblIntranetPermissions';
protected $primaryKey = 'PermissionID';
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['PermissionID', 'PermissionName', 'Description'];
public function roles()
{
return $this->belongsToMany('App\Role', 'tblIntranetRoles_Permissions', 'PermissionID', 'RoleID');
}
public function detachAllRoles()
{
$roles = $this->roles;
foreach($roles as $role){
$role->permissions()->detach($this);
}
}
}
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\User;
class Role extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblIntranetRoles';
protected $primaryKey = 'RoleID';
public $timestamps = false;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['RoleID', 'RoleName', 'Description'];
public function permissions()
{
return $this->belongsToMany('App\Permission', 'tblIntranetRoles_Permissions', 'RoleID', 'PermissionID');
}
public function givePermissionTo(Permission $permission)
{
return $this->permissions()->save($permission);
}
public function getUsers()
{
$users = User::orderBy('UserID')->get();
$roleusers = collect();
foreach($users as $user){
if($user->hasRole($this->name)){
$roleusers->push($user);
}
}
return $roleusers;
}
public function detachAllUsers()
{
$users = $this->getUsers();
foreach($users as $user){
$user->roles()->detach($this);
}
}
public function detachAllPermissions()
{
$permissions = $this->permissions;
foreach($permissions as $permission){
$permission->roles()->detach($this);
}
}
}
namespace App\Providers;
use App\Report, App\Permission;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
];
/**
* Register any application authentication / authorization services.
*
* #param \Illuminate\Contracts\Auth\Access\Gate $gate
* #return void
*/
public function boot(GateContract $gate)
{
$this->registerPolicies($gate);
foreach ($this->getPermissions() as $permission){
$gate->before(function ($user) {
if ($user->isSuperAdmin()) {
return true;
}
});
$gate->define($permission->name, function($user) use ($permission){
return $user->hasRole($permission->roles);
});
}
}
protected function getPermissions()
{
return Permission::with('roles')->get();
}
}
So, thanks to this, I was able to create various roles and assign permissions to them which allows them to access certain sections of the Intranet as well as see certain reports. For example, I can define the following:
Role: Analyst
Access: Section 1, 2, 3
Reports: 1,15,41
Role: Developer
Access: All sections
Reports: All reports
It would be fine if every analyst could see and access the same sections... but of course that's not the case. Same goes for developers. Following this model, it basically means I need to have one role for every user as well as one permission for every possible element on the Intranet. Given that there's roughly 200 reports available as well as about 30 users, this makes for a lot of "show_report_1", "show_report_2", "show_section_1", "show_section_2" permissions (Laravel identifies permissions by name).
So, in order to make things a bit more... orderly I guess, I've been wondering if there wouldn't be a way to have one permission named "show_report" with the reportID stored in another field and to avoid having one role per user.
I'm not sure of the "proper" way to do this, but you could add an extra row to one of your pivot tables (probably the role_permission one) and use that to store more specific data about the permission. (eg. sections they can access)
Check out here for accessing pivot values: https://laravel.com/docs/5.5/eloquent-relationships#many-to-many
$role = App\Role::find(1);
foreach ($role->permissions as $permission) {
echo $permission->pivot->permission_settings; // [1,2,3]
}
That way you could have a single permission of "access_section", and then just check the pivot to see what sections they can access.
(There is probably a better or 'proper' way to do this though)

Laravel 5 RESTful API - Understanding resource concepts

I am developing a RESTful API with Laravel 5. I have some resource in my routes.php file and everything works properly.
But now I had added auth.basic middleware and I want introduce the user roles and I got confused.
In my Controller I have a constructor to call 2 middleware, auth.basic and roles middleware, but cannot continue because of lack of knowledge.
What do I need? Well, I need to set the user roles who can access every controller, but cant achieve this. Im the Controller I would like to access the user to check his role and compare it with the role stablished on the Controller, but I don't know how to access the user, can you help me?
EDIT:
I put this on the constructor of the Controller
public function __construct(Request $request)
{
$actions = $request->route()->setAction( ['roles' => ['admin', 'seller', 'buyer']]);
$this->middleware('auth.basic');
$this->middleware('roles');
}
Basically I inject the request in the controller constructor and then I set an action called roles.
Then I call middleware auth.basic to set the user.
And last call middleware roles which checks the user role against the roles array in the request, if it has the role or if he is root, then result is true, else I return error:
return response([
'error' => [
'code' => 'INSUFFICIENT_ROLE',
'description' => 'You are not authorized to access this resource.'
]
], 401);
Now I have the error that always get:
{"error":{"code":"INSUFFICIENT_ROLE","description":"You are not authorized to access this resource."}}
Thats because User model dont returns a Role. See my class:
class User extends Model implements AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['username', 'email', 'password', 'role_id'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = ['password', 'remember_token'];
//Comprobacion del rol del usuario
public function hasRole($roles)
{
$this->have_role = $this->getUserRole();
// Check if the user is a root account
if($this->have_role->name == 'root') {
return true;
}
if(is_array($roles)){
foreach($roles as $need_role){
if($this->checkIfUserHasRole($need_role)) {
return true;
}
}
} else{
return $this->checkIfUserHasRole($roles);
}
return false;
}
private function getUserRole()
{
return $this->role()->getResults();
}
private function checkIfUserHasRole($need_role)
{
return (strtolower($need_role)==strtolower($this->have_role->name)) ? true : false;
}
//User relation with role
public function role(){
return $this->belongsTo('App\Role');
}
}
Whats wrong?
From your question, I got the following:
How do I handle auth middleware...
Well, let's assume that you have two middlewares auth.basic, auth.admin
You can then have your routes be like:
Route::post('/api/getResponse', ['middleware' => 'auth', function () {
$var = "you have access to this route";
return json_encode($var);
}]);
Here, you set whether or not, and, who has access to this particular route, in this case, only people that have admin rights would be able to access it.
If, for example, you do not have a middleware for "admin" you can create it by running the artisan command php artisan make:middleware admin and then you put your logic inside of the file that has been created. In this instance, the logic would check to see if the user (signed in) has admin rights.
EDIT:
As you pointed out in your reply:
I dont user Route::post, I use Route::resource for handling RESTful API requests
You can therefore use a group, see:
Route::group(['middleware' => 'admin'], function () {
Route::resource('API_USER', 'API_USER_CONTROLLER');
});
This will then allow you to use your admin group as GROUP and therefore, all your routes that have access can go in here. In the past, I have just created separate groups for all of my user groups, i.e. admin would have it's own, user would have its own and moderator would have it's own. But, I believe you can use the following:
Route::group(['before' => 'auth|admin'], function()
{
}
This group reads: should be open to auth users OR admin but I have not fully tested this.
Found the solution!!!!! Thanks Phorce for guiding me, you gave me the idea basics. I post it here for everyone who needs. How to get Role Authentication for a RESTful API with Laravel 5.
Explanation. In the controller of the route I call a constructor for the middleware, first add the attribute roles (sets which roles can access this route) with an injected $request object. Then I call middleware auth.basic for requesting user, and then another middleware to check roles. Ando thats it! All working.
Middleware:
<?php
namespace App\Http\Middleware;
use Closure;
class CheckRole
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
//return $next($request);
// Get the required roles from the route
$roles = $this->getRequiredRoleForRoute($request->route());
// Check if a role is required for the route, and
// if so, ensure that the user has that role.
//print "HasRole:".$request->user()->hasRole($roles).".";
if($request->user()->hasRole($roles) || !$roles)
{
return $next($request);
}
return response([
'error' => [
'code' => 'INSUFFICIENT_ROLE',
'description' => 'You are not authorized to access this resource.'
]
], 401);
}
private function getRequiredRoleForRoute($route)
{
$actions = $route->getAction();
//print "actinos:".print_r($actions);
return isset($actions['roles']) ? $actions['roles'] : null;
}
}
User Model
class User extends Model implements AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['username', 'email', 'password', 'role_id'];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = ['password', 'remember_token'];
protected $have_role;
protected $profile;
//Comprobacion del rol del usuario
public function hasRole($roles)
{
$this->have_role = $this->getUserRole();
//$this->have_role = $this->role()->getResults();
// Check if the user is a root account
if($this->have_role->nombre == 'root') {
return true;
}
if(is_array($roles)){
foreach($roles as $need_role){
if($this->checkIfUserHasRole($need_role)) {
return true;
}
}
} else{
return $this->checkIfUserHasRole($roles);
}
return false;
}
private function getUserRole()
{
return $this->role()->getResults();
}
private function checkIfUserHasRole($need_role)
{
if($need_role === $this->have_role->nombre){
return true;
}else{
return false;
}
//return (strtolower($need_role)==strtolower($this->have_role->name)) ? true : false;
}
//Relaciones de user
public function role(){
return $this->belongsTo('App\Role');
}
}
Routes:
Route::resource('perfiles','PerfilesUsuariocontroller',[ 'only'=>['index','show'] ]);
Controller Constructor method
public function __construct(Request $request)
{
$actions = $request->route()->setAction( ['roles' => ['root', 'admin', 'seller']]);
$this->middleware('auth.basic');
$this->middleware('roles');
}

Categories