Skip to content

Commit 6e3a259

Browse files
Implement ApiProperty security attribute (#3503)
* Implement ApiProperty security attribute * add tests * fix test Co-authored-by: Frédéric Barthelet <[email protected]>
1 parent fe72105 commit 6e3a259

File tree

15 files changed

+220
-35
lines changed

15 files changed

+220
-35
lines changed

features/authorization/deny.feature

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ Feature: Authorization checking
5959
{
6060
"title": "Special Title",
6161
"description": "Description",
62-
"owner": "dunglas"
62+
"owner": "dunglas",
63+
"adminOnlyProperty": "secret"
6364
}
6465
"""
6566
Then the response status code should be 201
@@ -100,3 +101,53 @@ Feature: Authorization checking
100101
}
101102
"""
102103
Then the response status code should be 200
104+
105+
Scenario: An admin retrieves a resource with an admin only viewable property
106+
When I add "Accept" header equal to "application/ld+json"
107+
And I add "Content-Type" header equal to "application/ld+json"
108+
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
109+
And I send a "GET" request to "/secured_dummies"
110+
Then the response status code should be 200
111+
And the response should contain "adminOnlyProperty"
112+
113+
Scenario: A user retrieves a resource with an admin only viewable property
114+
When I add "Accept" header equal to "application/ld+json"
115+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
116+
And I send a "GET" request to "/secured_dummies"
117+
Then the response status code should be 200
118+
And the response should not contain "adminOnlyProperty"
119+
120+
Scenario: An admin can create a secured resource with a secured Property
121+
When I add "Accept" header equal to "application/ld+json"
122+
And I add "Content-Type" header equal to "application/ld+json"
123+
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
124+
And I send a "POST" request to "/secured_dummies" with body:
125+
"""
126+
{
127+
"title": "Common Title",
128+
"description": "Description",
129+
"owner": "dunglas",
130+
"adminOnlyProperty": "Is it safe?"
131+
}
132+
"""
133+
Then the response status code should be 201
134+
And the response should contain "adminOnlyProperty"
135+
And the JSON node "adminOnlyProperty" should be equal to the string "Is it safe?"
136+
137+
Scenario: A user cannot update a secured property
138+
When I add "Accept" header equal to "application/ld+json"
139+
And I add "Content-Type" header equal to "application/ld+json"
140+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
141+
And I send a "PUT" request to "/secured_dummies/3" with body:
142+
"""
143+
{
144+
"adminOnlyProperty": "Yes it is!"
145+
}
146+
"""
147+
Then the response status code should be 200
148+
And the response should not contain "adminOnlyProperty"
149+
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
150+
And I send a "GET" request to "/secured_dummies"
151+
Then the response status code should be 200
152+
And the response should contain "adminOnlyProperty"
153+
And the JSON node "hydra:member[2].adminOnlyProperty" should be equal to the string "Is it safe?"

features/graphql/authorization.feature

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Feature: Authorization checking
9191
When I send the following GraphQL request:
9292
"""
9393
mutation {
94-
createSecuredDummy(input: {owner: "me", title: "Hi", description: "Desc", clientMutationId: "auth"}) {
94+
createSecuredDummy(input: {owner: "me", title: "Hi", description: "Desc", adminOnlyProperty: "secret", clientMutationId: "auth"}) {
9595
securedDummy {
9696
title
9797
owner
@@ -112,7 +112,7 @@ Feature: Authorization checking
112112
And I send the following GraphQL request:
113113
"""
114114
mutation {
115-
createSecuredDummy(input: {owner: "someone", title: "Hi", description: "Desc"}) {
115+
createSecuredDummy(input: {owner: "someone", title: "Hi", description: "Desc", adminOnlyProperty: "secret"}) {
116116
securedDummy {
117117
id
118118
title
@@ -131,7 +131,7 @@ Feature: Authorization checking
131131
And I send the following GraphQL request:
132132
"""
133133
mutation {
134-
createSecuredDummy(input: {owner: "dunglas", title: "Hi", description: "Desc"}) {
134+
createSecuredDummy(input: {owner: "dunglas", title: "Hi", description: "Desc", adminOnlyProperty: "secret"}) {
135135
securedDummy {
136136
id
137137
title

src/Annotation/ApiProperty.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* @Attribute("openapiContext", type="array"),
3030
* @Attribute("jsonldContext", type="array"),
3131
* @Attribute("push", type="bool"),
32+
* @Attribute("security", type="string"),
3233
* @Attribute("swaggerContext", type="array")
3334
* )
3435
*/
@@ -128,6 +129,13 @@ final class ApiProperty
128129
*/
129130
private $push;
130131

132+
/**
133+
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
134+
*
135+
* @var string
136+
*/
137+
private $security;
138+
131139
/**
132140
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
133141
*

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
<argument>null</argument>
116116
<argument type="tagged" tag="api_platform.data_transformer" on-invalid="ignore" />
117117
<argument type="service" id="api_platform.metadata.resource.metadata_factory" on-invalid="ignore" />
118-
<argument>false</argument>
118+
<argument type="service" id="api_platform.security.resource_access_checker" />
119119

120120
<!-- Run before serializer.normalizer.json_serializable -->
121121
<tag name="serializer.normalizer" priority="-895" />

src/Bridge/Symfony/Bundle/Resources/config/hal.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<argument type="collection" />
4141
<argument type="tagged" tag="api_platform.data_transformer" on-invalid="ignore" />
4242
<argument type="service" id="api_platform.metadata.resource.metadata_factory" on-invalid="ignore" />
43-
<argument>false</argument>
43+
<argument type="service" id="api_platform.security.resource_access_checker" />
4444

4545
<!-- Run before serializer.normalizer.json_serializable -->
4646
<tag name="serializer.normalizer" priority="-890" />

src/Bridge/Symfony/Bundle/Resources/config/jsonapi.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
4242
<argument type="collection" />
4343
<argument type="tagged" tag="api_platform.data_transformer" on-invalid="ignore" />
44-
<argument>false</argument>
44+
<argument type="service" id="api_platform.security.resource_access_checker" />
4545

4646
<!-- Run before serializer.normalizer.json_serializable -->
4747
<tag name="serializer.normalizer" priority="-890" />

src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<argument type="service" id="serializer.mapping.class_metadata_factory" on-invalid="ignore" />
2828
<argument type="collection" />
2929
<argument type="tagged" tag="api_platform.data_transformer" on-invalid="ignore" />
30-
<argument>false</argument>
30+
<argument type="service" id="api_platform.security.resource_access_checker" />
3131

3232
<!-- Run before serializer.normalizer.json_serializable -->
3333
<tag name="serializer.normalizer" priority="-890" />

src/JsonApi/Serializer/ItemNormalizer.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2121
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
2222
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
23+
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
2324
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
2425
use ApiPlatform\Core\Serializer\CacheKeyTrait;
2526
use ApiPlatform\Core\Serializer\ContextTrait;
@@ -49,9 +50,9 @@ final class ItemNormalizer extends AbstractItemNormalizer
4950

5051
private $componentsCache = [];
5152

52-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [])
53+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor, ?NameConverterInterface $nameConverter, ResourceMetadataFactoryInterface $resourceMetadataFactory, array $defaultContext = [], iterable $dataTransformers = [], ResourceAccessCheckerInterface $resourceAccessChecker = null)
5354
{
54-
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory);
55+
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, null, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker);
5556
}
5657

5758
/**

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2020
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2121
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
22+
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
2223
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
2324
use ApiPlatform\Core\Serializer\ContextTrait;
2425
use ApiPlatform\Core\Util\ClassInfoTrait;
@@ -43,9 +44,9 @@ final class ItemNormalizer extends AbstractItemNormalizer
4344

4445
private $contextBuilder;
4546

46-
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 = [])
47+
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)
4748
{
48-
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory);
49+
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, null, false, $defaultContext, $dataTransformers, $resourceMetadataFactory, $resourceAccessChecker);
4950

5051
$this->contextBuilder = $contextBuilder;
5152
}

src/Serializer/AbstractItemNormalizer.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2525
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
2626
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
27+
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
2728
use ApiPlatform\Core\Util\ClassInfoTrait;
2829
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
2930
use Symfony\Component\PropertyAccess\PropertyAccess;
@@ -55,13 +56,14 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
5556
protected $propertyMetadataFactory;
5657
protected $iriConverter;
5758
protected $resourceClassResolver;
59+
protected $resourceAccessChecker;
5860
protected $propertyAccessor;
5961
protected $itemDataProvider;
6062
protected $allowPlainIdentifiers;
6163
protected $dataTransformers = [];
6264
protected $localCache = [];
6365

64-
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)
66+
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)
6567
{
6668
if (!isset($defaultContext['circular_reference_handler'])) {
6769
$defaultContext['circular_reference_handler'] = function ($object) {
@@ -83,6 +85,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
8385
$this->allowPlainIdentifiers = $allowPlainIdentifiers;
8486
$this->dataTransformers = $dataTransformers;
8587
$this->resourceMetadataFactory = $resourceMetadataFactory;
88+
$this->resourceAccessChecker = $resourceAccessChecker;
8689
}
8790

8891
/**
@@ -349,6 +352,25 @@ protected function getAllowedAttributes($classOrObject, array $context, $attribu
349352
return $allowedAttributes;
350353
}
351354

355+
/**
356+
* {@inheritdoc}
357+
*/
358+
protected function isAllowedAttribute($classOrObject, $attribute, $format = null, array $context = [])
359+
{
360+
if (!parent::isAllowedAttribute($classOrObject, $attribute, $format, $context)) {
361+
return false;
362+
}
363+
364+
$options = $this->getFactoryOptions($context);
365+
$propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options);
366+
$security = $propertyMetadata->getAttribute('security');
367+
if ($this->resourceAccessChecker && $security) {
368+
return $this->resourceAccessChecker->isGranted($attribute, $security);
369+
}
370+
371+
return true;
372+
}
373+
352374
/**
353375
* {@inheritdoc}
354376
*/

0 commit comments

Comments
 (0)