JosephSilber / bouncer

Laravel Eloquent roles and abilities.
MIT License
3.43k stars 330 forks source link

How To Add / Remove Roles Temporarily (for current request only) ? #636

Closed ievgen-klymenko-uvoteam closed 1 year ago

ievgen-klymenko-uvoteam commented 1 year ago

Hi!

I'm searching for a way to dynamically add/remove roles without touching database (only for current request).

Am I missing something, or this is not possible?

Is it hard to implement?..

**update* Well, Bouncer does not** naturally have the way to do this. IF you still need to do this, you can .... use my ugly workaround .. https://github.com/JosephSilber/bouncer/issues/636#issuecomment-1692921330 .... i guess...

JosephSilber commented 1 year ago

What would be the use case for that?

ievgen-klymenko-uvoteam commented 1 year ago

We have a set of targeted roles, that are applied only with relation to other models - the models who are owners of abilities' entity targets (so, only 'users' currently). Actual ability [entity] targets are like smaller parts of a 'user'.

Scopes don't fit because these 'role-target-entity-owner-models' are determined on runtime by model filters, which are customizable by admin, so can result to different sets of models('users') each time.

My current hack workaround is overriding Silber\Bouncer\Database\Queries\Abilities:

class AbilitiesQueries extends Abilities
{
    protected static array $dynamicRoles = [];

    public static function addDynamicRoles($roles)
    {
        static::$dynamicRoles = [
            ...static::$dynamicRoles,
            ...$roles,
        ];
    }

    public static function forAuthority(Model $authority, $allowed = true)
    {
        return Models::ability()->where(function ($query) use ($authority, $allowed) {
            $query->whereExists(static::getRoleConstraint($authority, $allowed));
                        if (static::$dynamicRoles) {
            ==>       $query->orWhereExists(static::getDynamicRoleConstraint($allowed));  //  <== This lines I added
                        }
            $query->orWhereExists(static::getAuthorityConstraint($authority, $allowed));
            $query->orWhereExists(static::getEveryoneConstraint($allowed));
        });
    }

    protected static function getDynamicRoleConstraint(bool $allowed)
    {
        return function ($query) use ($allowed) {
            $permissions = Models::table('permissions');
            $abilities = Models::table('abilities');
            $roles = Models::table('roles');

            $query
                ->from($roles)
                ->join($permissions, "$roles.id", '=', "$permissions.entity_id")
                ->whereColumn("$permissions.ability_id", "$abilities.id")
             ==> ->whereIn("$roles.name", static::$dynamicRoles)  //  <== This is the line I added
                ->where("$permissions.forbidden", !$allowed)
                ->where("$permissions.entity_type", Models::role()->getMorphClass());

            Models::scope()->applyToModelQuery($query, $roles);
            Models::scope()->applyToRelationQuery($query, $permissions);
        };
    }
}

Then I override Silber\Bouncer\CachedClipboard:

class AbilityClipboard extends CachedClipboard
{
    public function getFreshAbilities(Model $authority, $allowed)
    {
        return AbilitiesQueries::forAuthority($authority, $allowed)->get();
    }
}

Then in the Middleware:

  1. check if authenticated user has any abnormal ('targeted') role;
  2. check if current route model-binding has 'target' model;
  3. apply filters to that model
  4. if it passes, call:
    AbilitiesQueries::addDynamicRoles($roles);

(made small adjustments for clarity)


It works currently, but I only tested it yesterday so I unsure which bugs and when will arise from this.

JosephSilber commented 1 year ago

I couldn't fully follow your example.

Is that not covered by a custom ownership callback?

ievgen-klymenko-uvoteam commented 1 year ago

I tried to explain as best as I can, sorry!

Ownership would work nice, but can only be used for one kind of filters ("my entities" / "self" filter).

But if we need to simultaneously keep separate ability sets for different ownerships ("my relatives' entities", "my slaves' entities", or any other relation-filter which admin decided to set in dashboard) this would require even harder hacks to do.

I would gladly stop on just "my" / "not-my" separation (and I also think this is enough degree of control) but service that we try to rebuild has this kind of possibility, and we have to imitate it also to be able to migrate....

JosephSilber commented 1 year ago

I unfortunately still do not fully understand the scenario.

Regardless, the answer is no. Bouncer does not have a way to temporarily assign roles in-memory.