Skip to content
Snippets Groups Projects
Commit fb3da145 authored by Tobias Gross-Vogt's avatar Tobias Gross-Vogt
Browse files

* local data mapping by config: default values to prevent 'source attribute not found' exception

* fixed timing issue with user identifier in abstractauthorizationservice (not set in user session when autowiring takes place)
* authorization expression config node definitions can now be requested from abstractauthorizationprovder
* empty function for expression language
parent 3e443713
No related branches found
No related tags found
No related merge requests found
Pipeline #216692 failed
...@@ -6,12 +6,13 @@ namespace Dbp\Relay\CoreBundle\Authorization; ...@@ -6,12 +6,13 @@ 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 Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
abstract class AbstractAuthorizationService abstract class AbstractAuthorizationService
{ {
public const RIGHTS_CONFIG_ATTRIBUTE = AuthorizationExpressionChecker::RIGHTS_CONFIG_ATTRIBUTE; private const AUTHORIZATION_ROOT_CONFIG_NODE = 'authorization';
public const ATTRIBUTES_CONFIG_ATTRIBUTE = AuthorizationExpressionChecker::ATTRIBUTES_CONFIG_ATTRIBUTE;
/** @var AuthorizationExpressionChecker */ /** @var AuthorizationExpressionChecker */
private $userAuthorizationChecker; private $userAuthorizationChecker;
...@@ -19,6 +20,7 @@ abstract class AbstractAuthorizationService ...@@ -19,6 +20,7 @@ abstract class AbstractAuthorizationService
/** @var AuthorizationUser */ /** @var AuthorizationUser */
private $currentAuthorizationUser; private $currentAuthorizationUser;
/** @var array|null */
private $config; private $config;
/** /**
...@@ -27,13 +29,13 @@ abstract class AbstractAuthorizationService ...@@ -27,13 +29,13 @@ abstract class AbstractAuthorizationService
public function _injectServices(UserSessionInterface $userSession, AuthorizationDataMuxer $mux) public function _injectServices(UserSessionInterface $userSession, AuthorizationDataMuxer $mux)
{ {
$this->userAuthorizationChecker = new AuthorizationExpressionChecker($mux); $this->userAuthorizationChecker = new AuthorizationExpressionChecker($mux);
$this->currentAuthorizationUser = new AuthorizationUser($userSession->getUserIdentifier(), $this->userAuthorizationChecker); $this->currentAuthorizationUser = new AuthorizationUser($userSession, $this->userAuthorizationChecker);
$this->updateConfig(); $this->updateConfig();
} }
public function setConfig(array $config) public function setConfig(array $config)
{ {
$this->config = $config; $this->config = $config[self::AUTHORIZATION_ROOT_CONFIG_NODE] ?? [];
$this->updateConfig(); $this->updateConfig();
} }
...@@ -76,8 +78,6 @@ abstract class AbstractAuthorizationService ...@@ -76,8 +78,6 @@ abstract class AbstractAuthorizationService
private function getAttributeInternal(string $attributeName, $defaultValue = null) private function getAttributeInternal(string $attributeName, $defaultValue = null)
{ {
$this->userAuthorizationChecker->init();
return $this->userAuthorizationChecker->evalAttributeExpression($this->currentAuthorizationUser, $attributeName, $defaultValue); return $this->userAuthorizationChecker->evalAttributeExpression($this->currentAuthorizationUser, $attributeName, $defaultValue);
} }
...@@ -86,8 +86,45 @@ abstract class AbstractAuthorizationService ...@@ -86,8 +86,45 @@ abstract class AbstractAuthorizationService
*/ */
private function isGrantedInternal(string $rightName, $subject = null): bool private function isGrantedInternal(string $rightName, $subject = null): bool
{ {
$this->userAuthorizationChecker->init();
return $this->userAuthorizationChecker->isGranted($this->currentAuthorizationUser, $rightName, $subject); 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,
],
];
}
} }
...@@ -11,8 +11,8 @@ use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionLanguage; ...@@ -11,8 +11,8 @@ use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionLanguage;
*/ */
class AuthorizationExpressionChecker class AuthorizationExpressionChecker
{ {
public const RIGHTS_CONFIG_ATTRIBUTE = 'rights'; public const RIGHTS_CONFIG_NODE = 'rights';
public const ATTRIBUTES_CONFIG_ATTRIBUTE = 'attributes'; public const ATTRIBUTES_CONFIG_NODE = 'attributes';
private const MAX_NUM_CALLS = 16; private const MAX_NUM_CALLS = 16;
...@@ -25,9 +25,6 @@ class AuthorizationExpressionChecker ...@@ -25,9 +25,6 @@ class AuthorizationExpressionChecker
/** @var array */ /** @var array */
private $attributeExpressions; private $attributeExpressions;
/** @var int */
private $callCounter;
/** @var AuthorizationDataMuxer */ /** @var AuthorizationDataMuxer */
private $dataMux; private $dataMux;
...@@ -40,7 +37,6 @@ class AuthorizationExpressionChecker ...@@ -40,7 +37,6 @@ class AuthorizationExpressionChecker
public function __construct(AuthorizationDataMuxer $dataMux) public function __construct(AuthorizationDataMuxer $dataMux)
{ {
$this->expressionLanguage = new ExpressionLanguage(); $this->expressionLanguage = new ExpressionLanguage();
$this->rightExpressions = []; $this->rightExpressions = [];
$this->attributeExpressions = []; $this->attributeExpressions = [];
$this->dataMux = $dataMux; $this->dataMux = $dataMux;
...@@ -50,13 +46,8 @@ class AuthorizationExpressionChecker ...@@ -50,13 +46,8 @@ class AuthorizationExpressionChecker
public function setConfig(array $config) public function setConfig(array $config)
{ {
$this->loadExpressions($config[self::RIGHTS_CONFIG_ATTRIBUTE] ?? [], $this->rightExpressions); $this->loadExpressions($config[self::RIGHTS_CONFIG_NODE] ?? [], $this->rightExpressions);
$this->loadExpressions($config[self::ATTRIBUTES_CONFIG_ATTRIBUTE] ?? [], $this->attributeExpressions); $this->loadExpressions($config[self::ATTRIBUTES_CONFIG_NODE] ?? [], $this->attributeExpressions);
}
public function init()
{
$this->callCounter = 0;
} }
/** /**
......
...@@ -4,6 +4,8 @@ declare(strict_types=1); ...@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Authorization; namespace Dbp\Relay\CoreBundle\Authorization;
use Dbp\Relay\CoreBundle\API\UserSessionInterface;
/** /**
* Provides the user interface available within privilege expressions. * Provides the user interface available within privilege expressions.
*/ */
...@@ -12,20 +14,18 @@ class AuthorizationUser ...@@ -12,20 +14,18 @@ class AuthorizationUser
/** @var AuthorizationExpressionChecker */ /** @var AuthorizationExpressionChecker */
private $authorizationChecker; private $authorizationChecker;
/** /** @var UserSessionInterface */
* @var string|null private $userSession;
*/
private $identifier;
public function __construct(?string $identifier, AuthorizationExpressionChecker $authorizationChecker) public function __construct(UserSessionInterface $userSession, AuthorizationExpressionChecker $authorizationChecker)
{ {
$this->userSession = $userSession;
$this->authorizationChecker = $authorizationChecker; $this->authorizationChecker = $authorizationChecker;
$this->identifier = $identifier;
} }
public function getIdentifier(): ?string public function getIdentifier(): ?string
{ {
return $this->identifier; return $this->userSession->getUserIdentifier();
} }
/** /**
......
...@@ -4,6 +4,7 @@ declare(strict_types=1); ...@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage; 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\FilterExpressionFunctionProvider;
use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionFunctionProviders\MapExpressionFunctionProvider; use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionFunctionProviders\MapExpressionFunctionProvider;
use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionFunctionProviders\PhpArrayExpressionFunctionProvider; use Dbp\Relay\CoreBundle\Authorization\ExpressionLanguage\ExpressionFunctionProviders\PhpArrayExpressionFunctionProvider;
...@@ -22,6 +23,7 @@ class ExpressionLanguage extends SymfonyExpressionLanguage ...@@ -22,6 +23,7 @@ class ExpressionLanguage extends SymfonyExpressionLanguage
new PhpArrayExpressionFunctionProvider(), new PhpArrayExpressionFunctionProvider(),
new PhpNumericExpressionFunctionProvider(), new PhpNumericExpressionFunctionProvider(),
new PhpStringExpressionFunctionProvider(), new PhpStringExpressionFunctionProvider(),
new ArrayExpressionFunctionProvider(),
], $providers); ], $providers);
parent::__construct($cache, $providers); parent::__construct($cache, $providers);
......
...@@ -5,17 +5,38 @@ declare(strict_types=1); ...@@ -5,17 +5,38 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\LocalData; namespace Dbp\Relay\CoreBundle\LocalData;
use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService; 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\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; 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 abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizationService implements EventSubscriberInterface
{ {
public const CONFIG_NODE = 'local_data_mapping'; protected const ROOT_CONFIG_NODE = 'local_data_mapping';
public const SOURCE_ATTRIBUTE_KEY = 'source_attribute'; protected const SOURCE_ATTRIBUTE_CONFIG_NODE = 'source_attribute';
public const LOCAL_DATA_ATTRIBUTE_KEY = 'local_data_attribute'; protected const LOCAL_DATA_ATTRIBUTE_CONFIG_NODE = 'local_data_attribute';
public const AUTHORIZATION_EXPRESSION_KEY = 'authorization_expression'; 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; private $attributeMapping;
public function __construct() public function __construct()
...@@ -25,21 +46,34 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio ...@@ -25,21 +46,34 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio
public function setConfig(array $config) public function setConfig(array $config)
{ {
$configNode = $config[self::CONFIG_NODE] ?? []; $configNode = $config[self::ROOT_CONFIG_NODE] ?? [];
$rightExpressions = [];
$rights = [];
foreach ($configNode as $configMappingEntry) { foreach ($configNode as $configMappingEntry) {
if (isset($this->attributeMapping[$configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY]])) { $localDataAttributeName = $configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE];
throw new \RuntimeException(sprintf('multiple mapping entries for local data attribute %s', $configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY]));
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 name of the local data attribute is used as name for the right to view that attribute
// the attribute is visible false by default // 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)) { if (!empty($rightExpressions)) {
parent::setConfig([AbstractAuthorizationService::RIGHTS_CONFIG_ATTRIBUTE => $rights]); parent::setConfig(parent::createConfig($rightExpressions));
} }
} }
...@@ -57,29 +91,51 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio ...@@ -57,29 +91,51 @@ abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizatio
{ {
$sourceData = $postEvent->getSourceData(); $sourceData = $postEvent->getSourceData();
foreach ($this->attributeMapping as $localDataAttributeName => $sourceAttributeName) { foreach ($this->attributeMapping as $localDataAttributeName => $attributeMapEntry) {
if ($this->isGranted($localDataAttributeName)) { if ($postEvent->isLocalDataAttributeRequested($localDataAttributeName)) {
if (($sourceAttributeValue = $sourceData[$sourceAttributeName] ?? null) !== null) { if (!$this->isGranted($localDataAttributeName)) {
$postEvent->trySetLocalDataAttribute($localDataAttributeName, $sourceAttributeValue); 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 { } 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() return $treeBuilder->getRootNode()
->arrayPrototype() ->arrayPrototype()
->children() ->children()
->scalarNode(self::SOURCE_ATTRIBUTE_KEY)->end() ->scalarNode(self::SOURCE_ATTRIBUTE_CONFIG_NODE)->end()
->scalarNode(self::LOCAL_DATA_ATTRIBUTE_KEY)->end() ->scalarNode(self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE)->end()
->scalarNode(self::AUTHORIZATION_EXPRESSION_KEY) ->scalarNode(self::AUTHORIZATION_EXPRESSION_CONFIG_NODE)
->defaultValue('false') ->defaultValue('false')
->end() ->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()
->end() ->end()
; ;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment