Skip to content

feat: Support hierarchical permissions in method #1253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions docs/references/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ public string $defaultGroup = 'user';
## Defining Available Permissions

All permissions must be added to the `AuthGroups` config file, also. A permission is simply a string consisting of
a scope and action, like `users.create`. The scope would be `users` and the action would be `create`. Each permission
can have a description for display within UIs if needed.
a scope and action, like `users.create`. The scope would be `users` and the action would be `create`. A scope can
have sub-scopes, as in `forum.posts.delete`, where the scope is `forum`, the sub-scope is `posts`, and the action
associated with this permission is `delete`. Each permission can have a description for display within UIs if needed.

```php
public array $permissions = [
Expand All @@ -47,7 +48,10 @@ public array $permissions = [
'users.create' => 'Can create new non-admin users',
'users.edit' => 'Can edit existing non-admin users',
'users.delete' => 'Can delete existing non-admin users',
'beta.access' => 'Can access beta-level features'
'beta.access' => 'Can access beta-level features',
'forum.posts.create' => 'Can create posts in the forum',
'forum.posts.edit' => 'Can edit posts in the forum',
'forum.posts.delete' => 'Can delete posts in the forum',
];
```

Expand All @@ -68,16 +72,17 @@ public array $matrix = [
'admin' => [
'admin.access',
'users.create', 'users.edit', 'users.delete',
'beta.access'
'beta.access',
'forum.posts.create', 'forum.posts.edit', 'forum.posts.delete',
],
];
```

You can use a wildcard within a scope to allow all actions within that scope, by using a `*` in place of the action.
You can use a wildcard within a scope or sub-scope to allow all actions within that scope or sub-scope, by using a `*` in place of the action.

```php
public array $matrix = [
'superadmin' => ['admin.*', 'users.*', 'beta.*'],
'superadmin' => ['admin.*', 'users.*', 'beta.*', 'forum.posts.*'],
];
```

Expand All @@ -87,9 +92,11 @@ The `Authorizable` trait on the `User` entity provides the following methods to

#### can()

Allows you to check if a user is permitted to do a specific action or group or actions. The permission string(s) should be passed as the argument(s). Returns
Allows you to check if a user is permitted to do a specific action or group of actions. The permission string(s) should be passed as the argument(s). Returns
boolean `true`/`false`. Will check the user's direct permissions (**user-level permissions**) first, and then check against all of the user's groups
permissions (**group-level permissions**) to determine if they are allowed.
permissions (**group-level permissions**) to determine if they are allowed. When checking against group-level permissions, this includes evaluating
hierarchical wildcard permissions. For example, if a user's group has the permission `forum.posts.*`, a check for `$user->can('forum.posts.create')`
would return `true`.

```php
if ($user->can('users.create')) {
Expand All @@ -100,8 +107,25 @@ if ($user->can('users.create')) {
if ($user->can('users.create', 'users.edit')) {
//
}

// Example with hierarchical wildcard check.
// Assuming the $user is in a group with 'forum.posts.*' permission.
if ($user->can('forum.posts.create')) {
// This will return true
}
```

When checking group-level permissions, Shield automatically creates a hierarchy check by examining parent permissions:

- For permission `forum.posts.create`, it checks: `forum.posts.create`, `forum.posts.*`, and `forum.*`
- For permission `admin.settings`, it checks: `admin.settings` and `admin.*`

This allows for flexible permission management where broader permissions automatically grant access to more specific actions.

!!! warning

Be cautious when granting wildcard permissions, especially at high levels like `admin.*`, as they will grant access to any future permissions added under that scope.

#### inGroup()

Checks if the user is in one of the groups passed in. Returns boolean `true`/`false`.
Expand Down
10 changes: 8 additions & 2 deletions src/Authorization/Traits/Authorizable.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,14 @@ public function can(string ...$permissions): bool
}

// Check wildcard match
$check = substr($permission, 0, strpos($permission, '.')) . '.*';
if (isset($matrix[$group]) && in_array($check, $matrix[$group], true)) {
$checks = [];
$parts = explode('.', $permission);

for ($i = count($parts); $i > 0; $i--) {
$check = implode('.', array_slice($parts, 0, $i)) . '.*';
$checks[] = $check;
}
if (isset($matrix[$group]) && array_intersect($checks, $matrix[$group]) !== []) {
return true;
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/Entities/Group.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,17 @@ public function can(string $permission): bool
}

// Check wildcard match
$check = substr($permission, 0, strpos($permission, '.')) . '.*';
$checks = [];
$parts = explode('.', $permission);

return $this->permissions !== null && $this->permissions !== [] && in_array($check, $this->permissions, true);
for ($i = count($parts); $i > 0; $i--) {
$check = implode('.', array_slice($parts, 0, $i)) . '.*';
$checks[] = $check;
}

return $this->permissions !== null
&& $this->permissions !== []
&& array_intersect($checks, $this->permissions) !== [];
}

/**
Expand Down
22 changes: 22 additions & 0 deletions tests/Authorization/GroupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,26 @@ public function testCan(): void
$this->assertTrue($group2->can('users.edit'));
$this->assertFalse($group2->can('foo.bar'));
}

public function testCanNestedPerms(): void
{
$group = $this->groups->info('user');
$group->addPermission('foo.bar.*');
$group->addPermission('foo.biz.buz.*');
$this->assertTrue($group->can('foo.bar'));
$this->assertTrue($group->can('foo.bar.*'));
$this->assertTrue($group->can('foo.bar.baz'));
$this->assertTrue($group->can('foo.bar.buz'));
$this->assertTrue($group->can('foo.bar.buz.biz'));
$this->assertTrue($group->can('foo.biz.buz'));
$this->assertTrue($group->can('foo.biz.buz.*'));
$this->assertTrue($group->can('foo.biz.buz.bar'));
$this->assertFalse($group->can('foo'));
$this->assertFalse($group->can('foo.*'));
$this->assertFalse($group->can('foo.biz'));
$this->assertFalse($group->can('foo.buz'));
$this->assertFalse($group->can('foo.biz.*'));
$this->assertFalse($group->can('foo.biz.bar'));
$this->assertFalse($group->can('foo.biz.bar.buz'));
}
}
Loading