Skip to content

Commit 16b9558

Browse files
committed
refactor: Refactor ReadProvider.php and AccessCheckerProvider.php to extract link security into their own providers
1 parent c374794 commit 16b9558

File tree

9 files changed

+284
-136
lines changed

9 files changed

+284
-136
lines changed

src/State/Provider/ReadProvider.php

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,15 @@
1313

1414
namespace ApiPlatform\State\Provider;
1515

16-
use ApiPlatform\Exception\InvalidIdentifierException;
17-
use ApiPlatform\Exception\InvalidUriVariableException;
1816
use ApiPlatform\Metadata\HttpOperation;
19-
use ApiPlatform\Metadata\Link;
2017
use ApiPlatform\Metadata\Operation;
2118
use ApiPlatform\Metadata\Put;
22-
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
2319
use ApiPlatform\Metadata\Util\CloneTrait;
2420
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
2521
use ApiPlatform\State\Exception\ProviderNotFoundException;
2622
use ApiPlatform\State\ProviderInterface;
2723
use ApiPlatform\State\UriVariablesResolverTrait;
24+
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
2825
use ApiPlatform\State\Util\RequestParser;
2926
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
3027

@@ -36,11 +33,11 @@
3633
final class ReadProvider implements ProviderInterface
3734
{
3835
use CloneTrait;
36+
use OperationRequestInitiatorTrait;
3937
use UriVariablesResolverTrait;
4038

4139
public function __construct(
4240
private readonly ProviderInterface $provider,
43-
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
4441
private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null,
4542
) {
4643
}
@@ -90,47 +87,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
9087
throw new NotFoundHttpException('Not Found');
9188
}
9289

93-
if ($operation->getUriVariables()) {
94-
foreach ($operation->getUriVariables() as $key => $uriVariable) {
95-
if (!$uriVariable instanceof Link || !$uriVariable->getSecurity()) {
96-
continue;
97-
}
98-
99-
$relationClass = $uriVariable->getFromClass() ?? $uriVariable->getToClass();
100-
101-
if (!$relationClass) {
102-
continue;
103-
}
104-
105-
$parentOperation = $this->resourceMetadataCollectionFactory
106-
->create($relationClass)
107-
->getOperation($operation->getExtraProperties()['parent_uri_template'] ?? null);
108-
try {
109-
$relation = $this->provider->provide($parentOperation, [$uriVariable->getIdentifiers()[0] => $request?->attributes->all()[$key]], $context);
110-
} catch (ProviderNotFoundException) {
111-
$relation = null;
112-
}
113-
if (!$relation) {
114-
throw new NotFoundHttpException('Not Found');
115-
}
116-
117-
try {
118-
$securityObjectName = $uriVariable->getSecurityObjectName();
119-
120-
if (!$securityObjectName) {
121-
$securityObjectName = $uriVariable->getToProperty() ?? $uriVariable->getFromProperty();
122-
}
123-
124-
if (!$securityObjectName) {
125-
continue;
126-
}
127-
$request?->attributes->set($securityObjectName, $relation);
128-
} catch (InvalidIdentifierException|InvalidUriVariableException $e) {
129-
throw new NotFoundHttpException('Invalid identifier value or configuration.', $e);
130-
}
131-
}
132-
}
133-
13490
$request?->attributes->set('data', $data);
13591
$request?->attributes->set('previous_data', $this->clone($data));
13692

src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public function load(array $configs, ContainerBuilder $container): void
159159
$this->registerSecurityConfiguration($container, $config, $loader);
160160
$this->registerMakerConfiguration($container, $config, $loader);
161161
$this->registerArgumentResolverConfiguration($loader);
162+
$this->registerLinkSecurityConfiguration($loader, $config);
162163

163164
$container->registerForAutoconfiguration(FilterInterface::class)
164165
->addTag('api_platform.filter');
@@ -881,4 +882,11 @@ private function registerInflectorConfiguration(array $config): void
881882
Inflector::keepLegacyInflector(false);
882883
}
883884
}
885+
886+
private function registerLinkSecurityConfiguration(XmlFileLoader $loader, array $config): void
887+
{
888+
if ($config['enable_link_security'] ?? true) {
889+
$loader->load('link_security.xml');
890+
}
891+
}
884892
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
9+
<service id="api_platform.state_provider.read_link" class="ApiPlatform\Symfony\Security\State\LinkedReadProvider" decorates="api_platform.state_provider.read" decoration-priority="499">
10+
<argument type="service" id="api_platform.state_provider.read_link.inner" />
11+
<argument type="service" id="api_platform.state_provider.locator" />
12+
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
13+
</service>
14+
15+
<service id="api_platform.state_provider.access_checker_linked" class="ApiPlatform\Symfony\Security\State\LinkAccessCheckerProvider" decorates="api_platform.state_provider.read">
16+
<argument type="service" id="api_platform.state_provider.access_checker_linked.inner" />
17+
<argument type="service" id="api_platform.security.resource_access_checker" />
18+
</service>
19+
</services>
20+
</container>

src/Symfony/Bundle/Resources/config/state.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
<service id="api_platform.state_provider.read" class="ApiPlatform\State\Provider\ReadProvider" decorates="api_platform.state_provider.main" decoration-priority="500">
2626
<argument type="service" id="api_platform.state_provider.read.inner" />
27-
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
2827
<argument type="service" id="api_platform.serializer.context_builder" />
2928
</service>
3029

src/Symfony/Security/State/AccessCheckerProvider.php

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
1717
use ApiPlatform\Metadata\GraphQl\QueryCollection;
1818
use ApiPlatform\Metadata\HttpOperation;
19-
use ApiPlatform\Metadata\Link;
2019
use ApiPlatform\Metadata\Operation;
2120
use ApiPlatform\State\ProviderInterface;
2221
use ApiPlatform\Symfony\Security\Exception\AccessDeniedException;
@@ -52,6 +51,9 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5251
}
5352

5453
$body = $this->decorated->provide($operation, $uriVariables, $context);
54+
if (null === $isGranted) {
55+
return $body;
56+
}
5557

5658
// On a GraphQl QueryCollection we want to perform security stage only on the top-level query
5759
if ($operation instanceof QueryCollection && null !== ($context['source'] ?? null)) {
@@ -73,31 +75,10 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
7375
];
7476
}
7577

76-
if ($isGranted && !$this->resourceAccessChecker->isGranted($operation->getClass(), $isGranted, $resourceAccessCheckerContext)) {
78+
if (!$this->resourceAccessChecker->isGranted($operation->getClass(), $isGranted, $resourceAccessCheckerContext)) {
7779
$operation instanceof GraphQlOperation ? throw new AccessDeniedHttpException($message ?? 'Access Denied.') : throw new AccessDeniedException($message ?? 'Access Denied.');
7880
}
7981

80-
if ($operation instanceof HttpOperation && $operation->getUriVariables()) {
81-
foreach ($operation->getUriVariables() as $uriVariable) {
82-
if (!$uriVariable instanceof Link || !$uriVariable->getSecurity()) {
83-
continue;
84-
}
85-
86-
$targetResource = $uriVariable->getFromClass() ?? $uriVariable->getToClass();
87-
88-
if (!$targetResource) {
89-
continue;
90-
}
91-
92-
// We need to add all attributes here again because we do not know the name of the security object
93-
$resourceAccessCheckerContext += $request->attributes->all();
94-
95-
if (!$this->resourceAccessChecker->isGranted($targetResource, $uriVariable->getSecurity(), $resourceAccessCheckerContext)) {
96-
throw new AccessDeniedException($uriVariable->getSecurityMessage() ?? 'Access Denied.');
97-
}
98-
}
99-
}
100-
10182
return $body;
10283
}
10384
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\Security\State;
15+
16+
use ApiPlatform\Metadata\HttpOperation;
17+
use ApiPlatform\Metadata\Link;
18+
use ApiPlatform\Metadata\Operation;
19+
use ApiPlatform\State\ProviderInterface;
20+
use ApiPlatform\Symfony\Security\Exception\AccessDeniedException;
21+
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
22+
23+
class LinkAccessCheckerProvider implements ProviderInterface
24+
{
25+
public function __construct(
26+
private readonly ProviderInterface $decorated,
27+
private readonly ResourceAccessCheckerInterface $resourceAccessChecker
28+
) {
29+
}
30+
31+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
32+
{
33+
$request = ($context['request'] ?? null);
34+
35+
$data = $this->decorated->provide($operation, $uriVariables, $context);
36+
37+
if ($operation instanceof HttpOperation && $operation->getUriVariables()) {
38+
foreach ($operation->getUriVariables() as $uriVariable) {
39+
if (!$uriVariable instanceof Link || !$uriVariable->getSecurity()) {
40+
continue;
41+
}
42+
43+
$targetResource = $uriVariable->getFromClass() ?? $uriVariable->getToClass();
44+
45+
if (!$targetResource) {
46+
continue;
47+
}
48+
49+
$propertyName = $uriVariable->getToProperty() ?? $uriVariable->getFromProperty();
50+
$securityObjectName = $uriVariable->getSecurityObjectName();
51+
52+
if (!$securityObjectName) {
53+
$securityObjectName = $propertyName;
54+
}
55+
56+
if (!$securityObjectName) {
57+
continue;
58+
}
59+
60+
$resourceAccessCheckerContext = [
61+
'object' => $data,
62+
'previous_object' => $request?->attributes->get('previous_data'),
63+
$securityObjectName => $request?->attributes->get($securityObjectName),
64+
'request' => $request,
65+
];
66+
67+
if (!$this->resourceAccessChecker->isGranted($targetResource, $uriVariable->getSecurity(), $resourceAccessCheckerContext)) {
68+
throw new AccessDeniedException($uriVariable->getSecurityMessage() ?? 'Access Denied.');
69+
}
70+
}
71+
}
72+
73+
return $data;
74+
}
75+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\Security\State;
15+
16+
use ApiPlatform\Exception\InvalidIdentifierException;
17+
use ApiPlatform\Exception\InvalidUriVariableException;
18+
use ApiPlatform\Metadata\HttpOperation;
19+
use ApiPlatform\Metadata\Link;
20+
use ApiPlatform\Metadata\Operation;
21+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
22+
use ApiPlatform\State\Exception\ProviderNotFoundException;
23+
use ApiPlatform\State\ProviderInterface;
24+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
25+
26+
class LinkedReadProvider implements ProviderInterface
27+
{
28+
public function __construct(
29+
private readonly ProviderInterface $decorated,
30+
private readonly ProviderInterface $locator,
31+
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory
32+
) {
33+
}
34+
35+
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
36+
{
37+
$data = $this->decorated->provide($operation, $uriVariables, $context);
38+
39+
if (!$operation instanceof HttpOperation) {
40+
return $data;
41+
}
42+
43+
$request = ($context['request'] ?? null);
44+
45+
if ($operation->getUriVariables()) {
46+
foreach ($operation->getUriVariables() as $key => $uriVariable) {
47+
if (!$uriVariable instanceof Link || !$uriVariable->getSecurity()) {
48+
continue;
49+
}
50+
51+
$relationClass = $uriVariable->getFromClass() ?? $uriVariable->getToClass();
52+
53+
if (!$relationClass) {
54+
continue;
55+
}
56+
57+
$parentOperation = $this->resourceMetadataCollectionFactory
58+
->create($relationClass)
59+
->getOperation($operation->getExtraProperties()['parent_uri_template'] ?? null);
60+
try {
61+
$relation = $this->locator->provide($parentOperation, [$uriVariable->getIdentifiers()[0] => $request?->attributes->all()[$key]], $context);
62+
} catch (ProviderNotFoundException) {
63+
$relation = null;
64+
}
65+
66+
if (!$relation) {
67+
throw new NotFoundHttpException('Relation for link security not found.');
68+
}
69+
70+
try {
71+
$securityObjectName = $uriVariable->getSecurityObjectName();
72+
73+
if (!$securityObjectName) {
74+
$securityObjectName = $uriVariable->getToProperty() ?? $uriVariable->getFromProperty();
75+
}
76+
77+
$request?->attributes->set($securityObjectName, $relation);
78+
} catch (InvalidIdentifierException|InvalidUriVariableException $e) {
79+
throw new NotFoundHttpException('Invalid identifier value or configuration.', $e);
80+
}
81+
}
82+
}
83+
84+
return $data;
85+
}
86+
}

0 commit comments

Comments
 (0)