Laravel Policies with Many-to-Many relationship - php

Role and Permission are two models and since they have many-to-many relationship, I have an intermediate table called permission_role table. But this doesn't have a Model. I am trying to attach a Permission to a Role. But $this->authorize('create', RolePermission::class); always fails with error "This action is unauthorized."
Route:
Route::post('/rolepermissions/{role}/addpermission', 'RolePermissionController#store')
->name('rolepermission.store');
RolePermissionController:
public function store(StoreRolePermission $request, Role $role)
{
$this->authorize('create', RolePermission::class);
...
}
RolePermissionPolicy:
public function create(User $user)
{
if (($user->usertype == 'ADMIN') || ($user->usertype == 'SUPERADMIN'))
{
return true;
}
else
{
return false;
}
}
Is it because the intermediate table does not have an associated Model?

I had a similar scenario, and I'm doing:
Auth:user()->can('create', RolePermission::class);, instead of authorize.
I think (have not tested) you can also do:
$this->authorize('create', [RolePermission::class, Auth::user()]). The second argument in the array will get passed as the only param in your policy.
I'm very new to Laravel, I got some help to solve a similar issue, hope this works for you.

Related

How to fetch data from database using nested many to many relationships in Laravel

I'm working on Laravel Access Control Level (ACL) system. where is table contains some many to many to relationship. User table has many to many belongsToMany with Role Table and inversely many to many Role has belongsToMany with User table.Again, Role table has many to many belongsToMany relationship with Permission table and inversely Permission has many to many belongsToMany with Role table.
I want to run a query from user table which is fetch the all permissions of a role. this role is assigned to current user through roles table.
Here is my code sample.
User Model
public function roles()
{
return $this->belongstoMany(Role::class);
}
Role Model
public function users()
{
return $this->belongsToMany(User::class);
}
public function permissions()
{
return $this->belongsToMany(Permission::class);
}
Permission Table
public function roles()
{
return $this->belongsToMany(Role::class);
}
I've tried this query using egar loading...
public function hasPermission($permission)
{
if($this->roles()->with('permissions')->get()->pluck('permissions'))
{
return true;
}
return false;
}
But it always return false.
There are many ways to check if one of the roles has given permission.
One example, add hasPermission method to User.php model
public function hasPermission($permission = '') {
if ( empty($permisison) ) {
return false;
}
/*
Find all user roles that have given permission
*/
$rolesWithPermission = $this
->roles()
->whereHas('permissions', function($query) use ($permission) {
$query->where('name', $permission);
})
->get();
/*
If there is at least one role with given permission,
user has permission
*/
return $rolesWithPermission->count() > 0;
}
Or you can do one query with multiple joins between users, roles and permissions, and filter out the result with where('permissions.name', '=', $permission)

Laravel: create a fake belongsToMany withPivot relationship

I have a belongsToMany() relationship between a User and a Group. The user has a level within any group he belongs to.
public function groups()
{
return $this->belongsToMany('App\Group', 'user_group', 'user_id', 'group_id')
->withPivot('level');
}
This works great.
However if the User is an admin, I would like the groups function to return ALL Groups with level = 3, regardless of whether that relationship exists in the pivot table or not.
I can successfully create a Collection which mirrors the data structure as follows:
\App\Group::all()->transform(function ($item, $key) use ($uid) {
$item->pivot = collect(['user_id'=>$uid,'group_id'=>$item->id,'level'=>3]);
return $item;
});
However, I cannot substitute the two outputs as one returns a belongsTo relationship instance and the other returns a Collection. This means I can call ->get() on the former but not the latter.
I thought about using the DB:: facade and creating a Builder for the latter, but I cannot add the Pivot values manually.
Any thoughts on how to achieve this?
-- UPDATE --
I am currently cheating by adding the ->get() inside the groups() method, but this is messy and I would still like to know if there is a better way to solve this problem.
public function groups()
{
if ($this->isAdmin()) {
return \App\Group::all()->transform(function ($item, $key) use ($uid) {
$item->pivot = collect(['user_id'=>$uid,'group_id'=>$item->id,'level'=>3]);
return $item;
});
} else {
return $this->belongsToMany('App\Group', 'user_group', 'user_id', 'group_id')
->withPivot('level')->get();
}
}
So this solution should work(not tested), but it is not the "cleanest" it would be better to access all groups through some other mechanism but because I don't know your admin implemention it is hard to guess.
public function groups()
{
return $this->belongsToMany('App\Group', 'user_group', 'user_id', 'group_id')
->withPivot('level');
}
public function scopeSpecialGroups($query)
{
return $query->when($this->role === 'admin',function($query){
return Group::where('level', '>', 3');
})->when($this->role != 'admin',function($query){
return $query->with('groups');
});
}
Then you should be able to call User::specialGroups()->get();

Laravel hasMany not returning collection

I am building the middleware for permissions.
Permissions are based on either user action types and/or module/element type (i.e. delete button).
The model ActionPermission (table action_permission) has all the permission types while table users__action_permission is the pivot for User & ActionPermission with all users and their permission IDs.
User
has a permissions() method that gets all the permissions for user
public function permissions()
{
return $this->hasMany('App\ActionPermission');
}
checkUserPermissions (middleware)
public function handle($request, Closure $next)
{
$response = $next($request);
$userId = Auth::id();
$userPermissions = User::findOrFail($userId)->permissions()->get();
dd($userPermissions);
}
Since permissions() is looking for user_id key, and ActionPermission model (table action_permission) does not have the relevant user_id key, I need the table users__action_permission which holds he user_id.
My question is if Laravel has a way for User::permissions() to access users__action_permission table, or do I need to build a model for that?
You do not need a model for the pivot table.
in User model
add
public function ActionPermissions() {
return $this->belongsToMany('App\User', 'users_action_permission');
}
in Action Permission model
add
public function Users() {
return $this->belongsToMany('App\ActionPermission', 'users_action_permission');
}

Laravel: Check if all values exists in a relation

I have a many to many relationship between User and Role models and want to check if a user has any role of a given list.
So I typed on model:
public function hasAnyRoles(array $roles) {
return $this->roles()->whereIn('name', $roles)->exists();
}
And on UserController:
// User has only "manager" role.
$user->hasAnyRoles(['admin', 'manager']) // true
The problem is that in some parts I need to verify if the user has all roles of the given list. For example:
// User has only "manager" role.
$user->hasRoles(['admin', 'manager']) // false
I wrote it in a "lazy" mode, that generates n + 1 queries:
public function hasRolesLazy($roles) {
return array_reduce($roles, function ($initial, $role) {
return $this->roles()->where('name', $role)->exists();
}, false);
}
How hasRoles(array $roles) method must be constructed to execute only one query in database? I'm a newbie in SQL so I can't figure out many solutions for this.
Try this:
public function hasAnyRoles(array $roles) {
return $this->whereHas('roles', function ($query) use ($roles) {
return $query->whereIn('name', $roles);
})->isNotEmpty();
}
My recommendation would be, to load all of the roles for the user once using the relationship. When you load a relationship by calling it like a property, Laravel will cache it under the hood, so it doesn't repeat any queries. You can then simply use the loaded roles to implement your two methods:
public function hasAnyRoles(array $roles)
{
// When you call $this->roles, the relationship is cached. Run through
// each of the user's roles.
foreach ($this->roles as $role) {
// If the user's role we're looking at is in the provided $roles
// array, then the user has at least one of them. Return true.
if (in_array($role->name, $roles) {
return true;
}
}
// If we get here, the user does not have any of the required roles.
// Return false.
return false;
}
and the second method:
public function hasRoles(array $roles)
{
// Run through each of the roles provided, that the user MUST have
foreach ($roles as $role) {
// Call the $this->roles relationship, which will return a collection
// and be cached. Find the first role that has the name of the role
// we're looking at.
$found = $this->roles->first(function ($r) use ($role) {
return $r->name == $role;
});
// If a role could not be found in the user's roles that matches the
// one we're looking, then the user does not have the role, return
// false
if (!$found) {
return false;
}
}
// If we get here, the user has all of the roles provided to the method
return true;
}
There are of course many different ways to implement these methods, particularly methods found in the Collection class will help you, but the point is that using $this->roles results in only a single query.

Custom Laravel Relations?

Hypothetical situation: Let's say we have 3 models:
User
Role
Permission
Let's also say User has a many-to-many relation with Role, and Role has a many-to-many relation with Permission.
So their models might look something like this. (I kept them brief on purpose.)
class User
{
public function roles() {
return $this->belongsToMany(Role::class);
}
}
class Role
{
public function users() {
return $this->belongsToMany(User::class);
}
public function permissions() {
return $this->belongsToMany(Permission::class);
}
}
class Permission
{
public function roles() {
return $this->belongsToMany(Role::class);
}
}
What if you wanted to get all the Permissions for a User? There isn't a BelongsToManyThrough.
It seems as though you are sort of stuck doing something that doesn't feel quite right and doesn't work with things like User::with('permissions') or User::has('permissions').
class User
{
public function permissions() {
$permissions = [];
foreach ($this->roles as $role) {
foreach ($role->permissions as $permission) {
$permissions = array_merge($permissions, $permission);
}
}
return $permissions;
}
}
This example is, just one example, don't read too much into it. The point is, how can you define a custom relationship? Another example could be the relationship between a facebook comment and the author's mother. Weird, I know, but hopefully you get the idea. Custom Relationships. How?
In my mind, a good solution would be for that relationship to be described in a similar way to how describe any other relationship in Laravel. Something that returns an Eloquent Relation.
class User
{
public function permissions() {
return $this->customRelation(Permission::class, ...);
}
}
Does something like this already exist?
The closest thing to a solution was what #biship posted in the comments. Where you would manually modify the properties of an existing Relation. This might work well in some scenarios. Really, it may be the right solution in some cases. However, I found I was having to strip down all of the constraints added by the Relation and manually add any new constraints I needed.
My thinking is this... If you're going to be stripping down the constraints each time so that the Relation is just "bare". Why not make a custom Relation that doesn't add any constraints itself and takes a Closure to help facilitate adding constraints?
Solution
Something like this seems to be working well for me. At least, this is the basic concept:
class Custom extends Relation
{
protected $baseConstraints;
public function __construct(Builder $query, Model $parent, Closure $baseConstraints)
{
$this->baseConstraints = $baseConstraints;
parent::__construct($query, $parent);
}
public function addConstraints()
{
call_user_func($this->baseConstraints, $this);
}
public function addEagerConstraints(array $models)
{
// not implemented yet
}
public function initRelation(array $models, $relation)
{
// not implemented yet
}
public function match(array $models, Collection $results, $relation)
{
// not implemented yet
}
public function getResults()
{
return $this->get();
}
}
The methods not implemented yet are used for eager loading and must be declared as they are abstract. I haven't that far yet. :)
And a trait to make this new Custom Relation easier to use.
trait HasCustomRelations
{
public function custom($related, Closure $baseConstraints)
{
$instance = new $related;
$query = $instance->newQuery();
return new Custom($query, $this, $baseConstraints);
}
}
Usage
// app/User.php
class User
{
use HasCustomRelations;
public function permissions()
{
return $this->custom(Permission::class, function ($relation) {
$relation->getQuery()
// join the pivot table for permission and roles
->join('permission_role', 'permission_role.permission_id', '=', 'permissions.id')
// join the pivot table for users and roles
->join('role_user', 'role_user.role_id', '=', 'permission_role.role_id')
// for this user
->where('role_user.user_id', $this->id);
});
}
}
// app/Permission.php
class Permission
{
use HasCustomRelations;
public function users()
{
return $this->custom(User::class, function ($relation) {
$relation->getQuery()
// join the pivot table for users and roles
->join('role_user', 'role_user.user_id', '=', 'users.id')
// join the pivot table for permission and roles
->join('permission_role', 'permission_role.role_id', '=', 'role_user.role_id')
// for this permission
->where('permission_role.permission_id', $this->id);
});
}
}
You could now do all the normal stuff for relations without having to query in-between relations first.
Github
I went a ahead and put all this on Github just in case there are more people who are interested in something like this. This is still sort of a science experiment in my opinion. But, hey, we can figure this out together. :)
Have you looked into the hasManyThrough relationship that Laravel offers?
Laravel HasManyThrough
It should help you retrieve all the permissions for a user.
I believe this concept already exists. You may choose on using Laravel ACL Roles and Permissions or Gate, or a package known as Entrust by zizaco.
Zizaco - Entrust
Laracast - watch video 13 and 14
Good luck!

Categories