JosephSilber / bouncer

Laravel Eloquent roles and abilities.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

User not getting access despite being granted role that has permission

abodnar opened this issue · comments

Per our conversation on Mastodon, I'm running into the situation where despite a user having the role that has the ability to view clients, I'm receiving a 403.

Setup:

$adminRole = Bouncer::role()->firstOrCreate([
    'name' => 'admin',
    'title' => 'Administrator',
]);

$viewerRole = Bouncer::role()->firstOrCreate([
    'name' => 'viewer',
    'title' => 'Viewer',
]);

// Set up the abilities/permissions
$userEditAbility = Bouncer::ability()->firstOrCreate([
    'name' => 'user_edit',
    'title' => 'Edit Users',
]);

$userViewAbility = Bouncer::ability()->firstOrCreate([
    'name' => 'user_view',
    'title' => 'View Users',
]);

$clientEditAbility = Bouncer::ability()->firstOrCreate([
    'name' => 'client_edit',
    'title' => 'Edit Clients',
]);

$clientViewAbility = Bouncer::ability()->firstOrCreate([
    'name' => 'client_view',
    'title' => 'View Clients',
]);

// Set up Admin role's abilities
Bouncer::allow($adminRole)->to($userEditAbility, User::class);
Bouncer::allow($adminRole)->to($userViewAbility, User::class);
Bouncer::allow($adminRole)->to($clientEditAbility, Client::class);
Bouncer::allow($adminRole)->to($clientViewAbility, Client::class);

// Set up Viewer role's abilities
Bouncer::allow($viewerRole)->to($userViewAbility, User::class);
Bouncer::allow($viewerRole)->to($clientViewAbility, Client::class);

$user = User::create([
'name' => 'Adam',
'email' => 'my@email.com'
]);

$user->assign('admin');

$client = Client::create([
'name' => 'Client X'
]);

User class:

class User extends Authenticatable
{
    use SoftDeletes, HasApiTokens, HasFactory, Notifiable, HasRolesAndAbilities;

    public function clients(): BelongsToMany
    {
        return $this->belongsToMany(Client::class)->withTimestamps();
    }
}

Client class:

class Client extends Model
{
    use SoftDeletes, HasFactory;

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class)->withTimestamps();
    }
}

Route:

Route::prefix('v1')->group(static function () {
    Route::middleware('auth:api')->group(function () {
        Route::prefix('clients')->as('clients.v1.')
            ->group(static function () {
                Route::get('/', \App\Http\Controllers\Clients\ListClientController::class)->name('list');
            });

Controller:

class ListClientController extends Controller
{
    public function __invoke(ListRequest $request, ClientsGetMany $clientsGetManyService): object
    {
        $this->authorize('client_view', Client::class);

        // Get clients

        if (! $clientsCollection) {
            return $this->response->success([], 'no data found');
        }

        return $this->response->success(ClientResource::collection($clientsCollection)->appends($request->query()));
    }
}

I tried two other situations.

I changed the $this->authorize('client_view'); and that did work for my user, but if I try $this->authorize('client_view', $client); it fails for the Admin user.

The problem lies in this piece of code:

$clientViewAbility = Bouncer::ability()->firstOrCreate([
    'name' => 'client_view',
    'title' => 'View Clients',
]);

This ability is not connected to any model. To create a model ability, you must set the entity_type:

$clientViewAbility = Bouncer::ability()->firstOrCreate([
    'name' => 'view', // You can use 'client_view', though I wouldn't recommend it
    'title' => 'View Clients',
    'entity_type' => Client::class,
]);

Then assign it using:

Bouncer::allow($viewerRole)->to($clientViewAbility);

What you were doing was mixing two separate things. The to method takes either an actual ability model or a name and entity. Not both.

So you can do either:

Bouncer::allow($viewerRole)->to($abilityModel);

or

Bouncer::allow($viewerRole)->to('view', Client::class);

...in which case Bouncer will automatically find/create the ability for you.


If you look at the to method:

public function to($abilities, $model = null, array $attributes = [])
{
if ($this->shouldConductLazy(...func_get_args())) {
return $this->conductLazy($abilities);
}
$ids = $this->getAbilityIds($abilities, $model, $attributes);

...and follow it down to the getAbilityIds method:

protected function getAbilityIds($abilities, $model = null, array $attributes = [])
{
if ($abilities instanceof Model) {
return [$abilities->getKey()];
}

...you'll see that if the first argument is an actual ability, the rest of the arguments are ignored.

🤦 I can't believe how simple that was. That totally solved it for me and make sense.

Thanks for working through this with me.