Skip to content
Commits on Source (4)
......@@ -6,58 +6,37 @@ namespace Dbp\Relay\CoreBundle\API;
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
* 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;
/**
* 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
* 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.
*
* 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
* 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
* 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.
*
* 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;
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\API;
interface UserSessionProviderInterface
{
/**
* 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.
*
* Can be derived from the session token for example.
*/
public function getUserIdentifier(): ?string;
/**
* 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.
* 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;
/**
* 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.
* 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).
*
* For example a hashed version of the token.
*
* Return null in case no appropriate cache key exists.
*/
public function getSessionCacheKey(): ?string;
/**
* Should return 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.
*
* 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;
}
......@@ -19,9 +19,15 @@ class ProxyAuthenticator extends AbstractAuthenticator
*/
private $authenticators;
public function __construct()
/**
* @var UserSession
*/
private $userSession;
public function __construct(UserSession $userSession)
{
$this->authenticators = [];
$this->userSession = $userSession;
}
public function addAuthenticator(AuthenticatorInterface $sub)
......@@ -54,7 +60,11 @@ class ProxyAuthenticator extends AbstractAuthenticator
$auth = $this->getAuthenticator($request);
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
......@@ -67,6 +77,8 @@ class ProxyAuthenticator extends AbstractAuthenticator
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$this->userSession->setProvider(null);
$auth = $this->getAuthenticator($request);
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();
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Authorization;
/**
* @deprecated use AbstractAuthorizationService instead
*/
abstract class AuthorizationService extends AbstractAuthorizationService
{
}
......@@ -80,7 +80,7 @@ class ApiConnection
/**
* @throws ConnectionException
*/
public function get(string $uri, array $options): ResponseInterface
public function get(string $uri, array $query): ResponseInterface
{
$requestOptions = [
Connection::REQUEST_OPTION_HEADERS => [
......@@ -88,7 +88,21 @@ class ApiConnection
],
];
return $this->getApiConnection()->get($uri, $options, $requestOptions);
return $this->getConnection()->get($uri, $query, $requestOptions);
}
/**
* @throws ConnectionException
*/
public function postJSON(string $uri, array $parameters): ResponseInterface
{
$requestOptions = [
Connection::REQUEST_OPTION_HEADERS => [
'Authorization' => 'Bearer '.$this->getAccessToken(),
],
];
return $this->getConnection()->postJSON($uri, $parameters, $requestOptions);
}
/**
......@@ -123,7 +137,7 @@ class ApiConnection
return $this->accessToken;
}
private function getApiConnection(): Connection
private function getConnection(): Connection
{
if ($this->connection === null) {
$connection = new Connection($this->config[self::API_URL_CONFIG_PARAMETER]);
......
......@@ -11,6 +11,7 @@ class ConnectionException extends \RuntimeException
{
public const REQUEST_EXCEPTION = 1;
public const JSON_EXCEPTION = 2;
public const INVALID_DATA_EXCEPTION = 3;
/** @var RequestInterface|null */
private $request;
......
......@@ -43,10 +43,7 @@ final class LoggingProcessor
$this->maskUserId($record);
// Add a session ID (the same during multiple requests for the same user session)
$loggingId = $this->userDataProvider->getSessionLoggingId();
if ($loggingId !== null) {
$record['context']['relay-session-id'] = $loggingId;
}
$record['context']['relay-session-id'] = $this->userDataProvider->getSessionLoggingId();
// Add a request ID (the same during the same client request)
$request = $this->requestStack->getMainRequest();
......
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\ProxyApi;
use Exception;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
abstract class AbstractProxyDataEventSubscriber implements EventSubscriberInterface
{
protected const NAMESPACE = '';
public static function getSubscribedEvents(): array
{
return [
ProxyApi::PROXY_DATA_EVENT_NAME.static::NAMESPACE => 'onProxyDataEvent',
];
}
/**
* @throws BadRequestException
*/
public function onProxyDataEvent(ProxyDataEvent $event): void
{
$event->setHandled();
$proxyData = $event->getProxyData();
$functionName = $proxyData->getFunctionName();
$arguments = $proxyData->getArguments();
$returnValue = null;
if ($this->isFunctionDefined($functionName) === false) {
throw new BadRequestException(sprintf('unknown function "%s" under namespace "%s"', $functionName, static::NAMESPACE));
} elseif ($this->areAllRequiredArgumentsDefined($functionName, $arguments) === false) {
throw new BadRequestException(sprintf('incomplete argument list for function "%s" under namespace "%s"', $functionName, static::NAMESPACE));
}
try {
$returnValue = $this->callFunction($functionName, $arguments);
} catch (Exception $exception) {
$proxyData->setErrorsFromException($exception);
}
$proxyData->setData($returnValue);
}
abstract protected function isFunctionDefined(string $functionName): bool;
abstract protected function areAllRequiredArgumentsDefined(string $functionName, array $arguments): bool;
abstract protected function callFunction(string $functionName, array $arguments);
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\ProxyApi;
use Dbp\Relay\CoreBundle\Helpers\Tools;
use Dbp\Relay\CoreBundle\Http\ApiConnection as BaseApiConnection;
use Dbp\Relay\CoreBundle\Http\ConnectionException as BaseConnectionException;
class ApiConnection extends BaseApiConnection
{
private const NAMESPACE_PARAMETER_NAME = 'namespace';
private const FUNCTION_NAME_PARAMETER_NAME = 'functionName';
private const ARGUMENTS_PARAMETER_NAME = 'arguments';
private const PROXY_DATA_URI = 'proxy/proxydata';
/**
* @throws ConnectionException
*/
public function callFunction(string $namespace, string $functionName, array $arguments = [])
{
$parameters = [
self::NAMESPACE_PARAMETER_NAME => $namespace,
self::FUNCTION_NAME_PARAMETER_NAME => $functionName,
self::ARGUMENTS_PARAMETER_NAME => $arguments,
];
$responseBody = (string) $this->postJSON(self::PROXY_DATA_URI, $parameters)->getBody();
try {
$proxyData = Tools::decodeJSON($responseBody, true);
} catch (\JsonException $exception) {
throw new ConnectionException('failed to JSON decode API response: '.$exception->getMessage(), BaseConnectionException::JSON_EXCEPTION);
}
try {
$errors = $proxyData[ProxyApi::PROXY_DATA_ERRORS_KEY];
$returnValue = $proxyData[ProxyApi::PROXY_DATA_RETURN_VALUE_KEY];
} catch (\Exception $exception) {
throw new ConnectionException('API returned invalid ProxyData object', BaseConnectionException::INVALID_DATA_EXCEPTION);
}
if (!empty($errors)) {
$topLevelError = $errors[0];
throw new ConnectionException(sprintf('call to API function "%s" under namespace "%s" resulted in an error: %s (code: %s)', $functionName, $namespace, $topLevelError['message'] ?? 'message not available', $topLevelError['code'] ?? 'code not available'), ConnectionException::API_ERROR);
}
return $returnValue;
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\ProxyApi;
class ConnectionException extends \Dbp\Relay\CoreBundle\Http\ConnectionException
{
public const API_ERROR = 101;
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\ProxyApi;
class ProxyApi
{
public const PROXY_DATA_EVENT_NAME = '';
public const PROXY_DATA_RETURN_VALUE_KEY = 'data';
public const PROXY_DATA_ERRORS_KEY = 'errors';
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\ProxyApi;
use Symfony\Contracts\EventDispatcher\Event;
class ProxyDataEvent extends Event
{
public const NAME = 'dbp.relay.proxy_bundle.proxy_data';
/** @var ProxyDataInterface */
private $proxyData;
/** @var bool */
private $wasHandled;
public function __construct(ProxyDataInterface $proxyData)
{
$this->proxyData = $proxyData;
$this->wasHandled = false;
}
public function getProxyData(): ProxyDataInterface
{
return $this->proxyData;
}
/**
* Indicate, that the event was handled, e.g. there was an event subscriber for the requested proxy data namespace.
*/
public function setHandled(): void
{
$this->wasHandled = true;
}
/**
* True, if the event was handled, e.g. there was an event subscriber for the requested proxy data namespace, false otherwise.
*/
public function wasHandled(): bool
{
return $this->wasHandled;
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\ProxyApi;
use Dbp\Relay\CoreBundle\Authorization\AuthorizationDataProviderProvider;
use Dbp\Relay\CoreBundle\Helpers\Tools;
use Exception;
class ProxyDataEventSubscriber extends AbstractProxyDataEventSubscriber
{
public const NAMESPACE = 'core';
public const GET_AVAILABLE_ATTRIBUTES_FUNCTION_NAME = 'getAvailableAttributes';
public const GET_USER_ATTRIBUTES_FUNCTION_NAME = 'getUserAttributes';
public const USER_ID_PARAMETER_NAME = 'userId';
/**
* @var AuthorizationDataProviderProvider
*/
private $provider;
public function __construct(AuthorizationDataProviderProvider $provider)
{
$this->provider = $provider;
}
protected function isFunctionDefined(string $functionName): bool
{
return
$functionName === self::GET_AVAILABLE_ATTRIBUTES_FUNCTION_NAME ||
$functionName === self::GET_USER_ATTRIBUTES_FUNCTION_NAME;
}
protected function areAllRequiredArgumentsDefined(string $functionName, array $arguments): bool
{
return
$functionName !== self::GET_USER_ATTRIBUTES_FUNCTION_NAME ||
!Tools::isNullOrEmpty($arguments[self::USER_ID_PARAMETER_NAME] ?? null);
}
/**
* @throws Exception
*/
protected function callFunction(string $functionName, array $arguments): ?array
{
$returnValue = null;
switch ($functionName) {
case self::GET_AVAILABLE_ATTRIBUTES_FUNCTION_NAME:
$returnValue = $this->getAvailableAttributes();
break;
case self::GET_USER_ATTRIBUTES_FUNCTION_NAME:
$returnValue = $this->getUserAttributes($arguments[self::USER_ID_PARAMETER_NAME]);
break;
}
return $returnValue;
}
private function getAvailableAttributes(): array
{
$availableAttributes = [];
foreach ($this->provider->getAuthorizationDataProviders() as $provider) {
$availableAttributes = array_merge($availableAttributes, $provider->getAvailableAttributes());
}
return $availableAttributes;
}
private function getUserAttributes(string $userId): array
{
$userAttributes = [];
foreach ($this->provider->getAuthorizationDataProviders() as $provider) {
$userAttributes = array_merge($userAttributes, $provider->getUserAttributes($userId));
}
return $userAttributes;
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\ProxyApi;
interface ProxyDataInterface
{
public function getArguments(): array;
public function getFunctionName(): ?string;
public function setData($data);
public function setErrorsFromException(\Exception $exception): void;
}
......@@ -58,6 +58,13 @@ services:
autowire: 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:
autowire: true
autoconfigure: true
......
......@@ -41,12 +41,12 @@ class TestUserSession implements UserSessionInterface
return $this->roles;
}
public function getSessionLoggingId(): ?string
public function getSessionLoggingId(): string
{
return 'logging-id';
}
public function getSessionCacheKey(): ?string
public function getSessionCacheKey(): string
{
return 'cache';
}
......
......@@ -6,6 +6,7 @@ namespace Dbp\Relay\CoreBundle\Tests\Auth;
use Dbp\Relay\CoreBundle\Auth\AuthenticatorCompilerPass;
use Dbp\Relay\CoreBundle\Auth\ProxyAuthenticator;
use Dbp\Relay\CoreBundle\Auth\UserSession;
use Dbp\Relay\CoreBundle\TestUtils\TestAuthenticator;
use Dbp\Relay\CoreBundle\TestUtils\TestUser;
use PHPUnit\Framework\TestCase;
......@@ -17,13 +18,13 @@ class AuthenticatorTest extends TestCase
{
public function testSupports()
{
$auth = new ProxyAuthenticator();
$auth = new ProxyAuthenticator(new UserSession());
$this->assertFalse($auth->supports(new Request()));
}
public function testSingle()
{
$auth = new ProxyAuthenticator();
$auth = new ProxyAuthenticator(new UserSession());
$user = new TestUser();
$sub = new TestAuthenticator($user, 'bla');
$auth->addAuthenticator($sub);
......