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

AuthorizationDataMuxer: add two events for mutating attributes

One event for changing the global attribute list and one for changing
the result for existing attributes and/or introducing new attributes.
parent 51966acf
Branches
Tags
No related merge requests found
......@@ -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);
}
}
/**
......
......@@ -4,32 +4,43 @@ 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 $attributes;
/** @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)
{
$this->authorizationDataProviders = $authorizationDataProviders;
$this->attributes = [];
}
private $availableCacheAll;
private function loadUserAttributesFromAuthorizationProvider(?string $userIdentifier, AuthorizationDataProviderInterface $authorizationDataProvider): void
public function __construct(AuthorizationDataProviderProvider $authorizationDataProviderProvider, EventDispatcherInterface $eventDispatcher)
{
$userAttributes = $authorizationDataProvider->getUserAttributes($userIdentifier);
foreach ($authorizationDataProvider->getAvailableAttributes() as $availableAttribute) {
if (array_key_exists($availableAttribute, $userAttributes)) {
$this->attributes[$availableAttribute] = $userAttributes[$availableAttribute];
}
}
$this->authorizationDataProviders = $authorizationDataProviderProvider->getAuthorizationDataProviders();
$this->eventDispatcher = $eventDispatcher;
$this->providerCache = [];
$this->availableCache = [];
$this->availableCacheAll = null;
$this->attributeStack = [];
}
/**
......@@ -39,48 +50,97 @@ class AuthorizationDataMuxer
*/
public function getAvailableAttributes(): array
{
if ($this->availableCacheAll === null) {
$res = [];
foreach ($this->authorizationDataProviders as $authorizationDataProvider) {
$availableAttributes = $authorizationDataProvider->getAvailableAttributes();
$availableAttributes = $this->getProviderAvailableAttributes($authorizationDataProvider);
$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 getAttribute(?string $userIdentifier, string $attributeName, $defaultValue = null)
private function getProviderUserAttributes(AuthorizationDataProviderInterface $prov, ?string $userIdentifier): array
{
if (array_key_exists($attributeName, $this->attributes) === false) {
$this->loadAttribute($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->attributes[$attributeName] ?? $defaultValue;
return $res[1];
}
/**
* @param mixed $defaultValue
*
* @return mixed
*
* @throws AuthorizationException
*/
private function loadAttribute(?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);
$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;
}
$event = new GetAttributeEvent($this, $attributeName, $userIdentifier);
if ($wasFound) {
$event->setValue($value);
}
if ($wasFound === false) {
throw new AuthorizationException(sprintf('custom attribute \'%s\' undefined', $attributeName), AuthorizationException::ATTRIBUTE_UNDEFINED);
// 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';
......
......@@ -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->getAttribute($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;
}
}
......@@ -4,9 +4,7 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\LocalData;
use Dbp\Relay\CoreBundle\API\UserSessionInterface;
use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService;
use Dbp\Relay\CoreBundle\Authorization\AuthorizationDataProviderProvider;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -20,10 +18,8 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio
/** @var string[] */
private $attributeMapping;
public function __construct(UserSessionInterface $userSession, AuthorizationDataProviderProvider $authorizationDataProviderProvider)
public function __construct()
{
parent::__construct($userSession, $authorizationDataProviderProvider);
$this->attributeMapping = [];
}
......
......@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment