bouncer
bouncer copied to clipboard
Scope only roles, not permissions
Hi there, I was reading through the scope contract and I'm generally having trouble figuring out a strategy for only scoping roles and not permissions / abilities. I'm doing this for a multi-tenant application. I'd like to basically have a set of static abilities / permissions that the tenant can't change, but I'd like them to create as many roles as they'd like or assign abilities to roles. Thanks for any advice!
Hmmm. This actually sounds like something that should be supported out-of-the-box.
Let me think about it a little.
Just thinking what the API should look like.
Probably something like:
Bouncer::scope()->to($tenantId)->onlyAbilities();
That's pretty much exactly what I was looking for, and it would be amazing if it was a feature out of the box! Thanks for the quick reply!
I was just thinking about this exactly instance. We have set permissions and I was looking into allowing for our users to create a custom role if they don't exactly want any of our predefined ones.
Hi @JosephSilber do you please have any ETA when and if this will be implemented? Also I would suggest a bit different API, if I get it correctly:
Bouncer::scope()->to($tenantId)->dontScopeAbilities();
I am building an app which has many companies and many suppliers, each user can be a member of multiple companies and suppliers. I want to have a static set of abilities which I use in policies/gates, same for all companies/suppliers, but I want the owner of each company/supplier to have ability to create their own roles, assign static abilities and assign those roles to users. So only the roles and relations should be scoped, thus I could get assigned abilities to the user scoped to specific company, through the scoped relation, yet if I want to remove some abilities or add new ones as the code develops, these changes would be reflected for all tenants.
The proposed API
Bouncer::scope()->to($tenantId)->onlyAbilities();
in my opinion would mean that only abilities would be scoped, not roles and not relations, but we want the opposite, to scope roles and relations, but not abilities. Thanks for your great work on this!
We have this need as well. We have Organization pseudo-'tenants'. Each Organization admin needs to be able to create there own roles with a pre-defined set of abilities, but not alter the 'global' abilities.
Then the roles need to be a scoped to the Orgs relations, but the associated abilities are global across all Orgs.
@genyded I have actually managed to do this, however it is not an easy nor nice process.. I introduced "Scope" header, which determines to which scope the request belongs. My main three scopes are "admin", "supplier" and "company". I then have a scrip which creates those abilities using these three scopes, they determine the global abailities. To assign an ability to a role, they don't have to be in the same scope. So the scope of my Roles is the actual tennant, for example: "company-133" - scope company, id: 133 It is a bit hard to wrap your head around it, however the key piece of info is that the scope of ability doesn't have to be the same as the scope of Role, thus you can create abilities manually using Ability model, then just assign those abilities to specific role.
Here is a snippet of the update role method
public function updateRole(
string $name,
array $abilities,
string $title = null
): Role {
$scope = $this->getScope(); // get current scope - contains type: company, admin, ... and Id of tenant
$role = null;
// Fetch the role specific to the scope type and tenant id
Bouncer::scope()->onceTo($scope->getName(), function () use (&$role, $name) { // $scope->getName() = 'company-133'
$role = Bouncer::role()->firstOrCreate([
'name' => $name
]);
});
// check if the ability names requested to be added to the role are actually available in the scope type
$abilityErrors = $this->verifyAbilityBelongsToScope->executeMultiple($abilities, $scope);
if (count($abilityErrors)) {
foreach ($abilityErrors as $abilityError) {
throw new NoSuchAbilityInScopeException(
__(
'Ability ":ability" is not available in scope ":scope"',
['ability' => $abilityError, 'scope' => $scope->getName()]
)
);
}
}
$role->abilities()->detach();
foreach ($abilities as $ability) {
// Scope once to the "type" scope = company, supplier, admin - without the specific tenant id to get the ability model
Bouncer::scope()->onceTo($scope->getType()->value, function () use ($scope, $ability, $role) {
$abilityModel = Bouncer::ability()
->where('scope', $scope->getType()->value)
->where('name', $ability)
->first();
if ($abilityModel) {
// Scope again to the specific tenant scope = company-133 and add the ability to the role
Bouncer::scope()->onceTo($scope->getName(), function () use ($role, $abilityModel) {
Bouncer::allow($role)->to($abilityModel);
});
}
});
}
if ($title) {
$role->title = $title;
$role->save();
}
return $role;
}
@InToSSH Very cool and thanks for taking the time to provide this!
We were already heading down a very similar path. While we LOVE the capability and function that Bouncer provides, some of the needed returns seem to be missing or make many assumptions that don't really meet most real-world needs. This area is one and here are a couple of others we have run into:
- Forbidden is not included in getAbilities(). So, we call both then merge them 99% of the time. Would be nice to also have a built in getAll() or something (we basically created our own)
- getRoles() only returns 'names' and when we want to show a list to API consumers, we typically want 'titles' to be shown so we have to do our own internal mapping
- There is no exposed Model for 'permissions' and that is where everything is 'glued' together when we need to do things like show a filtered list of roles/abilities - we added our own
On the plus side though, it works overall and gives us (so far) the flexibility we need to meet our complex requirements. The scope and toOwn are awesome!