From 3c696f31a84b65cec38a975d4adaf64e3bc2fdcb Mon Sep 17 00:00:00 2001
From: Tobias Gross-Vogt <tgros@tugraz.at>
Date: Wed, 30 Nov 2022 13:11:21 +0100
Subject: [PATCH] BREAKING CHANGES: automatic adding of local data attributes
 via config; renmaed LocalDataAware -> LocalData where used falsely

---
 .../AbstractAuthorizationService.php          |  3 +
 src/Entity/NamedEntityInterface.php           | 15 ++++
 .../AbstractLocalDataPostEventSubscriber.php  | 82 +++++++++++++++++++
 src/LocalData/LocalData.php                   |  5 ++
 ...tcher.php => LocalDataEventDispatcher.php} | 12 +--
 ...rePostEvent.php => LocalDataPostEvent.php} | 17 +++-
 ...warePreEvent.php => LocalDataPreEvent.php} |  2 +-
 7 files changed, 126 insertions(+), 10 deletions(-)
 create mode 100644 src/Entity/NamedEntityInterface.php
 create mode 100644 src/LocalData/AbstractLocalDataPostEventSubscriber.php
 rename src/LocalData/{LocalDataAwareEventDispatcher.php => LocalDataEventDispatcher.php} (96%)
 rename src/LocalData/{LocalDataAwarePostEvent.php => LocalDataPostEvent.php} (87%)
 rename src/LocalData/{LocalDataAwarePreEvent.php => LocalDataPreEvent.php} (94%)

diff --git a/src/Authorization/AbstractAuthorizationService.php b/src/Authorization/AbstractAuthorizationService.php
index 9911ced..fc69b1a 100644
--- a/src/Authorization/AbstractAuthorizationService.php
+++ b/src/Authorization/AbstractAuthorizationService.php
@@ -10,6 +10,9 @@ use Symfony\Component\HttpFoundation\Response;
 
 abstract class AbstractAuthorizationService
 {
+    public const RIGHTS_CONFIG_ATTRIBUTE = AuthorizationExpressionChecker::RIGHTS_CONFIG_ATTRIBUTE;
+    public const ATTRIBUTES_CONFIG_ATTRIBUTE = AuthorizationExpressionChecker::ATTRIBUTES_CONFIG_ATTRIBUTE;
+
     /** @var AuthorizationExpressionChecker */
     private $userAuthorizationChecker;
 
diff --git a/src/Entity/NamedEntityInterface.php b/src/Entity/NamedEntityInterface.php
new file mode 100644
index 0000000..6ef8159
--- /dev/null
+++ b/src/Entity/NamedEntityInterface.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\Entity;
+
+/**
+ * Interface for entities with an identifier and a name.
+ */
+interface NamedEntityInterface
+{
+    public function getIdentifier();
+
+    public function getName();
+}
diff --git a/src/LocalData/AbstractLocalDataPostEventSubscriber.php b/src/LocalData/AbstractLocalDataPostEventSubscriber.php
new file mode 100644
index 0000000..9af529e
--- /dev/null
+++ b/src/LocalData/AbstractLocalDataPostEventSubscriber.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Dbp\Relay\CoreBundle\LocalData;
+
+use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService;
+use Symfony\Component\Config\Definition\Builder\TreeBuilder;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+abstract class AbstractLocalDataPostEventSubscriber extends AbstractAuthorizationService implements EventSubscriberInterface
+{
+    public const CONFIG_NODE = 'local_data_mapping';
+    public const SOURCE_ATTRIBUTE_KEY = 'source_attribute';
+    public const LOCAL_DATA_ATTRIBUTE_KEY = 'local_data_attribute';
+    public const AUTHORIZATION_EXPRESSION_KEY = 'authorization_expression';
+
+    /** @var string[] */
+    private $attributeMapping;
+
+    public function setConfig(array $config)
+    {
+        $configNode = $config[self::CONFIG_NODE] ?? [];
+
+        $rights = [];
+        foreach ($configNode as $configMappingEntry) {
+            if (isset($this->attributeMapping[$configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY]])) {
+                throw new \RuntimeException(sprintf('multiple mapping entries for local data attribute %s', $configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY]));
+            }
+            $this->attributeMapping[$configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY]] = $configMappingEntry[self::SOURCE_ATTRIBUTE_KEY];
+            // 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
+            $rights[$configMappingEntry[self::LOCAL_DATA_ATTRIBUTE_KEY]] = $configMappingEntry[self::AUTHORIZATION_EXPRESSION_KEY] ?? 'false';
+        }
+
+        if (!empty($rights)) {
+            parent::setConfig([AbstractAuthorizationService::RIGHTS_CONFIG_ATTRIBUTE => $rights]);
+        }
+    }
+
+    public static function getSubscribedEvents(): array
+    {
+        return [static::getSubscribedEventName() => 'onPost'];
+    }
+
+    public static function getSubscribedEventName(): string
+    {
+        throw new \RuntimeException(sprintf('child classes must implement the \'%s\' method', __METHOD__));
+    }
+
+    public function onPost(LocalDataPostEvent $postEvent)
+    {
+        $sourceData = $postEvent->getSourceData();
+
+        foreach ($this->attributeMapping as $localDataAttributeName => $sourceAttributeName) {
+            if ($this->isGranted($localDataAttributeName)) {
+                if ($sourceAttributeValue = $sourceData[$sourceAttributeName] ?? null) {
+                    $postEvent->trySetLocalDataAttribute($localDataAttributeName, $sourceAttributeValue);
+                } else {
+                    throw new \RuntimeException(sprintf('attribute \'%s\' not available in source data', $sourceAttributeName));
+                }
+            }
+        }
+    }
+
+    public static function getConfigNode()
+    {
+        $treeBuilder = new TreeBuilder(self::CONFIG_NODE);
+
+        return $treeBuilder->getRootNode()
+            ->arrayPrototype()
+                ->children()
+                    ->scalarNode(self::SOURCE_ATTRIBUTE_KEY)->end()
+                    ->scalarNode(self::LOCAL_DATA_ATTRIBUTE_KEY)->end()
+                    ->scalarNode(self::AUTHORIZATION_EXPRESSION_KEY)
+                        ->defaultValue('false')
+                    ->end()
+                ->end()
+            ->end()
+        ;
+    }
+}
diff --git a/src/LocalData/LocalData.php b/src/LocalData/LocalData.php
index 547d0e5..14bc0ba 100644
--- a/src/LocalData/LocalData.php
+++ b/src/LocalData/LocalData.php
@@ -35,4 +35,9 @@ class LocalData
     {
         return $options[self::QUERY_PARAMETER_NAME] ?? null;
     }
+
+    public static function toIncludeLocalParameterValue(array $attributeNames): string
+    {
+        return implode(LocalDataEventDispatcher::SEPARATOR, $attributeNames);
+    }
 }
diff --git a/src/LocalData/LocalDataAwareEventDispatcher.php b/src/LocalData/LocalDataEventDispatcher.php
similarity index 96%
rename from src/LocalData/LocalDataAwareEventDispatcher.php
rename to src/LocalData/LocalDataEventDispatcher.php
index d3d8e0b..90085e4 100644
--- a/src/LocalData/LocalDataAwareEventDispatcher.php
+++ b/src/LocalData/LocalDataEventDispatcher.php
@@ -13,8 +13,10 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Contracts\EventDispatcher\Event;
 
-class LocalDataAwareEventDispatcher
+class LocalDataEventDispatcher
 {
+    public const SEPARATOR = ',';
+
     /** @var array */
     private $queryParameters;
 
@@ -79,10 +81,10 @@ class LocalDataAwareEventDispatcher
      */
     public function dispatch(Event $event, string $eventName): void
     {
-        if ($event instanceof LocalDataAwarePreEvent) {
+        if ($event instanceof LocalDataPreEvent) {
             $event->setQueryParameters($this->queryParameters);
             $this->eventDispatcher->dispatch($event, $eventName);
-        } elseif ($event instanceof LocalDataAwarePostEvent) {
+        } elseif ($event instanceof LocalDataPostEvent) {
             $event->setRequestedAttributes($this->requestedAttributes);
             $this->eventDispatcher->dispatch($event, $eventName);
 
@@ -130,7 +132,7 @@ class LocalDataAwareEventDispatcher
         $this->requestedAttributes = [];
 
         if (!Tools::isNullOrEmpty($includeParameter)) {
-            $requestedAttributes = explode(',', $includeParameter);
+            $requestedAttributes = explode(self::SEPARATOR, $includeParameter);
 
             foreach ($requestedAttributes as $requestedAttribute) {
                 $requestedAttribute = trim($requestedAttribute);
@@ -155,7 +157,7 @@ class LocalDataAwareEventDispatcher
         $this->queryParameters = [];
 
         if (!Tools::isNullOrEmpty($queryParameter)) {
-            $localQueryParameters = explode(',', $queryParameter);
+            $localQueryParameters = explode(self::SEPARATOR, $queryParameter);
 
             foreach ($localQueryParameters as $localQueryParameter) {
                 $localQueryParameter = trim($localQueryParameter);
diff --git a/src/LocalData/LocalDataAwarePostEvent.php b/src/LocalData/LocalDataPostEvent.php
similarity index 87%
rename from src/LocalData/LocalDataAwarePostEvent.php
rename to src/LocalData/LocalDataPostEvent.php
index f49a420..a8b8c9d 100644
--- a/src/LocalData/LocalDataAwarePostEvent.php
+++ b/src/LocalData/LocalDataPostEvent.php
@@ -9,22 +9,31 @@ use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerAwareTrait;
 use Symfony\Contracts\EventDispatcher\Event;
 
-class LocalDataAwarePostEvent extends Event implements LoggerAwareInterface
+class LocalDataPostEvent extends Event implements LoggerAwareInterface
 {
     use LoggerAwareTrait;
 
     /** @var LocalDataAwareInterface */
     private $entity;
 
+    /** @var array */
+    private $sourceData;
+
     /** @var array */
     private $requestedAttributes;
 
-    protected function __construct(LocalDataAwareInterface $entity)
+    public function __construct(LocalDataAwareInterface $entity, array $sourceData)
     {
         $this->entity = $entity;
+        $this->sourceData = $sourceData;
+    }
+
+    public function getSourceData(): array
+    {
+        return $this->sourceData;
     }
 
-    protected function getEntityInternal(): LocalDataAwareInterface
+    public function getEntity(): LocalDataAwareInterface
     {
         return $this->entity;
     }
@@ -100,7 +109,7 @@ class LocalDataAwarePostEvent extends Event implements LoggerAwareInterface
         if ($arrayKey === false) {
             if ($warnfNotFound) {
                 if ($this->logger !== null) {
-                    $this->logger->warning(sprintf("trying to set local data attribute '%s', which was not requested for entity '%s'", $key, LocalDataAwareEventDispatcher::getUniqueEntityName(get_class($this->entity))));
+                    $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))));
                 }
                 assert(false);
             } else {
diff --git a/src/LocalData/LocalDataAwarePreEvent.php b/src/LocalData/LocalDataPreEvent.php
similarity index 94%
rename from src/LocalData/LocalDataAwarePreEvent.php
rename to src/LocalData/LocalDataPreEvent.php
index de1a7a6..fcc7428 100644
--- a/src/LocalData/LocalDataAwarePreEvent.php
+++ b/src/LocalData/LocalDataPreEvent.php
@@ -6,7 +6,7 @@ namespace Dbp\Relay\CoreBundle\LocalData;
 
 use Symfony\Contracts\EventDispatcher\Event;
 
-class LocalDataAwarePreEvent extends Event
+class LocalDataPreEvent extends Event
 {
     public const NAME = 'dbp.relay.relay_core.local_data_aware_event.pre';
 
-- 
GitLab