From 6190de8bc684b2b7ce2852aeecf722b1ef66f339 Mon Sep 17 00:00:00 2001 From: Tobias Gross-Vogt <tobias.gross-vogt@tugraz.at> Date: Mon, 20 Feb 2023 08:50:01 +0100 Subject: [PATCH] entity attribute authorization now part of the AbstractAuthorizationService and config is found under 'authorization.entities --- .../AbstractAuthorizationService.php | 226 +++++++++++++++--- .../AuthorizationExpressionChecker.php | 32 +-- src/Helpers/Tools.php | 9 + .../AbstractLocalDataEventSubscriber.php | 4 +- 4 files changed, 216 insertions(+), 55 deletions(-) diff --git a/src/Authorization/AbstractAuthorizationService.php b/src/Authorization/AbstractAuthorizationService.php index 2d3e209..4944f0a 100644 --- a/src/Authorization/AbstractAuthorizationService.php +++ b/src/Authorization/AbstractAuthorizationService.php @@ -6,13 +6,39 @@ namespace Dbp\Relay\CoreBundle\Authorization; use Dbp\Relay\CoreBundle\API\UserSessionInterface; use Dbp\Relay\CoreBundle\Exception\ApiError; +use Dbp\Relay\CoreBundle\Helpers\Tools; use Symfony\Component\Config\Definition\Builder\NodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; -abstract class AbstractAuthorizationService +abstract class AbstractAuthorizationService implements ContextAwareNormalizerInterface, NormalizerAwareInterface { + use NormalizerAwareTrait; + + /* config array keys */ private const AUTHORIZATION_ROOT_CONFIG_NODE = 'authorization'; + private const ROLES_CONFIG_NODE = 'roles'; + private const ATTRIBUTES_CONFIG_NODE = 'attributes'; + private const ENTITIES_CONFIG_NODE = 'entities'; + private const ENTITY_READ_ACCESS_CONFIG_NODE = 'read_access'; + private const ENTITY_WRITE_ACCESS_CONFIG_NODE = 'write_access'; + private const ENTITY_CLASS_NAME_CONFIG_NODE = 'class_name'; + + /* internal array keys */ + private const ROLES_KEY = 'roles'; + private const ATTRIBUTES_KEY = 'attributes'; + private const ENTITIES_KEY = 'entities'; + private const ENTITY_READ_ACCESS_KEY = 'read_access'; + private const ENTITY_WRITE_ACCESS_KEY = 'write_access'; + private const ENTITY_SHORT_NAME_KEY = 'short_name'; + private const ENTITY_CLASS_NAME_KEY = 'class_name'; + + private const ENTITY_READ_ACCESS_ATTRIBUTE_NAMES_KEY = 'read_attribute_names'; + private const ENTITY_OBJECT_ALIAS = 'entity'; + private const CONTEXT_GROUPS_KEY = 'groups'; /** @var AuthorizationExpressionChecker */ private $userAuthorizationChecker; @@ -23,6 +49,14 @@ abstract class AbstractAuthorizationService /** @var array|null */ private $config; + /** @var array */ + private $entityClassNameToAttributeNamesMapping; + + public function __construct() + { + $this->entityClassNameToAttributeNamesMapping = []; + } + /** * @required */ @@ -30,20 +64,22 @@ abstract class AbstractAuthorizationService { $this->userAuthorizationChecker = new AuthorizationExpressionChecker($mux); $this->currentAuthorizationUser = new AuthorizationUser($userSession, $this->userAuthorizationChecker); - $this->updateConfig(); + $this->loadConfig(); } public function setConfig(array $config) { $this->config = $config[self::AUTHORIZATION_ROOT_CONFIG_NODE] ?? []; - $this->updateConfig(); + $this->loadConfig(); } - private function updateConfig() + public function configure(array $roleMapping = [], array $attributeMapping = []): void { - if ($this->userAuthorizationChecker !== null && $this->config !== null) { - $this->userAuthorizationChecker->setConfig($this->config); - } + $this->config = [ + self::ROLES_CONFIG_NODE => $roleMapping, + self::ATTRIBUTES_CONFIG_NODE => $attributeMapping, + ]; + $this->loadConfig(); } /** @@ -76,6 +112,80 @@ abstract class AbstractAuthorizationService return $this->getAttributeInternal($attributeName, $defaultValue); } + /** + * {@inheritDoc} + */ + public function normalize($object, $format = null, array $context = []) + { + $entityClassName = get_class($object); + $mapEntry = $this->entityClassNameToAttributeNamesMapping[$entityClassName]; + $entityShortName = $mapEntry[self::ENTITY_SHORT_NAME_KEY]; + + foreach ($mapEntry[self::ENTITY_READ_ACCESS_ATTRIBUTE_NAMES_KEY] as $attributeName) { + $attributeId = self::toAttributeId($entityShortName, $attributeName); + if ($this->isGranted($attributeId, $object, self::ENTITY_OBJECT_ALIAS)) { + $context[self::CONTEXT_GROUPS_KEY][] = $attributeId; + } + } + + $context[self::getUniqueAlreadyCalledKeyForEntity($entityClassName)] = true; + + return $this->normalizer->normalize($object, $format, $context); + } + + /** + * {@inheritDoc} + */ + public function supportsNormalization($data, $format = null, array $context = []): bool + { + if ($this->entityClassNameToAttributeNamesMapping === null || is_object($data) === false) { + return false; + } + + $entityClassName = get_class($data); + + // Make sure we're not called twice + if (isset($context[self::getUniqueAlreadyCalledKeyForEntity($entityClassName)])) { + return false; + } + + return array_key_exists($entityClassName, $this->entityClassNameToAttributeNamesMapping); + } + + private function loadConfig() + { + if ($this->userAuthorizationChecker !== null && $this->config !== null) { + $roleExpressions = $this->config[self::ROLES_CONFIG_NODE] ?? []; + $attributeExpressions = $this->config[self::ATTRIBUTES_CONFIG_NODE] ?? []; + + if (isset($this->config[self::ENTITIES_CONFIG_NODE])) { + $entitiesRoleExpressions = $this->loadEntityConfig($this->config[self::ENTITIES_CONFIG_NODE]); + $roleExpressions = array_merge($roleExpressions, $entitiesRoleExpressions); + } + + $this->userAuthorizationChecker->setExpressions($roleExpressions, $attributeExpressions); + } + } + + private function loadEntityConfig(array $entitiesConfigNode): array + { + $roleExpressions = []; + foreach ($entitiesConfigNode as $entityShortName => $entityNode) { + $entityClassName = $entityNode[self::ENTITY_CLASS_NAME_CONFIG_NODE]; + $attributeNames = []; + foreach ($entityNode[self::ENTITY_READ_ACCESS_CONFIG_NODE] ?? [] as $attributeName => $attributeAuthorizationExpression) { + $roleExpressions[self::toAttributeId($entityShortName, $attributeName)] = $attributeAuthorizationExpression; + $attributeNames[] = $attributeName; + } + $this->entityClassNameToAttributeNamesMapping[$entityClassName] = [ + self::ENTITY_SHORT_NAME_KEY => $entityShortName, + self::ENTITY_READ_ACCESS_ATTRIBUTE_NAMES_KEY => $attributeNames, + ]; + } + + return $roleExpressions; + } + private function getAttributeInternal(string $attributeName, $defaultValue = null) { return $this->userAuthorizationChecker->evalAttributeExpression($this->currentAuthorizationUser, $attributeName, $defaultValue); @@ -90,47 +200,101 @@ abstract class AbstractAuthorizationService } /** - * Create the 'authorization' config node definition with the given right and attribute definitions. - * A definition is an array of the following form: - * [0 => <nameString>, 1 => <defaultExpressionString> (optional, default: 'false'), 2 => <infoString> (optional, default: 'null')]. - * - * @param array[] $rights the list of right definitions - * @param array[] $attributes the list of attribute definitions + * Create the 'authorization' config node definition with the given config definition. */ - public static function getAuthorizationConfigNodeDefinition(array $rights = [], array $attributes = []): NodeDefinition + public static function getAuthorizationConfigNodeDefinition(array $configDefinition): NodeDefinition { $treeBuilder = new TreeBuilder(self::AUTHORIZATION_ROOT_CONFIG_NODE); - $rightsNodeChildBuilder = $treeBuilder->getRootNode()->children()->arrayNode(AuthorizationExpressionChecker::ROLES_CONFIG_NODE) + $rightsNodeChildBuilder = $treeBuilder->getRootNode()->children()->arrayNode(self::ROLES_CONFIG_NODE) ->addDefaultsIfNotSet() ->children(); - foreach ($rights as $right) { - $rightsNodeChildBuilder->scalarNode($right[0]) - ->defaultValue($right[1] ?? 'false') - ->info($right[2] ?? '') + foreach ($configDefinition[self::ROLES_KEY] ?? [] as $roleDefinition) { + $rightsNodeChildBuilder->scalarNode($roleDefinition[0]) + ->defaultValue($roleDefinition[1] ?? 'false') + ->info($roleDefinition[2] ?? '') ->end(); } - $attributesNodeChildBuilder = $treeBuilder->getRootNode()->children()->arrayNode(AuthorizationExpressionChecker::ATTRIBUTES_CONFIG_NODE) + $attributesNodeChildBuilder = $treeBuilder->getRootNode()->children()->arrayNode(self::ATTRIBUTES_CONFIG_NODE) ->addDefaultsIfNotSet() ->children(); - foreach ($attributes as $attribute) { - $attributesNodeChildBuilder->scalarNode($attribute[0]) - ->defaultValue($attribute[1] ?? 'null') - ->info($attribute[2] ?? '') + foreach ($configDefinition[self::ATTRIBUTES_KEY] ?? [] as $attributeDefinition) { + $attributesNodeChildBuilder->scalarNode($attributeDefinition[0]) + ->defaultValue($attributeDefinition[1] ?? 'null') + ->info($attributeDefinition[2] ?? '') ->end(); } + $entitiesNodeChildBuilder = $treeBuilder->getRootNode()->children()->arrayNode(self::ENTITIES_CONFIG_NODE) + ->children(); + foreach ($configDefinition[self::ENTITIES_KEY] ?? [] as $entityDefinition) { + $entityChildBuilder = $entitiesNodeChildBuilder->arrayNode($entityDefinition[self::ENTITY_SHORT_NAME_KEY]) + ->children(); + $entityChildBuilder->scalarNode(self::ENTITY_CLASS_NAME_CONFIG_NODE) + ->defaultValue($entityDefinition[self::ENTITY_CLASS_NAME_KEY]) + ->info('The entity class name. There is no need to change the default value.') + ->end(); + $entityReadAccessChildBuilder = $entityChildBuilder->arrayNode(self::ENTITY_READ_ACCESS_CONFIG_NODE) + ->children(); + foreach ($entityDefinition[self::ENTITY_READ_ACCESS_KEY] ?? [] as $attributeName) { + $entityReadAccessChildBuilder->scalarNode($attributeName) + ->defaultValue('false') + ->info(sprintf('The conditional reader role expression for attribute \'%s\'.', $attributeName)) + ->end(); + } + + $entityWriteAccessChildBuilder = $entitiesNodeChildBuilder->arrayNode(self::ENTITY_WRITE_ACCESS_CONFIG_NODE) + ->children(); + foreach ($entityDefinition[self::ENTITY_WRITE_ACCESS_KEY] ?? [] as $attributeName) { + $entityWriteAccessChildBuilder->scalarNode($attributeName) + ->defaultValue('false') + ->info(sprintf('The conditional writer role expression for attribute \'%s\'.', $attributeName)) + ->end(); + } + } + return $treeBuilder->getRootNode(); } - public static function createConfig(array $rightExpressions = [], array $attributeExpressions = []): array + public static function configDefinitionCreate(): array { - return [ - AbstractAuthorizationService::AUTHORIZATION_ROOT_CONFIG_NODE => [ - AuthorizationExpressionChecker::ROLES_CONFIG_NODE => $rightExpressions, - AuthorizationExpressionChecker::ATTRIBUTES_CONFIG_NODE => $attributeExpressions, - ], - ]; + return []; + } + + public static function configDefinitionAddRole(array &$configDefinition, string $roleName, string $defaultExpression = 'false', string $info = ''): array + { + Tools::pushToSubarray($configDefinition, self::ROLES_KEY, [$roleName, $defaultExpression, $info]); + + return $configDefinition; + } + + public static function configDefinitionAddAttribute(array &$configDefinition, string $attributeName, string $defaultExpression = 'false', string $info = ''): array + { + Tools::pushToSubarray($configDefinition, self::ATTRIBUTES_KEY, [$attributeName, $defaultExpression, $info]); + + return $configDefinition; + } + + public static function configDefinitionAddEntity(array &$configDefinition, string $entityShortName, string $entityClassName, array $readAttributes = [], array $writeAttributes = []): array + { + Tools::pushToSubarray($configDefinition, self::ENTITIES_KEY, [ + self::ENTITY_SHORT_NAME_KEY => $entityShortName, + self::ENTITY_CLASS_NAME_KEY => $entityClassName, + self::ENTITY_READ_ACCESS_KEY => $readAttributes, + self::ENTITY_WRITE_ACCESS_KEY => $writeAttributes, + ]); + + return $configDefinition; + } + + private static function toAttributeId(string $entityShortName, string $attributeName): string + { + return $entityShortName.':'.$attributeName; + } + + private static function getUniqueAlreadyCalledKeyForEntity(string $entityClassName): string + { + return self::class.$entityClassName; } } diff --git a/src/Authorization/AuthorizationExpressionChecker.php b/src/Authorization/AuthorizationExpressionChecker.php index b99ec9f..fe931b1 100644 --- a/src/Authorization/AuthorizationExpressionChecker.php +++ b/src/Authorization/AuthorizationExpressionChecker.php @@ -11,9 +11,6 @@ use Dbp\Relay\CoreBundle\ExpressionLanguage\ExpressionLanguage; */ class AuthorizationExpressionChecker { - public const ROLES_CONFIG_NODE = 'roles'; - public const ATTRIBUTES_CONFIG_NODE = 'attributes'; - private const USER_VARIBLE_NAME = 'user'; private const DEFAULT_OBJECT_VARIBLE_NAME = 'object'; @@ -21,7 +18,7 @@ class AuthorizationExpressionChecker private $expressionLanguage; /** @var array */ - private $rightExpressions; + private $roleExpressions; /** @var array */ private $attributeExpressions; @@ -30,7 +27,7 @@ class AuthorizationExpressionChecker private $dataMux; /** @var array */ - private $rightExpressionStack; + private $roleExpressionStack; /** @var array */ private $attributeExpressionStack; @@ -38,17 +35,17 @@ class AuthorizationExpressionChecker public function __construct(AuthorizationDataMuxer $dataMux) { $this->expressionLanguage = new ExpressionLanguage(); - $this->rightExpressions = []; + $this->roleExpressions = []; $this->attributeExpressions = []; $this->dataMux = $dataMux; - $this->rightExpressionStack = []; + $this->roleExpressionStack = []; $this->attributeExpressionStack = []; } - public function setConfig(array $config) + public function setExpressions(array $roleExpressions, array $attributeExpressions) { - $this->loadExpressions($config[self::ROLES_CONFIG_NODE] ?? [], $this->rightExpressions); - $this->loadExpressions($config[self::ATTRIBUTES_CONFIG_NODE] ?? [], $this->attributeExpressions); + $this->roleExpressions = $roleExpressions; + $this->attributeExpressions = $attributeExpressions; } /** @@ -98,13 +95,13 @@ class AuthorizationExpressionChecker */ public function isGranted(AuthorizationUser $currentAuthorizationUser, string $rightName, $object, string $objectAlias = null): bool { - if (in_array($rightName, $this->rightExpressionStack, true)) { + if (in_array($rightName, $this->roleExpressionStack, true)) { throw new AuthorizationException(sprintf('infinite loop caused by authorization right expression %s detected', $rightName), AuthorizationException::INFINITE_EXRPESSION_LOOP_DETECTED); } - array_push($this->rightExpressionStack, $rightName); + array_push($this->roleExpressionStack, $rightName); try { - $rightExpression = $this->rightExpressions[$rightName] ?? null; + $rightExpression = $this->roleExpressions[$rightName] ?? null; if ($rightExpression === null) { throw new AuthorizationException(sprintf('right \'%s\' undefined', $rightName), AuthorizationException::PRIVILEGE_UNDEFINED); } @@ -114,14 +111,7 @@ class AuthorizationExpressionChecker $objectAlias ?? self::DEFAULT_OBJECT_VARIBLE_NAME => $object, ]); } finally { - array_pop($this->rightExpressionStack); - } - } - - private function loadExpressions(array $expressions, array &$target): void - { - foreach ($expressions as $name => $expression) { - $target[$name] = $expression; + array_pop($this->roleExpressionStack); } } } diff --git a/src/Helpers/Tools.php b/src/Helpers/Tools.php index c26a426..607da0b 100644 --- a/src/Helpers/Tools.php +++ b/src/Helpers/Tools.php @@ -40,4 +40,13 @@ class Tools { return $str === null || $str === ''; } + + public static function pushToSubarray(array &$parentArray, $childArrayKey, $value) + { + if (!isset($parentArray[$childArrayKey])) { + $parentArray[$childArrayKey] = [$value]; + } else { + $parentArray[$childArrayKey][] = $value; + } + } } diff --git a/src/LocalData/AbstractLocalDataEventSubscriber.php b/src/LocalData/AbstractLocalDataEventSubscriber.php index 97e971d..de6f1be 100644 --- a/src/LocalData/AbstractLocalDataEventSubscriber.php +++ b/src/LocalData/AbstractLocalDataEventSubscriber.php @@ -82,9 +82,7 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer $rightExpressions[$localDataAttributeName] = $configMappingEntry[self::AUTHORIZATION_EXPRESSION_CONFIG_NODE] ?? 'false'; } - if (!empty($rightExpressions)) { - parent::setConfig(parent::createConfig($rightExpressions)); - } + parent::configure($rightExpressions); } public static function getSubscribedEvents(): array -- GitLab