Skip to content

Commit 5489a17

Browse files
committed
refactor(state): merge parameter and link security
1 parent e6e7760 commit 5489a17

File tree

15 files changed

+270
-207
lines changed

15 files changed

+270
-207
lines changed

src/Metadata/HttpOperation.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class HttpOperation extends Operation
3434
* @param array<int|string, string|string[]>|string|null $formats {@see https://api-platform.com/docs/core/content-negotiation/#configuring-formats-for-a-specific-resource-or-operation}
3535
* @param array<int|string, string|string[]>|string|null $inputFormats {@see https://api-platform.com/docs/core/content-negotiation/#configuring-formats-for-a-specific-resource-or-operation}
3636
* @param array<int|string, string|string[]>|string|null $outputFormats {@see https://api-platform.com/docs/core/content-negotiation/#configuring-formats-for-a-specific-resource-or-operation}
37-
* @param array<string,array{
37+
* @param Parameters|array<string,array{
3838
* 0: string,
3939
* 1: string
4040
* }|array{
@@ -344,11 +344,17 @@ public function withOutputFormats($outputFormats = null): static
344344
return $self;
345345
}
346346

347-
public function getUriVariables()
347+
/**
348+
* @return Parameters|array<string, mixed>
349+
*/
350+
public function getUriVariables(): mixed
348351
{
349352
return $this->uriVariables;
350353
}
351354

355+
/**
356+
* @param Parameters|array<string, mixed> $uriVariables
357+
*/
352358
public function withUriVariables($uriVariables): static
353359
{
354360
$self = clone $this;

src/Metadata/Link.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use ApiPlatform\OpenApi;
1717

1818
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
19-
final class Link extends Parameter
19+
final class Link extends Parameter implements UriVariableParameterInterface
2020
{
2121
public function __construct(
2222
private ?string $parameterName = null,

src/Metadata/Parameter.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ public function getValue(mixed $default = new ParameterNotFound()): mixed
133133
return $this->extraProperties['_api_values'] ?? $default;
134134
}
135135

136+
/**
137+
* Only use this in a parameter provider, the ApiPlatform\State\Provider\ParameterProvider
138+
* resets this value to extract the correct value on each request.
139+
* It's also possible to set the `_api_query_parameters` request attribute directly and
140+
* API Platform will extract the value from there.
141+
*/
136142
public function setValue(mixed $value): static
137143
{
138144
$this->extraProperties['_api_values'] = $value;

src/Metadata/Parameters.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,19 @@ public function has(string $key, string $parameterClass = QueryParameter::class)
122122
return false;
123123
}
124124

125+
/**
126+
* @return list<string>
127+
*/
128+
public function keys(): array
129+
{
130+
$keys = [];
131+
foreach ($this->parameters as [$key]) {
132+
$keys[] = $key;
133+
}
134+
135+
return $keys;
136+
}
137+
125138
public function count(): int
126139
{
127140
return \count($this->parameters);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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\Metadata;
15+
16+
/**
17+
* @experimental
18+
*/
19+
interface UriVariableParameterInterface
20+
{
21+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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\State\ParameterProvider;
15+
16+
use ApiPlatform\Metadata\IriConverterInterface;
17+
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\State\ParameterNotFound;
20+
use ApiPlatform\State\ParameterProviderInterface;
21+
22+
/**
23+
* @experimental
24+
*
25+
* @author Vincent Amstoutz
26+
*/
27+
final readonly class IriConverterParameterProvider implements ParameterProviderInterface
28+
{
29+
public function __construct(
30+
private IriConverterInterface $iriConverter,
31+
) {
32+
}
33+
34+
public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
35+
{
36+
$operation = $context['operation'] ?? null;
37+
if (!($value = $parameter->getValue()) || $value instanceof ParameterNotFound) {
38+
return $operation;
39+
}
40+
41+
if (!\is_array($value)) {
42+
$value = [$value];
43+
}
44+
45+
$entities = [];
46+
foreach ($value as $v) {
47+
$entities[] = $this->iriConverter->getResourceFromIri($v, [
48+
'fetch_data' => $parameter->getExtraProperties()['fetch_data'] ?? false,
49+
]);
50+
}
51+
52+
$parameter->setValue($entities);
53+
54+
return $operation;
55+
}
56+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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\State\ParameterProvider;
15+
16+
use ApiPlatform\Metadata\Link;
17+
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\Metadata\Parameter;
19+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
20+
use ApiPlatform\State\Exception\ProviderNotFoundException;
21+
use ApiPlatform\State\ParameterProviderInterface;
22+
use ApiPlatform\State\ProviderInterface;
23+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
24+
25+
/**
26+
* Checks if the linked resources have security attributes and prepares them for access checking.
27+
*
28+
* @experimental
29+
*/
30+
final class ReadLinkParameterProvider implements ParameterProviderInterface
31+
{
32+
/**
33+
* @param ProviderInterface<mixed> $locator
34+
*/
35+
public function __construct(
36+
private readonly ProviderInterface $locator,
37+
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
38+
) {
39+
}
40+
41+
public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
42+
{
43+
$operation = $context['operation'];
44+
$extraProperties = $parameter->getExtraProperties();
45+
46+
if ($parameter instanceof Link) {
47+
$linkClass = $parameter->getFromClass() ?? $parameter->getToClass();
48+
$securityObjectName = $parameter->getSecurityObjectName() ?? $parameter->getToProperty() ?? $parameter->getFromProperty();
49+
}
50+
51+
$securityObjectName ??= $parameter->getKey();
52+
53+
$linkClass ??= $extraProperties['resource_class'] ?? $operation->getClass();
54+
55+
if (!$linkClass) {
56+
return $operation;
57+
}
58+
59+
$linkOperation = $this->resourceMetadataCollectionFactory
60+
->create($linkClass)
61+
->getOperation($operation->getExtraProperties()['parent_uri_template'] ?? $extraProperties['uri_template'] ?? null);
62+
63+
try {
64+
$relation = $this->locator->provide($linkOperation, [$parameter->getKey() => $parameter->getValue()], $context);
65+
} catch (ProviderNotFoundException) {
66+
$relation = null;
67+
}
68+
69+
$parameter->setValue($relation);
70+
71+
if (!$relation && true === ($extraProperties['throw_not_found'] ?? true)) {
72+
throw new NotFoundHttpException('Relation for link security not found.');
73+
}
74+
75+
$context['request']?->attributes->set($securityObjectName, $relation);
76+
77+
return $operation;
78+
}
79+
}

src/State/Provider/ParameterProvider.php

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515

1616
use ApiPlatform\Metadata\HttpOperation;
1717
use ApiPlatform\Metadata\Operation;
18+
use ApiPlatform\Metadata\Parameters;
1819
use ApiPlatform\State\Exception\ParameterNotSupportedException;
1920
use ApiPlatform\State\Exception\ProviderNotFoundException;
2021
use ApiPlatform\State\ParameterNotFound;
22+
use ApiPlatform\State\ParameterProvider\ReadLinkParameterProvider;
2123
use ApiPlatform\State\ProviderInterface;
2224
use ApiPlatform\State\Util\ParameterParserTrait;
2325
use ApiPlatform\State\Util\RequestParser;
@@ -50,25 +52,41 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5052
$request->attributes->set('_api_header_parameters', $request->headers->all());
5153
}
5254

53-
$parameters = $operation->getParameters();
55+
if ($request && null === $request->attributes->get('_api_path_parameters')) {
56+
$request->attributes->set('_api_path_parameters', $request->attributes->all());
57+
}
58+
59+
$parameters = $operation->getParameters() ?? new Parameters();
5460

55-
if ($operation instanceof HttpOperation && true === $operation->getStrictQueryParameterValidation()) {
56-
$keys = [];
57-
foreach ($parameters as $parameter) {
58-
$keys[] = $parameter->getKey();
61+
if ($operation instanceof HttpOperation) {
62+
// TODO: this should return Parameters but its a BC, prepare that change in 4.3
63+
foreach ($operation->getUriVariables() ?? [] as $key => $uriVariable) {
64+
if ($uriVariable->getSecurity() && !$uriVariable->getProvider()) {
65+
$uriVariable = $uriVariable->withProvider(ReadLinkParameterProvider::class);
66+
}
67+
68+
$parameters->add($key, $uriVariable->withKey($key));
5969
}
6070

61-
foreach (array_keys($request->attributes->get('_api_query_parameters')) as $key) {
62-
if (!\in_array($key, $keys, true)) {
63-
throw new ParameterNotSupportedException($key);
71+
if (true === $operation->getStrictQueryParameterValidation()) {
72+
$keys = [];
73+
foreach ($parameters as $parameter) {
74+
$keys[] = $parameter->getKey();
75+
}
76+
77+
foreach (array_keys($request->attributes->get('_api_query_parameters')) as $key) {
78+
if (!\in_array($key, $keys, true)) {
79+
throw new ParameterNotSupportedException($key);
80+
}
6481
}
6582
}
6683
}
6784

68-
foreach ($parameters ?? [] as $parameter) {
69-
$extraProperties = $parameter->getExtraProperties();
70-
unset($extraProperties['_api_values']);
71-
$parameters->add($parameter->getKey(), $parameter = $parameter->withExtraProperties($extraProperties));
85+
foreach ($parameters as $parameter) {
86+
// we force API Platform's value extraction, use _api_query_parameters or _api_header_parameters if you need to set a value
87+
if (isset($parameter->getExtraProperties()['_api_values'])) {
88+
unset($parameter->getExtraProperties()['_api_values']);
89+
}
7290

7391
$context = ['operation' => $operation] + $context;
7492
$values = $this->getParameterValues($parameter, $request, $context);
@@ -78,14 +96,12 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
7896
$value = $default;
7997
}
8098

99+
$parameter->setValue($value);
100+
81101
if ($value instanceof ParameterNotFound) {
82102
continue;
83103
}
84104

85-
$parameters->add($parameter->getKey(), $parameter = $parameter->withExtraProperties(
86-
$parameter->getExtraProperties() + ['_api_values' => $value]
87-
));
88-
89105
if (null === ($provider = $parameter->getProvider())) {
90106
continue;
91107
}
@@ -111,7 +127,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
111127
}
112128
}
113129

114-
if ($parameters) {
130+
if (\count($parameters)) {
115131
$operation = $operation->withParameters($parameters);
116132
}
117133
$request?->attributes->set('_api_operation', $operation);

src/State/Provider/SecurityParameterProvider.php

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace ApiPlatform\State\Provider;
1515

1616
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
17+
use ApiPlatform\Metadata\Link;
1718
use ApiPlatform\Metadata\Operation;
1819
use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
1920
use ApiPlatform\State\ParameterNotFound;
@@ -25,11 +26,18 @@
2526
/**
2627
* Loops over parameters to check parameter security.
2728
* Throws an exception if security is not granted.
29+
*
30+
* @experimental
31+
*
32+
* @implements ProviderInterface<object>
2833
*/
2934
final class SecurityParameterProvider implements ProviderInterface
3035
{
3136
use ParameterParserTrait;
3237

38+
/**
39+
* @param ProviderInterface<object> $decorated
40+
*/
3341
public function __construct(private readonly ProviderInterface $decorated, private readonly ?ResourceAccessCheckerInterface $resourceAccessChecker = null)
3442
{
3543
}
@@ -41,6 +49,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
4149

4250
$operation = $request?->attributes->get('_api_operation') ?? $operation;
4351
foreach ($operation->getParameters() ?? [] as $parameter) {
52+
$extraProperties = $parameter->getExtraProperties();
4453
if (null === $security = $parameter->getSecurity()) {
4554
continue;
4655
}
@@ -49,8 +58,32 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
4958
continue;
5059
}
5160

52-
$securityContext = [$parameter->getKey() => $v, 'object' => $body, 'operation' => $operation];
53-
if (!$this->resourceAccessChecker->isGranted($context['resource_class'], $security, $securityContext)) {
61+
if ($parameter instanceof Link) {
62+
$targetResource = $parameter->getFromClass() ?? $parameter->getToClass() ?? null;
63+
}
64+
65+
$targetResource ??= $extraProperties['resource_class'] ?? $context['resource_class'] ?? null;
66+
67+
if (!$targetResource) {
68+
continue;
69+
}
70+
71+
if ($parameter instanceof Link) {
72+
$securityObjectName = $parameter->getSecurityObjectName() ?? $parameter->getToProperty() ?? $parameter->getFromProperty() ?? null;
73+
}
74+
75+
$securityObjectName ??= $parameter->getKey();
76+
77+
$securityContext = [
78+
$parameter->getKey() => $v,
79+
'object' => $body,
80+
'operation' => $operation,
81+
'previous_object' => $request?->attributes->get('previous_data'),
82+
'request' => $request,
83+
$securityObjectName => $request?->attributes->get($securityObjectName),
84+
];
85+
86+
if (!$this->resourceAccessChecker->isGranted($targetResource, $security, $securityContext)) {
5487
throw $operation instanceof GraphQlOperation ? new AccessDeniedHttpException($parameter->getSecurityMessage() ?? 'Access Denied.') : new AccessDeniedException($parameter->getSecurityMessage() ?? 'Access Denied.');
5588
}
5689
}

0 commit comments

Comments
 (0)