+ @endif
@if(Auth::user()->isOpenIdServerAdmin() || Auth::user()->isOAuth2ServerAdmin() || Auth::user()->isSuperAdmin())
diff --git a/resources/views/oauth2/profile/clients.blade.php b/resources/views/oauth2/profile/clients.blade.php
index ab985315..486a8531 100644
--- a/resources/views/oauth2/profile/clients.blade.php
+++ b/resources/views/oauth2/profile/clients.blade.php
@@ -63,6 +63,7 @@
serverPrivateKeysAdminText: '{{ __("Private Keys") }}',
settingsText: '{{ __('Settings') }}',
usersAdminText: '{{ __("Users") }}',
+ canAccessOAuth2Console: parseInt('{{ Auth::user()->canAccessOAuth2Console() ? 1 : 0 }}') === 1,
isOAuth2ServerAdmin: parseInt('{{ Auth::user()->isOAuth2ServerAdmin() }}') === 1 ? true : false,
isOpenIdServerAdmin: parseInt('{{ Auth::user()->isOpenIdServerAdmin() }}') === 1 ? true : false,
isSuperAdmin: parseInt('{{ Auth::user()->isSuperAdmin() }}') === 1 ? true : false
diff --git a/resources/views/oauth2/profile/edit-client.blade.php b/resources/views/oauth2/profile/edit-client.blade.php
index 2a68b9ba..4e2e453d 100644
--- a/resources/views/oauth2/profile/edit-client.blade.php
+++ b/resources/views/oauth2/profile/edit-client.blade.php
@@ -77,6 +77,7 @@
serverPrivateKeysAdminText: '{{ __("Private Keys") }}',
settingsText: '{{ __('Settings') }}',
usersAdminText: '{{ __("Users") }}',
+ canAccessOAuth2Console: parseInt('{{ Auth::user()->canAccessOAuth2Console() ? 1 : 0 }}') === 1,
isOAuth2ServerAdmin: parseInt('{{ Auth::user()->isOAuth2ServerAdmin() }}') === 1 ? true : false,
isOpenIdServerAdmin: parseInt('{{ Auth::user()->isOpenIdServerAdmin() }}') === 1 ? true : false,
isSuperAdmin: parseInt('{{ Auth::user()->isSuperAdmin() }}') === 1 ? true : false
diff --git a/resources/views/profile.blade.php b/resources/views/profile.blade.php
index 335094e7..c59b733f 100644
--- a/resources/views/profile.blade.php
+++ b/resources/views/profile.blade.php
@@ -72,6 +72,7 @@
serverPrivateKeysAdminText: '{{ __("Private Keys") }}',
settingsText: '{{ __('Settings') }}',
usersAdminText: '{{ __("Users") }}',
+ canAccessOAuth2Console: parseInt('{{ Auth::user()->canAccessOAuth2Console() ? 1 : 0 }}') === 1,
isOAuth2ServerAdmin: parseInt('{{ Auth::user()->isOAuth2ServerAdmin() }}') === 1 ? true : false,
isOpenIdServerAdmin: parseInt('{{ Auth::user()->isOpenIdServerAdmin() }}') === 1 ? true : false,
isSuperAdmin: parseInt('{{ Auth::user()->isSuperAdmin() }}') === 1 ? true : false
diff --git a/routes/web.php b/routes/web.php
index b49a3547..270845cb 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -135,9 +135,11 @@
Route::group(['prefix' => 'admin', 'middleware' => ['ssl', 'auth']], function () {
//client admin UI
- Route::get('clients/edit/{id}', ['middleware' => ['oauth2.currentuser.allow.client.edition'], 'uses' => 'AdminController@editRegisteredClient']);
- Route::get('clients', 'AdminController@listOAuth2Clients');
- Route::get('/grants', 'AdminController@editIssuedGrants');
+ Route::group(['middleware' => ['oauth2.console.access']], function () {
+ Route::get('clients/edit/{id}', ['middleware' => ['oauth2.currentuser.allow.client.edition'], 'uses' => 'AdminController@editRegisteredClient']);
+ Route::get('clients', 'AdminController@listOAuth2Clients');
+ Route::get('/grants', 'AdminController@editIssuedGrants');
+ });
//oauth2 server admin UI
Route::group(['middleware' => ['oauth2.currentuser.serveradmin']], function () {
@@ -241,7 +243,7 @@
});
//client api
- Route::group(array('prefix' => 'clients'), function () {
+ Route::group(array('prefix' => 'clients', 'middleware' => ['oauth2.console.access.json']), function () {
Route::get('', 'ClientApiController@getAll');
Route::post('', 'ClientApiController@create');
diff --git a/tests/OAuth2ConsoleAccessTest.php b/tests/OAuth2ConsoleAccessTest.php
new file mode 100644
index 00000000..d56f78ca
--- /dev/null
+++ b/tests/OAuth2ConsoleAccessTest.php
@@ -0,0 +1,63 @@
+makePartial();
+ $user->shouldReceive('belongToGroup')->andReturnUsing(fn(string $slug) => in_array($slug, $slugs, true));
+ return $user;
+ }
+
+ public function testDeniedWhenConfigIsEmpty()
+ {
+ Config::set('oauth2.console_allowed_groups', []);
+ // no bypass, not even for super admins
+ $this->assertFalse($this->userInGroups(['super-admins', 'oauth2-server-admins'])->canAccessOAuth2Console());
+ }
+
+ public function testDeniedWhenConfigIsMissing()
+ {
+ Config::offsetUnset('oauth2.console_allowed_groups');
+ $this->assertFalse($this->userInGroups(['super-admins'])->canAccessOAuth2Console());
+ }
+
+ public function testAllowedForMemberOfConfiguredGroup()
+ {
+ Config::set('oauth2.console_allowed_groups', ['oauth2-console-users', 'sponsors']);
+ $this->assertTrue($this->userInGroups(['sponsors'])->canAccessOAuth2Console());
+ }
+
+ public function testDeniedForNonMember()
+ {
+ Config::set('oauth2.console_allowed_groups', ['oauth2-console-users']);
+ $this->assertFalse($this->userInGroups(['raw-users', 'super-admins'])->canAccessOAuth2Console());
+ }
+
+ public function testEnvParsingIgnoresBlanksAndWhitespace()
+ {
+ $parse = fn(string $v) => array_values(array_filter(array_map('trim', explode(',', $v))));
+ $this->assertSame([], $parse(''));
+ $this->assertSame([], $parse(' , ,'));
+ $this->assertSame(['a', 'b'], $parse(' a , ,b '));
+ }
+}
diff --git a/tests/OAuth2ConsoleRoutesTest.php b/tests/OAuth2ConsoleRoutesTest.php
new file mode 100644
index 00000000..93642d57
--- /dev/null
+++ b/tests/OAuth2ConsoleRoutesTest.php
@@ -0,0 +1,183 @@
+ https redirect of the 'ssl' middleware masking the gate responses
+ Config::set('server.ssl_enabled', false);
+ }
+
+ private function findUser(string $identifier): User
+ {
+ // drop seeder-built in-memory entities so Doctrine hydrates fully initialized ones from the DB
+ EntityManager::clear();
+ return EntityManager::getRepository(User::class)->findOneBy(['identifier' => $identifier]);
+ }
+
+ private function superAdmin(): User
+ {
+ return $this->findUser('sebastian.marcet');
+ }
+
+ private function plainUser(): User
+ {
+ $user = $this->findUser('2');
+ if (!$this->plainUserPrepared) {
+ // seeded users are all super admins: strip their groups once to get a plain user
+ $user->getGroups()->clear();
+ EntityManager::persist($user);
+ EntityManager::flush();
+ $this->plainUserPrepared = true;
+ $this->assertFalse($user->isSuperAdmin());
+ $this->assertFalse($user->belongToGroup(self::AllowedSlug));
+ }
+ return $user;
+ }
+
+ private function addToAllowedGroup(User $user): void
+ {
+ $group = EntityManager::getRepository(Group::class)->findOneBy(['slug' => self::AllowedSlug]);
+ $user->addToGroup($group);
+ EntityManager::persist($user);
+ EntityManager::flush();
+ }
+
+ private function webUrls(): array
+ {
+ $client = EntityManager::getRepository(Client::class)->findOneBy(['app_name' => 'oauth2_test_app']);
+ return ['/admin/clients', '/admin/grants', '/admin/clients/edit/' . $client->id];
+ }
+
+ private function apiUrls(): array
+ {
+ return [
+ '/admin/api/v1/clients',
+ '/admin/api/v1/clients/me/access-tokens',
+ '/admin/api/v1/clients/me/refresh-tokens',
+ ];
+ }
+
+ public function testGuestIsRedirectedToLogin()
+ {
+ Config::set('oauth2.console_allowed_groups', [self::AllowedSlug]);
+ $this->call('GET', '/admin/clients');
+ $this->assertResponseStatus(302);
+ $this->assertStringContainsString('/auth/login', $this->response->headers->get('Location'));
+ }
+
+ public function testEverybodyDeniedWhenConfigIsEmpty()
+ {
+ Config::set('oauth2.console_allowed_groups', []);
+ $this->addToAllowedGroup($this->plainUser());
+
+ foreach ([$this->superAdmin(), $this->plainUser()] as $user) {
+ $this->be($user);
+ foreach ($this->webUrls() as $url) {
+ $this->call('GET', $url);
+ $this->assertResponseStatus(404, "web $url");
+ }
+ foreach ($this->apiUrls() as $url) {
+ $this->call('GET', $url);
+ $this->assertResponseStatus(403, "api $url");
+ }
+ }
+ }
+
+ public function testApiCreateDeniedWhenConfigIsEmpty()
+ {
+ Config::set('oauth2.console_allowed_groups', []);
+ $this->be($this->superAdmin());
+ Session::start();
+ $before = count(EntityManager::getRepository(Client::class)->findAll());
+
+ $this->call('POST', '/admin/api/v1/clients', ['_token' => Session::token(), 'app_name' => 'blocked_app']);
+ $this->assertResponseStatus(403);
+
+ EntityManager::clear();
+ $this->assertCount($before, EntityManager::getRepository(Client::class)->findAll());
+ }
+
+ public function testAllowedForMemberOfConfiguredGroup()
+ {
+ Config::set('oauth2.console_allowed_groups', [self::AllowedSlug]);
+ $user = $this->plainUser();
+ $this->addToAllowedGroup($user);
+ $this->be($user);
+
+ foreach ($this->apiUrls() as $url) {
+ $this->call('GET', $url);
+ $this->assertNotEquals(403, $this->response->getStatusCode(), "api $url");
+ $this->assertNotEquals(404, $this->response->getStatusCode(), "api $url");
+ }
+ foreach (['/admin/clients', '/admin/grants'] as $url) {
+ $this->call('GET', $url);
+ $this->assertResponseStatus(200, "web $url");
+ }
+ }
+
+ public function testNonMemberDeniedWhenGroupConfigured()
+ {
+ Config::set('oauth2.console_allowed_groups', [self::AllowedSlug]);
+ // super admin is not in the configured group: no bypass
+ foreach ([$this->superAdmin(), $this->plainUser()] as $user) {
+ $this->be($user);
+ foreach ($this->webUrls() as $url) {
+ $this->call('GET', $url);
+ $this->assertResponseStatus(404, "web $url");
+ }
+ foreach ($this->apiUrls() as $url) {
+ $this->call('GET', $url);
+ $this->assertResponseStatus(403, "api $url");
+ }
+ }
+ }
+
+ public function testMenuVisibilityFollowsGate()
+ {
+ $user = $this->plainUser();
+ // add to the group before authenticating: only super admins can alter memberships
+ $this->addToAllowedGroup($user);
+ $this->be($user);
+
+ // member of the group but empty config: still denied
+ Config::set('oauth2.console_allowed_groups', []);
+ $this->call('GET', '/accounts/user/profile');
+ $this->assertResponseStatus(200);
+ $this->assertStringContainsString('canAccessOAuth2Console: parseInt(\'0\')', $this->response->getContent());
+
+ Config::set('oauth2.console_allowed_groups', [self::AllowedSlug]);
+ $this->call('GET', '/accounts/user/profile');
+ $this->assertResponseStatus(200);
+ $this->assertStringContainsString('canAccessOAuth2Console: parseInt(\'1\')', $this->response->getContent());
+ }
+}