diff --git a/src/Authorization/Serializer/AbstractEntityNormalizer.php b/src/Authorization/Serializer/AbstractEntityNormalizer.php new file mode 100644 index 0000000000000000000000000000000000000000..2861f18828048211d298eaeb6e0de5239b08e002 --- /dev/null +++ b/src/Authorization/Serializer/AbstractEntityNormalizer.php @@ -0,0 +1,146 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\Authorization\Serializer; + +use Dbp\Relay\CoreBundle\Authorization\AbstractAuthorizationService; +use Dbp\Relay\CoreBundle\Exception\ApiError; +use Dbp\Relay\CoreBundle\Helpers\ApiPlatformHelperFunctions; +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; + +abstract class AbstractEntityNormalizer extends AbstractAuthorizationService implements ContextAwareNormalizerInterface, NormalizerAwareInterface +{ + use NormalizerAwareTrait; + + private const ROOT_CONFIG_NODE = 'attribute_access'; + + private const ENTITY_SHORT_NAME_KEY = 'short_name'; + private const ATTRIBUTE_NAMES_KEY = 'attribute_names'; + + private const ENTITY_OBJECT_ALIAS = 'entity'; + + /** @var Security */ + private $security; + + /** @var array */ + private $entityClassNames; + + /** @var array */ + private $entityClassNameToAttributeNamesMapping; + + public static function getAttributeAccessConfigNodeDefinition(array $entityShortNameToAttributeNamesMapping): NodeDefinition + { + $treeBuilder = new TreeBuilder(self::ROOT_CONFIG_NODE); + + foreach ($entityShortNameToAttributeNamesMapping as $entityShortName => $attributeNames) { + $attributeNodeBuilder = $treeBuilder->getRootNode()->children()->arrayNode($entityShortName) + ->addDefaultsIfNotSet() + ->children(); + foreach ($attributeNames as $attributeName) { + $attributeNodeBuilder->scalarNode($attributeName) + ->defaultValue('false') + ->info(sprintf('viewer role expression for attribute \'%s\' under entity \'%s\'', $attributeName, $entityShortName)) + ->end(); + } + } + + return $treeBuilder->getRootNode(); + } + + private static function toAttributeId(string $entityShortName, string $attributeName): string + { + return $entityShortName.':'.$attributeName; + } + + private static function getUniqueAlreadyCalledKeyForEntity(string $entityClassName): string + { + return self::class.$entityClassName; + } + + protected function __construct(array $entityClassNames) + { + $this->entityClassNames = $entityClassNames; + } + + public function setConfig(array $config) + { + $configNode = $config[self::ROOT_CONFIG_NODE] ?? []; + $rightExpressions = []; + + foreach ($this->entityClassNames as $entityClassName) { + $entityShortName = ApiPlatformHelperFunctions::getShortNameForResource($entityClassName); + $entityNode = $configNode[$entityShortName] ?? null; + if ($entityNode === null) { + throw ApiError::withDetails(Response::HTTP_INTERNAL_SERVER_ERROR, sprintf('attribute access not configured for entity \'%s\'', $entityShortName)); + } + + $attributeNames = []; + foreach ($entityNode as $attributeName => $attributeAuthorizationExpression) { + $rightExpressions[self::toAttributeId($entityShortName, $attributeName)] = $attributeAuthorizationExpression; + $attributeNames[] = $attributeName; + } + $this->entityClassNameToAttributeNamesMapping[$entityClassName] = [ + self::ENTITY_SHORT_NAME_KEY => $entityShortName, + self::ATTRIBUTE_NAMES_KEY => $attributeNames, + ]; + } + + parent::setConfig(parent::createConfig($rightExpressions)); + } + + /** + * @required + */ + public function __inject(Security $security) + { + $this->security = $security; + } + + /** + * {@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::ATTRIBUTE_NAMES_KEY] as $attributeName) { + $attributeId = self::toAttributeId($entityShortName, $attributeName); + dump($attributeId); + if ($this->isGranted($attributeId, $object, self::ENTITY_OBJECT_ALIAS)) { + $context['groups'][] = $attributeId; + } + } + + $context[self::getUniqueAlreadyCalledKeyForEntity($entityClassName)] = true; + + return $this->normalizer->normalize($object, $format, $context); + } + + /** + * {@inheritDoc} + */ + public function supportsNormalization($data, $format = null, array $context = []): bool + { + if (!is_object($data)) { + 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); + } +} diff --git a/src/Helpers/ApiPlatformHelperFunctions.php b/src/Helpers/ApiPlatformHelperFunctions.php new file mode 100644 index 0000000000000000000000000000000000000000..bfa5c74a7610dabbe80ebabc233a93b9f9c2f850 --- /dev/null +++ b/src/Helpers/ApiPlatformHelperFunctions.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\Helpers; + +use ApiPlatform\Core\Exception\ResourceClassNotFoundException; +use ApiPlatform\Core\Metadata\Resource\Factory\AnnotationResourceMetadataFactory; +use Dbp\Relay\CoreBundle\Exception\ApiError; +use Doctrine\Common\Annotations\AnnotationReader; + +class ApiPlatformHelperFunctions +{ + /** + * Returns the 'shortName' attribute of the ApiResource annotation of the entity with the given class name. + * + * @throws ApiError if the ApiResource annotation of $resourceClass is not found or doesn't have a non-empty 'shortName' attribute + */ + public static function getShortNameForResource(string $resourceClass): string + { + $resourceMetadataFactory = new AnnotationResourceMetadataFactory(new AnnotationReader()); + try { + $resourceMetadata = $resourceMetadataFactory->create($resourceClass); + } catch (ResourceClassNotFoundException $exc) { + throw new ApiError(500, $exc->getMessage()); + } + + $uniqueName = $resourceMetadata->getShortName() ?? ''; + if (Tools::isNullOrEmpty($uniqueName)) { + throw new ApiError(500, sprintf("'shortName' attribute missing in ApiResource annotation of resource class '%s'", $resourceClass)); + } elseif (str_contains($uniqueName, '.') || str_contains($uniqueName, ',')) { + throw new ApiError(500, sprintf("'shortName' attribute of resource class '%s' must not contain '.' or ',' characters: '%s'", $resourceClass, $uniqueName)); + } + + return $uniqueName; + } +} diff --git a/src/LocalData/LocalDataEventDispatcher.php b/src/LocalData/LocalDataEventDispatcher.php index 93f7d9238890db8e6bb3dc6eede4103b3d6f1570..a3140ea4cc8e258378a91029d0c52ec1d72edadc 100644 --- a/src/LocalData/LocalDataEventDispatcher.php +++ b/src/LocalData/LocalDataEventDispatcher.php @@ -4,11 +4,9 @@ declare(strict_types=1); namespace Dbp\Relay\CoreBundle\LocalData; -use ApiPlatform\Core\Exception\ResourceClassNotFoundException; -use ApiPlatform\Core\Metadata\Resource\Factory\AnnotationResourceMetadataFactory; use Dbp\Relay\CoreBundle\Exception\ApiError; +use Dbp\Relay\CoreBundle\Helpers\ApiPlatformHelperFunctions; use Dbp\Relay\CoreBundle\Helpers\Tools; -use Doctrine\Common\Annotations\AnnotationReader; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Contracts\EventDispatcher\Event; @@ -111,21 +109,7 @@ class LocalDataEventDispatcher */ public static function getUniqueEntityName(string $resourceClass): string { - $resourceMetadataFactory = new AnnotationResourceMetadataFactory(new AnnotationReader()); - try { - $resourceMetadata = $resourceMetadataFactory->create($resourceClass); - } catch (ResourceClassNotFoundException $exc) { - throw new ApiError(500, $exc->getMessage()); - } - - $uniqueName = $resourceMetadata->getShortName() ?? ''; - if (Tools::isNullOrEmpty($uniqueName)) { - throw new ApiError(500, sprintf("'shortName' attribute missing in ApiResource annotation of resource class '%s'", $resourceClass)); - } elseif (str_contains($uniqueName, '.') || str_contains($uniqueName, ',')) { - throw new ApiError(500, sprintf("'shortName' attribute of resource class '%s' must not contain '.' or ',' characters: '%s'", $resourceClass, $uniqueName)); - } - - return $uniqueName; + return ApiPlatformHelperFunctions::getShortNameForResource($resourceClass); } /**