diff --git a/src/LocalData/AbstractLocalDataEventSubscriber.php b/src/LocalData/AbstractLocalDataEventSubscriber.php index 20a7b600cc9e00db070520f28d34ddefef11a9ff..e35e035e936c97809c081582984814521e9f772b 100644 --- a/src/LocalData/AbstractLocalDataEventSubscriber.php +++ b/src/LocalData/AbstractLocalDataEventSubscriber.php @@ -97,30 +97,27 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer public function onEvent(Event $event) { if ($event instanceof LocalDataPreEvent) { - $queryParametersOut = []; - - // matriculationNumber:0011675 - foreach ($event->getQueryParameters() as $queryParameterName => $queryParameterValue) { - if (($attributeMapEntry = $this->attributeMapping[$queryParameterName] ?? null) !== null) { + foreach ($event->getPendingQueryParametersIn() as $localDataAttributeName => $localDataAttributeValue) { + 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)); + } $sourceAttributeName = $attributeMapEntry[self::SOURCE_ATTRIBUTES_KEY][0]; - $queryParametersOut[$sourceAttributeName] = $queryParameterValue; + $event->addQueryParameterOut($sourceAttributeName, $localDataAttributeValue); + $event->acknowledgeQueryParameterIn($localDataAttributeName); } } - $event->setQueryParameters($queryParametersOut); - $this->onPre($event); + $this->onPreEvent($event); } elseif ($event instanceof LocalDataPostEvent) { - $sourceData = $event->getSourceData(); - - foreach ($this->attributeMapping as $localDataAttributeName => $attributeMapEntry) { - if ($event->isLocalDataAttributeRequested($localDataAttributeName)) { + 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 = $sourceData[$sourceAttributeName] ?? null) !== null) { + if (($value = $event->getSourceData()[$sourceAttributeName] ?? null) !== null) { $attributeValue = $value; break; } @@ -134,7 +131,7 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer } } } - $this->onPost($event); + $this->onPostEvent($event); } } @@ -182,11 +179,11 @@ abstract class AbstractLocalDataEventSubscriber extends AbstractAuthorizationSer throw new \RuntimeException(sprintf('child classes must implement the \'%s\' method', __METHOD__)); } - protected function onPre(LocalDataPreEvent $preEvent) + protected function onPreEvent(LocalDataPreEvent $preEvent) { } - protected function onPost(LocalDataPostEvent $postEvent) + protected function onPostEvent(LocalDataPostEvent $postEvent) { } } diff --git a/src/LocalData/LocalDataEventDispatcher.php b/src/LocalData/LocalDataEventDispatcher.php index 0e60a3dc06d5dd4ad80eab1c85e9b7335383d78f..c606aade965c66cb5696046c6359a37669d65695 100644 --- a/src/LocalData/LocalDataEventDispatcher.php +++ b/src/LocalData/LocalDataEventDispatcher.php @@ -83,15 +83,20 @@ class LocalDataEventDispatcher public function dispatch(Event $event, string $eventName = null): void { if ($event instanceof LocalDataPreEvent) { - $event->setQueryParameters($this->queryParameters); + $event->initQueryParametersIn($this->queryParameters); $this->eventDispatcher->dispatch($event, $eventName); + + $pendingAttributes = $event->getPendingQueryParametersIn(); + 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(', ', $pendingAttributes))); + } } elseif ($event instanceof LocalDataPostEvent) { $event->setRequestedAttributes($this->requestedAttributes); $this->eventDispatcher->dispatch($event, $eventName); - $remainingLocalDataAttributes = $event->getRemainingRequestedAttributes(); - if (!empty($remainingLocalDataAttributes)) { - throw ApiError::withDetails(Response::HTTP_BAD_REQUEST, sprintf("the following requested local data attributes could not be provided for resource '%s': %s", $this->uniqueEntityName, implode(', ', $remainingLocalDataAttributes))); + $pendingAttributes = $event->getPendingRequestedAttributes(); + if (count($pendingAttributes) !== 0) { + throw ApiError::withDetails(Response::HTTP_BAD_REQUEST, sprintf("the following requested local data attributes could not be provided for resource '%s': %s", $this->uniqueEntityName, implode(', ', $pendingAttributes))); } } else { $this->eventDispatcher->dispatch($event, $eventName); diff --git a/src/LocalData/LocalDataPostEvent.php b/src/LocalData/LocalDataPostEvent.php index a8b8c9dc1615b38f8296a3f5d61c0d6c2a27b131..c0f2a5f4e8398407c80619be5bdc9813ffe5806f 100644 --- a/src/LocalData/LocalDataPostEvent.php +++ b/src/LocalData/LocalDataPostEvent.php @@ -53,7 +53,7 @@ class LocalDataPostEvent extends Event implements LoggerAwareInterface * * @retrun string[] */ - public function getRemainingRequestedAttributes(): array + public function getPendingRequestedAttributes(): array { return $this->requestedAttributes; } @@ -103,11 +103,11 @@ class LocalDataPostEvent extends Event implements LoggerAwareInterface * * @throws ApiError if attribute $key is not in the set of requested attributes */ - private function setLocalDataAttributeInternal(string $key, $value, bool $warnfNotFound): void + private function setLocalDataAttributeInternal(string $key, $value, bool $warnIfNotFound): void { $arrayKey = array_search($key, $this->requestedAttributes, true); if ($arrayKey === false) { - if ($warnfNotFound) { + if ($warnIfNotFound) { if ($this->logger !== null) { $this->logger->warning(sprintf("trying to set local data attribute '%s', which was not requested for entity '%s'", $key, LocalDataEventDispatcher::getUniqueEntityName(get_class($this->entity)))); } diff --git a/src/LocalData/LocalDataPreEvent.php b/src/LocalData/LocalDataPreEvent.php index c3ac8a5380d5569222782e6f70be4c86d3f309ee..633ca7dc2ad06026066afe599d5be286cf68e4dd 100644 --- a/src/LocalData/LocalDataPreEvent.php +++ b/src/LocalData/LocalDataPreEvent.php @@ -8,23 +8,40 @@ use Symfony\Contracts\EventDispatcher\Event; class LocalDataPreEvent extends Event { - public const NAME = 'dbp.relay.relay_core.local_data_aware_event.pre'; + /** @var string[] */ + private $queryParametersIn; - /** @var array */ - private $queryParameters; + /** @var string[] */ + private $queryParametersOut; public function __construct() { - $this->queryParameters = []; + $this->queryParametersIn = []; + $this->queryParametersOut = []; } - public function setQueryParameters(array $queryParameters): void + public function initQueryParametersIn(array $queryParametersIn): void { - $this->queryParameters = $queryParameters; + $this->queryParametersIn = $queryParametersIn; } - public function getQueryParameters(): array + public function getPendingQueryParametersIn(): array { - return $this->queryParameters; + return $this->queryParametersIn; + } + + public function acknowledgeQueryParameterIn(string $queryParameterName): void + { + unset($this->queryParametersIn[$queryParameterName]); + } + + public function addQueryParameterOut(string $queryParameterName, string $queryParameterValue): void + { + $this->queryParametersOut[$queryParameterName] = $queryParameterValue; + } + + public function getQueryParameterOut(): array + { + return $this->queryParametersOut; } } diff --git a/tests/LocalData/LocalDataTest.php b/tests/LocalData/LocalDataTest.php index d41d1cef341b26714699e200d9986c92ca19c4cd..dd51cbf0009aba4b72f88c089038324b005accdf 100644 --- a/tests/LocalData/LocalDataTest.php +++ b/tests/LocalData/LocalDataTest.php @@ -19,14 +19,14 @@ class LocalDataTest extends TestCase /** @var LocalDataEventDispatcher */ private $localDataEventDispatcher; - /** @var TestEntityLocalDataPostEventSubscriber */ + /** @var TestEntityLocalDataEventSubscriber */ private $localDataEventSubscriber; protected function setUp(): void { parent::setUp(); - $localDataEventSubscriber = new TestEntityLocalDataPostEventSubscriber(); + $localDataEventSubscriber = new TestEntityLocalDataEventSubscriber(); $localDataEventSubscriber->_injectServices(new TestUserSession('testuser'), new AuthorizationDataMuxer(new AuthorizationDataProviderProvider([]), new EventDispatcher())); $localDataEventSubscriber->setConfig(self::createConfig()); @@ -96,10 +96,66 @@ class LocalDataTest extends TestCase } } + public function testLocalDataQuery() + { + // 'attribute_1' has a configured source attribute 'src_attribute_1'. + // Post-condition: options contain the mapped attribute 'src_attribute_1' as a key with the given value 'value_1'. + $localDataAttributeName = 'attribute_1'; + + $options = []; + $options[LocalData::QUERY_PARAMETER_NAME] = $localDataAttributeName.':value_1'; + + $this->localDataEventDispatcher->onNewOperation($options); + $preEvent = new TestEntityPreEvent(); + $this->localDataEventDispatcher->dispatch($preEvent); + + $filters = $preEvent->getQueryParameterOut(); + $this->assertArrayHasKey('src_attribute_1', $filters); + $this->assertEquals('value_1', $filters['src_attribute_1']); + } + + public function testLocalDataQueryAttributeUnacknowledged() + { + // 'attribute_4' has no configured source attribute. + // Throw bad request error because no event subscriber acknowledged local query parameter 'attribute_4'. + $localDataAttributeName = 'attribute_4'; + + $options = []; + $options[LocalData::QUERY_PARAMETER_NAME] = $localDataAttributeName.':value_4'; + + try { + $this->localDataEventDispatcher->onNewOperation($options); + $preEvent = new TestEntityPreEvent(); + $this->localDataEventDispatcher->dispatch($preEvent); + } catch (ApiError $exception) { + $this->assertEquals(Response::HTTP_BAD_REQUEST, $exception->getStatusCode()); + } + } + + public function testLocalDataQueryAccessDenied() + { + // authorization expression of attribute evaluates to false -> deny access + $localDataAttributeName = 'attribute_3'; + + $options = []; + $options[LocalData::QUERY_PARAMETER_NAME] = $localDataAttributeName.':value_1'; + + try { + $this->localDataEventDispatcher->onNewOperation($options); + $preEvent = new TestEntityPreEvent(); + $this->localDataEventDispatcher->dispatch($preEvent); + } catch (ApiError $exception) { + $this->assertEquals(Response::HTTP_UNAUTHORIZED, $exception->getStatusCode()); + } + } + private function getTestEntity(string $includeLocal, array $sourceData): TestEntity { $testEntity = new TestEntity(); - $options = [LocalData::INCLUDE_PARAMETER_NAME => $includeLocal]; + + $options = []; + $options[LocalData::INCLUDE_PARAMETER_NAME] = $includeLocal; + $this->localDataEventDispatcher->onNewOperation($options); $this->localDataEventDispatcher->dispatch(new TestEntityPostEvent($testEntity, $sourceData)); diff --git a/tests/LocalData/TestEntityLocalDataEventSubscriber.php b/tests/LocalData/TestEntityLocalDataEventSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..a85a1d84d6a154592e4147fb2097abc1753c36e8 --- /dev/null +++ b/tests/LocalData/TestEntityLocalDataEventSubscriber.php @@ -0,0 +1,18 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\Tests\LocalData; + +use Dbp\Relay\CoreBundle\LocalData\AbstractLocalDataPostEventSubscriber; + +class TestEntityLocalDataEventSubscriber extends AbstractLocalDataPostEventSubscriber +{ + public static function getSubscribedEventNames(): array + { + return [ + TestEntityPostEvent::class, + TestEntityPreEvent::class, + ]; + } +} diff --git a/tests/LocalData/TestEntityLocalDataPostEventSubscriber.php b/tests/LocalData/TestEntityLocalDataPostEventSubscriber.php deleted file mode 100644 index 878c51c408e28565d3e9abb2c660cdb63896805e..0000000000000000000000000000000000000000 --- a/tests/LocalData/TestEntityLocalDataPostEventSubscriber.php +++ /dev/null @@ -1,15 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Dbp\Relay\CoreBundle\Tests\LocalData; - -use Dbp\Relay\CoreBundle\LocalData\AbstractLocalDataPostEventSubscriber; - -class TestEntityLocalDataPostEventSubscriber extends AbstractLocalDataPostEventSubscriber -{ - public static function getSubscribedEventName(): string - { - return TestEntityPostEvent::class; - } -} diff --git a/tests/LocalData/TestEntityPreEvent.php b/tests/LocalData/TestEntityPreEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..da717aca123c70a0b3e9cee18fa71de6c7d1d39d --- /dev/null +++ b/tests/LocalData/TestEntityPreEvent.php @@ -0,0 +1,11 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\Tests\LocalData; + +use Dbp\Relay\CoreBundle\LocalData\LocalDataPreEvent; + +class TestEntityPreEvent extends LocalDataPreEvent +{ +}