Skip to content

Commit 7cf4d8b

Browse files
initial file changes from PR 1253
Co-authored-by: bgeneto <bgeneto@duck.com>
1 parent 903ae01 commit 7cf4d8b

File tree

4 files changed

+76
-13
lines changed

4 files changed

+76
-13
lines changed

docs/references/authorization.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ public string $defaultGroup = 'user';
3636
## Defining Available Permissions
3737

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

4243
```php
4344
public array $permissions = [
@@ -47,7 +48,10 @@ public array $permissions = [
4748
'users.create' => 'Can create new non-admin users',
4849
'users.edit' => 'Can edit existing non-admin users',
4950
'users.delete' => 'Can delete existing non-admin users',
50-
'beta.access' => 'Can access beta-level features'
51+
'beta.access' => 'Can access beta-level features',
52+
'forum.posts.create' => 'Can create posts in the forum',
53+
'forum.posts.edit' => 'Can edit posts in the forum',
54+
'forum.posts.delete' => 'Can delete posts in the forum',
5155
];
5256
```
5357

@@ -68,16 +72,19 @@ public array $matrix = [
6872
'admin' => [
6973
'admin.access',
7074
'users.create', 'users.edit', 'users.delete',
71-
'beta.access'
75+
'beta.access',
76+
'forum.posts.create',
77+
'forum.posts.edit',
78+
'forum.posts.delete',
7279
],
7380
];
7481
```
7582

76-
You can use a wildcard within a scope to allow all actions within that scope, by using a `*` in place of the action.
83+
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.
7784

7885
```php
7986
public array $matrix = [
80-
'superadmin' => ['admin.*', 'users.*', 'beta.*'],
87+
'superadmin' => ['admin.*', 'users.*', 'beta.*', 'forum.posts.*'],
8188
];
8289
```
8390

@@ -87,9 +94,11 @@ The `Authorizable` trait on the `User` entity provides the following methods to
8794

8895
#### can()
8996

90-
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
97+
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
9198
boolean `true`/`false`. Will check the user's direct permissions (**user-level permissions**) first, and then check against all of the user's groups
92-
permissions (**group-level permissions**) to determine if they are allowed.
99+
permissions (**group-level permissions**) to determine if they are allowed. When checking against group-level permissions, this includes evaluating
100+
hierarchical wildcard permissions. For example, if a user's group has the permission `forum.posts.*`, a check for `$user->can('forum.posts.create')`
101+
would return `true`.
93102

94103
```php
95104
if ($user->can('users.create')) {
@@ -100,8 +109,26 @@ if ($user->can('users.create')) {
100109
if ($user->can('users.create', 'users.edit')) {
101110
//
102111
}
112+
113+
// Example with hierarchical wildcard check.
114+
// Assuming the $user is in a group with 'forum.posts.*' permission.
115+
if ($user->can('forum.posts.create')) {
116+
// This will return true
117+
}
103118
```
104119

120+
When checking group-level permissions, Shield automatically creates a hierarchy check by examining parent permissions:
121+
122+
- For permission `forum.posts.create`, it checks: `forum.posts.create`, `forum.posts.*`, and `forum.*`
123+
- For permission `admin.settings`, it checks: `admin.settings` and `admin.*`
124+
125+
This allows for flexible permission management where broader permissions automatically grant access to more specific actions.
126+
127+
!!! warning
128+
129+
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.
130+
131+
105132
#### inGroup()
106133

107134
Checks if the user is in one of the groups passed in. Returns boolean `true`/`false`.

src/Authorization/Traits/Authorizable.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ public function can(string ...$permissions): bool
274274
if (! str_contains($permission, '.')) {
275275
throw new LogicException(
276276
'A permission must be a string consisting of a scope and action, like `users.create`.'
277-
. ' Invalid permission: ' . $permission,
277+
. ' Invalid permission: ' . $permission,
278278
);
279279
}
280280

@@ -296,8 +296,14 @@ public function can(string ...$permissions): bool
296296
}
297297

298298
// Check wildcard match
299-
$check = substr($permission, 0, strpos($permission, '.')) . '.*';
300-
if (isset($matrix[$group]) && in_array($check, $matrix[$group], true)) {
299+
$checks = [];
300+
$parts = explode('.', $permission);
301+
302+
for ($i = count($parts); $i > 0; $i--) {
303+
$check = implode('.', array_slice($parts, 0, $i)) . '.*';
304+
$checks[] = $check;
305+
}
306+
if (isset($matrix[$group]) && array_intersect($checks, $matrix[$group]) !== []) {
301307
return true;
302308
}
303309
}

src/Entities/Group.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,17 @@ public function can(string $permission): bool
8585
}
8686

8787
// Check wildcard match
88-
$check = substr($permission, 0, strpos($permission, '.')) . '.*';
88+
$checks = [];
89+
$parts = explode('.', $permission);
8990

90-
return $this->permissions !== null && $this->permissions !== [] && in_array($check, $this->permissions, true);
91+
for ($i = count($parts); $i > 0; $i--) {
92+
$check = implode('.', array_slice($parts, 0, $i)) . '.*';
93+
$checks[] = $check;
94+
}
95+
96+
return $this->permissions !== null
97+
&& $this->permissions !== []
98+
&& array_intersect($checks, $this->permissions) !== [];
9199
}
92100

93101
/**

tests/Authorization/GroupTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,26 @@ public function testCan(): void
8787
$this->assertTrue($group2->can('users.edit'));
8888
$this->assertFalse($group2->can('foo.bar'));
8989
}
90+
91+
public function testCanNestedPerms(): void
92+
{
93+
$group = $this->groups->info('user');
94+
$group->addPermission('foo.bar.*');
95+
$group->addPermission('foo.biz.buz.*');
96+
$this->assertTrue($group->can('foo.bar'));
97+
$this->assertTrue($group->can('foo.bar.*'));
98+
$this->assertTrue($group->can('foo.bar.baz'));
99+
$this->assertTrue($group->can('foo.bar.buz'));
100+
$this->assertTrue($group->can('foo.bar.buz.biz'));
101+
$this->assertTrue($group->can('foo.biz.buz'));
102+
$this->assertTrue($group->can('foo.biz.buz.*'));
103+
$this->assertTrue($group->can('foo.biz.buz.bar'));
104+
$this->assertFalse($group->can('foo'));
105+
$this->assertFalse($group->can('foo.*'));
106+
$this->assertFalse($group->can('foo.biz'));
107+
$this->assertFalse($group->can('foo.buz'));
108+
$this->assertFalse($group->can('foo.biz.*'));
109+
$this->assertFalse($group->can('foo.biz.bar'));
110+
$this->assertFalse($group->can('foo.biz.bar.buz'));
111+
}
90112
}

0 commit comments

Comments
 (0)