Skip to content
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,6 @@ L5_SWAGGER_CONST_TOKEN_URL='/oauth2/token'
# L5_FORMAT_TO_USE_FOR_DOCS=yaml
L5_SWAGGER_GENERATE_ALWAYS=true # Dev setting
L5_SWAGGER_OPEN_API_SPEC_VERSION=3.1.2

# Comma separated group slugs allowed to use the OAUTH2 Console (apps / grants). Empty = nobody.
OAUTH2_CONSOLE_ALLOWED_GROUPS=
2 changes: 2 additions & 0 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ class Kernel extends HttpKernel
'oauth2.currentuser.serveradmin.json' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdminJson::class,
'openstackid.currentuser.serveradmin' => \App\Http\Middleware\CurrentUserIsOpenIdServerAdmin::class,
'openstackid.currentuser.serveradmin.json' => \App\Http\Middleware\CurrentUserIsOpenIdServerAdminJson::class,
'oauth2.console.access' => \App\Http\Middleware\CurrentUserCanAccessOAuth2Console::class,
'oauth2.console.access.json' => \App\Http\Middleware\CurrentUserCanAccessOAuth2ConsoleJson::class,
'oauth2.currentuser.allow.client.edition' => \App\Http\Middleware\CurrentUserCanEditOAuth2Client::class,
'oauth2.currentuser.owns.client' => \App\Http\Middleware\CurrentUserOwnsOAuth2Client::class,
'service.account' => \App\Http\Middleware\EnsureServiceAccount::class,
Expand Down
44 changes: 44 additions & 0 deletions app/Http/Middleware/CurrentUserCanAccessOAuth2Console.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php namespace App\Http\Middleware;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Response;

/**
* Class CurrentUserCanAccessOAuth2Console
* @package App\Http\Middleware
*/
final class CurrentUserCanAccessOAuth2Console
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->guest())
{
return Response::view('errors.404', [], 404);
}
if(!Auth::user()->canAccessOAuth2Console())
{
return Response::view('errors.404', [], 404);
}
return $next($request);
}
}
45 changes: 45 additions & 0 deletions app/Http/Middleware/CurrentUserCanAccessOAuth2ConsoleJson.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php namespace App\Http\Middleware;
/**
* Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Response;

/**
* Class CurrentUserCanAccessOAuth2ConsoleJson
* @package App\Http\Middleware
*/
final class CurrentUserCanAccessOAuth2ConsoleJson
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->guest())
{
return Response::json(array('error' => 'you are not allowed to perform this operation'), 403);
}
if(!Auth::user()->canAccessOAuth2Console())
{
return Response::json(array('error' => 'you are not allowed to perform this operation'), 403);
}

return $next($request);
}
}
15 changes: 15 additions & 0 deletions app/libs/Auth/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,21 @@ public function isOAuth2ServerAdmin(): bool
return $this->belongToGroup(IOAuth2User::OAuth2ServerAdminGroup);
}

/**
* Access to the OAUTH2 Console is granted only to members of the groups
* listed in config('oauth2.console_allowed_groups'); empty list denies everybody.
* @return bool
*/
public function canAccessOAuth2Console(): bool
{
$allowed = Config::get('oauth2.console_allowed_groups', []);
if (!is_array($allowed)) return false;
foreach ($allowed as $slug) {
if (is_string($slug) && $slug !== '' && $this->belongToGroup($slug)) return true;
}
return false;
}

/**
* @return bool
*/
Expand Down
12 changes: 12 additions & 0 deletions config/oauth2.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,16 @@
|
*/
'validate_resource_server_ip' => env('OAUTH2_VALIDATE_RESOURCE_SERVER_IP', false),

/*
|--------------------------------------------------------------------------
| OAuth2 Console Allowed Groups
|--------------------------------------------------------------------------
|
| Comma separated list of group slugs whose members can see the
| "OAUTH2 Console" menu and use /admin/clients, /admin/grants and the
| related admin API. If empty or unset, NOBODY has access (secure default).
|
*/
'console_allowed_groups' => array_values(array_filter(array_map('trim', explode(',', (string)env('OAUTH2_CONSOLE_ALLOWED_GROUPS', ''))))),
];
2 changes: 2 additions & 0 deletions resources/js/components/drawer/drawer.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ function DrawerComponent() {
{menuConfig.settingsText}
</Button>
</ListItem>
{menuConfig.canAccessOAuth2Console &&
<ListItem>
<Button aria-controls="oauth-menu"
aria-haspopup="true"
Expand All @@ -83,6 +84,7 @@ function DrawerComponent() {
onClick={() => goTo(`${menuConfig.oauthGrantsURL}`)}>{menuConfig.oauthGrantsText}</MenuItem>
</Menu>
</ListItem>
}
{(menuConfig.isOAuth2ServerAdmin || menuConfig.isOpenIdServerAdmin || menuConfig.isSuperAdmin) &&
<>
<ListItem>
Expand Down
4 changes: 4 additions & 0 deletions resources/js/components/navbar/navbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export default function NavBar({menuConfig}) {
<Button onClick={() => goTo(`${menuConfig.settingURL}`)}>
{menuConfig.settingsText}
</Button>
{menuConfig.canAccessOAuth2Console &&
<>
<Button aria-controls="oauth-menu"
aria-haspopup="true"
onClick={handleOauthMenuClick}
Expand All @@ -89,6 +91,8 @@ export default function NavBar({menuConfig}) {
<MenuItem
onClick={() => goTo(`${menuConfig.oauthGrantsURL}`)}>{menuConfig.oauthGrantsText}</MenuItem>
</Menu>
</>
}
{(menuConfig.isOAuth2ServerAdmin || menuConfig.isOpenIdServerAdmin || menuConfig.isSuperAdmin) &&
<>
<Button aria-controls="server-admin-menu"
Expand Down
1 change: 1 addition & 0 deletions resources/views/admin/edit-user.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,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
Expand Down
2 changes: 2 additions & 0 deletions resources/views/menu.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<div id="navbar" class="navbar-collapse collapse">
<ul id='main-menu' class="nav navbar-nav">
<li id="profile"><a target="_self" href='{!! URL::action("UserController@getProfile") !!}'>{{ __('Settings') }}</a></li>
@if(Auth::user()->canAccessOAuth2Console())
<li id="oauth2-console" class="dropdown">
<a target="_self" href="#" class="dropdown-toggle" data-toggle="dropdown">
{{ __('OAUTH2 Console') }}<b class="caret"></b>
Expand All @@ -23,6 +24,7 @@
<li><a target="_self" href='{!!URL::action("AdminController@editIssuedGrants")!!}'>{{ __('Issued OAUTH2 Grants') }}</a></li>
</ul>
</li>
@endif
@if(Auth::user()->isOpenIdServerAdmin() || Auth::user()->isOAuth2ServerAdmin() || Auth::user()->isSuperAdmin())
<li id='server-admin' class="dropdown">
<a target="_self" href="#" class="dropdown-toggle" data-toggle="dropdown">
Expand Down
1 change: 1 addition & 0 deletions resources/views/oauth2/profile/clients.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions resources/views/oauth2/profile/edit-client.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions resources/views/profile.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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');
Expand Down
63 changes: 63 additions & 0 deletions tests/OAuth2ConsoleAccessTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php namespace Tests;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Auth\User;
use Illuminate\Support\Facades\Config;
use Mockery;

/**
* Class OAuth2ConsoleAccessTest
* @package Tests
*/
class OAuth2ConsoleAccessTest extends TestCase
{
private function userInGroups(array $slugs): User
{
$user = Mockery::mock(User::class)->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 '));
}
}
Loading
Loading