Skip to content

Commit 705fd64

Browse files
Fix #1646
1 parent 2c904ef commit 705fd64

27 files changed

+814
-117
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
Feature: Authorization checking
2+
In order to use the API
3+
As a client software user
4+
I need to be authorized to access a given resource using legacy access_control attribute.
5+
6+
@createSchema
7+
Scenario: An anonymous user retrieves a secured resource
8+
When I add "Accept" header equal to "application/ld+json"
9+
And I send a "GET" request to "/legacy_secured_dummies"
10+
Then the response status code should be 401
11+
12+
Scenario: An authenticated user retrieve a secured resource
13+
When I add "Accept" header equal to "application/ld+json"
14+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
15+
And I send a "GET" request to "/legacy_secured_dummies"
16+
Then the response status code should be 200
17+
And the response should be in JSON
18+
19+
Scenario: A standard user cannot create a secured resource
20+
When I add "Accept" header equal to "application/ld+json"
21+
And I add "Content-Type" header equal to "application/ld+json"
22+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
23+
And I send a "POST" request to "/legacy_secured_dummies" with body:
24+
"""
25+
{
26+
"title": "Title",
27+
"description": "Description",
28+
"owner": "foo"
29+
}
30+
"""
31+
Then the response status code should be 403
32+
33+
Scenario: An admin can create a secured resource
34+
When I add "Accept" header equal to "application/ld+json"
35+
And I add "Content-Type" header equal to "application/ld+json"
36+
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
37+
And I send a "POST" request to "/legacy_secured_dummies" with body:
38+
"""
39+
{
40+
"title": "Title",
41+
"description": "Description",
42+
"owner": "someone"
43+
}
44+
"""
45+
Then the response status code should be 201
46+
47+
Scenario: An admin can create another secured resource
48+
When I add "Accept" header equal to "application/ld+json"
49+
And I add "Content-Type" header equal to "application/ld+json"
50+
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
51+
And I send a "POST" request to "/legacy_secured_dummies" with body:
52+
"""
53+
{
54+
"title": "Special Title",
55+
"description": "Description",
56+
"owner": "dunglas"
57+
}
58+
"""
59+
Then the response status code should be 201
60+
61+
Scenario: A user cannot retrieve an item they doesn't own
62+
When I add "Accept" header equal to "application/ld+json"
63+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
64+
And I send a "GET" request to "/legacy_secured_dummies/1"
65+
Then the response status code should be 403
66+
And the response should be in JSON
67+
68+
Scenario: A user can retrieve an item they owns
69+
When I add "Accept" header equal to "application/ld+json"
70+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
71+
And I send a "GET" request to "/legacy_secured_dummies/2"
72+
Then the response status code should be 200
73+
74+
Scenario: A user can't assign to themself an item they doesn't own
75+
When I add "Accept" header equal to "application/ld+json"
76+
And I add "Content-Type" header equal to "application/ld+json"
77+
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
78+
And I send a "PUT" request to "/legacy_secured_dummies/2" with body:
79+
"""
80+
{
81+
"owner": "kitten"
82+
}
83+
"""
84+
Then the response status code should be 403
85+
86+
Scenario: A user can update an item they owns and transfer it
87+
When I add "Accept" header equal to "application/ld+json"
88+
And I add "Content-Type" header equal to "application/ld+json"
89+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
90+
And I send a "PUT" request to "/legacy_secured_dummies/2" with body:
91+
"""
92+
{
93+
"owner": "vincent"
94+
}
95+
"""
96+
Then the response status code should be 200

src/Annotation/ApiResource.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
* @Attributes(
2626
* @Attribute("accessControl", type="string"),
2727
* @Attribute("accessControlMessage", type="string"),
28+
* @Attribute("security", type="string"),
29+
* @Attribute("securityMessage", type="string"),
30+
* @Attribute("securityPostDenormalize", type="string"),
31+
* @Attribute("securityPostDenormalizeMessage", type="string"),
2832
* @Attribute("attributes", type="array"),
2933
* @Attribute("cacheHeaders", type="array"),
3034
* @Attribute("collectionOperations", type="array"),
@@ -108,14 +112,28 @@ final class ApiResource
108112
*
109113
* @var string
110114
*/
111-
private $accessControl;
115+
private $security;
112116

113117
/**
114118
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
115119
*
116120
* @var string
117121
*/
118-
private $accessControlMessage;
122+
private $securityMessage;
123+
124+
/**
125+
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
126+
*
127+
* @var string
128+
*/
129+
private $securityPostDenormalize;
130+
131+
/**
132+
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
133+
*
134+
* @var string
135+
*/
136+
private $securityPostDenormalizeMessage;
119137

120138
/**
121139
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@
1111

1212
<service id="api_platform.graphql.resolver.factory.item" class="ApiPlatform\Core\GraphQl\Resolver\Factory\ItemResolverFactory" public="false">
1313
<argument type="service" id="api_platform.graphql.resolver.stage.read" />
14-
<argument type="service" id="api_platform.graphql.resolver.stage.deny_access" />
14+
<argument type="service" id="api_platform.graphql.resolver.stage.security" />
15+
<argument type="service" id="api_platform.graphql.resolver.stage.security_post_denormalize" />
1516
<argument type="service" id="api_platform.graphql.resolver.stage.serialize" />
1617
<argument type="service" id="api_platform.graphql.query_resolver_locator" />
1718
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
1819
</service>
1920

2021
<service id="api_platform.graphql.resolver.factory.collection" class="ApiPlatform\Core\GraphQl\Resolver\Factory\CollectionResolverFactory" public="false">
2122
<argument type="service" id="api_platform.graphql.resolver.stage.read" />
22-
<argument type="service" id="api_platform.graphql.resolver.stage.deny_access" />
23+
<argument type="service" id="api_platform.graphql.resolver.stage.security" />
24+
<argument type="service" id="api_platform.graphql.resolver.stage.security_post_denormalize" />
2325
<argument type="service" id="api_platform.graphql.resolver.stage.serialize" />
2426
<argument type="service" id="api_platform.graphql.query_resolver_locator" />
2527
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
@@ -28,7 +30,8 @@
2830

2931
<service id="api_platform.graphql.resolver.factory.item_mutation" class="ApiPlatform\Core\GraphQl\Resolver\Factory\ItemMutationResolverFactory" public="false">
3032
<argument type="service" id="api_platform.graphql.resolver.stage.read" />
31-
<argument type="service" id="api_platform.graphql.resolver.stage.deny_access" />
33+
<argument type="service" id="api_platform.graphql.resolver.stage.security" />
34+
<argument type="service" id="api_platform.graphql.resolver.stage.security_post_denormalize" />
3235
<argument type="service" id="api_platform.graphql.resolver.stage.serialize" />
3336
<argument type="service" id="api_platform.graphql.resolver.stage.deserialize" />
3437
<argument type="service" id="api_platform.graphql.resolver.stage.write" />
@@ -47,7 +50,12 @@
4750
<argument type="service" id="api_platform.graphql.serializer.context_builder" />
4851
</service>
4952

50-
<service id="api_platform.graphql.resolver.stage.deny_access" class="ApiPlatform\Core\GraphQl\Resolver\Stage\DenyAccessStage" public="false">
53+
<service id="api_platform.graphql.resolver.stage.security" class="ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityStage" public="false">
54+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
55+
<argument type="service" id="api_platform.security.resource_access_checker" />
56+
</service>
57+
58+
<service id="api_platform.graphql.resolver.stage.security_post_denormalize" class="ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityPostDenormalizeStage" public="false">
5159
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
5260
<argument type="service" id="api_platform.security.resource_access_checker" />
5361
</service>

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
2121
<argument type="service" id="api_platform.security.resource_access_checker" />
2222

23-
<!-- This listener must be executed only when the current object is available -->
24-
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="1" />
23+
<!-- This method must be executed only when the current object is available, before deserialization -->
24+
<tag name="kernel.event_listener" event="kernel.request" method="onSecurity" priority="3" />
25+
<!-- This method must be executed only when the current object is available, after deserialization -->
26+
<tag name="kernel.event_listener" event="kernel.request" method="onSecurityPostDenormalize" priority="1" />
2527
</service>
2628

2729
<service id="api_platform.security.expression_language_provider" class="ApiPlatform\Core\Security\Core\Authorization\ExpressionLanguageProvider" public="false">

src/GraphQl/Resolver/Factory/CollectionResolverFactory.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;
1515

1616
use ApiPlatform\Core\GraphQl\Resolver\QueryCollectionResolverInterface;
17-
use ApiPlatform\Core\GraphQl\Resolver\Stage\DenyAccessStageInterface;
1817
use ApiPlatform\Core\GraphQl\Resolver\Stage\ReadStageInterface;
18+
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
19+
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityStageInterface;
1920
use ApiPlatform\Core\GraphQl\Resolver\Stage\SerializeStageInterface;
2021
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2122
use ApiPlatform\Core\Util\CloneTrait;
@@ -30,22 +31,25 @@
3031
*
3132
* @author Alan Poulain <contact@alanpoulain.eu>
3233
* @author Kévin Dunglas <dunglas@gmail.com>
34+
* @author Vincent Chalamon <vincentchalamon@gmail.com>
3335
*/
3436
final class CollectionResolverFactory implements ResolverFactoryInterface
3537
{
3638
use CloneTrait;
3739

3840
private $readStage;
39-
private $denyAccessStage;
41+
private $securityStage;
42+
private $securityPostDenormalizeStage;
4043
private $serializeStage;
4144
private $queryResolverLocator;
4245
private $requestStack;
4346
private $resourceMetadataFactory;
4447

45-
public function __construct(ReadStageInterface $readStage, DenyAccessStageInterface $denyAccessStage, SerializeStageInterface $serializeStage, ContainerInterface $queryResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory, RequestStack $requestStack = null)
48+
public function __construct(ReadStageInterface $readStage, SecurityStageInterface $securityStage, SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, SerializeStageInterface $serializeStage, ContainerInterface $queryResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory, RequestStack $requestStack = null)
4649
{
4750
$this->readStage = $readStage;
48-
$this->denyAccessStage = $denyAccessStage;
51+
$this->securityStage = $securityStage;
52+
$this->securityPostDenormalizeStage = $securityPostDenormalizeStage;
4953
$this->serializeStage = $serializeStage;
5054
$this->queryResolverLocator = $queryResolverLocator;
5155
$this->requestStack = $requestStack;
@@ -83,7 +87,12 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
8387
$collection = $queryResolver($collection, $resolverContext);
8488
}
8589

86-
($this->denyAccessStage)($resourceClass, $operationName, $resolverContext + [
90+
($this->securityStage)($resourceClass, $operationName, $resolverContext + [
91+
'extra_variables' => [
92+
'object' => $collection,
93+
],
94+
]);
95+
($this->securityPostDenormalizeStage)($resourceClass, $operationName, $resolverContext + [
8796
'extra_variables' => [
8897
'object' => $collection,
8998
'previous_object' => $this->clone($collection),

src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;
1515

1616
use ApiPlatform\Core\GraphQl\Resolver\MutationResolverInterface;
17-
use ApiPlatform\Core\GraphQl\Resolver\Stage\DenyAccessStageInterface;
1817
use ApiPlatform\Core\GraphQl\Resolver\Stage\DeserializeStageInterface;
1918
use ApiPlatform\Core\GraphQl\Resolver\Stage\ReadStageInterface;
19+
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
20+
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityStageInterface;
2021
use ApiPlatform\Core\GraphQl\Resolver\Stage\SerializeStageInterface;
2122
use ApiPlatform\Core\GraphQl\Resolver\Stage\ValidateStageInterface;
2223
use ApiPlatform\Core\GraphQl\Resolver\Stage\WriteStageInterface;
@@ -33,25 +34,28 @@
3334
* @experimental
3435
*
3536
* @author Alan Poulain <contact@alanpoulain.eu>
37+
* @author Vincent Chalamon <vincentchalamon@gmail.com>
3638
*/
3739
final class ItemMutationResolverFactory implements ResolverFactoryInterface
3840
{
3941
use ClassInfoTrait;
4042
use CloneTrait;
4143

4244
private $readStage;
43-
private $denyAccessStage;
45+
private $securityStage;
46+
private $securityPostDenormalizeStage;
4447
private $serializeStage;
4548
private $deserializeStage;
4649
private $writeStage;
4750
private $validateStage;
4851
private $mutationResolverLocator;
4952
private $resourceMetadataFactory;
5053

51-
public function __construct(ReadStageInterface $readStage, DenyAccessStageInterface $denyAccessStage, SerializeStageInterface $serializeStage, DeserializeStageInterface $deserializeStage, WriteStageInterface $writeStage, ValidateStageInterface $validateStage, ContainerInterface $mutationResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory)
54+
public function __construct(ReadStageInterface $readStage, SecurityStageInterface $securityStage, SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, SerializeStageInterface $serializeStage, DeserializeStageInterface $deserializeStage, WriteStageInterface $writeStage, ValidateStageInterface $validateStage, ContainerInterface $mutationResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory)
5255
{
5356
$this->readStage = $readStage;
54-
$this->denyAccessStage = $denyAccessStage;
57+
$this->securityStage = $securityStage;
58+
$this->securityPostDenormalizeStage = $securityPostDenormalizeStage;
5559
$this->serializeStage = $serializeStage;
5660
$this->deserializeStage = $deserializeStage;
5761
$this->writeStage = $writeStage;
@@ -73,16 +77,20 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
7377
if (null !== $item && !\is_object($item)) {
7478
throw new \LogicException('Item from read stage should be a nullable object.');
7579
}
80+
($this->securityStage)($resourceClass, $operationName, $resolverContext + [
81+
'extra_variables' => [
82+
'object' => $item,
83+
],
84+
]);
7685
$previousItem = $this->clone($item);
7786

7887
if ('delete' === $operationName) {
79-
($this->denyAccessStage)($resourceClass, $operationName, $resolverContext + [
88+
($this->securityPostDenormalizeStage)($resourceClass, $operationName, $resolverContext + [
8089
'extra_variables' => [
8190
'object' => $item,
8291
'previous_object' => $previousItem,
8392
],
8493
]);
85-
8694
$item = ($this->writeStage)($item, $resourceClass, $operationName, $resolverContext);
8795

8896
return ($this->serializeStage)($item, $resourceClass, $operationName, $resolverContext);
@@ -102,7 +110,7 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
102110
}
103111
}
104112

105-
($this->denyAccessStage)($resourceClass, $operationName, $resolverContext + [
113+
($this->securityPostDenormalizeStage)($resourceClass, $operationName, $resolverContext + [
106114
'extra_variables' => [
107115
'object' => $item,
108116
'previous_object' => $previousItem,

src/GraphQl/Resolver/Factory/ItemResolverFactory.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;
1515

1616
use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface;
17-
use ApiPlatform\Core\GraphQl\Resolver\Stage\DenyAccessStageInterface;
1817
use ApiPlatform\Core\GraphQl\Resolver\Stage\ReadStageInterface;
18+
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
19+
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityStageInterface;
1920
use ApiPlatform\Core\GraphQl\Resolver\Stage\SerializeStageInterface;
2021
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2122
use ApiPlatform\Core\Util\ClassInfoTrait;
@@ -31,22 +32,25 @@
3132
*
3233
* @author Alan Poulain <contact@alanpoulain.eu>
3334
* @author Kévin Dunglas <dunglas@gmail.com>
35+
* @author Vincent Chalamon <vincentchalamon@gmail.com>
3436
*/
3537
final class ItemResolverFactory implements ResolverFactoryInterface
3638
{
3739
use CloneTrait;
3840
use ClassInfoTrait;
3941

4042
private $readStage;
41-
private $denyAccessStage;
43+
private $securityStage;
44+
private $securityPostDenormalizeStage;
4245
private $serializeStage;
4346
private $queryResolverLocator;
4447
private $resourceMetadataFactory;
4548

46-
public function __construct(ReadStageInterface $readStage, DenyAccessStageInterface $denyAccessStage, SerializeStageInterface $serializeStage, ContainerInterface $queryResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory)
49+
public function __construct(ReadStageInterface $readStage, SecurityStageInterface $securityStage, SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, SerializeStageInterface $serializeStage, ContainerInterface $queryResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory)
4750
{
4851
$this->readStage = $readStage;
49-
$this->denyAccessStage = $denyAccessStage;
52+
$this->securityStage = $securityStage;
53+
$this->securityPostDenormalizeStage = $securityPostDenormalizeStage;
5054
$this->serializeStage = $serializeStage;
5155
$this->queryResolverLocator = $queryResolverLocator;
5256
$this->resourceMetadataFactory = $resourceMetadataFactory;
@@ -79,7 +83,12 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
7983
$resourceClass = $this->getResourceClass($item, $resourceClass, $info, sprintf('Custom query resolver "%s"', $queryResolverId).' has to return an item of class %s but returned an item of class %s.');
8084
}
8185

82-
($this->denyAccessStage)($resourceClass, $operationName, $resolverContext + [
86+
($this->securityStage)($resourceClass, $operationName, $resolverContext + [
87+
'extra_variables' => [
88+
'object' => $item,
89+
],
90+
]);
91+
($this->securityPostDenormalizeStage)($resourceClass, $operationName, $resolverContext + [
8392
'extra_variables' => [
8493
'object' => $item,
8594
'previous_object' => $this->clone($item),

0 commit comments

Comments
 (0)