Skip to content
Commits on Source (5)
......@@ -16,19 +16,32 @@ abstract class AbstractAuthorizationService
/** @var AuthorizationExpressionChecker */
private $userAuthorizationChecker;
/** @var AuthorizationUser|null */
/** @var AuthorizationUser */
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($muxer);
$this->userAuthorizationChecker = new AuthorizationExpressionChecker($mux);
$this->currentAuthorizationUser = new AuthorizationUser($userSession->getUserIdentifier(), $this->userAuthorizationChecker);
$this->updateConfig();
}
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
{
$this->userAuthorizationChecker->init();
return $this->userAuthorizationChecker->getAttribute($this->currentAuthorizationUser, $attributeName, $defaultValue);
return $this->userAuthorizationChecker->evalAttributeExpression($this->currentAuthorizationUser, $attributeName, $defaultValue);
}
/**
......
......@@ -4,83 +4,143 @@ declare(strict_types=1);
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
{
/** @var iterable */
/** @var iterable<AuthorizationDataProviderInterface> */
private $authorizationDataProviders;
/** @var array */
private $customAttributes;
/** @var array<string, array> */
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;
public function __construct(AuthorizationDataProviderProvider $authorizationDataProviderProvider, EventDispatcherInterface $eventDispatcher)
{
$this->authorizationDataProviders = $authorizationDataProviders;
$this->customAttributes = [];
$this->authorizationDataProviders = $authorizationDataProviderProvider->getAuthorizationDataProviders();
$this->eventDispatcher = $eventDispatcher;
$this->providerCache = [];
$this->availableCache = [];
$this->availableCacheAll = null;
$this->attributeStack = [];
}
private function loadUserAttributesFromAuthorizationProvider(?string $userIdentifier, AuthorizationDataProviderInterface $authorizationDataProvider): void
/**
* Returns an array of available attributes.
*
* @return string[]
*/
public function getAvailableAttributes(): array
{
$userAttributes = $authorizationDataProvider->getUserAttributes($userIdentifier);
foreach ($authorizationDataProvider->getAvailableAttributes() as $availableAttribute) {
if (array_key_exists($availableAttribute, $userAttributes)) {
$this->customAttributes[$availableAttribute] = $userAttributes[$availableAttribute];
if ($this->availableCacheAll === null) {
$res = [];
foreach ($this->authorizationDataProviders as $authorizationDataProvider) {
$availableAttributes = $this->getProviderAvailableAttributes($authorizationDataProvider);
$res = array_merge($res, $availableAttributes);
}
$event = new GetAvailableAttributesEvent($res);
$this->eventDispatcher->dispatch($event);
$this->availableCacheAll = $event->getAttributes();
}
return $this->availableCacheAll;
}
/**
* Returns an array of available attributes.
* Returns a cached list for available attributes for the provider.
*
* @return string[]
*/
public function getAvailableAttributes(): array
private function getProviderAvailableAttributes(AuthorizationDataProviderInterface $prov): array
{
$res = [];
foreach ($this->authorizationDataProviders as $authorizationDataProvider) {
$availableAttributes = $authorizationDataProvider->getAvailableAttributes();
$res = array_merge($res, $availableAttributes);
// Caches getAvailableAttributes for each provider
$provKey = get_class($prov);
if (!array_key_exists($provKey, $this->availableCache)) {
$this->availableCache[$provKey] = $prov->getAvailableAttributes();
}
return $res;
return $this->availableCache[$provKey];
}
/**
* @param mixed|null $defaultValue
*
* @return mixed|null
* 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) {
$this->loadCustomAttribute($userIdentifier, $attributeName);
// We cache the attributes for each provider, but only for the last userIdentifier
$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
*/
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;
$value = null;
foreach ($this->authorizationDataProviders as $authorizationDataProvider) {
$availableAttributes = $authorizationDataProvider->getAvailableAttributes();
if (in_array($attributeName, $availableAttributes, true)) {
$this->loadUserAttributesFromAuthorizationProvider($userIdentifier, $authorizationDataProvider);
$wasFound = true;
break;
$availableAttributes = $this->getProviderAvailableAttributes($authorizationDataProvider);
if (!in_array($attributeName, $availableAttributes, true)) {
continue;
}
$userAttributes = $this->getProviderUserAttributes($authorizationDataProvider, $userIdentifier);
if (!array_key_exists($attributeName, $userAttributes)) {
continue;
}
$value = $userAttributes[$attributeName];
$wasFound = true;
break;
}
if ($wasFound === false) {
throw new AuthorizationException(sprintf('custom attribute \'%s\' undefined', $attributeName), AuthorizationException::ATTRIBUTE_UNDEFINED);
$event = new GetAttributeEvent($this, $attributeName, $userIdentifier);
if ($wasFound) {
$event->setValue($value);
}
// Avoid endless recursions by only emitting an event for each attribtue only once
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;
use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionLanguage;
/**
* @internal
*/
class AuthorizationExpressionChecker
{
public const RIGHTS_CONFIG_ATTRIBUTE = 'rights';
......@@ -53,19 +56,19 @@ class AuthorizationExpressionChecker
*
* @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) {
$attribute = $this->expressionLanguage->evaluate($attributeExpression, [
if (($expression = $this->attributeExpressions[$expressionName] ?? null) !== null) {
$result = $this->expressionLanguage->evaluate($expression, [
'user' => $currentAuthorizationUser,
]);
} 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
*
* @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
*/
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
*/
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
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();
$this->provider = $provider;
$this->mux = $mux;
}
protected function configure()
......@@ -38,15 +38,12 @@ class DebugCommand extends Command implements LoggerAwareInterface
{
$username = $input->getArgument('username');
// Fetch all attributes first (to get potential log spam first)
$providers = $this->provider->getAuthorizationDataProviders();
$mux = new AuthorizationDataMuxer($providers);
$attrs = $mux->getAvailableAttributes();
$attrs = $this->mux->getAvailableAttributes();
$all = [];
$default = new \stdClass();
sort($attrs, SORT_STRING | SORT_FLAG_CASE);
foreach ($attrs as $attr) {
$all[$attr] = $mux->getCustomAttribute($username, $attr, $default);
$all[$attr] = $this->mux->getAttribute($username, $attr, $default);
}
// 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;
}
}
......@@ -18,6 +18,11 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio
/** @var string[] */
private $attributeMapping;
public function __construct()
{
$this->attributeMapping = [];
}
public function setConfig(array $config)
{
$configNode = $config[self::CONFIG_NODE] ?? [];
......@@ -54,7 +59,7 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio
foreach ($this->attributeMapping as $localDataAttributeName => $sourceAttributeName) {
if ($this->isGranted($localDataAttributeName)) {
if ($sourceAttributeValue = $sourceData[$sourceAttributeName] ?? null) {
if (($sourceAttributeValue = $sourceData[$sourceAttributeName] ?? null) !== null) {
$postEvent->trySetLocalDataAttribute($localDataAttributeName, $sourceAttributeValue);
} else {
throw new \RuntimeException(sprintf('attribute \'%s\' not available in source data', $sourceAttributeName));
......
......@@ -93,6 +93,10 @@ services:
autoconfigure: true
arguments: [ !tagged auth.authorization_data_provider ]
Dbp\Relay\CoreBundle\Authorization\AuthorizationDataMuxer:
autowire: true
autoconfigure: true
Dbp\Relay\CoreBundle\Helpers\Locale:
autowire: true
autoconfigure: true
......