Skip to content

Commit 337ffdf

Browse files
authored
GraphQL: honor access control rules (#1602)
1 parent 414f297 commit 337ffdf

30 files changed

+534
-123
lines changed

features/authorization/deny.feature

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
Feature: Authorization checking
22
In order to use the API
3-
As a client software developer
3+
As a client software user
44
I need to be authorized to access a given resource.
55

66
@createSchema
7-
Scenario: An anonymous user retrieve a secured resource
7+
Scenario: An anonymous user retrieves a secured resource
88
When I add "Accept" header equal to "application/ld+json"
99
And I send a "GET" request to "/secured_dummies"
1010
Then the response status code should be 401
@@ -16,7 +16,6 @@ Feature: Authorization checking
1616
Then the response status code should be 200
1717
And the response should be in JSON
1818

19-
2019
Scenario: A standard user cannot create a secured resource
2120
When I add "Accept" header equal to "application/ld+json"
2221
And I add "Content-Type" header equal to "application/ld+json"
@@ -59,7 +58,7 @@ Feature: Authorization checking
5958
"""
6059
Then the response status code should be 201
6160

62-
Scenario: An user retrieve cannot retrieve an item he doesn't own
61+
Scenario: An user cannot retrieve an item he doesn't own
6362
When I add "Accept" header equal to "application/ld+json"
6463
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
6564
And I send a "GET" request to "/secured_dummies/1"

features/bootstrap/FeatureContext.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedDummy;
4141
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelatedToDummyFriend;
4242
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\RelationEmbedder;
43+
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\SecuredDummy;
4344
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\ThirdLevel;
4445
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\User;
4546
use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\UuidIdentifierDummy;
@@ -555,6 +556,23 @@ public function thereIsDummyObjectsWithRelationEmbeddedDummyBoolean(int $nb, str
555556
$this->manager->flush();
556557
}
557558

559+
/**
560+
* @Given there are :nb SecuredDummy objects
561+
*/
562+
public function thereAreSecuredDummyObjects(int $nb)
563+
{
564+
for ($i = 1; $i <= $nb; ++$i) {
565+
$securedDummy = new SecuredDummy();
566+
$securedDummy->setTitle("#$i");
567+
$securedDummy->setDescription("Hello #$i");
568+
$securedDummy->setOwner('notexist');
569+
570+
$this->manager->persist($securedDummy);
571+
}
572+
573+
$this->manager->flush();
574+
}
575+
558576
/**
559577
* @Given there is a RelationEmbedder object
560578
*/
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Feature: Authorization checking
2+
In order to use the GraphQL API
3+
As a client software user
4+
I need to be authorized to access a given resource.
5+
6+
@createSchema
7+
Scenario: An anonymous user tries to retrieve a secured item
8+
Given there are 1 SecuredDummy objects
9+
When I send the following GraphQL request:
10+
"""
11+
{
12+
securedDummy(id: "/secured_dummies/1") {
13+
title
14+
description
15+
}
16+
}
17+
"""
18+
Then the response status code should be 400
19+
And the response should be in JSON
20+
And the header "Content-Type" should be equal to "application/json"
21+
And the JSON node "errors[0].message" should be equal to "Access Denied."
22+
23+
Scenario: An anonymous user tries to retrieve a secured collection
24+
Given there are 1 SecuredDummy objects
25+
When I send the following GraphQL request:
26+
"""
27+
{
28+
securedDummies {
29+
edges {
30+
node {
31+
title
32+
description
33+
}
34+
}
35+
}
36+
}
37+
"""
38+
Then the response status code should be 400
39+
And the response should be in JSON
40+
And the header "Content-Type" should be equal to "application/json"
41+
And the JSON node "errors[0].message" should be equal to "Access Denied."
42+
43+
@dropSchema
44+
Scenario: An anonymous user tries to create a resource he is not allowed to
45+
When I send the following GraphQL request:
46+
"""
47+
mutation {
48+
createSecuredDummy(input: {owner: "me", title: "Hi", description: "Desc", clientMutationId: "auth"}) {
49+
title
50+
owner
51+
}
52+
}
53+
"""
54+
Then the response status code should be 400
55+
And the response should be in JSON
56+
And the header "Content-Type" should be equal to "application/json"
57+
And the JSON node "errors[0].message" should be equal to "Access Denied."

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,41 @@
55
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
66

77
<services>
8-
<service id="api_platform.graphql.executor" class="ApiPlatform\Core\Graphql\Executor" public="false" />
8+
<service id="api_platform.graphql.executor" class="ApiPlatform\Core\GraphQl\Executor" public="false" />
99

1010
<!-- Resolvers -->
1111

12-
<service id="api_platform.graphql.resolver.factory.collection" class="ApiPlatform\Core\Graphql\Resolver\Factory\CollectionResolverFactory" public="false">
12+
<service id="api_platform.graphql.resolver.factory.collection" class="ApiPlatform\Core\GraphQl\Resolver\Factory\CollectionResolverFactory" public="false">
1313
<argument type="service" id="api_platform.collection_data_provider" />
1414
<argument type="service" id="api_platform.subresource_data_provider" />
1515
<argument type="service" id="serializer" />
1616
<argument type="service" id="api_platform.identifiers_extractor.cached" />
1717
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
18+
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="null" />
1819
<argument type="service" id="request_stack" />
1920
<argument>%api_platform.collection.pagination.enabled%</argument>
2021
</service>
2122

22-
<service id="api_platform.graphql.resolver.factory.item_mutation" class="ApiPlatform\Core\Graphql\Resolver\Factory\ItemMutationResolverFactory" public="false">
23+
<service id="api_platform.graphql.resolver.factory.item_mutation" class="ApiPlatform\Core\GraphQl\Resolver\Factory\ItemMutationResolverFactory" public="false">
2324
<argument type="service" id="api_platform.iri_converter" />
2425
<argument type="service" id="api_platform.data_persister" />
2526
<argument type="service" id="serializer" />
2627
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
28+
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
2729
</service>
2830

29-
<service id="api_platform.graphql.resolver.item" class="ApiPlatform\Core\Graphql\Resolver\ItemResolver" public="false">
31+
<service id="api_platform.graphql.resolver.item" class="ApiPlatform\Core\GraphQl\Resolver\ItemResolver" public="false">
3032
<argument type="service" id="api_platform.iri_converter" />
3133
<argument type="service" id="serializer" />
3234
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
35+
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
3336
</service>
3437

35-
<service id="api_platform.graphql.resolver.resource_field" class="ApiPlatform\Core\Graphql\Resolver\ResourceFieldResolver" public="false">
38+
<service id="api_platform.graphql.resolver.resource_field" class="ApiPlatform\Core\GraphQl\Resolver\ResourceFieldResolver" public="false">
3639
<argument type="service" id="api_platform.iri_converter" />
3740
</service>
3841

39-
<service id="api_platform.graphql.schema_builder" class="ApiPlatform\Core\Graphql\Type\SchemaBuilder" public="false">
42+
<service id="api_platform.graphql.schema_builder" class="ApiPlatform\Core\GraphQl\Type\SchemaBuilder" public="false">
4043
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
4144
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
4245
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" />
@@ -50,7 +53,7 @@
5053

5154
<!-- Action -->
5255

53-
<service id="api_platform.graphql.action.entrypoint" class="ApiPlatform\Core\Graphql\Action\EntrypointAction" public="true">
56+
<service id="api_platform.graphql.action.entrypoint" class="ApiPlatform\Core\GraphQl\Action\EntrypointAction" public="true">
5457
<argument type="service" id="api_platform.graphql.schema_builder" />
5558
<argument type="service" id="api_platform.graphql.executor" />
5659
<argument type="service" id="twig" />
@@ -61,7 +64,7 @@
6164

6265
<!-- Serializer -->
6366

64-
<service id="api_platform.graphql.normalizer.item" class="ApiPlatform\Core\Graphql\Serializer\ItemNormalizer" public="false">
67+
<service id="api_platform.graphql.normalizer.item" class="ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer" public="false">
6568
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
6669
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
6770
<argument type="service" id="api_platform.iri_converter" />

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77
<services>
88
<service id="api_platform.security.expression_language" class="ApiPlatform\Core\Security\ExpressionLanguage" public="false" />
99

10-
<!-- This listener must be executed only when the current object is available -->
11-
<service id="api_platform.security.listener.request.deny_access" class="ApiPlatform\Core\Security\EventListener\DenyAccessListener">
12-
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
10+
<service id="api_platform.security.resource_access_checker" class="ApiPlatform\Core\Security\ResourceAccessChecker" public="false">
1311
<argument type="service" id="api_platform.security.expression_language" on-invalid="null" />
1412
<argument type="service" id="security.authentication.trust_resolver" on-invalid="null" />
1513
<argument type="service" id="security.role_hierarchy" on-invalid="null" />
1614
<argument type="service" id="security.token_storage" on-invalid="null" />
1715
<argument type="service" id="security.authorization_checker" on-invalid="null" />
16+
</service>
17+
<service id="ApiPlatform\Core\Security\ResourceAccessCheckerInterface" alias="api_platform.security.resource_access_checker" />
18+
19+
<service id="api_platform.security.listener.request.deny_access" class="ApiPlatform\Core\Security\EventListener\DenyAccessListener">
20+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
21+
<argument type="service" id="api_platform.security.resource_access_checker" />
1822

23+
<!-- This listener must be executed only when the current object is available -->
1924
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="1" />
2025
</service>
2126
</services>

src/Graphql/Action/EntrypointAction.php renamed to src/GraphQl/Action/EntrypointAction.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Core\Graphql\Action;
14+
namespace ApiPlatform\Core\GraphQl\Action;
1515

16-
use ApiPlatform\Core\Graphql\ExecutorInterface;
17-
use ApiPlatform\Core\Graphql\Type\SchemaBuilderInterface;
16+
use ApiPlatform\Core\GraphQl\ExecutorInterface;
17+
use ApiPlatform\Core\GraphQl\Type\SchemaBuilderInterface;
1818
use GraphQL\Error\Error;
1919
use GraphQL\Executor\ExecutionResult;
2020
use Symfony\Component\HttpFoundation\JsonResponse;

src/Graphql/Executor.php renamed to src/GraphQl/Executor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Core\Graphql;
14+
namespace ApiPlatform\Core\GraphQl;
1515

1616
use GraphQL\Executor\ExecutionResult;
1717
use GraphQL\GraphQL;

src/Graphql/ExecutorInterface.php renamed to src/GraphQl/ExecutorInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Core\Graphql;
14+
namespace ApiPlatform\Core\GraphQl;
1515

1616
use GraphQL\Executor\ExecutionResult;
1717
use GraphQL\Type\Schema;

src/Graphql/Resolver/Factory/CollectionResolverFactory.php renamed to src/GraphQl/Resolver/Factory/CollectionResolverFactory.php

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

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Core\Graphql\Resolver\Factory;
14+
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;
1515

1616
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
1717
use ApiPlatform\Core\DataProvider\CollectionDataProviderInterface;
1818
use ApiPlatform\Core\DataProvider\PaginatorInterface;
1919
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
2020
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
21-
use ApiPlatform\Core\Graphql\Serializer\ItemNormalizer;
21+
use ApiPlatform\Core\GraphQl\Resolver\ResourceAccessCheckerTrait;
22+
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
2223
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
24+
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
2325
use GraphQL\Error\Error;
2426
use GraphQL\Type\Definition\ResolveInfo;
2527
use Symfony\Component\HttpFoundation\RequestStack;
@@ -35,20 +37,24 @@
3537
*/
3638
final class CollectionResolverFactory implements ResolverFactoryInterface
3739
{
40+
use ResourceAccessCheckerTrait;
41+
3842
private $collectionDataProvider;
3943
private $subresourceDataProvider;
4044
private $normalizer;
4145
private $identifiersExtractor;
46+
private $resourceAccessChecker;
4247
private $requestStack;
4348
private $paginationEnabled;
4449
private $resourceMetadataFactory;
4550

46-
public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, IdentifiersExtractorInterface $identifiersExtractor, ResourceMetadataFactoryInterface $resourceMetadataFactory, RequestStack $requestStack = null, bool $paginationEnabled = false)
51+
public function __construct(CollectionDataProviderInterface $collectionDataProvider, SubresourceDataProviderInterface $subresourceDataProvider, NormalizerInterface $normalizer, IdentifiersExtractorInterface $identifiersExtractor, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null, RequestStack $requestStack = null, bool $paginationEnabled = false)
4752
{
4853
$this->subresourceDataProvider = $subresourceDataProvider;
4954
$this->collectionDataProvider = $collectionDataProvider;
5055
$this->normalizer = $normalizer;
5156
$this->identifiersExtractor = $identifiersExtractor;
57+
$this->resourceAccessChecker = $resourceAccessChecker;
5258
$this->requestStack = $requestStack;
5359
$this->paginationEnabled = $paginationEnabled;
5460
$this->resourceMetadataFactory = $resourceMetadataFactory;
@@ -72,9 +78,17 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
7278
$collection = $this->collectionDataProvider->getCollection($resourceClass);
7379
}
7480

75-
$normalizationContext = $this->resourceMetadataFactory
76-
->create($resourceClass)
77-
->getGraphqlAttribute('query', 'normalization_context', [], true);
81+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
82+
$this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $collection);
83+
84+
if (null !== $this->resourceAccessChecker) {
85+
$isGranted = $resourceMetadata->getGraphqlAttribute('query', 'access_control', null, true);
86+
if (null !== $isGranted && !$this->resourceAccessChecker->isGranted($resourceClass, $isGranted, ['object' => $collection])) {
87+
throw Error::createLocatedError('Access Denied.', $info->fieldNodes, $info->path);
88+
}
89+
}
90+
91+
$normalizationContext = $resourceMetadata->getGraphqlAttribute('query', 'normalization_context', [], true);
7892
$normalizationContext['attributes'] = $this->fieldsToAttributes($info);
7993

8094
if (!$this->paginationEnabled) {

src/Graphql/Resolver/Factory/ItemMutationResolverFactory.php renamed to src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Core\Graphql\Resolver\Factory;
14+
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;
1515

1616
use ApiPlatform\Core\Api\IriConverterInterface;
1717
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
1818
use ApiPlatform\Core\Exception\InvalidArgumentException;
1919
use ApiPlatform\Core\Exception\ItemNotFoundException;
20-
use ApiPlatform\Core\Graphql\Serializer\ItemNormalizer;
20+
use ApiPlatform\Core\GraphQl\Resolver\ResourceAccessCheckerTrait;
21+
use ApiPlatform\Core\GraphQl\Serializer\ItemNormalizer;
2122
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
23+
use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
2224
use GraphQL\Error\Error;
2325
use GraphQL\Type\Definition\ResolveInfo;
2426
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
@@ -33,12 +35,15 @@
3335
*/
3436
final class ItemMutationResolverFactory implements ResolverFactoryInterface
3537
{
38+
use ResourceAccessCheckerTrait;
39+
3640
private $iriConverter;
3741
private $dataPersister;
3842
private $normalizer;
3943
private $resourceMetadataFactory;
44+
private $resourceAccessChecker;
4045

41-
public function __construct(IriConverterInterface $iriConverter, DataPersisterInterface $dataPersister, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory)
46+
public function __construct(IriConverterInterface $iriConverter, DataPersisterInterface $dataPersister, NormalizerInterface $normalizer, ResourceMetadataFactoryInterface $resourceMetadataFactory, ResourceAccessCheckerInterface $resourceAccessChecker = null)
4247
{
4348
if (!$normalizer instanceof DenormalizerInterface) {
4449
throw new InvalidArgumentException(sprintf('The normalizer must implements the "%s" interface', DenormalizerInterface::class));
@@ -48,6 +53,7 @@ public function __construct(IriConverterInterface $iriConverter, DataPersisterIn
4853
$this->dataPersister = $dataPersister;
4954
$this->normalizer = $normalizer;
5055
$this->resourceMetadataFactory = $resourceMetadataFactory;
56+
$this->resourceAccessChecker = $resourceAccessChecker;
5157
}
5258

5359
public function __invoke(string $resourceClass = null, string $rootClass = null, string $operationName = null): callable
@@ -65,6 +71,7 @@ public function __invoke(string $resourceClass = null, string $rootClass = null,
6571
}
6672

6773
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
74+
$this->canAccess($this->resourceAccessChecker, $resourceMetadata, $resourceClass, $info, $item);
6875

6976
switch ($operationName) {
7077
case 'create':

src/Graphql/Resolver/Factory/ResolverFactoryInterface.php renamed to src/GraphQl/Resolver/Factory/ResolverFactoryInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
declare(strict_types=1);
1313

14-
namespace ApiPlatform\Core\Graphql\Resolver\Factory;
14+
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;
1515

1616
/**
1717
* Builds a GraphQL resolver.

0 commit comments

Comments
 (0)