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

Merge branch 'main' of gitlab.tugraz.at:dbp/relay/dbp-relay-core-bundle into main

parents d6f6b180 f1469c60
Branches
Tags v0.1.66
No related merge requests found
Pipeline #212865 passed
...@@ -16,19 +16,32 @@ abstract class AbstractAuthorizationService ...@@ -16,19 +16,32 @@ abstract class AbstractAuthorizationService
/** @var AuthorizationExpressionChecker */ /** @var AuthorizationExpressionChecker */
private $userAuthorizationChecker; private $userAuthorizationChecker;
/** @var AuthorizationUser|null */ /** @var AuthorizationUser */
private $currentAuthorizationUser; private $currentAuthorizationUser;
public function __construct(UserSessionInterface $userSession, AuthorizationDataProviderProvider $authorizationDataProviderProvider) private $config;
/**
* @required
*/
public function _injectServices(UserSessionInterface $userSession, AuthorizationDataMuxer $mux)
{ {
$muxer = new AuthorizationDataMuxer($authorizationDataProviderProvider->getAuthorizationDataProviders()); $this->userAuthorizationChecker = new AuthorizationExpressionChecker($mux);
$this->userAuthorizationChecker = new AuthorizationExpressionChecker($muxer);
$this->currentAuthorizationUser = new AuthorizationUser($userSession->getUserIdentifier(), $this->userAuthorizationChecker); $this->currentAuthorizationUser = new AuthorizationUser($userSession->getUserIdentifier(), $this->userAuthorizationChecker);
$this->updateConfig();
} }
public function setConfig(array $config) public function setConfig(array $config)
{ {
$this->userAuthorizationChecker->setConfig($config); $this->config = $config;
$this->updateConfig();
}
private function updateConfig()
{
if ($this->userAuthorizationChecker !== null && $this->config !== null) {
$this->userAuthorizationChecker->setConfig($this->config);
}
} }
/** /**
...@@ -65,7 +78,7 @@ abstract class AbstractAuthorizationService ...@@ -65,7 +78,7 @@ abstract class AbstractAuthorizationService
{ {
$this->userAuthorizationChecker->init(); $this->userAuthorizationChecker->init();
return $this->userAuthorizationChecker->getAttribute($this->currentAuthorizationUser, $attributeName, $defaultValue); return $this->userAuthorizationChecker->evalAttributeExpression($this->currentAuthorizationUser, $attributeName, $defaultValue);
} }
/** /**
......
...@@ -4,32 +4,43 @@ declare(strict_types=1); ...@@ -4,32 +4,43 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Authorization; namespace Dbp\Relay\CoreBundle\Authorization;
use Dbp\Relay\CoreBundle\Authorization\Event\GetAttributeEvent;
use Dbp\Relay\CoreBundle\Authorization\Event\GetAvailableAttributesEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* @internal
*/
class AuthorizationDataMuxer class AuthorizationDataMuxer
{ {
/** @var iterable */ /** @var iterable<AuthorizationDataProviderInterface> */
private $authorizationDataProviders; private $authorizationDataProviders;
/** @var array */ /** @var array<string, array> */
private $customAttributes; private $providerCache;
/** @var array<string, string[]> */
private $availableCache;
/** @var EventDispatcherInterface */
private $eventDispatcher;
/** @var string[] */
private $attributeStack;
/** /**
* @param iterable<AuthorizationDataProviderInterface> $authorizationDataProviders * @var ?string[]
*/ */
public function __construct(iterable $authorizationDataProviders) private $availableCacheAll;
{
$this->authorizationDataProviders = $authorizationDataProviders;
$this->customAttributes = [];
}
private function loadUserAttributesFromAuthorizationProvider(?string $userIdentifier, AuthorizationDataProviderInterface $authorizationDataProvider): void public function __construct(AuthorizationDataProviderProvider $authorizationDataProviderProvider, EventDispatcherInterface $eventDispatcher)
{ {
$userAttributes = $authorizationDataProvider->getUserAttributes($userIdentifier); $this->authorizationDataProviders = $authorizationDataProviderProvider->getAuthorizationDataProviders();
$this->eventDispatcher = $eventDispatcher;
foreach ($authorizationDataProvider->getAvailableAttributes() as $availableAttribute) { $this->providerCache = [];
if (array_key_exists($availableAttribute, $userAttributes)) { $this->availableCache = [];
$this->customAttributes[$availableAttribute] = $userAttributes[$availableAttribute]; $this->availableCacheAll = null;
} $this->attributeStack = [];
}
} }
/** /**
...@@ -39,48 +50,97 @@ class AuthorizationDataMuxer ...@@ -39,48 +50,97 @@ class AuthorizationDataMuxer
*/ */
public function getAvailableAttributes(): array public function getAvailableAttributes(): array
{ {
if ($this->availableCacheAll === null) {
$res = []; $res = [];
foreach ($this->authorizationDataProviders as $authorizationDataProvider) { foreach ($this->authorizationDataProviders as $authorizationDataProvider) {
$availableAttributes = $authorizationDataProvider->getAvailableAttributes(); $availableAttributes = $this->getProviderAvailableAttributes($authorizationDataProvider);
$res = array_merge($res, $availableAttributes); $res = array_merge($res, $availableAttributes);
} }
return $res; $event = new GetAvailableAttributesEvent($res);
$this->eventDispatcher->dispatch($event);
$this->availableCacheAll = $event->getAttributes();
}
return $this->availableCacheAll;
} }
/** /**
* @param mixed|null $defaultValue * Returns a cached list for available attributes for the provider.
* *
* @return mixed|null * @return string[]
*/
private function getProviderAvailableAttributes(AuthorizationDataProviderInterface $prov): array
{
// Caches getAvailableAttributes for each provider
$provKey = get_class($prov);
if (!array_key_exists($provKey, $this->availableCache)) {
$this->availableCache[$provKey] = $prov->getAvailableAttributes();
}
return $this->availableCache[$provKey];
}
/**
* Returns a cached map of available user attributes.
* *
* @throws AuthorizationException * @return array<string, mixed>
*/ */
public function getCustomAttribute(?string $userIdentifier, string $attributeName, $defaultValue = null) private function getProviderUserAttributes(AuthorizationDataProviderInterface $prov, ?string $userIdentifier): array
{ {
if (array_key_exists($attributeName, $this->customAttributes) === false) { // We cache the attributes for each provider, but only for the last userIdentifier
$this->loadCustomAttribute($userIdentifier, $attributeName); $provKey = get_class($prov);
if (!array_key_exists($provKey, $this->providerCache) || $this->providerCache[$provKey][0] !== $userIdentifier) {
$this->providerCache[$provKey] = [$userIdentifier, $prov->getUserAttributes($userIdentifier)];
} }
$res = $this->providerCache[$provKey];
assert($res[0] === $userIdentifier);
return $this->customAttributes[$attributeName] ?? $defaultValue; return $res[1];
} }
/** /**
* @param mixed $defaultValue
*
* @return mixed
*
* @throws AuthorizationException * @throws AuthorizationException
*/ */
private function loadCustomAttribute(?string $userIdentifier, string $attributeName): void public function getAttribute(?string $userIdentifier, string $attributeName, $defaultValue = null)
{ {
if (!in_array($attributeName, $this->getAvailableAttributes(), true)) {
throw new AuthorizationException(sprintf('attribute \'%s\' undefined', $attributeName), AuthorizationException::ATTRIBUTE_UNDEFINED);
}
$wasFound = false; $wasFound = false;
$value = null;
foreach ($this->authorizationDataProviders as $authorizationDataProvider) { foreach ($this->authorizationDataProviders as $authorizationDataProvider) {
$availableAttributes = $authorizationDataProvider->getAvailableAttributes(); $availableAttributes = $this->getProviderAvailableAttributes($authorizationDataProvider);
if (in_array($attributeName, $availableAttributes, true)) { if (!in_array($attributeName, $availableAttributes, true)) {
$this->loadUserAttributesFromAuthorizationProvider($userIdentifier, $authorizationDataProvider); continue;
}
$userAttributes = $this->getProviderUserAttributes($authorizationDataProvider, $userIdentifier);
if (!array_key_exists($attributeName, $userAttributes)) {
continue;
}
$value = $userAttributes[$attributeName];
$wasFound = true; $wasFound = true;
break; break;
} }
$event = new GetAttributeEvent($this, $attributeName, $userIdentifier);
if ($wasFound) {
$event->setValue($value);
} }
if ($wasFound === false) { // Avoid endless recursions by only emitting an event for each attribtue only once
throw new AuthorizationException(sprintf('custom attribute \'%s\' undefined', $attributeName), AuthorizationException::ATTRIBUTE_UNDEFINED); if (!in_array($attributeName, $this->attributeStack, true)) {
array_push($this->attributeStack, $attributeName);
$this->eventDispatcher->dispatch($event);
array_pop($this->attributeStack);
} }
return $event->getValue($defaultValue);
} }
} }
...@@ -6,6 +6,9 @@ namespace Dbp\Relay\CoreBundle\Authorization; ...@@ -6,6 +6,9 @@ namespace Dbp\Relay\CoreBundle\Authorization;
use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionLanguage; use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionLanguage;
/**
* @internal
*/
class AuthorizationExpressionChecker class AuthorizationExpressionChecker
{ {
public const RIGHTS_CONFIG_ATTRIBUTE = 'rights'; public const RIGHTS_CONFIG_ATTRIBUTE = 'rights';
...@@ -53,19 +56,19 @@ class AuthorizationExpressionChecker ...@@ -53,19 +56,19 @@ class AuthorizationExpressionChecker
* *
* @return mixed|null * @return mixed|null
*/ */
public function getAttribute(AuthorizationUser $currentAuthorizationUser, string $attributeName, $defaultValue = null) public function evalAttributeExpression(AuthorizationUser $currentAuthorizationUser, string $expressionName, $defaultValue = null)
{ {
$this->tryIncreaseRecursionCounter($attributeName); $this->tryIncreaseRecursionCounter($expressionName);
if (($attributeExpression = $this->attributeExpressions[$attributeName] ?? null) !== null) { if (($expression = $this->attributeExpressions[$expressionName] ?? null) !== null) {
$attribute = $this->expressionLanguage->evaluate($attributeExpression, [ $result = $this->expressionLanguage->evaluate($expression, [
'user' => $currentAuthorizationUser, 'user' => $currentAuthorizationUser,
]); ]);
} else { } else {
throw new AuthorizationException(sprintf('attribute \'%s\' undefined', $attributeName), AuthorizationException::ATTRIBUTE_UNDEFINED); throw new AuthorizationException(sprintf('expression \'%s\' undefined', $expressionName), AuthorizationException::ATTRIBUTE_UNDEFINED);
} }
return $attribute ?? $defaultValue; return $result ?? $defaultValue;
} }
/** /**
...@@ -75,9 +78,9 @@ class AuthorizationExpressionChecker ...@@ -75,9 +78,9 @@ class AuthorizationExpressionChecker
* *
* @throws AuthorizationException * @throws AuthorizationException
*/ */
public function getCustomAttribute(AuthorizationUser $currentAuthorizationUser, string $attributeName, $defaultValue = null) public function getUserAttribute(AuthorizationUser $currentAuthorizationUser, string $attributeName, $defaultValue = null)
{ {
return $this->dataMux->getCustomAttribute($currentAuthorizationUser->getIdentifier(), $attributeName, $defaultValue); return $this->dataMux->getAttribute($currentAuthorizationUser->getIdentifier(), $attributeName, $defaultValue);
} }
/** /**
......
...@@ -37,7 +37,7 @@ class AuthorizationUser ...@@ -37,7 +37,7 @@ class AuthorizationUser
*/ */
public function getAttribute(string $attributeName, $defaultValue = null) public function getAttribute(string $attributeName, $defaultValue = null)
{ {
return $this->authorizationChecker->getAttribute($this, $attributeName, $defaultValue); return $this->authorizationChecker->evalAttributeExpression($this, $attributeName, $defaultValue);
} }
/** /**
...@@ -59,6 +59,6 @@ class AuthorizationUser ...@@ -59,6 +59,6 @@ class AuthorizationUser
*/ */
public function get(string $attributeName, $defaultValue = null) public function get(string $attributeName, $defaultValue = null)
{ {
return $this->authorizationChecker->getCustomAttribute($this, $attributeName, $defaultValue); return $this->authorizationChecker->getUserAttribute($this, $attributeName, $defaultValue);
} }
} }
...@@ -18,14 +18,14 @@ class DebugCommand extends Command implements LoggerAwareInterface ...@@ -18,14 +18,14 @@ class DebugCommand extends Command implements LoggerAwareInterface
protected static $defaultName = 'dbp:relay:core:auth-debug'; protected static $defaultName = 'dbp:relay:core:auth-debug';
/** /**
* @var AuthorizationDataProviderProvider * @var AuthorizationDataMuxer
*/ */
private $provider; private $mux;
public function __construct(AuthorizationDataProviderProvider $provider) public function __construct(AuthorizationDataMuxer $mux)
{ {
parent::__construct(); parent::__construct();
$this->provider = $provider; $this->mux = $mux;
} }
protected function configure() protected function configure()
...@@ -38,15 +38,12 @@ class DebugCommand extends Command implements LoggerAwareInterface ...@@ -38,15 +38,12 @@ class DebugCommand extends Command implements LoggerAwareInterface
{ {
$username = $input->getArgument('username'); $username = $input->getArgument('username');
// Fetch all attributes first (to get potential log spam first) $attrs = $this->mux->getAvailableAttributes();
$providers = $this->provider->getAuthorizationDataProviders();
$mux = new AuthorizationDataMuxer($providers);
$attrs = $mux->getAvailableAttributes();
$all = []; $all = [];
$default = new \stdClass(); $default = new \stdClass();
sort($attrs, SORT_STRING | SORT_FLAG_CASE); sort($attrs, SORT_STRING | SORT_FLAG_CASE);
foreach ($attrs as $attr) { foreach ($attrs as $attr) {
$all[$attr] = $mux->getCustomAttribute($username, $attr, $default); $all[$attr] = $this->mux->getAttribute($username, $attr, $default);
} }
// Now print them out // Now print them out
......
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Authorization\Event;
use Dbp\Relay\CoreBundle\Authorization\AuthorizationDataMuxer;
use Symfony\Contracts\EventDispatcher\Event;
/**
* This hook can be used to change the value of attributes at the time they are requested.
*
* Can be used to change existing attribute values, and introduce new attributes.
* In case of new attributes you have to make sure to also handle GetAvailableAttributesEvent
* and register your new attribute there.
*/
class GetAttributeEvent extends Event
{
/**
* @var AuthorizationDataMuxer
*/
private $mux;
/**
* @var ?string
*/
private $userIdentifier;
/**
* @var string
*/
private $name;
/**
* @var mixed
*/
private $value;
/**
* @var bool
*/
private $hasValue;
public function __construct(AuthorizationDataMuxer $mux, string $name, ?string $userIdentifier)
{
$this->mux = $mux;
$this->userIdentifier = $userIdentifier;
$this->name = $name;
$this->hasValue = false;
}
/**
* @param mixed|null $defaultValue
*
* @return mixed|null
*/
public function getAttribute(string $attributeName, $defaultValue = null)
{
return $this->mux->getAttribute($this->userIdentifier, $attributeName, $defaultValue);
}
public function getAttributeName(): string
{
return $this->name;
}
/**
* @param mixed $value
*/
public function setValue($value): void
{
$this->value = $value;
$this->hasValue = true;
}
/**
* @param mixed $default
*
* @return mixed
*/
public function getValue($default = null)
{
if (!$this->hasValue) {
return $default;
}
return $this->value;
}
}
<?php
declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Authorization\Event;
use Symfony\Contracts\EventDispatcher\Event;
/**
* This hook can be used to change the set of available attributes.
*
* You can extend the set, or remove attributes.
*/
class GetAvailableAttributesEvent extends Event
{
/**
* @var string[]
*/
private $attributes;
/**
* @param string[] $attributes
*/
public function __construct(array $attributes)
{
$this->attributes = $attributes;
}
/**
* @return string[]
*/
public function getAttributes(): array
{
return $this->attributes;
}
/**
* @param string[] $attributes
*/
public function setAttributes(array $attributes): void
{
$this->attributes = $attributes;
}
}
...@@ -4,9 +4,7 @@ declare(strict_types=1); ...@@ -4,9 +4,7 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\LocalData; namespace Dbp\Relay\CoreBundle\LocalData;
use Dbp\Relay\CoreBundle\API\UserSessionInterface;
use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService; use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService;
use Dbp\Relay\CoreBundle\Authorization\AuthorizationDataProviderProvider;
use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
...@@ -20,10 +18,8 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio ...@@ -20,10 +18,8 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio
/** @var string[] */ /** @var string[] */
private $attributeMapping; private $attributeMapping;
public function __construct(UserSessionInterface $userSession, AuthorizationDataProviderProvider $authorizationDataProviderProvider) public function __construct()
{ {
parent::__construct($userSession, $authorizationDataProviderProvider);
$this->attributeMapping = []; $this->attributeMapping = [];
} }
......
...@@ -93,6 +93,10 @@ services: ...@@ -93,6 +93,10 @@ services:
autoconfigure: true autoconfigure: true
arguments: [ !tagged auth.authorization_data_provider ] arguments: [ !tagged auth.authorization_data_provider ]
Dbp\Relay\CoreBundle\Authorization\AuthorizationDataMuxer:
autowire: true
autoconfigure: true
Dbp\Relay\CoreBundle\Helpers\Locale: Dbp\Relay\CoreBundle\Helpers\Locale:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment