Skip to content
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

feat: add support for multiple permissions check for users #791

Merged
merged 7 commits into from
Aug 22, 2023
Merged
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
7 changes: 6 additions & 1 deletion docs/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,19 @@ 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. The only argument is the permission string. Returns
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
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.

```php
if ($user->can('users.create')) {
//
}

// If multiple permissions are specified, true is returned if the user has any of them.
if ($user->can('users.create', 'users.edit')) {
//
}
```

#### inGroup()
Expand Down
65 changes: 35 additions & 30 deletions src/Authorization/Traits/Authorizable.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,49 +226,54 @@ public function hasPermission(string $permission): bool

/**
* Checks user permissions and their group permissions
* to see if the user has a specific permission.
* to see if the user has a specific permission or group
* of permissions.
*
* @param string $permission string consisting of a scope and action, like `users.create`
* @param string $permissions string(s) consisting of a scope and action, like `users.create`
*/
public function can(string $permission): bool
public function can(string ...$permissions): bool
{
if (strpos($permission, '.') === false) {
throw new LogicException(
'A permission must be a string consisting of a scope and action, like `users.create`.'
. ' Invalid permission: ' . $permission
);
}

// Get user's permissions and store in cache
$this->populatePermissions();

$permission = strtolower($permission);

// Check user's permissions
if (in_array($permission, $this->permissionsCache, true)) {
return true;
}

// Check the groups the user belongs to
$this->populateGroups();

if (! count($this->groupCache)) {
return false;
}
foreach ($permissions as $permission) {
// Permission must contain a scope and action
if (strpos($permission, '.') === false) {
throw new LogicException(
'A permission must be a string consisting of a scope and action, like `users.create`.'
. ' Invalid permission: ' . $permission
);
}

$matrix = function_exists('setting')
? setting('AuthGroups.matrix')
: config('AuthGroups')->matrix;
$permission = strtolower($permission);

foreach ($this->groupCache as $group) {
// Check exact match
if (isset($matrix[$group]) && in_array($permission, $matrix[$group], true)) {
// Check user's permissions
if (in_array($permission, $this->permissionsCache, true)) {
return true;
}

// Check wildcard match
$check = substr($permission, 0, strpos($permission, '.')) . '.*';
if (isset($matrix[$group]) && in_array($check, $matrix[$group], true)) {
return true;
if (! count($this->groupCache)) {
return false;
}

$matrix = function_exists('setting')
? setting('AuthGroups.matrix')
: config('AuthGroups')->matrix;

foreach ($this->groupCache as $group) {
// Check exact match
if (isset($matrix[$group]) && in_array($permission, $matrix[$group], true)) {
return true;
}

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

Expand Down
22 changes: 22 additions & 0 deletions tests/Authorization/AuthorizableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
$this->assertSame($this->user, $this->user->removeGroup('admin'));
}

public function testRemoveGroupExistingGroup(): void

Check warning on line 101 in tests/Authorization/AuthorizableTest.php

View workflow job for this annotation

GitHub Actions / PHP 7.4 - MySQLi - highest

Took 2.00s from 0.50s limit to run Tests\\Authorization\\AuthorizableTest::testRemoveGroupExistingGroup
{
$this->hasInDatabase($this->tables['groups_users'], [
'user_id' => $this->user->id,
Expand Down Expand Up @@ -127,7 +127,7 @@
]);
}

public function testSyncGroups(): void

Check warning on line 130 in tests/Authorization/AuthorizableTest.php

View workflow job for this annotation

GitHub Actions / PHP 7.4 - MySQLi - highest

Took 1.98s from 0.50s limit to run Tests\\Authorization\\AuthorizableTest::testSyncGroups
{
$this->hasInDatabase($this->tables['groups_users'], [
'user_id' => $this->user->id,
Expand Down Expand Up @@ -172,7 +172,7 @@
$this->assertFalse($this->user->can('user.manage'));
}

public function testAddPermissionWithExistingPermissions(): void

Check warning on line 175 in tests/Authorization/AuthorizableTest.php

View workflow job for this annotation

GitHub Actions / PHP 7.4 - MySQLi - highest

Took 1.96s from 0.50s limit to run Tests\\Authorization\\AuthorizableTest::testAddPermissionWithExistingPermissions
{
$this->hasInDatabase($this->tables['permissions_users'], [
'user_id' => $this->user->id,
Expand Down Expand Up @@ -207,7 +207,7 @@
$this->assertTrue($this->user->can('users.manage'));
}

public function testAddPermissionsWithUnknownPermission(): void

Check warning on line 210 in tests/Authorization/AuthorizableTest.php

View workflow job for this annotation

GitHub Actions / PHP 7.4 - MySQLi - highest

Took 1.98s from 0.50s limit to run Tests\\Authorization\\AuthorizableTest::testAddPermissionsWithUnknownPermission
{
$this->expectException(AuthorizationException::class);

Expand All @@ -219,7 +219,7 @@
$this->assertSame($this->user, $this->user->removePermission('Admin.access'));
}

public function testRemovePermissionExistingPermissions(): void

Check warning on line 222 in tests/Authorization/AuthorizableTest.php

View workflow job for this annotation

GitHub Actions / PHP 7.4 - MySQLi - highest

Took 2.00s from 0.50s limit to run Tests\\Authorization\\AuthorizableTest::testRemovePermissionExistingPermissions
{
$this->hasInDatabase($this->tables['permissions_users'], [
'user_id' => $this->user->id,
Expand Down Expand Up @@ -305,10 +305,32 @@
$this->assertTrue($this->user->can('developer'));
}

/**
* @see https://github.com/codeigniter4/shield/pull/791#discussion_r1297712860
*/
public function testCanWorksWithMultiplePermissions(): void
{
// Check for user's direct permissions (user-level permissions)
$this->user->addPermission('users.create', 'users.edit');

$this->assertTrue($this->user->can('users.create', 'users.edit'));
$this->assertFalse($this->user->can('beta.access', 'admin.access'));

$this->user->removePermission('users.create', 'users.edit');

$this->assertFalse($this->user->can('users.edit', 'users.create'));

// Check for user's group permissions (group-level permissions)
$this->user->addGroup('superadmin');

$this->assertTrue($this->user->can('admin.access', 'beta.access'));
$this->assertTrue($this->user->can('admin.*', 'users.*'));
}

/**
* @see https://github.com/codeigniter4/shield/pull/238
*/
public function testCreatedAtIfDefaultLocaleSetFaWithAddGroup(): void

Check warning on line 333 in tests/Authorization/AuthorizableTest.php

View workflow job for this annotation

GitHub Actions / PHP 7.4 - MySQLi - highest

Took 1.96s from 0.50s limit to run Tests\\Authorization\\AuthorizableTest::testCreatedAtIfDefaultLocaleSetFaWithAddGroup
{
$currentLocale = Locale::getDefault();
Locale::setDefault('fa');
Expand Down
Loading