Skip to content

Commit

Permalink
refactor: Refactor ReadProvider.php and AccessCheckerProvider.php to …
Browse files Browse the repository at this point in the history
…extract link security into their own providers
  • Loading branch information
KDederichs committed Jan 18, 2024
1 parent c374794 commit 16b9558
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 136 deletions.
48 changes: 2 additions & 46 deletions src/State/Provider/ReadProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@

namespace ApiPlatform\State\Provider;

use ApiPlatform\Exception\InvalidIdentifierException;
use ApiPlatform\Exception\InvalidUriVariableException;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Put;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Util\CloneTrait;
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
use ApiPlatform\State\Exception\ProviderNotFoundException;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\State\UriVariablesResolverTrait;
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
use ApiPlatform\State\Util\RequestParser;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

Expand All @@ -36,11 +33,11 @@
final class ReadProvider implements ProviderInterface
{
use CloneTrait;
use OperationRequestInitiatorTrait;
use UriVariablesResolverTrait;

public function __construct(
private readonly ProviderInterface $provider,
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
private readonly ?SerializerContextBuilderInterface $serializerContextBuilder = null,
) {
}
Expand Down Expand Up @@ -90,47 +87,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
throw new NotFoundHttpException('Not Found');
}

if ($operation->getUriVariables()) {
foreach ($operation->getUriVariables() as $key => $uriVariable) {
if (!$uriVariable instanceof Link || !$uriVariable->getSecurity()) {
continue;
}

$relationClass = $uriVariable->getFromClass() ?? $uriVariable->getToClass();

if (!$relationClass) {
continue;
}

$parentOperation = $this->resourceMetadataCollectionFactory
->create($relationClass)
->getOperation($operation->getExtraProperties()['parent_uri_template'] ?? null);
try {
$relation = $this->provider->provide($parentOperation, [$uriVariable->getIdentifiers()[0] => $request?->attributes->all()[$key]], $context);
} catch (ProviderNotFoundException) {
$relation = null;
}
if (!$relation) {
throw new NotFoundHttpException('Not Found');
}

try {
$securityObjectName = $uriVariable->getSecurityObjectName();

if (!$securityObjectName) {
$securityObjectName = $uriVariable->getToProperty() ?? $uriVariable->getFromProperty();
}

if (!$securityObjectName) {
continue;
}
$request?->attributes->set($securityObjectName, $relation);
} catch (InvalidIdentifierException|InvalidUriVariableException $e) {
throw new NotFoundHttpException('Invalid identifier value or configuration.', $e);
}
}
}

$request?->attributes->set('data', $data);
$request?->attributes->set('previous_data', $this->clone($data));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->registerSecurityConfiguration($container, $config, $loader);
$this->registerMakerConfiguration($container, $config, $loader);
$this->registerArgumentResolverConfiguration($loader);
$this->registerLinkSecurityConfiguration($loader, $config);

$container->registerForAutoconfiguration(FilterInterface::class)
->addTag('api_platform.filter');
Expand Down Expand Up @@ -881,4 +882,11 @@ private function registerInflectorConfiguration(array $config): void
Inflector::keepLegacyInflector(false);
}
}

private function registerLinkSecurityConfiguration(XmlFileLoader $loader, array $config): void
{
if ($config['enable_link_security'] ?? true) {
$loader->load('link_security.xml');
}
}
}
20 changes: 20 additions & 0 deletions src/Symfony/Bundle/Resources/config/link_security.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>

<service id="api_platform.state_provider.read_link" class="ApiPlatform\Symfony\Security\State\LinkedReadProvider" decorates="api_platform.state_provider.read" decoration-priority="499">
<argument type="service" id="api_platform.state_provider.read_link.inner" />
<argument type="service" id="api_platform.state_provider.locator" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
</service>

<service id="api_platform.state_provider.access_checker_linked" class="ApiPlatform\Symfony\Security\State\LinkAccessCheckerProvider" decorates="api_platform.state_provider.read">
<argument type="service" id="api_platform.state_provider.access_checker_linked.inner" />
<argument type="service" id="api_platform.security.resource_access_checker" />
</service>
</services>
</container>
1 change: 0 additions & 1 deletion src/Symfony/Bundle/Resources/config/state.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

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

Expand Down
27 changes: 4 additions & 23 deletions src/Symfony/Security/State/AccessCheckerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Symfony\Security\Exception\AccessDeniedException;
Expand Down Expand Up @@ -52,6 +51,9 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
}

$body = $this->decorated->provide($operation, $uriVariables, $context);
if (null === $isGranted) {
return $body;
}

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

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

if ($operation instanceof HttpOperation && $operation->getUriVariables()) {
foreach ($operation->getUriVariables() as $uriVariable) {
if (!$uriVariable instanceof Link || !$uriVariable->getSecurity()) {
continue;
}

$targetResource = $uriVariable->getFromClass() ?? $uriVariable->getToClass();

if (!$targetResource) {
continue;
}

// We need to add all attributes here again because we do not know the name of the security object
$resourceAccessCheckerContext += $request->attributes->all();

if (!$this->resourceAccessChecker->isGranted($targetResource, $uriVariable->getSecurity(), $resourceAccessCheckerContext)) {
throw new AccessDeniedException($uriVariable->getSecurityMessage() ?? 'Access Denied.');
}
}
}

return $body;
}
}
75 changes: 75 additions & 0 deletions src/Symfony/Security/State/LinkAccessCheckerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?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\Symfony\Security\State;

use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use ApiPlatform\Symfony\Security\Exception\AccessDeniedException;
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;

class LinkAccessCheckerProvider implements ProviderInterface
{
public function __construct(
private readonly ProviderInterface $decorated,
private readonly ResourceAccessCheckerInterface $resourceAccessChecker
) {
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$request = ($context['request'] ?? null);

$data = $this->decorated->provide($operation, $uriVariables, $context);

if ($operation instanceof HttpOperation && $operation->getUriVariables()) {
foreach ($operation->getUriVariables() as $uriVariable) {
if (!$uriVariable instanceof Link || !$uriVariable->getSecurity()) {
continue;
}

$targetResource = $uriVariable->getFromClass() ?? $uriVariable->getToClass();

if (!$targetResource) {
continue;
}

$propertyName = $uriVariable->getToProperty() ?? $uriVariable->getFromProperty();
$securityObjectName = $uriVariable->getSecurityObjectName();

if (!$securityObjectName) {
$securityObjectName = $propertyName;
}

if (!$securityObjectName) {
continue;
}

$resourceAccessCheckerContext = [
'object' => $data,
'previous_object' => $request?->attributes->get('previous_data'),
$securityObjectName => $request?->attributes->get($securityObjectName),
'request' => $request,
];

if (!$this->resourceAccessChecker->isGranted($targetResource, $uriVariable->getSecurity(), $resourceAccessCheckerContext)) {
throw new AccessDeniedException($uriVariable->getSecurityMessage() ?? 'Access Denied.');
}
}
}

return $data;
}
}
86 changes: 86 additions & 0 deletions src/Symfony/Security/State/LinkedReadProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?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\Symfony\Security\State;

use ApiPlatform\Exception\InvalidIdentifierException;
use ApiPlatform\Exception\InvalidUriVariableException;
use ApiPlatform\Metadata\HttpOperation;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\State\Exception\ProviderNotFoundException;
use ApiPlatform\State\ProviderInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class LinkedReadProvider implements ProviderInterface
{
public function __construct(
private readonly ProviderInterface $decorated,
private readonly ProviderInterface $locator,
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory
) {
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$data = $this->decorated->provide($operation, $uriVariables, $context);

if (!$operation instanceof HttpOperation) {
return $data;
}

$request = ($context['request'] ?? null);

if ($operation->getUriVariables()) {
foreach ($operation->getUriVariables() as $key => $uriVariable) {
if (!$uriVariable instanceof Link || !$uriVariable->getSecurity()) {
continue;
}

$relationClass = $uriVariable->getFromClass() ?? $uriVariable->getToClass();

if (!$relationClass) {
continue;
}

$parentOperation = $this->resourceMetadataCollectionFactory
->create($relationClass)
->getOperation($operation->getExtraProperties()['parent_uri_template'] ?? null);
try {
$relation = $this->locator->provide($parentOperation, [$uriVariable->getIdentifiers()[0] => $request?->attributes->all()[$key]], $context);
} catch (ProviderNotFoundException) {
$relation = null;
}

if (!$relation) {
throw new NotFoundHttpException('Relation for link security not found.');
}

try {
$securityObjectName = $uriVariable->getSecurityObjectName();

if (!$securityObjectName) {
$securityObjectName = $uriVariable->getToProperty() ?? $uriVariable->getFromProperty();
}

$request?->attributes->set($securityObjectName, $relation);
} catch (InvalidIdentifierException|InvalidUriVariableException $e) {
throw new NotFoundHttpException('Invalid identifier value or configuration.', $e);
}
}
}

return $data;
}
}
Loading

0 comments on commit 16b9558

Please sign in to comment.