Skip to content

Commit

Permalink
Add support for security_post_validation
Browse files Browse the repository at this point in the history
  • Loading branch information
lyrixx committed Aug 23, 2021
1 parent ccc297b commit fee00b0
Show file tree
Hide file tree
Showing 14 changed files with 339 additions and 4 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Expand Up @@ -41,6 +41,7 @@
* DataProvider: new `ApiPlatform\State\ProviderInterface` that replaces DataProviders (#4351)
* DataPersister: new `ApiPlatform\State\ProcessorInterface` that replaces DataPersisters (#4351)
* A new configuration is available to keep old services (IriConverter, IdentifiersExtractor and OpenApiFactory) `metadata_backward_compatibility_layer` (defaults to false) (#4351)
* Add support for `security_post_validation` attribute

## 2.6.5

Expand Down Expand Up @@ -231,7 +232,7 @@ For compatibility reasons with Symfony 5.2 and PHP 8, we do not test anymore the
* Doctrine: Order filter doesn't throw anymore with numeric key (#3673 and #3687)
* Doctrine: Fix ODM check change tracking deferred (#3629)
* Doctrine: Allow 2inflector version 2.0 (#3607)
* OpenAPI: Allow subresources context to be added (#3685)
* OpenAPI: Allow subresources context to be added (#3685)
* OpenAPI: Fix pagination documentation on subresources (#3678)
* Subresource: Fix query when using a custom identifier (#3529 and #3671)
* GraphQL: Fix relation types without Doctrine (#3591)
Expand Down Expand Up @@ -337,7 +338,7 @@ For compatibility reasons with Symfony 5.2 and PHP 8, we do not test anymore the
## 2.5.0 beta 1

* Add an HTTP client dedicated to functional API testing (#2608)
* Add PATCH support (#2895)
* Add PATCH support (#2895)
Note: with JSON Merge Patch, responses will skip null values. As this may break on some endpoints, you need to manually [add the `merge-patch+json` format](https://api-platform.com/docs/core/content-negotiation/#configuring-patch-formats) to enable PATCH support. This will be the default behavior in API Platform 3.
* Add a command to generate json schemas `api:json-schema:generate` (#2996)
* Add infrastructure to generate a JSON Schema from a Resource `ApiPlatform\Core\JsonSchema\SchemaFactoryInterface` (#2983)
Expand Down
6 changes: 6 additions & 0 deletions src/Core/Annotation/ApiResource.php
Expand Up @@ -63,6 +63,8 @@
* @Attribute("securityMessage", type="string"),
* @Attribute("securityPostDenormalize", type="string"),
* @Attribute("securityPostDenormalizeMessage", type="string"),
* @Attribute("securityPostValidation", type="string"),
* @Attribute("securityPostValidationMessage", type="string"),
* @Attribute("shortName", type="string"),
* @Attribute("stateless", type="bool"),
* @Attribute("subresourceOperations", type="array"),
Expand Down Expand Up @@ -167,6 +169,8 @@ final class ApiResource
* @param string $securityMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
* @param string $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
* @param string $securityPostDenormalizeMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
* @param string $securityPostValidation https://api-platform.com/docs/core/security/#executing-access-control-rules-after-validation
* @param string $securityPostValidationMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
* @param bool $stateless
* @param string $sunset https://api-platform.com/docs/core/deprecations/#setting-the-sunset-http-header-to-indicate-when-a-resource-or-an-operation-will-be-removed
* @param array $swaggerContext https://api-platform.com/docs/core/openapi/#using-the-openapi-and-swagger-contexts
Expand Down Expand Up @@ -218,6 +222,8 @@ public function __construct(
?string $securityMessage = null,
?string $securityPostDenormalize = null,
?string $securityPostDenormalizeMessage = null,
?string $securityPostValidation = null,
?string $securityPostValidationMessage = null,
?bool $stateless = null,
?string $sunset = null,
?array $swaggerContext = null,
Expand Down
6 changes: 6 additions & 0 deletions src/Core/Bridge/Symfony/Bundle/Resources/config/graphql.xml
Expand Up @@ -38,6 +38,7 @@
<argument type="service" id="api_platform.graphql.resolver.stage.validate" />
<argument type="service" id="api_platform.graphql.mutation_resolver_locator" />
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
<argument type="service" id="api_platform.graphql.resolver.stage.security_post_validation" />
</service>

<service id="api_platform.graphql.resolver.factory.item_subscription" class="ApiPlatform\GraphQl\Resolver\Factory\ItemSubscriptionResolverFactory" public="false">
Expand Down Expand Up @@ -70,6 +71,11 @@
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
</service>

<service id="api_platform.graphql.resolver.stage.security_post_validation" class="ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
</service>

<service id="api_platform.graphql.resolver.stage.serialize" class="ApiPlatform\GraphQl\Resolver\Stage\SerializeStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
<argument type="service" id="serializer" />
Expand Down
2 changes: 2 additions & 0 deletions src/Core/Bridge/Symfony/Bundle/Resources/config/security.xml
Expand Up @@ -24,6 +24,8 @@
<tag name="kernel.event_listener" event="kernel.request" method="onSecurity" priority="3" />
<!-- This method must be executed only when the current object is available, after deserialization -->
<tag name="kernel.event_listener" event="kernel.request" method="onSecurityPostDenormalize" priority="1" />
<!-- This method must be executed only when the current object is available, after validation -->
<tag name="kernel.event_listener" event="kernel.view" method="onSecurityPostValidation" priority="63" />
</service>

<service id="api_platform.security.expression_language_provider" class="ApiPlatform\Core\Security\Core\Authorization\ExpressionLanguageProvider" public="false">
Expand Down
9 changes: 9 additions & 0 deletions src/Core/Security/EventListener/DenyAccessListener.php
Expand Up @@ -22,6 +22,7 @@
use ApiPlatform\Util\OperationRequestInitiatorTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
Expand Down Expand Up @@ -85,6 +86,14 @@ public function onSecurityPostDenormalize(RequestEvent $event): void
]);
}

public function onSecurityPostValidation(ViewEvent $event): void
{
$request = $event->getRequest();
$this->checkSecurity($request, 'security_post_validation', false, [
'previous_object' => $request->attributes->get('previous_data'),
]);
}

/**
* @throws AccessDeniedException
*/
Expand Down
12 changes: 11 additions & 1 deletion src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
Expand Up @@ -19,6 +19,7 @@
use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStageInterface;
use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface;
use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStageInterface;
use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface;
use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface;
use ApiPlatform\GraphQl\Resolver\Stage\ValidateStageInterface;
Expand Down Expand Up @@ -49,8 +50,9 @@ final class ItemMutationResolverFactory implements ResolverFactoryInterface
private $validateStage;
private $mutationResolverLocator;
private $resourceMetadataCollectionFactory;
private $securityPostValidationStage;

public function __construct(ReadStageInterface $readStage, SecurityStageInterface $securityStage, SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, SerializeStageInterface $serializeStage, DeserializeStageInterface $deserializeStage, WriteStageInterface $writeStage, ValidateStageInterface $validateStage, ContainerInterface $mutationResolverLocator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory)
public function __construct(ReadStageInterface $readStage, SecurityStageInterface $securityStage, SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, SerializeStageInterface $serializeStage, DeserializeStageInterface $deserializeStage, WriteStageInterface $writeStage, ValidateStageInterface $validateStage, ContainerInterface $mutationResolverLocator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, SecurityPostValidationStageInterface $securityPostValidationStage)
{
$this->readStage = $readStage;
$this->securityStage = $securityStage;
Expand All @@ -61,6 +63,7 @@ public function __construct(ReadStageInterface $readStage, SecurityStageInterfac
$this->validateStage = $validateStage;
$this->mutationResolverLocator = $mutationResolverLocator;
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->securityPostValidationStage = $securityPostValidationStage;
}

public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?string $operationName = null): callable
Expand Down Expand Up @@ -120,6 +123,13 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
if (null !== $item) {
($this->validateStage)($item, $resourceClass, $operationName, $resolverContext);

($this->securityPostValidationStage)($resourceClass, $operationName, $resolverContext + [
'extra_variables' => [
'object' => $item,
'previous_object' => $previousItem,
],
]);

$persistResult = ($this->writeStage)($item, $resourceClass, $operationName, $resolverContext);
}

Expand Down
58 changes: 58 additions & 0 deletions src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php
@@ -0,0 +1,58 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\GraphQl\Resolver\Stage;

use ApiPlatform\Core\Security\ResourceAccessCheckerInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
* Security post Validation stage of GraphQL resolvers.
*
* @experimental
*
* @author Vincent Chalamon <vincentchalamon@gmail.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
final class SecurityPostValidationStage implements SecurityPostValidationStageInterface
{
private $resourceMetadataCollectionFactory;
private $resourceAccessChecker;

public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, ?ResourceAccessCheckerInterface $resourceAccessChecker)
{
$this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
$this->resourceAccessChecker = $resourceAccessChecker;
}

/**
* {@inheritdoc}
*/
public function __invoke(string $resourceClass, string $operationName, array $context): void
{
$resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass);
$operation = $resourceMetadataCollection->getGraphQlOperation($operationName);
$isGranted = $operation->getSecurityPostValidation();

if (null !== $isGranted && null === $this->resourceAccessChecker) {
throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".');
}

if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) {
return;
}

throw new AccessDeniedHttpException($operation->getSecurityPostValidationMessage() ?? 'Access Denied.');
}
}
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\GraphQl\Resolver\Stage;

use GraphQL\Error\Error;

/**
* Security post validation stage of GraphQL resolvers.
*
* @experimental
*
* @author Vincent Chalamon <vincentchalamon@gmail.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
interface SecurityPostValidationStageInterface
{
/**
* @throws Error
*/
public function __invoke(string $resourceClass, string $operationName, array $context): void;
}
32 changes: 32 additions & 0 deletions src/Metadata/ApiResource.php
Expand Up @@ -153,6 +153,8 @@ class ApiResource
* @param string $securityMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
* @param string $securityPostDenormalize https://api-platform.com/docs/core/security/#executing-access-control-rules-after-denormalization
* @param string $securityPostDenormalizeMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
* @param string $securityPostValidation https://api-platform.com/docs/core/security/#executing-access-control-rules-after-validation
* @param string $securityPostValidationMessage https://api-platform.com/docs/core/security/#configuring-the-access-control-error-message
* @param bool $compositeIdentifier
*/
public function __construct(
Expand Down Expand Up @@ -210,6 +212,8 @@ public function __construct(
?string $securityMessage = null,
?string $securityPostDenormalize = null,
?string $securityPostDenormalizeMessage = null,
?string $securityPostValidation = null,
?string $securityPostValidationMessage = null,
?bool $compositeIdentifier = null,
array $exceptionToStatus = [],
?bool $queryParameterValidationEnabled = null,
Expand Down Expand Up @@ -270,6 +274,8 @@ public function __construct(
$this->securityMessage = $securityMessage;
$this->securityPostDenormalize = $securityPostDenormalize;
$this->securityPostDenormalizeMessage = $securityPostDenormalizeMessage;
$this->securityPostValidation = $securityPostValidation;
$this->securityPostValidationMessage = $securityPostValidationMessage;
$this->compositeIdentifier = $compositeIdentifier;
$this->exceptionToStatus = $exceptionToStatus;
$this->queryParameterValidationEnabled = $queryParameterValidationEnabled;
Expand Down Expand Up @@ -1033,6 +1039,32 @@ public function withSecurityPostDenormalizeMessage(?string $securityPostDenormal
return $self;
}

public function getSecurityPostValidation(): ?string
{
return $this->securityPostValidation;
}

public function withSecurityPostValidation(?string $securityPostValidation = null): self
{
$self = clone $this;
$self->securityPostValidation = $securityPostValidation;

return $self;
}

public function getSecurityPostValidationMessage(): ?string
{
return $this->securityPostValidationMessage;
}

public function withSecurityPostValidationMessage(?string $securityPostValidationMessage = null): self
{
$self = clone $this;
$self->securityPostValidationMessage = $securityPostValidationMessage;

return $self;
}

public function getCompositeIdentifier(): ?bool
{
return $this->compositeIdentifier;
Expand Down
34 changes: 34 additions & 0 deletions src/Metadata/GraphQl/Operation.php
Expand Up @@ -47,6 +47,8 @@ class Operation
private $securityMessage;
private $securityPostDenormalize;
private $securityPostDenormalizeMessage;
private $securityPostValidation;
private $securityPostValidationMessage;
private $deprecationReason;
/**
* @var string[]
Expand Down Expand Up @@ -103,6 +105,8 @@ class Operation
* @param string $securityMessage
* @param string $securityPostDenormalize
* @param string $securityPostDenormalizeMessage
* @param string $securityPostValidation
* @param string $securityPostValidationMessage
* @param string $deprecationReason
* @param string[] $filters
* @param bool|string|array $mercure
Expand Down Expand Up @@ -140,6 +144,8 @@ public function __construct(
?string $securityMessage = null,
?string $securityPostDenormalize = null,
?string $securityPostDenormalizeMessage = null,
?string $securityPostValidation = null,
?string $securityPostValidationMessage = null,
?string $deprecationReason = null,
array $filters = [],
array $validationContext = [],
Expand Down Expand Up @@ -185,6 +191,8 @@ public function __construct(
$this->securityMessage = $securityMessage;
$this->securityPostDenormalize = $securityPostDenormalize;
$this->securityPostDenormalizeMessage = $securityPostDenormalizeMessage;
$this->securityPostValidation = $securityPostValidation;
$this->securityPostValidationMessage = $securityPostValidationMessage;
$this->deprecationReason = $deprecationReason;
$this->filters = $filters;
$this->validationContext = $validationContext;
Expand Down Expand Up @@ -536,6 +544,32 @@ public function withSecurityPostDenormalizeMessage(?string $securityPostDenormal
return $self;
}

public function getSecurityPostValidation(): ?string
{
return $this->securityPostValidation;
}

public function withSecurityPostValidation(?string $securityPostValidation = null): self
{
$self = clone $this;
$self->securityPostValidation = $securityPostValidation;

return $self;
}

public function getSecurityPostValidationMessage(): ?string
{
return $this->securityPostValidationMessage;
}

public function withSecurityPostValidationMessage(?string $securityPostValidationMessage = null): self
{
$self = clone $this;
$self->securityPostValidationMessage = $securityPostValidationMessage;

return $self;
}

public function getDeprecationReason(): ?string
{
return $this->deprecationReason;
Expand Down

0 comments on commit fee00b0

Please sign in to comment.