diff --git a/config/api/routes/users.php b/config/api/routes/users.php index 88658d877a..203e2d2a8c 100644 --- a/config/api/routes/users.php +++ b/config/api/routes/users.php @@ -203,7 +203,9 @@ function ($source, $filename) use ($id) { 'users/(:any)/roles', ], 'action' => function (string $id) { - return $this->user($id)->roles(); + $kirby = $this->kirby(); + $purpose = $kirby->request()->get('purpose'); + return $this->user($id)->roles($purpose); } ], [ diff --git a/src/Cms/User.php b/src/Cms/User.php index 6bc3ac4ce4..96fd48a70d 100644 --- a/src/Cms/User.php +++ b/src/Cms/User.php @@ -574,33 +574,36 @@ public function role(): Role } /** - * Returns all available roles - * for this user, that can be selected - * by the authenticated user + * Returns all available roles for this user, + * that can be selected by the authenticated user + * + * @param string|null $purpose User action for which the roles are used (create, change) */ - public function roles(): Roles + public function roles(string|null $purpose = null): Roles { $kirby = $this->kirby(); $roles = $kirby->roles(); - // a collection with just the one role of the user - $myRole = $roles->filter('id', $this->role()->id()); + // for the last admin, only their current role (admin) is available + if ($this->isLastAdmin() === true) { + // a collection with just the one role of the user + return $roles->filter('id', $this->role()->id()); + } - // if there's an authenticated user … - // admin users can select pretty much any role - if ($kirby->user()?->isAdmin() === true) { - // except if the user is the last admin - if ($this->isLastAdmin() === true) { - // in which case they have to stay admin - return $myRole; - } + // filter roles based on the user action + // as user permissions and/or options can restrict these further + $roles = match ($purpose) { + 'create' => $roles->canBeCreated(), + 'change' => $roles->canBeChanged(), + default => $roles + }; - // return all roles for mighty admins - return $roles; + // exclude the admin role, if the user isn't an admin themselves + if ($kirby->user()?->isAdmin() !== true) { + $roles = $roles->filter(fn ($role) => $role->name() !== 'admin'); } - // any other user can only keep their role - return $myRole; + return $roles; } /** diff --git a/tests/Cms/Users/UserTest.php b/tests/Cms/Users/UserTest.php index e57ed2382a..e25855a64a 100644 --- a/tests/Cms/Users/UserTest.php +++ b/tests/Cms/Users/UserTest.php @@ -341,6 +341,107 @@ public static function passwordProvider(): array ]; } + /** + * @covers ::roles + */ + public function testRoles(): void + { + $app = new App([ + 'roots' => [ + 'index' => '/dev/null' + ], + 'roles' => [ + ['name' => 'admin'], + ['name' => 'editor'], + ['name' => 'guest'] + ], + 'users' => [ + [ + 'email' => 'admin@getkirby.com', + 'role' => 'admin' + ], + [ + 'email' => 'editor@getkirby.com', + 'role' => 'editor' + ] + ], + ]); + + // last admin has only admin role as option + $user = $app->user('admin@getkirby.com'); + $roles = $user->roles()->values(fn ($role) => $role->id()); + $this->assertSame(['admin'], $roles); + + // normal user should not have admin as option + $user = $app->user('editor@getkirby.com'); + $roles = $user->roles()->values(fn ($role) => $role->id()); + $this->assertSame(['editor', 'guest'], $roles); + + // only if current user is admin, normal user can also have admin option + $app->impersonate('admin@getkirby.com'); + $user = $app->user('editor@getkirby.com'); + $roles = $user->roles()->values(fn ($role) => $role->id()); + $this->assertSame(['admin', 'editor', 'guest'], $roles); + } + + /** + * @covers ::roles + */ + public function testRolesFilteredForPurpose(): void + { + $app = new App([ + 'roots' => [ + 'index' => '/dev/null' + ], + 'blueprints' => [ + 'users/admin' => [ + 'name' => 'admin', + ], + 'users/editor' => [ + 'name' => 'editor', + ], + 'users/client' => [ + 'name' => 'client', + 'options' => [ + 'create' => [ + 'editor' => false + ] + ] + ], + 'users/guest' => [ + 'name' => 'guest', + 'options' => [ + 'changeRole' => [ + 'editor' => false + ] + ] + ] + ], + 'users' => [ + [ + 'email' => 'admin@getkirby.com', + 'role' => 'admin' + ], + [ + 'email' => 'editor@getkirby.com', + 'role' => 'editor' + ] + ], + ]); + + $app->impersonate('editor@getkirby.com'); + $user = $app->user('editor@getkirby.com'); + + $roles = $user->roles()->values(fn ($role) => $role->id()); + $this->assertSame(['client', 'editor', 'guest'], $roles); + + $roles = $user->roles('create')->values(fn ($role) => $role->id()); + $this->assertSame(['editor', 'guest'], $roles); + + $roles = $user->roles('change')->values(fn ($role) => $role->id()); + $this->assertSame(['client', 'editor'], $roles); + } + public function testSecret() { $app = new App([