Skip to content

Commit

Permalink
Merge pull request #2992 from vincentchalamon/issue/1646
Browse files Browse the repository at this point in the history
Deprecate access_control attribute, add security + security_post_denormalize attributes
  • Loading branch information
dunglas committed Aug 20, 2019
2 parents 3c98fdb + 1f06b4e commit 23d0f8f
Show file tree
Hide file tree
Showing 27 changed files with 814 additions and 117 deletions.
96 changes: 96 additions & 0 deletions features/authorization/legacy_deny.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
Feature: Authorization checking
In order to use the API
As a client software user
I need to be authorized to access a given resource using legacy access_control attribute.

@createSchema
Scenario: An anonymous user retrieves a secured resource
When I add "Accept" header equal to "application/ld+json"
And I send a "GET" request to "/legacy_secured_dummies"
Then the response status code should be 401

Scenario: An authenticated user retrieve a secured resource
When I add "Accept" header equal to "application/ld+json"
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
And I send a "GET" request to "/legacy_secured_dummies"
Then the response status code should be 200
And the response should be in JSON

Scenario: A standard user cannot create a secured resource
When I add "Accept" header equal to "application/ld+json"
And I add "Content-Type" header equal to "application/ld+json"
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
And I send a "POST" request to "/legacy_secured_dummies" with body:
"""
{
"title": "Title",
"description": "Description",
"owner": "foo"
}
"""
Then the response status code should be 403

Scenario: An admin can create a secured resource
When I add "Accept" header equal to "application/ld+json"
And I add "Content-Type" header equal to "application/ld+json"
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
And I send a "POST" request to "/legacy_secured_dummies" with body:
"""
{
"title": "Title",
"description": "Description",
"owner": "someone"
}
"""
Then the response status code should be 201

Scenario: An admin can create another secured resource
When I add "Accept" header equal to "application/ld+json"
And I add "Content-Type" header equal to "application/ld+json"
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
And I send a "POST" request to "/legacy_secured_dummies" with body:
"""
{
"title": "Special Title",
"description": "Description",
"owner": "dunglas"
}
"""
Then the response status code should be 201

Scenario: A user cannot retrieve an item they doesn't own
When I add "Accept" header equal to "application/ld+json"
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
And I send a "GET" request to "/legacy_secured_dummies/1"
Then the response status code should be 403
And the response should be in JSON

Scenario: A user can retrieve an item they owns
When I add "Accept" header equal to "application/ld+json"
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
And I send a "GET" request to "/legacy_secured_dummies/2"
Then the response status code should be 200

Scenario: A user can't assign to themself an item they doesn't own
When I add "Accept" header equal to "application/ld+json"
And I add "Content-Type" header equal to "application/ld+json"
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
And I send a "PUT" request to "/legacy_secured_dummies/2" with body:
"""
{
"owner": "kitten"
}
"""
Then the response status code should be 403

Scenario: A user can update an item they owns and transfer it
When I add "Accept" header equal to "application/ld+json"
And I add "Content-Type" header equal to "application/ld+json"
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
And I send a "PUT" request to "/legacy_secured_dummies/2" with body:
"""
{
"owner": "vincent"
}
"""
Then the response status code should be 200
22 changes: 20 additions & 2 deletions src/Annotation/ApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
* @Attributes(
* @Attribute("accessControl", type="string"),
* @Attribute("accessControlMessage", type="string"),
* @Attribute("security", type="string"),
* @Attribute("securityMessage", type="string"),
* @Attribute("securityPostDenormalize", type="string"),
* @Attribute("securityPostDenormalizeMessage", type="string"),
* @Attribute("attributes", type="array"),
* @Attribute("cacheHeaders", type="array"),
* @Attribute("collectionOperations", type="array"),
Expand Down Expand Up @@ -108,14 +112,28 @@ final class ApiResource
*
* @var string
*/
private $accessControl;
private $security;

/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
* @var string
*/
private $accessControlMessage;
private $securityMessage;

/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
* @var string
*/
private $securityPostDenormalize;

/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
*
* @var string
*/
private $securityPostDenormalizeMessage;

/**
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
Expand Down
16 changes: 12 additions & 4 deletions src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@

<service id="api_platform.graphql.resolver.factory.item" class="ApiPlatform\Core\GraphQl\Resolver\Factory\ItemResolverFactory" public="false">
<argument type="service" id="api_platform.graphql.resolver.stage.read" />
<argument type="service" id="api_platform.graphql.resolver.stage.deny_access" />
<argument type="service" id="api_platform.graphql.resolver.stage.security" />
<argument type="service" id="api_platform.graphql.resolver.stage.security_post_denormalize" />
<argument type="service" id="api_platform.graphql.resolver.stage.serialize" />
<argument type="service" id="api_platform.graphql.query_resolver_locator" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
</service>

<service id="api_platform.graphql.resolver.factory.collection" class="ApiPlatform\Core\GraphQl\Resolver\Factory\CollectionResolverFactory" public="false">
<argument type="service" id="api_platform.graphql.resolver.stage.read" />
<argument type="service" id="api_platform.graphql.resolver.stage.deny_access" />
<argument type="service" id="api_platform.graphql.resolver.stage.security" />
<argument type="service" id="api_platform.graphql.resolver.stage.security_post_denormalize" />
<argument type="service" id="api_platform.graphql.resolver.stage.serialize" />
<argument type="service" id="api_platform.graphql.query_resolver_locator" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
Expand All @@ -28,7 +30,8 @@

<service id="api_platform.graphql.resolver.factory.item_mutation" class="ApiPlatform\Core\GraphQl\Resolver\Factory\ItemMutationResolverFactory" public="false">
<argument type="service" id="api_platform.graphql.resolver.stage.read" />
<argument type="service" id="api_platform.graphql.resolver.stage.deny_access" />
<argument type="service" id="api_platform.graphql.resolver.stage.security" />
<argument type="service" id="api_platform.graphql.resolver.stage.security_post_denormalize" />
<argument type="service" id="api_platform.graphql.resolver.stage.serialize" />
<argument type="service" id="api_platform.graphql.resolver.stage.deserialize" />
<argument type="service" id="api_platform.graphql.resolver.stage.write" />
Expand All @@ -47,7 +50,12 @@
<argument type="service" id="api_platform.graphql.serializer.context_builder" />
</service>

<service id="api_platform.graphql.resolver.stage.deny_access" class="ApiPlatform\Core\GraphQl\Resolver\Stage\DenyAccessStage" public="false">
<service id="api_platform.graphql.resolver.stage.security" class="ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.security.resource_access_checker" />
</service>

<service id="api_platform.graphql.resolver.stage.security_post_denormalize" class="ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityPostDenormalizeStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.security.resource_access_checker" />
</service>
Expand Down
6 changes: 4 additions & 2 deletions src/Bridge/Symfony/Bundle/Resources/config/security.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.security.resource_access_checker" />

<!-- This listener must be executed only when the current object is available -->
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="1" />
<!-- This method must be executed only when the current object is available, before deserialization -->
<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" />
</service>

<service id="api_platform.security.expression_language_provider" class="ApiPlatform\Core\Security\Core\Authorization\ExpressionLanguageProvider" public="false">
Expand Down
19 changes: 14 additions & 5 deletions src/GraphQl/Resolver/Factory/CollectionResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;

use ApiPlatform\Core\GraphQl\Resolver\QueryCollectionResolverInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\DenyAccessStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\ReadStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\SerializeStageInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Util\CloneTrait;
Expand All @@ -30,22 +31,25 @@
*
* @author Alan Poulain <contact@alanpoulain.eu>
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Vincent Chalamon <vincentchalamon@gmail.com>
*/
final class CollectionResolverFactory implements ResolverFactoryInterface
{
use CloneTrait;

private $readStage;
private $denyAccessStage;
private $securityStage;
private $securityPostDenormalizeStage;
private $serializeStage;
private $queryResolverLocator;
private $requestStack;
private $resourceMetadataFactory;

public function __construct(ReadStageInterface $readStage, DenyAccessStageInterface $denyAccessStage, SerializeStageInterface $serializeStage, ContainerInterface $queryResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory, RequestStack $requestStack = null)
public function __construct(ReadStageInterface $readStage, SecurityStageInterface $securityStage, SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, SerializeStageInterface $serializeStage, ContainerInterface $queryResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory, RequestStack $requestStack = null)
{
$this->readStage = $readStage;
$this->denyAccessStage = $denyAccessStage;
$this->securityStage = $securityStage;
$this->securityPostDenormalizeStage = $securityPostDenormalizeStage;
$this->serializeStage = $serializeStage;
$this->queryResolverLocator = $queryResolverLocator;
$this->requestStack = $requestStack;
Expand Down Expand Up @@ -83,7 +87,12 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
$collection = $queryResolver($collection, $resolverContext);
}

($this->denyAccessStage)($resourceClass, $operationName, $resolverContext + [
($this->securityStage)($resourceClass, $operationName, $resolverContext + [
'extra_variables' => [
'object' => $collection,
],
]);
($this->securityPostDenormalizeStage)($resourceClass, $operationName, $resolverContext + [
'extra_variables' => [
'object' => $collection,
'previous_object' => $this->clone($collection),
Expand Down
22 changes: 15 additions & 7 deletions src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;

use ApiPlatform\Core\GraphQl\Resolver\MutationResolverInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\DenyAccessStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\DeserializeStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\ReadStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\SerializeStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\ValidateStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\WriteStageInterface;
Expand All @@ -33,25 +34,28 @@
* @experimental
*
* @author Alan Poulain <contact@alanpoulain.eu>
* @author Vincent Chalamon <vincentchalamon@gmail.com>
*/
final class ItemMutationResolverFactory implements ResolverFactoryInterface
{
use ClassInfoTrait;
use CloneTrait;

private $readStage;
private $denyAccessStage;
private $securityStage;
private $securityPostDenormalizeStage;
private $serializeStage;
private $deserializeStage;
private $writeStage;
private $validateStage;
private $mutationResolverLocator;
private $resourceMetadataFactory;

public function __construct(ReadStageInterface $readStage, DenyAccessStageInterface $denyAccessStage, SerializeStageInterface $serializeStage, DeserializeStageInterface $deserializeStage, WriteStageInterface $writeStage, ValidateStageInterface $validateStage, ContainerInterface $mutationResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory)
public function __construct(ReadStageInterface $readStage, SecurityStageInterface $securityStage, SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, SerializeStageInterface $serializeStage, DeserializeStageInterface $deserializeStage, WriteStageInterface $writeStage, ValidateStageInterface $validateStage, ContainerInterface $mutationResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory)
{
$this->readStage = $readStage;
$this->denyAccessStage = $denyAccessStage;
$this->securityStage = $securityStage;
$this->securityPostDenormalizeStage = $securityPostDenormalizeStage;
$this->serializeStage = $serializeStage;
$this->deserializeStage = $deserializeStage;
$this->writeStage = $writeStage;
Expand All @@ -73,16 +77,20 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
if (null !== $item && !\is_object($item)) {
throw new \LogicException('Item from read stage should be a nullable object.');
}
($this->securityStage)($resourceClass, $operationName, $resolverContext + [
'extra_variables' => [
'object' => $item,
],
]);
$previousItem = $this->clone($item);

if ('delete' === $operationName) {
($this->denyAccessStage)($resourceClass, $operationName, $resolverContext + [
($this->securityPostDenormalizeStage)($resourceClass, $operationName, $resolverContext + [
'extra_variables' => [
'object' => $item,
'previous_object' => $previousItem,
],
]);

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

return ($this->serializeStage)($item, $resourceClass, $operationName, $resolverContext);
Expand All @@ -102,7 +110,7 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
}
}

($this->denyAccessStage)($resourceClass, $operationName, $resolverContext + [
($this->securityPostDenormalizeStage)($resourceClass, $operationName, $resolverContext + [
'extra_variables' => [
'object' => $item,
'previous_object' => $previousItem,
Expand Down
19 changes: 14 additions & 5 deletions src/GraphQl/Resolver/Factory/ItemResolverFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
namespace ApiPlatform\Core\GraphQl\Resolver\Factory;

use ApiPlatform\Core\GraphQl\Resolver\QueryItemResolverInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\DenyAccessStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\ReadStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\SecurityStageInterface;
use ApiPlatform\Core\GraphQl\Resolver\Stage\SerializeStageInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Util\ClassInfoTrait;
Expand All @@ -31,22 +32,25 @@
*
* @author Alan Poulain <contact@alanpoulain.eu>
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Vincent Chalamon <vincentchalamon@gmail.com>
*/
final class ItemResolverFactory implements ResolverFactoryInterface
{
use CloneTrait;
use ClassInfoTrait;

private $readStage;
private $denyAccessStage;
private $securityStage;
private $securityPostDenormalizeStage;
private $serializeStage;
private $queryResolverLocator;
private $resourceMetadataFactory;

public function __construct(ReadStageInterface $readStage, DenyAccessStageInterface $denyAccessStage, SerializeStageInterface $serializeStage, ContainerInterface $queryResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory)
public function __construct(ReadStageInterface $readStage, SecurityStageInterface $securityStage, SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, SerializeStageInterface $serializeStage, ContainerInterface $queryResolverLocator, ResourceMetadataFactoryInterface $resourceMetadataFactory)
{
$this->readStage = $readStage;
$this->denyAccessStage = $denyAccessStage;
$this->securityStage = $securityStage;
$this->securityPostDenormalizeStage = $securityPostDenormalizeStage;
$this->serializeStage = $serializeStage;
$this->queryResolverLocator = $queryResolverLocator;
$this->resourceMetadataFactory = $resourceMetadataFactory;
Expand Down Expand Up @@ -79,7 +83,12 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul
$resourceClass = $this->getResourceClass($item, $resourceClass, $info, sprintf('Custom query resolver "%s"', $queryResolverId).' has to return an item of class %s but returned an item of class %s.');
}

($this->denyAccessStage)($resourceClass, $operationName, $resolverContext + [
($this->securityStage)($resourceClass, $operationName, $resolverContext + [
'extra_variables' => [
'object' => $item,
],
]);
($this->securityPostDenormalizeStage)($resourceClass, $operationName, $resolverContext + [
'extra_variables' => [
'object' => $item,
'previous_object' => $this->clone($item),
Expand Down
Loading

0 comments on commit 23d0f8f

Please sign in to comment.