Skip to content
Snippets Groups Projects
Commit 6190de8b authored by Groß-Vogt, Tobias's avatar Groß-Vogt, Tobias
Browse files

entity attribute authorization now part of the AbstractAuthorizationService...

entity attribute authorization now part of the AbstractAuthorizationService and config is found under 'authorization.entities
parent 3671c777
Branches
Tags
No related merge requests found
Pipeline #232213 failed
...@@ -6,13 +6,39 @@ namespace Dbp\Relay\CoreBundle\Authorization; ...@@ -6,13 +6,39 @@ namespace Dbp\Relay\CoreBundle\Authorization;
use Dbp\Relay\CoreBundle\API\UserSessionInterface; use Dbp\Relay\CoreBundle\API\UserSessionInterface;
use Dbp\Relay\CoreBundle\Exception\ApiError; 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\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\HttpFoundation\Response; 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 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 */ /** @var AuthorizationExpressionChecker */
private $userAuthorizationChecker; private $userAuthorizationChecker;
...@@ -23,6 +49,14 @@ abstract class AbstractAuthorizationService ...@@ -23,6 +49,14 @@ abstract class AbstractAuthorizationService
/** @var array|null */ /** @var array|null */
private $config; private $config;
/** @var array */
private $entityClassNameToAttributeNamesMapping;
public function __construct()
{
$this->entityClassNameToAttributeNamesMapping = [];
}
/** /**
* @required * @required
*/ */
...@@ -30,20 +64,22 @@ abstract class AbstractAuthorizationService ...@@ -30,20 +64,22 @@ abstract class AbstractAuthorizationService
{ {
$this->userAuthorizationChecker = new AuthorizationExpressionChecker($mux); $this->userAuthorizationChecker = new AuthorizationExpressionChecker($mux);
$this->currentAuthorizationUser = new AuthorizationUser($userSession, $this->userAuthorizationChecker); $this->currentAuthorizationUser = new AuthorizationUser($userSession, $this->userAuthorizationChecker);
$this->updateConfig(); $this->loadConfig();
} }
public function setConfig(array $config) public function setConfig(array $config)
{ {
$this->config = $config[self::AUTHORIZATION_ROOT_CONFIG_NODE] ?? []; $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->config = [
$this->userAuthorizationChecker->setConfig($this->config); self::ROLES_CONFIG_NODE => $roleMapping,
} self::ATTRIBUTES_CONFIG_NODE => $attributeMapping,
];
$this->loadConfig();
} }
/** /**
...@@ -76,6 +112,80 @@ abstract class AbstractAuthorizationService ...@@ -76,6 +112,80 @@ abstract class AbstractAuthorizationService
return $this->getAttributeInternal($attributeName, $defaultValue); 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) private function getAttributeInternal(string $attributeName, $defaultValue = null)
{ {
return $this->userAuthorizationChecker->evalAttributeExpression($this->currentAuthorizationUser, $attributeName, $defaultValue); return $this->userAuthorizationChecker->evalAttributeExpression($this->currentAuthorizationUser, $attributeName, $defaultValue);
...@@ -90,47 +200,101 @@ abstract class AbstractAuthorizationService ...@@ -90,47 +200,101 @@ abstract class AbstractAuthorizationService
} }
/** /**
* Create the 'authorization' config node definition with the given right and attribute definitions. * Create the 'authorization' config node definition with the given config definition.
* 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
*/ */
public static function getAuthorizationConfigNodeDefinition(array $rights = [], array $attributes = []): NodeDefinition public static function getAuthorizationConfigNodeDefinition(array $configDefinition): NodeDefinition
{ {
$treeBuilder = new TreeBuilder(self::AUTHORIZATION_ROOT_CONFIG_NODE); $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() ->addDefaultsIfNotSet()
->children(); ->children();
foreach ($rights as $right) { foreach ($configDefinition[self::ROLES_KEY] ?? [] as $roleDefinition) {
$rightsNodeChildBuilder->scalarNode($right[0]) $rightsNodeChildBuilder->scalarNode($roleDefinition[0])
->defaultValue($right[1] ?? 'false') ->defaultValue($roleDefinition[1] ?? 'false')
->info($right[2] ?? '') ->info($roleDefinition[2] ?? '')
->end(); ->end();
} }
$attributesNodeChildBuilder = $treeBuilder->getRootNode()->children()->arrayNode(AuthorizationExpressionChecker::ATTRIBUTES_CONFIG_NODE) $attributesNodeChildBuilder = $treeBuilder->getRootNode()->children()->arrayNode(self::ATTRIBUTES_CONFIG_NODE)
->addDefaultsIfNotSet() ->addDefaultsIfNotSet()
->children(); ->children();
foreach ($attributes as $attribute) { foreach ($configDefinition[self::ATTRIBUTES_KEY] ?? [] as $attributeDefinition) {
$attributesNodeChildBuilder->scalarNode($attribute[0]) $attributesNodeChildBuilder->scalarNode($attributeDefinition[0])
->defaultValue($attribute[1] ?? 'null') ->defaultValue($attributeDefinition[1] ?? 'null')
->info($attribute[2] ?? '') ->info($attributeDefinition[2] ?? '')
->end(); ->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(); return $treeBuilder->getRootNode();
} }
public static function createConfig(array $rightExpressions = [], array $attributeExpressions = []): array public static function configDefinitionCreate(): array
{ {
return [ return [];
AbstractAuthorizationService::AUTHORIZATION_ROOT_CONFIG_NODE => [ }
AuthorizationExpressionChecker::ROLES_CONFIG_NODE => $rightExpressions,
AuthorizationExpressionChecker::ATTRIBUTES_CONFIG_NODE => $attributeExpressions, 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;
} }
} }
...@@ -11,9 +11,6 @@ use Dbp\Relay\CoreBundle\ExpressionLanguage\ExpressionLanguage; ...@@ -11,9 +11,6 @@ use Dbp\Relay\CoreBundle\ExpressionLanguage\ExpressionLanguage;
*/ */
class AuthorizationExpressionChecker class AuthorizationExpressionChecker
{ {
public const ROLES_CONFIG_NODE = 'roles';
public const ATTRIBUTES_CONFIG_NODE = 'attributes';
private const USER_VARIBLE_NAME = 'user'; private const USER_VARIBLE_NAME = 'user';
private const DEFAULT_OBJECT_VARIBLE_NAME = 'object'; private const DEFAULT_OBJECT_VARIBLE_NAME = 'object';
...@@ -21,7 +18,7 @@ class AuthorizationExpressionChecker ...@@ -21,7 +18,7 @@ class AuthorizationExpressionChecker
private $expressionLanguage; private $expressionLanguage;
/** @var array */ /** @var array */
private $rightExpressions; private $roleExpressions;
/** @var array */ /** @var array */
private $attributeExpressions; private $attributeExpressions;
...@@ -30,7 +27,7 @@ class AuthorizationExpressionChecker ...@@ -30,7 +27,7 @@ class AuthorizationExpressionChecker
private $dataMux; private $dataMux;
/** @var array */ /** @var array */
private $rightExpressionStack; private $roleExpressionStack;
/** @var array */ /** @var array */
private $attributeExpressionStack; private $attributeExpressionStack;
...@@ -38,17 +35,17 @@ class AuthorizationExpressionChecker ...@@ -38,17 +35,17 @@ class AuthorizationExpressionChecker
public function __construct(AuthorizationDataMuxer $dataMux) public function __construct(AuthorizationDataMuxer $dataMux)
{ {
$this->expressionLanguage = new ExpressionLanguage(); $this->expressionLanguage = new ExpressionLanguage();
$this->rightExpressions = []; $this->roleExpressions = [];
$this->attributeExpressions = []; $this->attributeExpressions = [];
$this->dataMux = $dataMux; $this->dataMux = $dataMux;
$this->rightExpressionStack = []; $this->roleExpressionStack = [];
$this->attributeExpressionStack = []; $this->attributeExpressionStack = [];
} }
public function setConfig(array $config) public function setExpressions(array $roleExpressions, array $attributeExpressions)
{ {
$this->loadExpressions($config[self::ROLES_CONFIG_NODE] ?? [], $this->rightExpressions); $this->roleExpressions = $roleExpressions;
$this->loadExpressions($config[self::ATTRIBUTES_CONFIG_NODE] ?? [], $this->attributeExpressions); $this->attributeExpressions = $attributeExpressions;
} }
/** /**
...@@ -98,13 +95,13 @@ class AuthorizationExpressionChecker ...@@ -98,13 +95,13 @@ class AuthorizationExpressionChecker
*/ */
public function isGranted(AuthorizationUser $currentAuthorizationUser, string $rightName, $object, string $objectAlias = null): bool 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); 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 { try {
$rightExpression = $this->rightExpressions[$rightName] ?? null; $rightExpression = $this->roleExpressions[$rightName] ?? null;
if ($rightExpression === null) { if ($rightExpression === null) {
throw new AuthorizationException(sprintf('right \'%s\' undefined', $rightName), AuthorizationException::PRIVILEGE_UNDEFINED); throw new AuthorizationException(sprintf('right \'%s\' undefined', $rightName), AuthorizationException::PRIVILEGE_UNDEFINED);
} }
...@@ -114,14 +111,7 @@ class AuthorizationExpressionChecker ...@@ -114,14 +111,7 @@ class AuthorizationExpressionChecker
$objectAlias ?? self::DEFAULT_OBJECT_VARIBLE_NAME => $object, $objectAlias ?? self::DEFAULT_OBJECT_VARIBLE_NAME => $object,
]); ]);
} finally { } finally {
array_pop($this->rightExpressionStack); array_pop($this->roleExpressionStack);
}
}
private function loadExpressions(array $expressions, array &$target): void
{
foreach ($expressions as $name => $expression) {
$target[$name] = $expression;
} }
} }
} }
...@@ -40,4 +40,13 @@ class Tools ...@@ -40,4 +40,13 @@ class Tools
{ {
return $str === null || $str === ''; return $str === null || $str === '';
} }
public static function pushToSubarray(array &$parentArray, $childArrayKey, $value)
{
if (!isset($parentArray[$childArrayKey])) {
$parentArray[$childArrayKey] = [$value];
} else {
$parentArray[$childArrayKey][] = $value;
}
}
} }
...@@ -82,9 +82,7 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer ...@@ -82,9 +82,7 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer
$rightExpressions[$localDataAttributeName] = $configMappingEntry[self::AUTHORIZATION_EXPRESSION_CONFIG_NODE] ?? 'false'; $rightExpressions[$localDataAttributeName] = $configMappingEntry[self::AUTHORIZATION_EXPRESSION_CONFIG_NODE] ?? 'false';
} }
if (!empty($rightExpressions)) { parent::configure($rightExpressions);
parent::setConfig(parent::createConfig($rightExpressions));
}
} }
public static function getSubscribedEvents(): array public static function getSubscribedEvents(): array
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment