diff --git a/src/Authorization/AbstractAuthorizationService.php b/src/Authorization/AbstractAuthorizationService.php index 5daddb85610fc936d88d0669cb975acb3bb2529a..0e7270da9b7d62c4256268ef0b3de4e7d2f54dd3 100644 --- a/src/Authorization/AbstractAuthorizationService.php +++ b/src/Authorization/AbstractAuthorizationService.php @@ -6,12 +6,13 @@ namespace Dbp\Relay\CoreBundle\Authorization; use Dbp\Relay\CoreBundle\API\UserSessionInterface; use Dbp\Relay\CoreBundle\Exception\ApiError; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\HttpFoundation\Response; abstract class AbstractAuthorizationService { - public const RIGHTS_CONFIG_ATTRIBUTE = AuthorizationExpressionChecker::RIGHTS_CONFIG_ATTRIBUTE; - public const ATTRIBUTES_CONFIG_ATTRIBUTE = AuthorizationExpressionChecker::ATTRIBUTES_CONFIG_ATTRIBUTE; + private const AUTHORIZATION_ROOT_CONFIG_NODE = 'authorization'; /** @var AuthorizationExpressionChecker */ private $userAuthorizationChecker; @@ -19,6 +20,7 @@ abstract class AbstractAuthorizationService /** @var AuthorizationUser */ private $currentAuthorizationUser; + /** @var array|null */ private $config; /** @@ -27,13 +29,13 @@ abstract class AbstractAuthorizationService public function _injectServices(UserSessionInterface $userSession, AuthorizationDataMuxer $mux) { $this->userAuthorizationChecker = new AuthorizationExpressionChecker($mux); - $this->currentAuthorizationUser = new AuthorizationUser($userSession->getUserIdentifier(), $this->userAuthorizationChecker); + $this->currentAuthorizationUser = new AuthorizationUser($userSession, $this->userAuthorizationChecker); $this->updateConfig(); } public function setConfig(array $config) { - $this->config = $config; + $this->config = $config[self::AUTHORIZATION_ROOT_CONFIG_NODE] ?? []; $this->updateConfig(); } @@ -76,8 +78,6 @@ abstract class AbstractAuthorizationService private function getAttributeInternal(string $attributeName, $defaultValue = null) { - $this->userAuthorizationChecker->init(); - return $this->userAuthorizationChecker->evalAttributeExpression($this->currentAuthorizationUser, $attributeName, $defaultValue); } @@ -86,8 +86,45 @@ abstract class AbstractAuthorizationService */ private function isGrantedInternal(string $rightName, $subject = null): bool { - $this->userAuthorizationChecker->init(); - return $this->userAuthorizationChecker->isGranted($this->currentAuthorizationUser, $rightName, $subject); } + + /** + * @param array $rights the mapping between right names and right default expressions + * @param array $attributes the mapping between attribute names and attribute default expressions + */ + public static function getAuthorizationConfigNodeDefinition(array $rights = [], array $attributes = []): NodeDefinition + { + $treeBuilder = new TreeBuilder(self::AUTHORIZATION_ROOT_CONFIG_NODE); + + $rightsNodeChildBuilder = $treeBuilder->getRootNode()->children()->arrayNode(AuthorizationExpressionChecker::RIGHTS_CONFIG_NODE) + ->addDefaultsIfNotSet() + ->children(); + foreach ($rights as $rightName => $defaultExpression) { + $rightsNodeChildBuilder->scalarNode($rightName) + ->defaultValue($defaultExpression ?? 'false') + ->end(); + } + + $attributesNodeChildBuilder = $treeBuilder->getRootNode()->children()->arrayNode(AuthorizationExpressionChecker::ATTRIBUTES_CONFIG_NODE) + ->addDefaultsIfNotSet() + ->children(); + foreach ($attributes as $attributeName => $defaultExpression) { + $attributesNodeChildBuilder->scalarNode($attributeName) + ->defaultValue($defaultExpression ?? 'null') + ->end(); + } + + return $treeBuilder->getRootNode(); + } + + public static function createConfig(array $rightExpressions = [], array $attributeExpressions = []): array + { + return [ + AbstractAuthorizationService::AUTHORIZATION_ROOT_CONFIG_NODE => [ + AuthorizationExpressionChecker::RIGHTS_CONFIG_NODE => $rightExpressions, + AuthorizationExpressionChecker::ATTRIBUTES_CONFIG_NODE => $attributeExpressions, + ], + ]; + } } diff --git a/src/Authorization/AuthorizationExpressionChecker.php b/src/Authorization/AuthorizationExpressionChecker.php index 545edd82523ace570baec86420d7dfd0e3741d04..e0d20eb25a8af1caf695635c7b13f1d818234fbf 100644 --- a/src/Authorization/AuthorizationExpressionChecker.php +++ b/src/Authorization/AuthorizationExpressionChecker.php @@ -11,8 +11,8 @@ use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionLanguage; */ class AuthorizationExpressionChecker { - public const RIGHTS_CONFIG_ATTRIBUTE = 'rights'; - public const ATTRIBUTES_CONFIG_ATTRIBUTE = 'attributes'; + public const RIGHTS_CONFIG_NODE = 'rights'; + public const ATTRIBUTES_CONFIG_NODE = 'attributes'; private const MAX_NUM_CALLS = 16; @@ -25,9 +25,6 @@ class AuthorizationExpressionChecker /** @var array */ private $attributeExpressions; - /** @var int */ - private $callCounter; - /** @var AuthorizationDataMuxer */ private $dataMux; @@ -40,7 +37,6 @@ class AuthorizationExpressionChecker public function __construct(AuthorizationDataMuxer $dataMux) { $this->expressionLanguage = new ExpressionLanguage(); - $this->rightExpressions = []; $this->attributeExpressions = []; $this->dataMux = $dataMux; @@ -50,13 +46,8 @@ class AuthorizationExpressionChecker public function setConfig(array $config) { - $this->loadExpressions($config[self::RIGHTS_CONFIG_ATTRIBUTE] ?? [], $this->rightExpressions); - $this->loadExpressions($config[self::ATTRIBUTES_CONFIG_ATTRIBUTE] ?? [], $this->attributeExpressions); - } - - public function init() - { - $this->callCounter = 0; + $this->loadExpressions($config[self::RIGHTS_CONFIG_NODE] ?? [], $this->rightExpressions); + $this->loadExpressions($config[self::ATTRIBUTES_CONFIG_NODE] ?? [], $this->attributeExpressions); } /** diff --git a/src/Authorization/AuthorizationUser.php b/src/Authorization/AuthorizationUser.php index 8a6a1361706f2517bd867eaaf1d9b27b75c0fc69..bfc17f173b5730063eb78ea29a6a461a1a97e5d2 100644 --- a/src/Authorization/AuthorizationUser.php +++ b/src/Authorization/AuthorizationUser.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Dbp\Relay\CoreBundle\Authorization; +use Dbp\Relay\CoreBundle\API\UserSessionInterface; + /** * Provides the user interface available within privilege expressions. */ @@ -12,20 +14,18 @@ class AuthorizationUser /** @var AuthorizationExpressionChecker */ private $authorizationChecker; - /** - * @var string|null - */ - private $identifier; + /** @var UserSessionInterface */ + private $userSession; - public function __construct(?string $identifier, AuthorizationExpressionChecker $authorizationChecker) + public function __construct(UserSessionInterface $userSession, AuthorizationExpressionChecker $authorizationChecker) { + $this->userSession = $userSession; $this->authorizationChecker = $authorizationChecker; - $this->identifier = $identifier; } public function getIdentifier(): ?string { - return $this->identifier; + return $this->userSession->getUserIdentifier(); } /** diff --git a/src/Authorization/ExpressionLanguage/ExpressionLanguage.php b/src/Authorization/ExpressionLanguage/ExpressionLanguage.php index a997ab81e79679de981405f1c2921d713ed13b33..49baa180195b76554f700e06ea49a9f0773ab75e 100644 --- a/src/Authorization/ExpressionLanguage/ExpressionLanguage.php +++ b/src/Authorization/ExpressionLanguage/ExpressionLanguage.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage; +use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionFunctionProviders\ArrayExpressionFunctionProvider; use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionFunctionProviders\FilterExpressionFunctionProvider; use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionFunctionProviders\MapExpressionFunctionProvider; use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionFunctionProviders\PhpArrayExpressionFunctionProvider; @@ -22,6 +23,7 @@ class ExpressionLanguage extends SymfonyExpressionLanguage new PhpArrayExpressionFunctionProvider(), new PhpNumericExpressionFunctionProvider(), new PhpStringExpressionFunctionProvider(), + new ArrayExpressionFunctionProvider(), ], $providers); parent::__construct($cache, $providers); diff --git a/src/LocalData/AbstractLocalDataPostEventSubscriber.php b/src/LocalData/AbstractLocalDataPostEventSubscriber.php index 6d6115c591104235c976e0fa55f2995369af2313..0879e34f4a70f04dda2aab13732fadf692f46f98 100644 --- a/src/LocalData/AbstractLocalDataPostEventSubscriber.php +++ b/src/LocalData/AbstractLocalDataPostEventSubscriber.php @@ -5,17 +5,38 @@ declare(strict_types=1); namespace Dbp\Relay\CoreBundle\LocalData; use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService; +use Dbp\Relay\CoreBundle\Exception\ApiError; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Response; +/* + * Abstract implementation of a configurable local data provider post event subscriber. + * It is intended to be derived by local data aware entity post event subscribers. + * A mapping between source attribute and local data attribute, + * and default values for the attributes can be specified by means of the deriving event subscriber's bundle config. + * If no default value is specified, an exception is thrown in the case the mapped source attribute is not found. + */ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizationService implements EventSubscriberInterface { - public const CONFIG_NODE = 'local_data_mapping'; - public const SOURCE_ATTRIBUTE_KEY = 'source_attribute'; - public const LOCAL_DATA_ATTRIBUTE_KEY = 'local_data_attribute'; - public const AUTHORIZATION_EXPRESSION_KEY = 'authorization_expression'; + protected const ROOT_CONFIG_NODE = 'local_data_mapping'; + protected const SOURCE_ATTRIBUTE_CONFIG_NODE = 'source_attribute'; + protected const LOCAL_DATA_ATTRIBUTE_CONFIG_NODE = 'local_data_attribute'; + protected const AUTHORIZATION_EXPRESSION_CONFIG_NODE = 'authorization_expression'; + protected const DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE = 'default_value'; + protected const DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE = 'default_values'; - /** @var string[] */ + private const SOURCE_ATTRIBUTE_KEY = 'source'; + private const DEFAULT_VALUE_KEY = 'default'; + + /* + * WORKAROUND: could not find a way to determine whether a Symfony config array node was NOT specified since it provides an empty + * array in case it is not specified. So I use an array value as default which does not seem to be reproducible by the configurator. + */ + private const ARRAY_VALUE_NOT_SPECIFIED = [null => null]; + + /** @var array */ private $attributeMapping; public function __construct() @@ -25,21 +46,34 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio public function setConfig(array $config) { - $configNode = $config[self::CONFIG_NODE] ?? []; + $configNode = $config[self::ROOT_CONFIG_NODE] ?? []; + $rightExpressions = []; - $rights = []; foreach ($configNode as $configMappingEntry) { - if (isset($this->attributeMapping[$configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY]])) { - throw new \RuntimeException(sprintf('multiple mapping entries for local data attribute %s', $configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY])); + $localDataAttributeName = $configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE]; + + if (isset($this->attributeMapping[$localDataAttributeName])) { + throw new \RuntimeException(sprintf('multiple mapping entries for local data attribute %s', $localDataAttributeName)); + } + + $attributeMapEntry = []; + $attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY] = $configMappingEntry[self::SOURCE_ATTRIBUTE_CONFIG_NODE]; + + $defaultValue = $configMappingEntry[self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE] ?? + ((($defaultArray = $configMappingEntry[self::DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE]) !== self::ARRAY_VALUE_NOT_SPECIFIED) ? $defaultArray : null); + if ($defaultValue !== null) { + $attributeMapEntry[self::DEFAULT_VALUE_KEY] = $defaultValue; } - $this->attributeMapping[$configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY]] = $configMappingEntry[self::SOURCE_ATTRIBUTE_KEY]; + + $this->attributeMapping[$localDataAttributeName] = $attributeMapEntry; + // the name of the local data attribute is used as name for the right to view that attribute // the attribute is visible false by default - $rights[$configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY]] = $configMappingEntry[self::AUTHORIZATION_EXPRESSION_KEY] ?? 'false'; + $rightExpressions[$localDataAttributeName] = $configMappingEntry[self::AUTHORIZATION_EXPRESSION_CONFIG_NODE] ?? 'false'; } - if (!empty($rights)) { - parent::setConfig([AbstractAuthorizationService::RIGHTS_CONFIG_ATTRIBUTE => $rights]); + if (!empty($rightExpressions)) { + parent::setConfig(parent::createConfig($rightExpressions)); } } @@ -57,29 +91,51 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio { $sourceData = $postEvent->getSourceData(); - foreach ($this->attributeMapping as $localDataAttributeName => $sourceAttributeName) { - if ($this->isGranted($localDataAttributeName)) { - if (($sourceAttributeValue = $sourceData[$sourceAttributeName] ?? null) !== null) { - $postEvent->trySetLocalDataAttribute($localDataAttributeName, $sourceAttributeValue); + foreach ($this->attributeMapping as $localDataAttributeName => $attributeMapEntry) { + if ($postEvent->isLocalDataAttributeRequested($localDataAttributeName)) { + if (!$this->isGranted($localDataAttributeName)) { + throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, sprintf('access to local data attribute \'%s\' denied', $localDataAttributeName)); + } + + $sourceAttributeName = $attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY]; + $attributeValue = $sourceData[$sourceAttributeName] ?? $attributeMapEntry[self::DEFAULT_VALUE_KEY] ?? null; + if ($attributeValue !== null) { + $postEvent->setLocalDataAttribute($localDataAttributeName, $attributeValue); } else { - throw new \RuntimeException(sprintf('attribute \'%s\' not available in source data', $sourceAttributeName)); + throw ApiError::withDetails(Response::HTTP_INTERNAL_SERVER_ERROR, sprintf('attribute \'%s\' not available in source data', $sourceAttributeName)); } } } } - public static function getConfigNode() + /** + * @deprecated Use getLocalDataMappingConfigNodeDefinition instead + */ + public static function getConfigNode(): NodeDefinition { - $treeBuilder = new TreeBuilder(self::CONFIG_NODE); + return self::getLocalDataMappingConfigNodeDefinition(); + } + + public static function getLocalDataMappingConfigNodeDefinition(): NodeDefinition + { + $treeBuilder = new TreeBuilder(self::ROOT_CONFIG_NODE); return $treeBuilder->getRootNode() ->arrayPrototype() ->children() - ->scalarNode(self::SOURCE_ATTRIBUTE_KEY)->end() - ->scalarNode(self::LOCAL_DATA_ATTRIBUTE_KEY)->end() - ->scalarNode(self::AUTHORIZATION_EXPRESSION_KEY) + ->scalarNode(self::SOURCE_ATTRIBUTE_CONFIG_NODE)->end() + ->scalarNode(self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE)->end() + ->scalarNode(self::AUTHORIZATION_EXPRESSION_CONFIG_NODE) ->defaultValue('false') ->end() + ->scalarNode(self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE) + ->info('The default value for scalar (non-array) attributes. If none is specified, an exception is thrown in the case the source attribute is not found.') + ->end() + ->arrayNode(self::DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE) + ->defaultValue(self::ARRAY_VALUE_NOT_SPECIFIED) + ->info('The default value for array type attributes. If none is specified, an exception is thrown in the case the source attribute is not found.') + ->scalarPrototype()->end() + ->end() ->end() ->end() ;