Skip to content
Snippets Groups Projects
Commit 89d4166c authored by Reiter, Christoph's avatar Reiter, Christoph :snake:
Browse files

Add an interface that allows customizing the user roles

This adds UserRolesInterface which is used for converting the
oauth2 scopes to symfony roles.

The default interface implementation converts them to "ROLE_SCOPE_FOO".

The interface also gets passed the user ID and can fetch roles from other
places as well, like LDAP, or ignore the scopes etc.

Fixes #4
parent 381e6cf2
No related branches found
No related tags found
No related merge requests found
Pipeline #57313 passed
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\API;
interface UserRolesInterface
{
/**
* @param string[] $scopes
*
* @return string[]
*/
public function getRoles(?string $userIdentifier, array $scopes): array;
}
...@@ -23,3 +23,10 @@ services: ...@@ -23,3 +23,10 @@ services:
Dbp\Relay\CoreBundle\API\UserSessionInterface: Dbp\Relay\CoreBundle\API\UserSessionInterface:
'@Dbp\Relay\AuthBundle\Service\OIDCUserSession' '@Dbp\Relay\AuthBundle\Service\OIDCUserSession'
Dbp\Relay\AuthBundle\Service\DefaultUserRoles:
autowire: true
autoconfigure: true
Dbp\Relay\AuthBundle\API\UserRolesInterface:
'@Dbp\Relay\AuthBundle\Service\DefaultUserRoles'
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Service;
use Dbp\Relay\AuthBundle\API\UserRolesInterface;
class DefaultUserRoles implements UserRolesInterface
{
/**
* The default implementation converts OAuth2 scopes to Symfony roles
* by prefixing them with "ROLE_SCOPE_" and converting to uppercase.
*/
public function getRoles(?string $userIdentifier, array $scopes): array
{
$roles = [];
foreach ($scopes as $scope) {
$roles[] = 'ROLE_SCOPE_'.mb_strtoupper($scope);
}
return $roles;
}
}
...@@ -4,6 +4,7 @@ declare(strict_types=1); ...@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Service; namespace Dbp\Relay\AuthBundle\Service;
use Dbp\Relay\AuthBundle\API\UserRolesInterface;
use Dbp\Relay\CoreBundle\API\UserSessionInterface; use Dbp\Relay\CoreBundle\API\UserSessionInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
...@@ -19,10 +20,16 @@ class OIDCUserSession implements UserSessionInterface ...@@ -19,10 +20,16 @@ class OIDCUserSession implements UserSessionInterface
*/ */
private $parameters; private $parameters;
public function __construct(ParameterBagInterface $parameters) /**
* @var UserRolesInterface
*/
private $userRoles;
public function __construct(ParameterBagInterface $parameters, UserRolesInterface $userRoles)
{ {
$this->jwt = null; $this->jwt = null;
$this->parameters = $parameters; $this->parameters = $parameters;
$this->userRoles = $userRoles;
} }
public function getUserIdentifier(): ?string public function getUserIdentifier(): ?string
...@@ -36,21 +43,18 @@ class OIDCUserSession implements UserSessionInterface ...@@ -36,21 +43,18 @@ class OIDCUserSession implements UserSessionInterface
return $this->jwt['username'] ?? null; return $this->jwt['username'] ?? null;
} }
private static function getScopes($jwt): array
{
return preg_split('/\s+/', $jwt['scope'] ?? '', -1, PREG_SPLIT_NO_EMPTY);
}
public function getUserRoles(): array public function getUserRoles(): array
{ {
assert($this->jwt !== null); assert($this->jwt !== null);
$scopes = self::getScopes($this->jwt);
$userIdentifier = $this->getUserIdentifier();
$scopes = []; return $this->userRoles->getRoles($userIdentifier, $scopes);
if ($this->jwt['scope'] ?? '' !== '') {
$scopes = explode(' ', $this->jwt['scope']);
}
$roles = [];
foreach ($scopes as $scope) {
$roles[] = 'ROLE_SCOPE_'.mb_strtoupper($scope);
}
return $roles;
} }
/** /**
...@@ -58,13 +62,11 @@ class OIDCUserSession implements UserSessionInterface ...@@ -58,13 +62,11 @@ class OIDCUserSession implements UserSessionInterface
*/ */
public static function isServiceAccountToken(array $jwt): bool public static function isServiceAccountToken(array $jwt): bool
{ {
if (!array_key_exists('scope', $jwt)) { $scopes = self::getScopes($jwt);
throw new \RuntimeException('Token missing scope key');
}
$scope = $jwt['scope'];
// XXX: This is the main difference I found compared to other flows, but that's a Keycloak // XXX: This is the main difference I found compared to other flows, but that's a Keycloak
// implementation detail I guess. // implementation detail I guess.
$has_openid_scope = in_array('openid', explode(' ', $scope), true); $has_openid_scope = in_array('openid', $scopes, true);
return !$has_openid_scope; return !$has_openid_scope;
} }
......
...@@ -4,6 +4,7 @@ declare(strict_types=1); ...@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests\Authenticator; namespace Dbp\Relay\AuthBundle\Tests\Authenticator;
use Dbp\Relay\AuthBundle\Service\DefaultUserRoles;
use Dbp\Relay\AuthBundle\Service\OIDCUserSession; use Dbp\Relay\AuthBundle\Service\OIDCUserSession;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
...@@ -21,7 +22,7 @@ class UserSessionTest extends TestCase ...@@ -21,7 +22,7 @@ class UserSessionTest extends TestCase
public function testGetLoggingId() public function testGetLoggingId()
{ {
$session = new OIDCUserSession(new ParameterBag()); $session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session->setSessionToken([]); $session->setSessionToken([]);
$this->assertSame('unknown-unknown', $session->getSessionLoggingId()); $this->assertSame('unknown-unknown', $session->getSessionLoggingId());
...@@ -31,7 +32,7 @@ class UserSessionTest extends TestCase ...@@ -31,7 +32,7 @@ class UserSessionTest extends TestCase
public function testGetUserRoles() public function testGetUserRoles()
{ {
$session = new OIDCUserSession(new ParameterBag()); $session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session->setSessionToken([]); $session->setSessionToken([]);
$this->assertSame([], $session->getUserRoles()); $this->assertSame([], $session->getUserRoles());
$session->setSessionToken(['scope' => 'foo bar quux-buz a_b']); $session->setSessionToken(['scope' => 'foo bar quux-buz a_b']);
...@@ -42,7 +43,7 @@ class UserSessionTest extends TestCase ...@@ -42,7 +43,7 @@ class UserSessionTest extends TestCase
public function testGetSessionCacheKey() public function testGetSessionCacheKey()
{ {
$session = new OIDCUserSession(new ParameterBag()); $session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session->setSessionToken(['scope' => 'foo']); $session->setSessionToken(['scope' => 'foo']);
$old = $session->getSessionCacheKey(); $old = $session->getSessionCacheKey();
$session->setSessionToken(['scope' => 'bar']); $session->setSessionToken(['scope' => 'bar']);
...@@ -52,7 +53,7 @@ class UserSessionTest extends TestCase ...@@ -52,7 +53,7 @@ class UserSessionTest extends TestCase
public function testGetSessionTTL() public function testGetSessionTTL()
{ {
$session = new OIDCUserSession(new ParameterBag()); $session = new OIDCUserSession(new ParameterBag(), new DefaultUserRoles());
$session->setSessionToken([]); $session->setSessionToken([]);
$this->assertSame(-1, $session->getSessionTTL()); $this->assertSame(-1, $session->getSessionTTL());
......
<?php
declare(strict_types=1);
namespace Dbp\Relay\AuthBundle\Tests;
use Dbp\Relay\AuthBundle\Service\DefaultUserRoles;
use PHPUnit\Framework\TestCase;
class DefaultUserRolesTest extends TestCase
{
public function testGetRoles()
{
$userRoles = new DefaultUserRoles();
$roles = $userRoles->getRoles(null, ['foo-bar']);
$this->assertSame(['ROLE_SCOPE_FOO-BAR'], $roles);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment