Skip to content

Commit b2b5f99

Browse files
authored
refactor(state): merge parameter and link security (#7200)
1 parent 4a99a5f commit b2b5f99

25 files changed

+764
-244
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\Exception;
15+
16+
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
17+
18+
final class AccessDeniedException extends AccessDeniedHttpException implements HttpExceptionInterface
19+
{
20+
public function getStatusCode(): int
21+
{
22+
return 403;
23+
}
24+
25+
public function getHeaders(): array
26+
{
27+
return [];
28+
}
29+
}

src/Metadata/HttpOperation.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ public function getUriVariables()
349349
return $this->uriVariables;
350350
}
351351

352+
/**
353+
* @param array<string, mixed>|array<int, Link>|list<string> $uriVariables
354+
*/
352355
public function withUriVariables($uriVariables): static
353356
{
354357
$self = clone $this;

src/Metadata/Link.php

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

1616
use ApiPlatform\OpenApi;
17+
use Symfony\Component\TypeInfo\Type;
1718

1819
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
1920
final class Link extends Parameter
@@ -27,7 +28,8 @@ public function __construct(
2728
private ?array $identifiers = null,
2829
private ?bool $compositeIdentifier = null,
2930
private ?string $expandedValue = null,
30-
?string $security = null,
31+
32+
string|\Stringable|null $security = null,
3133
?string $securityMessage = null,
3234
private ?string $securityObjectName = null,
3335

@@ -37,9 +39,15 @@ public function __construct(
3739
mixed $provider = null,
3840
mixed $filter = null,
3941
?string $property = null,
42+
?array $properties = null,
4043
?string $description = null,
4144
?bool $required = null,
4245
array $extraProperties = [],
46+
47+
mixed $constraints = null,
48+
array|string|null $filterContext = null,
49+
?Type $nativeType = null,
50+
?bool $castToArray = null,
4351
) {
4452
// For the inverse property shortcut
4553
if ($this->parameterName && class_exists($this->parameterName)) {
@@ -53,11 +61,16 @@ public function __construct(
5361
provider: $provider,
5462
filter: $filter,
5563
property: $property,
64+
properties: $properties,
5665
description: $description,
5766
required: $required,
67+
constraints: $constraints,
5868
security: $security,
5969
securityMessage: $securityMessage,
60-
extraProperties: $extraProperties
70+
extraProperties: $extraProperties,
71+
filterContext: $filterContext,
72+
nativeType: $nativeType,
73+
castToArray: $castToArray,
6174
);
6275
}
6376

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;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
$iriConverterContext = ['fetch_data' => $parameter->getExtraProperties()['fetch_data'] ?? false];
42+
43+
if (\is_array($value)) {
44+
$entities = [];
45+
foreach ($value as $v) {
46+
$entities[] = $this->iriConverter->getResourceFromIri($v, $iriConverterContext);
47+
}
48+
49+
$parameter->setValue($entities);
50+
51+
return $operation;
52+
}
53+
54+
$parameter->setValue($this->iriConverter->getResourceFromIri($value, $iriConverterContext));
55+
56+
return $operation;
57+
}
58+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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\GraphQl\Operation as GraphQlOperation;
17+
use ApiPlatform\Metadata\HttpOperation;
18+
use ApiPlatform\Metadata\Link;
19+
use ApiPlatform\Metadata\Operation;
20+
use ApiPlatform\Metadata\Parameter;
21+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
22+
use ApiPlatform\State\Exception\ProviderNotFoundException;
23+
use ApiPlatform\State\ParameterProviderInterface;
24+
use ApiPlatform\State\ProviderInterface;
25+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
26+
27+
/**
28+
* Checks if the linked resources have security attributes and prepares them for access checking.
29+
*
30+
* @experimental
31+
*/
32+
final class ReadLinkParameterProvider implements ParameterProviderInterface
33+
{
34+
/**
35+
* @param ProviderInterface<object> $locator
36+
*/
37+
public function __construct(
38+
private readonly ProviderInterface $locator,
39+
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
40+
) {
41+
}
42+
43+
public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
44+
{
45+
$operation = $context['operation'];
46+
$extraProperties = $parameter->getExtraProperties();
47+
48+
if ($parameter instanceof Link) {
49+
$linkClass = $parameter->getFromClass() ?? $parameter->getToClass();
50+
$securityObjectName = $parameter->getSecurityObjectName() ?? $parameter->getToProperty() ?? $parameter->getFromProperty();
51+
}
52+
53+
$securityObjectName ??= $parameter->getKey();
54+
55+
$linkClass ??= $extraProperties['resource_class'] ?? $operation->getClass();
56+
57+
if (!$linkClass) {
58+
return $operation;
59+
}
60+
61+
$linkOperation = $this->resourceMetadataCollectionFactory
62+
->create($linkClass)
63+
->getOperation($operation->getExtraProperties()['parent_uri_template'] ?? $extraProperties['uri_template'] ?? null);
64+
65+
$value = $parameter->getValue();
66+
67+
if (\is_array($value) && array_is_list($value)) {
68+
$relation = [];
69+
70+
foreach ($value as $v) {
71+
try {
72+
$relation[] = $this->locator->provide($linkOperation, $this->getUriVariables($v, $parameter, $linkOperation), $context);
73+
} catch (ProviderNotFoundException) {
74+
}
75+
}
76+
} else {
77+
try {
78+
$relation = $this->locator->provide($linkOperation, $this->getUriVariables($value, $parameter, $linkOperation), $context);
79+
} catch (ProviderNotFoundException) {
80+
$relation = null;
81+
}
82+
}
83+
84+
$parameter->setValue($relation);
85+
86+
if (null === $relation && true === ($extraProperties['throw_not_found'] ?? true)) {
87+
throw new NotFoundHttpException('Relation for link security not found.');
88+
}
89+
90+
$context['request']?->attributes->set($securityObjectName, $relation);
91+
92+
return $operation;
93+
}
94+
95+
/**
96+
* @return array<string, string>
97+
*/
98+
private function getUriVariables(mixed $value, Parameter $parameter, Operation $operation): array
99+
{
100+
$extraProperties = $parameter->getExtraProperties();
101+
102+
if ($operation instanceof HttpOperation) {
103+
$links = $operation->getUriVariables();
104+
} elseif ($operation instanceof GraphQlOperation) {
105+
$links = $operation->getLinks();
106+
} else {
107+
$links = [];
108+
}
109+
110+
if (!\is_array($value)) {
111+
$uriVariables = [];
112+
113+
foreach ($links as $key => $link) {
114+
if (!\is_string($key)) {
115+
$key = $link->getParameterName() ?? $extraProperties['uri_variable'] ?? $link->getFromProperty();
116+
}
117+
118+
if (!$key || !\is_string($key)) {
119+
continue;
120+
}
121+
122+
$uriVariables[$key] = $value;
123+
}
124+
125+
return $uriVariables;
126+
}
127+
128+
return $value;
129+
}
130+
}

0 commit comments

Comments
 (0)