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

updated local query

parent 33857526
No related branches found
No related tags found
No related merge requests found
Pipeline #230132 passed
......@@ -22,14 +22,16 @@ use Symfony\Contracts\EventDispatcher\Event;
abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationService implements EventSubscriberInterface
{
protected const ROOT_CONFIG_NODE = 'local_data_mapping';
protected const SOURCE_ATTRIBUTES_CONFIG_NODE = 'source_attributes';
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 ALLOW_LOCAL_QUERY_CONFIG_NODE = 'allow_query';
protected const DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE = 'default_value';
protected const DEFAULT_VALUES_ATTRIBUTE_CONFIG_NODE = 'default_values';
private const SOURCE_ATTRIBUTES_KEY = 'source';
private const SOURCE_ATTRIBUTE_KEY = 'source';
private const DEFAULT_VALUE_KEY = 'default';
private const QUERYABLE_KEY = 'queryable';
/*
* WORKAROUND: could not find a way to determine whether a Symfony config array node was NOT specified since it provides an empty
......@@ -58,7 +60,8 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer
}
$attributeMapEntry = [];
$attributeMapEntry[self::SOURCE_ATTRIBUTES_KEY] = $configMappingEntry[self::SOURCE_ATTRIBUTES_CONFIG_NODE];
$attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY] = $configMappingEntry[self::SOURCE_ATTRIBUTE_CONFIG_NODE];
$attributeMapEntry[self::QUERYABLE_KEY] = $configMappingEntry[self::ALLOW_LOCAL_QUERY_CONFIG_NODE];
$defaultValue = $configMappingEntry[self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE] ?? null;
if ($defaultValue === null) {
......@@ -97,33 +100,30 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer
public function onEvent(Event $event)
{
if ($event instanceof LocalDataPreEvent) {
foreach ($event->getPendingQueryParametersIn() as $localDataAttributeName => $localDataAttributeValue) {
if (($attributeMapEntry = $this->attributeMapping[$localDataAttributeName] ?? null) !== null) {
$localQueryParameters = [];
foreach ($event->getPendingQueryParameters() as $localDataAttributeName => $localDataAttributeValue) {
if (($attributeMapEntry = $this->attributeMapping[$localDataAttributeName] ?? null) !== null &&
$attributeMapEntry[self::QUERYABLE_KEY] === true) {
if (!$this->isGranted($localDataAttributeName)) {
throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, sprintf('access to local data attribute \'%s\' denied', $localDataAttributeName));
}
$sourceAttributeName = $attributeMapEntry[self::SOURCE_ATTRIBUTES_KEY][0];
$event->addQueryParameterOut($sourceAttributeName, $localDataAttributeValue);
$event->acknowledgeQueryParameterIn($localDataAttributeName);
$sourceAttributeName = $attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY];
$localQueryParameters[$sourceAttributeName] = $localDataAttributeValue;
$event->acknowledgeQueryParameter($localDataAttributeName);
}
}
$this->onPreEvent($event);
$this->onPreEvent($event, $localQueryParameters);
} elseif ($event instanceof LocalDataPostEvent) {
foreach ($event->getPendingRequestedAttributes() as $localDataAttributeName) {
if (($attributeMapEntry = $this->attributeMapping[$localDataAttributeName] ?? null) !== null) {
if (!$this->isGranted($localDataAttributeName)) {
throw ApiError::withDetails(Response::HTTP_UNAUTHORIZED, sprintf('access to local data attribute \'%s\' denied', $localDataAttributeName));
}
$attributeValue = null;
foreach ($attributeMapEntry[self::SOURCE_ATTRIBUTES_KEY] as $sourceAttributeName) {
if (($value = $event->getSourceData()[$sourceAttributeName] ?? null) !== null) {
$attributeValue = $value;
break;
}
}
$attributeValue = $event->getSourceData()[$attributeMapEntry[self::SOURCE_ATTRIBUTE_KEY]] ?? null;
$attributeValue = $attributeValue ?? $attributeMapEntry[self::DEFAULT_VALUE_KEY] ?? null;
if ($attributeValue !== null) {
$event->setLocalDataAttribute($localDataAttributeName, $attributeValue);
} else {
......@@ -153,20 +153,23 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer
->scalarNode(self::LOCAL_DATA_ATTRIBUTE_CONFIG_NODE)
->info('The name of the local data attribute.')
->end()
->arrayNode(self::SOURCE_ATTRIBUTES_CONFIG_NODE)
->info('The list of source attributes to map to the local data attribute ordered by preferred usage. If an attribute is not found, the next attribute in the list is used.')
->scalarPrototype()->end()
->scalarNode(self::SOURCE_ATTRIBUTE_CONFIG_NODE)
->info('The source attribute to map to the local data attribute. If the source attribute is not found, the default value is used.')
->end()
->scalarNode(self::AUTHORIZATION_EXPRESSION_CONFIG_NODE)
->defaultValue('false')
->info('A boolean expression evaluable by the Symfony Expression Language determining whether the current user may request read the local data attribute.')
->end()
->booleanNode(self::ALLOW_LOCAL_QUERY_CONFIG_NODE)
->defaultValue('false')
->info('Indicates whether the local data attribute can be used in local queries.')
->end()
->scalarNode(self::DEFAULT_VALUE_ATTRIBUTE_CONFIG_NODE)
->info('The default value for scalar (i.e. non-array) attributes. If none is specified, an exception is thrown in case none of the source attributes is found.')
->info('The default value for scalar (i.e. non-array) attributes. If none is specified, an exception is thrown in 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 case none of the source attributes is found.')
->info('The default value for array type attributes. If none is specified, an exception is thrown in case the source attribute is not found.')
->scalarPrototype()->end()
->end()
->end()
......@@ -179,7 +182,7 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer
throw new \RuntimeException(sprintf('child classes must implement the \'%s\' method', __METHOD__));
}
protected function onPreEvent(LocalDataPreEvent $preEvent)
protected function onPreEvent(LocalDataPreEvent $preEvent, array $localQueryParameters)
{
}
......
......@@ -83,10 +83,10 @@ class LocalDataEventDispatcher
public function dispatch(Event $event, string $eventName = null): void
{
if ($event instanceof LocalDataPreEvent) {
$event->initQueryParametersIn($this->queryParameters);
$event->initQueryParameters($this->queryParameters);
$this->eventDispatcher->dispatch($event, $eventName);
$pendingAttributes = $event->getPendingQueryParametersIn();
$pendingAttributes = $event->getPendingQueryParameters();
if (count($pendingAttributes) !== 0) {
throw ApiError::withDetails(Response::HTTP_BAD_REQUEST, sprintf("the following local query attributes were not acknowledged for resource '%s': %s", $this->uniqueEntityName, implode(', ', array_keys($pendingAttributes))));
}
......
......@@ -11,45 +11,37 @@ class LocalDataPreEvent extends Event
/** @var string[] */
private $queryParametersIn;
/** @var string[] */
private $queryParametersOut;
/** @var array */
private $options;
public function __construct()
public function __construct(array $options)
{
$this->queryParametersIn = [];
$this->queryParametersOut = [];
}
/**
* @deprecated Use getQueryParametersOut
*/
public function getQueryParameters(): array
{
return $this->queryParametersOut;
$this->options = $options;
}
public function initQueryParametersIn(array $queryParametersIn): void
public function initQueryParameters(array $queryParametersIn): void
{
$this->queryParametersIn = $queryParametersIn;
}
public function getPendingQueryParametersIn(): array
public function getPendingQueryParameters(): array
{
return $this->queryParametersIn;
}
public function acknowledgeQueryParameterIn(string $queryParameterName): void
public function acknowledgeQueryParameter(string $queryParameterName): void
{
unset($this->queryParametersIn[$queryParameterName]);
}
public function addQueryParameterOut(string $queryParameterName, string $queryParameterValue): void
public function getOptions(): array
{
$this->queryParametersOut[$queryParameterName] = $queryParameterValue;
return $this->options;
}
public function getQueryParametersOut(): array
public function setOptions(array $options): void
{
return $this->queryParametersOut;
$this->options = $options;
}
}
......@@ -73,18 +73,6 @@ class LocalDataTest extends TestCase
$this->assertEquals([0], $testEntity->getLocalDataValue($localDataAttributeName));
}
public function testLocalDataMappingFallback()
{
// first source attribute specified in config is present in source data -> return first source attribute value
$localDataAttributeName = 'attribute_2';
$testEntity = $this->getTestEntity($localDataAttributeName, ['src_attribute_2_1' => 'value_2_1', 'src_attribute_2_2' => 'value_2_2']);
$this->assertEquals('value_2_1', $testEntity->getLocalDataValue($localDataAttributeName));
// first source attribute specified in config is not present in source data, however second attribute is preset -> return second source attribute value
$testEntity = $this->getTestEntity($localDataAttributeName, ['src_attribute_2_2' => 'value_2_2']);
$this->assertEquals('value_2_2', $testEntity->getLocalDataValue($localDataAttributeName));
}
public function testLocalDataMappingAccessDenied()
{
// authorization expression of attribute evaluates to false -> deny access
......@@ -106,15 +94,15 @@ class LocalDataTest extends TestCase
$options[LocalData::QUERY_PARAMETER_NAME] = $localDataAttributeName.':value_1';
$this->localDataEventDispatcher->onNewOperation($options);
$preEvent = new TestEntityPreEvent();
$preEvent = new TestEntityPreEvent($options);
$this->localDataEventDispatcher->dispatch($preEvent);
$filters = $preEvent->getQueryParametersOut();
$this->assertArrayHasKey('src_attribute_1', $filters);
$this->assertEquals('value_1', $filters['src_attribute_1']);
$options = $preEvent->getOptions();
$this->assertArrayHasKey('src_attribute_1', $options);
$this->assertEquals('value_1', $options['src_attribute_1']);
}
public function testLocalDataQueryAttributeUnacknowledged()
public function testLocalDataQueryAttributeUnacknowledgedNotConfigure()
{
// 'attribute_4' has no configured source attribute.
// Throw bad request error because no event subscriber acknowledged local query parameter 'attribute_4'.
......@@ -125,7 +113,25 @@ class LocalDataTest extends TestCase
try {
$this->localDataEventDispatcher->onNewOperation($options);
$preEvent = new TestEntityPreEvent();
$preEvent = new TestEntityPreEvent($options);
$this->localDataEventDispatcher->dispatch($preEvent);
} catch (ApiError $exception) {
$this->assertEquals(Response::HTTP_BAD_REQUEST, $exception->getStatusCode());
}
}
public function testLocalDataQueryAttributeUnacknowledgedNotQueryable()
{
// 'attribute_2' is configured 'allow_query': false (default value)
// Throw bad request error because no event subscriber acknowledged local query parameter 'attribute_2'.
$localDataAttributeName = 'attribute_2';
$options = [];
$options[LocalData::QUERY_PARAMETER_NAME] = $localDataAttributeName.':value_2';
try {
$this->localDataEventDispatcher->onNewOperation($options);
$preEvent = new TestEntityPreEvent($options);
$this->localDataEventDispatcher->dispatch($preEvent);
} catch (ApiError $exception) {
$this->assertEquals(Response::HTTP_BAD_REQUEST, $exception->getStatusCode());
......@@ -142,7 +148,7 @@ class LocalDataTest extends TestCase
try {
$this->localDataEventDispatcher->onNewOperation($options);
$preEvent = new TestEntityPreEvent();
$preEvent = new TestEntityPreEvent($options);
$this->localDataEventDispatcher->dispatch($preEvent);
} catch (ApiError $exception) {
$this->assertEquals(Response::HTTP_UNAUTHORIZED, $exception->getStatusCode());
......@@ -168,24 +174,28 @@ class LocalDataTest extends TestCase
$config['local_data_mapping'] = [
[
'local_data_attribute' => 'attribute_1',
'source_attributes' => ['src_attribute_1'],
'source_attribute' => 'src_attribute_1',
'authorization_expression' => 'true',
'allow_query' => true,
'default_value' => 0,
],
[
'local_data_attribute' => 'attribute_2',
'source_attributes' => ['src_attribute_2_1', 'src_attribute_2_2'],
'source_attribute' => 'src_attribute_2_1',
'authorization_expression' => 'true',
'allow_query' => false,
],
[
'local_data_attribute' => 'attribute_3',
'source_attributes' => ['src_attribute_3'],
'source_attribute' => 'src_attribute_3',
'authorization_expression' => 'false',
'allow_query' => true,
],
[
'local_data_attribute' => 'array_attribute_1',
'source_attributes' => ['array_src_attribute_1'],
'source_attribute' => 'array_src_attribute_1',
'authorization_expression' => 'true',
'allow_query' => false,
'default_values' => [0],
],
];
......
......@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Dbp\Relay\CoreBundle\Tests\LocalData;
use Dbp\Relay\CoreBundle\LocalData\AbstractLocalDataPostEventSubscriber;
use Dbp\Relay\CoreBundle\LocalData\LocalDataPreEvent;
class TestEntityLocalDataEventSubscriber extends AbstractLocalDataPostEventSubscriber
{
......@@ -15,4 +16,9 @@ class TestEntityLocalDataEventSubscriber extends AbstractLocalDataPostEventSubsc
TestEntityPreEvent::class,
];
}
protected function onPreEvent(LocalDataPreEvent $preEvent, array $localQueryParameters)
{
$preEvent->setOptions($localQueryParameters);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment