Skip to content

refactor(state): merge parameter and link security #7195

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/Metadata/Exception/AccessDeniedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Metadata\Exception;

use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

final class AccessDeniedException extends AccessDeniedHttpException implements HttpExceptionInterface
{
public function getStatusCode(): int
{
return 403;
}

public function getHeaders(): array
{
return [];
}
}
3 changes: 3 additions & 0 deletions src/Metadata/HttpOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,9 @@ public function getUriVariables()
return $this->uriVariables;
}

/**
* @param array<string, mixed>|array<int, Link>|list<string> $uriVariables
*/
public function withUriVariables($uriVariables): static
{
$self = clone $this;
Expand Down
17 changes: 15 additions & 2 deletions src/Metadata/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
namespace ApiPlatform\Metadata;

use ApiPlatform\OpenApi;
use Symfony\Component\TypeInfo\Type;

#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
final class Link extends Parameter
Expand All @@ -27,7 +28,8 @@ public function __construct(
private ?array $identifiers = null,
private ?bool $compositeIdentifier = null,
private ?string $expandedValue = null,
?string $security = null,

string|\Stringable|null $security = null,
?string $securityMessage = null,
private ?string $securityObjectName = null,

Expand All @@ -37,9 +39,15 @@ public function __construct(
mixed $provider = null,
mixed $filter = null,
?string $property = null,
?array $properties = null,
?string $description = null,
?bool $required = null,
array $extraProperties = [],

mixed $constraints = null,
array|string|null $filterContext = null,
?Type $nativeType = null,
?bool $castToArray = null,
) {
// For the inverse property shortcut
if ($this->parameterName && class_exists($this->parameterName)) {
Expand All @@ -53,11 +61,16 @@ public function __construct(
provider: $provider,
filter: $filter,
property: $property,
properties: $properties,
description: $description,
required: $required,
constraints: $constraints,
security: $security,
securityMessage: $securityMessage,
extraProperties: $extraProperties
extraProperties: $extraProperties,
filterContext: $filterContext,
nativeType: $nativeType,
castToArray: $castToArray,
);
}

Expand Down
6 changes: 6 additions & 0 deletions src/Metadata/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ public function getValue(mixed $default = new ParameterNotFound()): mixed
return $this->extraProperties['_api_values'] ?? $default;
}

/**
* Only use this in a parameter provider, the ApiPlatform\State\Provider\ParameterProvider
* resets this value to extract the correct value on each request.
* It's also possible to set the `_api_query_parameters` request attribute directly and
* API Platform will extract the value from there.
*/
public function setValue(mixed $value): static
{
$this->extraProperties['_api_values'] = $value;
Expand Down
13 changes: 13 additions & 0 deletions src/Metadata/Parameters.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ public function has(string $key, string $parameterClass = QueryParameter::class)
return false;
}

/**
* @return list<string>
*/
public function keys(): array
{
$keys = [];
foreach ($this->parameters as [$key]) {
$keys[] = $key;
}

return $keys;
}

public function count(): int
{
return \count($this->parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ private function normalizeUriVariables(ApiResource|HttpOperation $operation): Ap
}

$normalizedUriVariable = $normalizedUriVariable->withParameterName($normalizedParameterName);
$normalizedUriVariables[$normalizedParameterName] = $normalizedUriVariable;
$normalizedUriVariables[$normalizedParameterName] = $normalizedUriVariable->withKey($normalizedParameterName);
}

return $operation->withUriVariables($normalizedUriVariables);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,37 @@ class: AttributeResource::class,
shortName: 'AttributeResource',
class: AttributeResource::class,
operations: [
'_api_/attribute_resources/{id}{._format}_get' => new Get(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_get'),
'_api_/attribute_resources/{id}{._format}_put' => new Put(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_put'),
'_api_/attribute_resources/{id}{._format}_delete' => new Delete(uriTemplate: '/attribute_resources/{id}{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')], name: '_api_/attribute_resources/{id}{._format}_delete'),
'_api_/attribute_resources{._format}_get_collection' => new GetCollection(uriTemplate: '/attribute_resources{._format}', shortName: 'AttributeResource', class: AttributeResource::class, controller: 'api_platform.action.placeholder', name: '_api_/attribute_resources{._format}_get_collection'),
'_api_/attribute_resources/{id}{._format}_get' => new Get(
uriTemplate: '/attribute_resources/{id}{._format}',
shortName: 'AttributeResource',
class: AttributeResource::class,
controller: 'api_platform.action.placeholder',
uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')],
name: '_api_/attribute_resources/{id}{._format}_get',
),
'_api_/attribute_resources/{id}{._format}_put' => new Put(
uriTemplate: '/attribute_resources/{id}{._format}',
shortName: 'AttributeResource',
class: AttributeResource::class,
controller: 'api_platform.action.placeholder',
uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')],
name: '_api_/attribute_resources/{id}{._format}_put',
),
'_api_/attribute_resources/{id}{._format}_delete' => new Delete(
uriTemplate: '/attribute_resources/{id}{._format}',
shortName: 'AttributeResource',
class: AttributeResource::class,
controller: 'api_platform.action.placeholder',
uriVariables: ['id' => new Link(fromClass: AttributeResource::class, identifiers: ['id'], parameterName: 'id')],
name: '_api_/attribute_resources/{id}{._format}_delete',
),
'_api_/attribute_resources{._format}_get_collection' => new GetCollection(
uriTemplate: '/attribute_resources{._format}',
shortName: 'AttributeResource',
class: AttributeResource::class,
controller: 'api_platform.action.placeholder',
name: '_api_/attribute_resources{._format}_get_collection',
),
]
),
new ApiResource(
Expand Down
58 changes: 58 additions & 0 deletions src/State/ParameterProvider/IriConverterParameterProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\State\ParameterProvider;

use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\State\ParameterNotFound;
use ApiPlatform\State\ParameterProviderInterface;

/**
* @experimental
*
* @author Vincent Amstoutz
*/
final readonly class IriConverterParameterProvider implements ParameterProviderInterface
{
public function __construct(
private IriConverterInterface $iriConverter,
) {
}

public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
{
$operation = $context['operation'] ?? null;
if (!($value = $parameter->getValue()) || $value instanceof ParameterNotFound) {
return $operation;
}

if (\is_array($value)) {
$entities = [];
foreach ($value as $v) {
$entities[] = $this->iriConverter->getResourceFromIri($v, [
'fetch_data' => $parameter->getExtraProperties()['fetch_data'] ?? false,
]);
}

$parameter->setValue($entities);
} else {
$parameter->setValue($this->iriConverter->getResourceFromIri($value, [
'fetch_data' => $parameter->getExtraProperties()['fetch_data'] ?? false,
]));
}

return $operation;
}
}
116 changes: 116 additions & 0 deletions src/State/ParameterProvider/ReadLinkParameterProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\State\ParameterProvider;

use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Parameter;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\State\Exception\ProviderNotFoundException;
use ApiPlatform\State\ParameterProviderInterface;
use ApiPlatform\State\ProviderInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
* Checks if the linked resources have security attributes and prepares them for access checking.
*
* @experimental
*/
final class ReadLinkParameterProvider implements ParameterProviderInterface
{
/**
* @param ProviderInterface<object> $locator
*/
public function __construct(
private readonly ProviderInterface $locator,
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
) {
}

public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation
{
$operation = $context['operation'];
$extraProperties = $parameter->getExtraProperties();

if ($parameter instanceof Link) {
$linkClass = $parameter->getFromClass() ?? $parameter->getToClass();
$securityObjectName = $parameter->getSecurityObjectName() ?? $parameter->getToProperty() ?? $parameter->getFromProperty();
}

$securityObjectName ??= $parameter->getKey();

$linkClass ??= $extraProperties['resource_class'] ?? $operation->getClass();

if (!$linkClass) {
return $operation;
}

$linkOperation = $this->resourceMetadataCollectionFactory
->create($linkClass)
->getOperation($operation->getExtraProperties()['parent_uri_template'] ?? $extraProperties['uri_template'] ?? null);

$value = $parameter->getValue();

if (\is_array($value) && array_is_list($value)) {
$relation = [];

foreach ($value as $v) {
try {
$relation[] = $this->locator->provide($linkOperation, $this->getUriVariables($v, $parameter, $linkOperation), $context);
} catch (ProviderNotFoundException) {
}
}
} else {
try {
$relation = $this->locator->provide($linkOperation, $this->getUriVariables($value, $parameter, $linkOperation), $context);
} catch (ProviderNotFoundException) {
$relation = null;
}
}

$parameter->setValue($relation);

if (null === $relation && true === ($extraProperties['throw_not_found'] ?? true)) {
throw new NotFoundHttpException('Relation for link security not found.');
}

$context['request']?->attributes->set($securityObjectName, $relation);

return $operation;
}

/**
* @return array<string, string>
*/
private function getUriVariables(mixed $value, Parameter $parameter, Operation $operation): array
{
$extraProperties = $parameter->getExtraProperties();

if ($operation instanceof HttpOperation) {
$links = $operation->getUriVariables();
} elseif ($operation instanceof GraphQlOperation) {
$links = $operation->getLinks();
} else {
$links = [];
}

if (!\is_array($value)) {
return [1 === \count($links) ? current($links)->getKey() : ($extraProperties['uri_variable'] ?? $parameter->getKey()) => $value];
}

return $value;
}
}
Loading