Skip to content
Snippets Groups Projects
Commit 9202b365 authored by Tobias Gross-Vogt's avatar Tobias Gross-Vogt
Browse files

proxyapi update; rename keycloak -> oidc

parents 6451917b d6167f7d
No related branches found
No related tags found
No related merge requests found
Pipeline #202770 passed
...@@ -18,6 +18,7 @@ $config->setRules([ ...@@ -18,6 +18,7 @@ $config->setRules([
'strict_param' => true, 'strict_param' => true,
'declare_strict_types' => true, 'declare_strict_types' => true,
'method_argument_space' => ['on_multiline' => 'ignore'], 'method_argument_space' => ['on_multiline' => 'ignore'],
'phpdoc_to_comment' => false,
]) ])
->setRiskyAllowed(true) ->setRiskyAllowed(true)
->setFinder($finder); ->setFinder($finder);
......
# v0.1.52
* new Locale service for setting a locale from a requests and forwarding
to other services
# v0.1.45 # v0.1.45
* dbp:relay:core:migrate: Work around issues in DoctrineMigrationsBundle which * dbp:relay:core:migrate: Work around issues in DoctrineMigrationsBundle which
......
...@@ -29,7 +29,8 @@ ...@@ -29,7 +29,8 @@
"symfony/twig-bundle": "^5.4", "symfony/twig-bundle": "^5.4",
"symfony/uid": "^5.4", "symfony/uid": "^5.4",
"symfony/validator": "^5.4", "symfony/validator": "^5.4",
"symfony/yaml": "^5.4" "symfony/yaml": "^5.4",
"ext-intl": "*"
}, },
"require-dev": { "require-dev": {
"brainmaestro/composer-git-hooks": "^2.8.5", "brainmaestro/composer-git-hooks": "^2.8.5",
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "9c42ac8c7816cdf96cd4c2f000cabaf3", "content-hash": "6ddd4889d724c8beabfe6b3b58bf35d7",
"packages": [ "packages": [
{ {
"name": "api-platform/core", "name": "api-platform/core",
...@@ -9843,7 +9843,8 @@ ...@@ -9843,7 +9843,8 @@
"platform": { "platform": {
"php": ">=7.3", "php": ">=7.3",
"ext-fileinfo": "*", "ext-fileinfo": "*",
"ext-json": "*" "ext-json": "*",
"ext-intl": "*"
}, },
"platform-dev": [], "platform-dev": [],
"platform-overrides": { "platform-overrides": {
......
...@@ -6,58 +6,37 @@ namespace Dbp\Relay\CoreBundle\API; ...@@ -6,58 +6,37 @@ namespace Dbp\Relay\CoreBundle\API;
interface UserSessionInterface interface UserSessionInterface
{ {
/**
* This gets called with the active JWT before any of the other methods are called.
*/
public function setSessionToken(?array $jwt): void;
/** /**
* The unique identifier of the authenticated user. Or null in case it is called * The unique identifier of the authenticated user. Or null in case it is called
* before the user is known or if the user is a system. * before the user is known or if the user is a system.
*
* Can be derived from the session token for example.
*/ */
public function getUserIdentifier(): ?string; public function getUserIdentifier(): ?string;
/**
* Returns a list of Symfony user roles, like ['ROLE_FOOBAR'].
*
* Can be derived from the session token for example.
*/
public function getUserRoles(): array;
/** /**
* Returns an ID represents a "session" of a user which can be used for logging. It should not be possible to * Returns an ID represents a "session" of a user which can be used for logging. It should not be possible to
* figure out which user is behind the ID based on the ID itself and the ID should change regularly. * figure out which user is behind the ID based on the ID itself and the ID should change regularly.
* This is useful for connecting various requests together for logging while not exposing details about the user. * This is useful for connecting various requests together for logging while not exposing details about the user.
*
* Can be derived from long running session IDs embedded in the token for example.
*
* Return null in case no logging ID exists
*/ */
public function getSessionLoggingId(): ?string; public function getSessionLoggingId(): string;
/**
* @deprecated
*/
public function getUserRoles(): array;
/** /**
* Returns a unique caching key that can be used to cache metadata related to the current user session like * Returns a unique caching key that can be used to cache metadata related to the current user session like
* any user metadata, authorization related information etc. * any user metadata, authorization related information etc.
* It should not be possible to figure out which user is behind the ID based on the ID itself and the ID should * It should not be possible to figure out which user is behind the ID based on the ID itself and the ID should
* change regularly (after a logout/login or a key refresh for example). * change regularly (after a logout/login or a key refresh for example).
*
* For example a hashed version of the token.
*
* Return null in case no appropriate cache key exists to disable any caching.
*/ */
public function getSessionCacheKey(): ?string; public function getSessionCacheKey(): string;
/** /**
* Should return the duration the session is valid (as a whole, not from now) in seconds. * Returns the duration the session is valid (as a whole, not from now) in seconds.
* After the specified amount of time has passed the logging ID and the caching key should have changed. * After the specified amount of time has passed the logging ID and the caching key should have changed.
* *
* This is mostly useful for limiting the cache. * This is mostly useful for limiting the cache.
*
* For example the lifespan of the token.
*
* Return <0 in case that information isn't available.
*/ */
public function getSessionTTL(): int; public function getSessionTTL(): int;
} }
...@@ -19,9 +19,15 @@ class ProxyAuthenticator extends AbstractAuthenticator ...@@ -19,9 +19,15 @@ class ProxyAuthenticator extends AbstractAuthenticator
*/ */
private $authenticators; private $authenticators;
public function __construct() /**
* @var UserSession
*/
private $userSession;
public function __construct(UserSession $userSession)
{ {
$this->authenticators = []; $this->authenticators = [];
$this->userSession = $userSession;
} }
public function addAuthenticator(AuthenticatorInterface $sub) public function addAuthenticator(AuthenticatorInterface $sub)
...@@ -54,7 +60,11 @@ class ProxyAuthenticator extends AbstractAuthenticator ...@@ -54,7 +60,11 @@ class ProxyAuthenticator extends AbstractAuthenticator
$auth = $this->getAuthenticator($request); $auth = $this->getAuthenticator($request);
assert($auth !== null); assert($auth !== null);
return $auth->authenticate($request); $passport = $auth->authenticate($request);
$provider = $passport->getAttribute('relay_user_session_provider');
$this->userSession->setProvider($provider);
return $passport;
} }
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
...@@ -67,6 +77,8 @@ class ProxyAuthenticator extends AbstractAuthenticator ...@@ -67,6 +77,8 @@ class ProxyAuthenticator extends AbstractAuthenticator
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{ {
$this->userSession->setProvider(null);
$auth = $this->getAuthenticator($request); $auth = $this->getAuthenticator($request);
assert($auth !== null); assert($auth !== null);
......
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Auth;
use Dbp\Relay\CoreBundle\API\UserSessionInterface;
use Dbp\Relay\CoreBundle\API\UserSessionProviderInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Uid\Uuid;
/**
* This service provides user session information, either sourcing information from the active auth provider
* or in case it is used from the CLI or unauthenticated then it returns some reasonable defaults.
*/
class UserSession implements UserSessionInterface
{
/**
* @var ?UserSessionProviderInterface
*/
private $provider;
/**
* @var Security
*/
private $security;
public function __construct(?Security $security = null)
{
$this->security = $security;
}
public function setProvider(?UserSessionProviderInterface $provider)
{
$this->provider = $provider;
}
public function getUserIdentifier(): ?string
{
if ($this->provider === null) {
return null;
}
return $this->provider->getUserIdentifier();
}
public function getSessionLoggingId(): string
{
$id = null;
if ($this->provider !== null) {
$id = $this->provider->getSessionLoggingId();
}
if ($id === null) {
$id = 'unknown';
}
return $id;
}
public function getSessionCacheKey(): string
{
$key = null;
if ($this->provider !== null) {
$key = $this->provider->getSessionCacheKey();
}
if ($key === null) {
$key = (Uuid::v4())->toRfc4122();
}
return $key;
}
public function getSessionTTL(): int
{
$ttl = -1;
if ($this->provider !== null) {
$ttl = $this->provider->getSessionTTL();
}
if ($ttl === -1) {
$ttl = 60;
}
return $ttl;
}
public function getUserRoles(): array
{
if ($this->provider === null) {
return [];
}
$user = $this->security->getUser();
if ($user === null) {
return [];
}
return $user->getRoles();
}
}
...@@ -6,6 +6,9 @@ namespace Dbp\Relay\CoreBundle\Helpers; ...@@ -6,6 +6,9 @@ namespace Dbp\Relay\CoreBundle\Helpers;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
/**
* @deprecated Use Locale/Locale instead
*/
class Locale class Locale
{ {
public const LANGUAGE_OPTION = 'lang'; public const LANGUAGE_OPTION = 'lang';
......
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Locale;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* A service which can be injected, which provides the current active language and allows setting the active
* language based on a query parameters.
*
* This assumes that Symfony is configured to apply the 'Accept-Language' header by default to all requests.
*/
class Locale
{
/** @var RequestStack */
private $requestStack;
/**
* @var ParameterBagInterface
*/
private $parameters;
public function __construct(RequestStack $requestStack, ParameterBagInterface $parameters)
{
$this->requestStack = $requestStack;
$this->parameters = $parameters;
}
/**
* Returns the primary language (in ISO 639‑1 format) for the current context.
* In case there is a request then the request language, otherwise the default language.
*/
public function getCurrentPrimaryLanguage(): string
{
$locale = $this->getCurrentLocale();
$lang = \Locale::getPrimaryLanguage($locale);
/** @psalm-suppress RedundantCondition */
assert($lang !== null);
return $lang;
}
/**
* Sets the locale for the active request via a query parameter.
* The query parameter format is the same as the 'Accept-Language' HTTP header format.
* In case the query parameter isn't part of the request then nothing changes.
*/
public function setCurrentRequestLocaleFromQuery(string $queryParam = 'lang'): void
{
$request = $this->requestStack->getCurrentRequest();
if ($request === null) {
throw new \RuntimeException('No active request');
}
self::setRequestLocaleFromQuery($request, $queryParam);
}
/**
* Returns the current locale, either from the active request, or the default one.
*/
private function getCurrentLocale(): string
{
$request = $this->requestStack->getCurrentRequest();
if ($request !== null) {
$locale = $request->getLocale();
} else {
$locale = $this->parameters->get('kernel.default_locale');
assert(is_string($locale));
}
return $locale;
}
/**
* Same as setCurrentRequestLocaleFromQuery(), but takes a request object.
*/
public static function setRequestLocaleFromQuery(Request $request, string $queryParam): void
{
if ($request->query->has($queryParam)) {
$lang = $request->query->get($queryParam);
assert(is_string($lang));
$locale = \Locale::acceptFromHttp($lang);
if ($locale === false) {
throw new \RuntimeException('Failed to parse Accept-Language');
}
$request->setLocale($locale);
}
}
}
...@@ -43,10 +43,7 @@ final class LoggingProcessor ...@@ -43,10 +43,7 @@ final class LoggingProcessor
$this->maskUserId($record); $this->maskUserId($record);
// Add a session ID (the same during multiple requests for the same user session) // Add a session ID (the same during multiple requests for the same user session)
$loggingId = $this->userDataProvider->getSessionLoggingId(); $record['context']['relay-session-id'] = $this->userDataProvider->getSessionLoggingId();
if ($loggingId !== null) {
$record['context']['relay-session-id'] = $loggingId;
}
// Add a request ID (the same during the same client request) // Add a request ID (the same during the same client request)
$request = $this->requestStack->getMainRequest(); $request = $this->requestStack->getMainRequest();
......
...@@ -58,6 +58,13 @@ services: ...@@ -58,6 +58,13 @@ services:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
Dbp\Relay\CoreBundle\Auth\UserSession:
autowire: true
autoconfigure: true
Dbp\Relay\CoreBundle\API\UserSessionInterface:
'@Dbp\Relay\CoreBundle\Auth\UserSession'
Dbp\Relay\CoreBundle\LocalData\LocalDataAwareEventDispatcher: Dbp\Relay\CoreBundle\LocalData\LocalDataAwareEventDispatcher:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
...@@ -97,3 +104,7 @@ services: ...@@ -97,3 +104,7 @@ services:
Dbp\Relay\CoreBundle\ProxyApi\ProxyDataEventSubscriber: Dbp\Relay\CoreBundle\ProxyApi\ProxyDataEventSubscriber:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
Dbp\Relay\CoreBundle\Locale\Locale:
autowire: true
autoconfigure: true
...@@ -41,12 +41,12 @@ class TestUserSession implements UserSessionInterface ...@@ -41,12 +41,12 @@ class TestUserSession implements UserSessionInterface
return $this->roles; return $this->roles;
} }
public function getSessionLoggingId(): ?string public function getSessionLoggingId(): string
{ {
return 'logging-id'; return 'logging-id';
} }
public function getSessionCacheKey(): ?string public function getSessionCacheKey(): string
{ {
return 'cache'; return 'cache';
} }
......
...@@ -6,6 +6,7 @@ namespace Dbp\Relay\CoreBundle\Tests\Auth; ...@@ -6,6 +6,7 @@ namespace Dbp\Relay\CoreBundle\Tests\Auth;
use Dbp\Relay\CoreBundle\Auth\AuthenticatorCompilerPass; use Dbp\Relay\CoreBundle\Auth\AuthenticatorCompilerPass;
use Dbp\Relay\CoreBundle\Auth\ProxyAuthenticator; use Dbp\Relay\CoreBundle\Auth\ProxyAuthenticator;
use Dbp\Relay\CoreBundle\Auth\UserSession;
use Dbp\Relay\CoreBundle\TestUtils\TestAuthenticator; use Dbp\Relay\CoreBundle\TestUtils\TestAuthenticator;
use Dbp\Relay\CoreBundle\TestUtils\TestUser; use Dbp\Relay\CoreBundle\TestUtils\TestUser;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
...@@ -17,13 +18,13 @@ class AuthenticatorTest extends TestCase ...@@ -17,13 +18,13 @@ class AuthenticatorTest extends TestCase
{ {
public function testSupports() public function testSupports()
{ {
$auth = new ProxyAuthenticator(); $auth = new ProxyAuthenticator(new UserSession());
$this->assertFalse($auth->supports(new Request())); $this->assertFalse($auth->supports(new Request()));
} }
public function testSingle() public function testSingle()
{ {
$auth = new ProxyAuthenticator(); $auth = new ProxyAuthenticator(new UserSession());
$user = new TestUser(); $user = new TestUser();
$sub = new TestAuthenticator($user, 'bla'); $sub = new TestAuthenticator($user, 'bla');
$auth->addAuthenticator($sub); $auth->addAuthenticator($sub);
......
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Tests;
use Dbp\Relay\CoreBundle\Locale\Locale;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
class LocaleTest extends TestCase
{
public function testWithRequest()
{
$stack = new RequestStack();
$request = new Request(['lang' => 'de']);
$request->setLocale(\Locale::acceptFromHttp('en'));
$stack->push($request);
$params = new ParameterBag([]);
$service = new Locale($stack, $params);
$lang = $service->getCurrentPrimaryLanguage();
$this->assertSame('en', $lang);
$service->setCurrentRequestLocaleFromQuery('lang');
$lang = $service->getCurrentPrimaryLanguage();
$this->assertSame('de', $lang);
}
public function testWithoutRequest()
{
$stack = new RequestStack();
$params = new ParameterBag(['kernel.default_locale' => \Locale::acceptFromHttp('de')]);
$service = new Locale($stack, $params);
$lang = $service->getCurrentPrimaryLanguage();
$this->assertSame('de', $lang);
}
public function testSetExplicit()
{
$stack = new RequestStack();
$params = new ParameterBag(['kernel.default_locale' => \Locale::acceptFromHttp('en')]);
$service = new Locale($stack, $params);
$request = new Request(['foo' => 'fr']);
$service->setRequestLocaleFromQuery($request, 'foo');
$stack->push($request);
$lang = $service->getCurrentPrimaryLanguage();
$this->assertSame('fr', $lang);
$stack->pop();
$lang = $service->getCurrentPrimaryLanguage();
$this->assertSame('en', $lang);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment