From 1de0ce9286a6559f1e9cf15ac863e72959b86dcb Mon Sep 17 00:00:00 2001 From: Christoph Reiter <reiter.christoph@gmail.com> Date: Wed, 30 Nov 2022 14:20:16 +0100 Subject: [PATCH] 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. --- .../AbstractAuthorizationService.php | 23 ++- src/Authorization/AuthorizationDataMuxer.php | 132 +++++++++++++----- .../AuthorizationExpressionChecker.php | 3 + src/Authorization/DebugCommand.php | 15 +- src/Authorization/Event/GetAttributeEvent.php | 89 ++++++++++++ .../Event/GetAvailableAttributesEvent.php | 44 ++++++ .../AbstractLocalDataPostEventSubscriber.php | 6 +- src/Resources/config/services.yaml | 4 + 8 files changed, 261 insertions(+), 55 deletions(-) create mode 100644 src/Authorization/Event/GetAttributeEvent.php create mode 100644 src/Authorization/Event/GetAvailableAttributesEvent.php diff --git a/src/Authorization/AbstractAuthorizationService.php b/src/Authorization/AbstractAuthorizationService.php index fc69b1a..28cefd4 100644 --- a/src/Authorization/AbstractAuthorizationService.php +++ b/src/Authorization/AbstractAuthorizationService.php @@ -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); + } } /** diff --git a/src/Authorization/AuthorizationDataMuxer.php b/src/Authorization/AuthorizationDataMuxer.php index 424d35d..f3e0673 100644 --- a/src/Authorization/AuthorizationDataMuxer.php +++ b/src/Authorization/AuthorizationDataMuxer.php @@ -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 $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) + private $availableCacheAll; + + public function __construct(AuthorizationDataProviderProvider $authorizationDataProviderProvider, EventDispatcherInterface $eventDispatcher) { - $this->authorizationDataProviders = $authorizationDataProviders; - $this->attributes = []; + $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->attributes[$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 + * Returns a cached map of available user attributes. * - * @return mixed|null - * - * @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); - $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); } } diff --git a/src/Authorization/AuthorizationExpressionChecker.php b/src/Authorization/AuthorizationExpressionChecker.php index 2363d9e..c903ff8 100644 --- a/src/Authorization/AuthorizationExpressionChecker.php +++ b/src/Authorization/AuthorizationExpressionChecker.php @@ -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'; diff --git a/src/Authorization/DebugCommand.php b/src/Authorization/DebugCommand.php index 619d0ac..174a98e 100644 --- a/src/Authorization/DebugCommand.php +++ b/src/Authorization/DebugCommand.php @@ -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 diff --git a/src/Authorization/Event/GetAttributeEvent.php b/src/Authorization/Event/GetAttributeEvent.php new file mode 100644 index 0000000..d825d52 --- /dev/null +++ b/src/Authorization/Event/GetAttributeEvent.php @@ -0,0 +1,89 @@ +<?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; + } +} diff --git a/src/Authorization/Event/GetAvailableAttributesEvent.php b/src/Authorization/Event/GetAvailableAttributesEvent.php new file mode 100644 index 0000000..8cefaa6 --- /dev/null +++ b/src/Authorization/Event/GetAvailableAttributesEvent.php @@ -0,0 +1,44 @@ +<?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; + } +} diff --git a/src/LocalData/AbstractLocalDataPostEventSubscriber.php b/src/LocalData/AbstractLocalDataPostEventSubscriber.php index 9000426..6d6115c 100644 --- a/src/LocalData/AbstractLocalDataPostEventSubscriber.php +++ b/src/LocalData/AbstractLocalDataPostEventSubscriber.php @@ -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 = []; } diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 192524e..7f13c44 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -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 -- GitLab