From 5f1bbf125525f2e5cd06846f319355bcb7be692b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Barthelet?= Date: Wed, 30 Oct 2019 22:11:04 +0100 Subject: [PATCH] Implement ApiProperty security attribute --- features/authorization/deny.feature | 18 +++- features/graphql/authorization.feature | 6 +- src/Annotation/ApiProperty.php | 8 ++ .../Symfony/Bundle/Resources/config/api.xml | 2 +- .../Symfony/Bundle/Resources/config/hal.xml | 2 +- .../Bundle/Resources/config/jsonapi.xml | 2 +- .../Bundle/Resources/config/jsonld.xml | 2 +- src/JsonApi/Serializer/ItemNormalizer.php | 5 +- src/JsonLd/Serializer/ItemNormalizer.php | 5 +- src/Serializer/AbstractItemNormalizer.php | 24 ++++- src/Serializer/ItemNormalizer.php | 5 +- tests/Annotation/ApiPropertyTest.php | 2 + .../TestBundle/Document/SecuredDummy.php | 21 +++- .../TestBundle/Entity/SecuredDummy.php | 21 +++- .../Serializer/AbstractItemNormalizerTest.php | 97 +++++++++++++++---- 15 files changed, 185 insertions(+), 35 deletions(-) diff --git a/features/authorization/deny.feature b/features/authorization/deny.feature index 404a8c45438..3a3bdde3f23 100644 --- a/features/authorization/deny.feature +++ b/features/authorization/deny.feature @@ -59,7 +59,8 @@ Feature: Authorization checking { "title": "Special Title", "description": "Description", - "owner": "dunglas" + "owner": "dunglas", + "adminOnlyProperty": "secret" } """ Then the response status code should be 201 @@ -100,3 +101,18 @@ Feature: Authorization checking } """ Then the response status code should be 200 + + Scenario: An admin retrieves a resource with an admin only viewable property + When I add "Accept" header equal to "application/ld+json" + And I add "Content-Type" header equal to "application/ld+json" + And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu" + And I send a "GET" request to "/secured_dummies" + Then the response status code should be 200 + And the response should contain "adminOnlyProperty" + + Scenario: A user retrieves a resource with an admin only viewable property + When I add "Accept" header equal to "application/ld+json" + And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg==" + And I send a "GET" request to "/secured_dummies" + Then the response status code should be 200 + And the response should not contain "adminOnlyProperty" diff --git a/features/graphql/authorization.feature b/features/graphql/authorization.feature index a4cc77c4ccd..165b0131c20 100644 --- a/features/graphql/authorization.feature +++ b/features/graphql/authorization.feature @@ -91,7 +91,7 @@ Feature: Authorization checking When I send the following GraphQL request: """ mutation { - createSecuredDummy(input: {owner: "me", title: "Hi", description: "Desc", clientMutationId: "auth"}) { + createSecuredDummy(input: {owner: "me", title: "Hi", description: "Desc", adminOnlyProperty: "secret", clientMutationId: "auth"}) { securedDummy { title owner @@ -112,7 +112,7 @@ Feature: Authorization checking And I send the following GraphQL request: """ mutation { - createSecuredDummy(input: {owner: "someone", title: "Hi", description: "Desc"}) { + createSecuredDummy(input: {owner: "someone", title: "Hi", description: "Desc", adminOnlyProperty: "secret"}) { securedDummy { id title @@ -131,7 +131,7 @@ Feature: Authorization checking And I send the following GraphQL request: """ mutation { - createSecuredDummy(input: {owner: "dunglas", title: "Hi", description: "Desc"}) { + createSecuredDummy(input: {owner: "dunglas", title: "Hi", description: "Desc", adminOnlyProperty: "secret"}) { securedDummy { id title diff --git a/src/Annotation/ApiProperty.php b/src/Annotation/ApiProperty.php index 2ec25e3545a..1c21ec1a05d 100644 --- a/src/Annotation/ApiProperty.php +++ b/src/Annotation/ApiProperty.php @@ -29,6 +29,7 @@ * @Attribute("openapiContext", type="array"), * @Attribute("jsonldContext", type="array"), * @Attribute("push", type="bool"), + * @Attribute("security", type="string"), * @Attribute("swaggerContext", type="array") * ) */ @@ -118,6 +119,13 @@ final class ApiProperty */ private $push; + /** + * @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112 + * + * @var string + */ + private $security; + /** * @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112 * diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index f34847662f7..057c887b9af 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -115,7 +115,7 @@ null - false + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml index 2828d13eec5..a42c976c90b 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml @@ -40,7 +40,7 @@ - false + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml index 410a327b43d..7d232706bc1 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml @@ -41,7 +41,7 @@ - false + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml index b40b0ed1c2d..3912ad1e1b4 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml @@ -27,7 +27,7 @@ - false + diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index ca869d67f0b..ade96fe723c 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -20,6 +20,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Security\ResourceAccessCheckerInterface; use ApiPlatform\Core\Serializer\AbstractItemNormalizer; use ApiPlatform\Core\Serializer\CacheKeyTrait; use ApiPlatform\Core\Serializer\ContextTrait; @@ -49,9 +50,9 @@ final class ItemNormalizer extends AbstractItemNormalizer private $componentsCache = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = []) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [], ResourceAccessCheckerInterface $resourceAccessChecker = null) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker); } /** diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index aca10e346d7..197050fdd55 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -19,6 +19,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Security\ResourceAccessCheckerInterface; use ApiPlatform\Core\Serializer\AbstractItemNormalizer; use ApiPlatform\Core\Serializer\ContextTrait; use ApiPlatform\Core\Util\ClassInfoTrait; @@ -43,9 +44,9 @@ final class ItemNormalizer extends AbstractItemNormalizer private $contextBuilder; - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], iterable $dataTransformers = []) + public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], iterable $dataTransformers = [], ResourceAccessCheckerInterface $resourceAccessChecker = null) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker); $this->contextBuilder = $contextBuilder; } diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 9046325f1d1..61b2f6a55bb 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -24,6 +24,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Property\PropertyMetadata; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Security\ResourceAccessCheckerInterface; use ApiPlatform\Core\Util\ClassInfoTrait; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -55,13 +56,14 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer protected $propertyMetadataFactory; protected $iriConverter; protected $resourceClassResolver; + protected $resourceAccessChecker; protected $propertyAccessor; protected $itemDataProvider; protected $allowPlainIdentifiers; protected $dataTransformers = []; protected $localCache = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, array $defaultContext = [], iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null) { if (!isset($defaultContext['circular_reference_handler'])) { $defaultContext['circular_reference_handler'] = function ($object) { @@ -83,6 +85,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName $this->allowPlainIdentifiers = $allowPlainIdentifiers; $this->dataTransformers = $dataTransformers; $this->resourceMetadataFactory = $resourceMetadataFactory; + $this->resourceAccessChecker = $resourceAccessChecker; } /** @@ -349,6 +352,25 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu return $allowedAttributes; } + /** + * {@inheritdoc} + */ + protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = []) + { + if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) { + return false; + } + + $options = $this->getFactoryOptions($context); + $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options); + $security = $propertyMetadata->getAttribute('security'); + if ($this->resourceAccessChecker && $security) { + return $this->resourceAccessChecker->isGranted($attribute, $security); + } + + return true; + } + /** * {@inheritdoc} */ diff --git a/src/Serializer/ItemNormalizer.php b/src/Serializer/ItemNormalizer.php index f593dafddbb..293b7955766 100644 --- a/src/Serializer/ItemNormalizer.php +++ b/src/Serializer/ItemNormalizer.php @@ -20,6 +20,7 @@ use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Security\ResourceAccessCheckerInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -36,9 +37,9 @@ class ItemNormalizer extends AbstractItemNormalizer { private $logger; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, ItemDataProviderInterface $itemDataProvider = null, bool $allowPlainIdentifiers = false, LoggerInterface $logger = null, iterable $dataTransformers = [], ResourceMetadataFactoryInterface $resourceMetadataFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, [], $dataTransformers, $resourceMetadataFactory); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $itemDataProvider, $allowPlainIdentifiers, [], $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker); $this->logger = $logger ?: new NullLogger(); } diff --git a/tests/Annotation/ApiPropertyTest.php b/tests/Annotation/ApiPropertyTest.php index ea08ce657d0..aa550d42e4f 100644 --- a/tests/Annotation/ApiPropertyTest.php +++ b/tests/Annotation/ApiPropertyTest.php @@ -52,6 +52,7 @@ public function testConstruct() 'fetchable' => true, 'fetchEager' => false, 'jsonldContext' => ['foo' => 'bar'], + 'security' => 'is_granted(\'ROLE_ADMIN\')', 'swaggerContext' => ['foo' => 'baz'], 'openapiContext' => ['foo' => 'baz'], 'push' => true, @@ -62,6 +63,7 @@ public function testConstruct() 'fetchable' => false, 'fetch_eager' => false, 'jsonld_context' => ['foo' => 'bar'], + 'security' => 'is_granted(\'ROLE_ADMIN\')', 'swagger_context' => ['foo' => 'baz'], 'openapi_context' => ['foo' => 'baz'], 'push' => true, diff --git a/tests/Fixtures/TestBundle/Document/SecuredDummy.php b/tests/Fixtures/TestBundle/Document/SecuredDummy.php index bc932469381..6820d13b0f5 100644 --- a/tests/Fixtures/TestBundle/Document/SecuredDummy.php +++ b/tests/Fixtures/TestBundle/Document/SecuredDummy.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Document; +use ApiPlatform\Core\Annotation\ApiProperty; use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Validator\Constraints as Assert; @@ -26,7 +27,7 @@ * @ApiResource( * attributes={"security"="is_granted('ROLE_USER')"}, * collectionOperations={ - * "get", + * "get"={"security"="is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"}, * "get_from_data_provider_generator"={ * "method"="GET", * "path"="custom_data_provider_generator", @@ -72,6 +73,14 @@ class SecuredDummy */ private $description = ''; + /** + * @var string The dummy secret property, only readable/writable by specific users + * + * @ODM\Field + * @ApiProperty(security="is_granted('ROLE_ADMIN')") + */ + private $adminOnlyProperty = ''; + /** * @var string The owner * @@ -105,6 +114,16 @@ public function setDescription(string $description) $this->description = $description; } + public function getAdminOnlyProperty(): ?string + { + return $this->adminOnlyProperty; + } + + public function setAdminOnlyProperty(?string $adminOnlyProperty) + { + $this->adminOnlyProperty = $adminOnlyProperty; + } + public function getOwner(): string { return $this->owner; diff --git a/tests/Fixtures/TestBundle/Entity/SecuredDummy.php b/tests/Fixtures/TestBundle/Entity/SecuredDummy.php index f01d909c33c..19ffc2cc816 100644 --- a/tests/Fixtures/TestBundle/Entity/SecuredDummy.php +++ b/tests/Fixtures/TestBundle/Entity/SecuredDummy.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity; +use ApiPlatform\Core\Annotation\ApiProperty; use ApiPlatform\Core\Annotation\ApiResource; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -25,7 +26,7 @@ * @ApiResource( * attributes={"security"="is_granted('ROLE_USER')"}, * collectionOperations={ - * "get", + * "get"={"security"="is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"}, * "get_from_data_provider_generator"={ * "method"="GET", * "path"="custom_data_provider_generator", @@ -73,6 +74,14 @@ class SecuredDummy */ private $description = ''; + /** + * @var string The dummy secret property, only readable/writable by specific users + * + * @ORM\Column + * @ApiProperty(security="is_granted('ROLE_ADMIN')") + */ + private $adminOnlyProperty = ''; + /** * @var string The owner * @@ -106,6 +115,16 @@ public function setDescription(string $description) $this->description = $description; } + public function getAdminOnlyProperty(): ?string + { + return $this->adminOnlyProperty; + } + + public function setAdminOnlyProperty(?string $adminOnlyProperty) + { + $this->adminOnlyProperty = $adminOnlyProperty; + } + public function getOwner(): string { return $this->owner; diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php index e10fbf07063..b09592cb865 100644 --- a/tests/Serializer/AbstractItemNormalizerTest.php +++ b/tests/Serializer/AbstractItemNormalizerTest.php @@ -25,6 +25,7 @@ use ApiPlatform\Core\Metadata\Property\PropertyNameCollection; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Security\ResourceAccessCheckerInterface; use ApiPlatform\Core\Serializer\AbstractItemNormalizer; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Dto\InputDto; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; @@ -33,6 +34,7 @@ use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritance; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild; use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\SecuredDummy; use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -110,7 +112,7 @@ public function testSupportNormalizationAndSupportDenormalization() [], [], null, - false, + null, ]); $this->assertTrue($normalizer->supportsNormalization($dummy)); @@ -177,7 +179,7 @@ public function testNormalize() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -196,6 +198,65 @@ public function testNormalize() ])); } + public function testNormalizeWithSecuredProperty() + { + $dummy = new SecuredDummy(); + $dummy->setTitle('myPublicTitle'); + $dummy->setAdminOnlyProperty('secret'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'adminOnlyProperty'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn(new PropertyMetadata(new Type(Type::BUILTIN_TYPE_STRING), '', true, null, null, null, null, null, null, null, ['security' => 'is_granted(\'ROLE_ADMIN\')'])); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromItem($dummy)->willReturn('/secured_dummies/1'); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->getValue($dummy, 'title')->willReturn('myPublicTitle'); + $propertyAccessorProphecy->getValue($dummy, 'adminOnlyProperty')->willReturn('secret'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(SecuredDummy::class); + + $resourceAccessChecker = $this->prophesize(ResourceAccessCheckerInterface::class); + $resourceAccessChecker->isGranted('adminOnlyProperty', 'is_granted(\'ROLE_ADMIN\')')->willReturn(false); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize('myPublicTitle', null, Argument::type('array'))->willReturn('myPublicTitle'); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + null, + false, + [], + [], + null, + $resourceAccessChecker->reveal(), + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + if (!interface_exists(AdvancedNameConverterInterface::class)) { + $normalizer->setIgnoredAttributes(['alias']); + } + + $expected = [ + 'title' => 'myPublicTitle', + ]; + $this->assertEquals($expected, $normalizer->normalize($dummy, null, [ + 'resources' => [], + ])); + } + public function testNormalizeReadableLinks() { $relatedDummy = new RelatedDummy(); @@ -253,7 +314,7 @@ public function testNormalizeReadableLinks() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -315,7 +376,7 @@ public function testDenormalize() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -365,7 +426,7 @@ public function testCanDenormalizeInputClassWithDifferentFieldsThanResourceClass $serializerProphecy->willImplement(DenormalizerInterface::class); $serializerProphecy->denormalize($data, DummyForAdditionalFieldsInput::class, 'json', $cleanedContext)->willReturn($dummyInputDto); - $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), null, null, null, null, false, [], [$inputDataTransformerProphecy->reveal()], null) extends AbstractItemNormalizer { + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), null, null, null, null, false, [], [$inputDataTransformerProphecy->reveal()], null, null) extends AbstractItemNormalizer { }; $normalizer->setSerializer($serializerProphecy->reveal()); @@ -424,7 +485,7 @@ public function testDenormalizeWritableLinks() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -486,7 +547,7 @@ public function testBadRelationType() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -544,7 +605,7 @@ public function testInnerDocumentNotAllowed() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -589,7 +650,7 @@ public function testBadType() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -631,7 +692,7 @@ public function testTypeChecksCanBeDisabled() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -677,7 +738,7 @@ public function testJsonAllowIntAsFloat() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -752,7 +813,7 @@ public function testDenormalizeBadKeyType() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -794,7 +855,7 @@ public function testNullable() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -854,7 +915,7 @@ public function testChildInheritedProperty(): void [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -906,7 +967,7 @@ public function testDenormalizeRelationWithPlainId() [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -969,7 +1030,7 @@ public function testDenormalizeRelationWithPlainIdNotFound() [], [], null, - true, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -1027,7 +1088,7 @@ public function testDoNotDenormalizeRelationWithPlainIdWhenPlainIdentifiersAreNo [], [], null, - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -1114,7 +1175,7 @@ public function testNormalizationWithDataTransformer() [], [$dataTransformerProphecy->reveal(), $secondDataTransformerProphecy->reveal()], $resourceMetadataFactoryProphecy->reveal(), - false, + null, ]); $normalizer->setSerializer($serializerProphecy->reveal());