Skip to content

Commit 1092033

Browse files
committed
Use expression language
1 parent 46a6d71 commit 1092033

File tree

8 files changed

+131
-30
lines changed

8 files changed

+131
-30
lines changed

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"symfony/config": "^3.2",
4747
"symfony/dependency-injection": "^2.7 || ^3.0",
4848
"symfony/doctrine-bridge": "^2.8 || ^3.0",
49+
"symfony/expression-language": "^2.8 || ^3.0",
4950
"symfony/phpunit-bridge": "^2.7 || ^3.0",
5051
"symfony/security": "^2.7 || ^3.0",
5152
"symfony/templating": "^2.7 || ^3.0",
@@ -59,7 +60,9 @@
5960
"phpdocumentor/reflection-docblock": "To support extracting metadata from PHPDoc.",
6061
"psr/cache-implementation": "To use metadata caching.",
6162
"symfony/cache": "To have metadata caching when using Symfony integration.",
63+
"symfony/expression-language": "To use authorization features.",
6264
"symfony/config": "To load XML configuration files.",
65+
"symfony/security": "To use authorization features.",
6366
"symfony/twig-bundle": "To use the Swagger UI integration."
6467
},
6568
"autoload": {

features/authorization/deny.feature

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,69 @@ Feature: Authorization checking
66
@createSchema
77
Scenario: An anonymous user retrieve a secured resource
88
When I add "Accept" header equal to "application/ld+json"
9-
And I send a "GET" request to "/secureds"
9+
And I send a "GET" request to "/secured_dummies"
1010
Then the response status code should be 401
1111

1212
Scenario: An authenticated user retrieve a secured resource
1313
When I add "Accept" header equal to "application/ld+json"
1414
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
15-
And I send a "GET" request to "/secureds"
15+
And I send a "GET" request to "/secured_dummies"
1616
Then the response status code should be 200
1717
And the response should be in JSON
1818

19-
@dropSchema
20-
Scenario: Only admins can create a secured resource
19+
20+
Scenario: A standard user cannot create a secured resource
2121
When I add "Accept" header equal to "application/ld+json"
22+
And I add "Content-Type" header equal to "application/ld+json"
2223
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
23-
And I send a "POST" request to "/secureds" with body:
24+
And I send a "POST" request to "/secured_dummies" with body:
2425
"""
2526
{
2627
"title": "Title",
27-
"description": "Description"
28+
"description": "Description",
29+
"owner": "foo"
2830
}
2931
"""
3032
Then the response status code should be 403
33+
34+
Scenario: An admin can create a secured resource
35+
When I add "Accept" header equal to "application/ld+json"
36+
And I add "Content-Type" header equal to "application/ld+json"
37+
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
38+
And I send a "POST" request to "/secured_dummies" with body:
39+
"""
40+
{
41+
"title": "Title",
42+
"description": "Description",
43+
"owner": "someone"
44+
}
45+
"""
46+
Then the response status code should be 201
47+
48+
Scenario: An admin can create another secured resource
49+
When I add "Accept" header equal to "application/ld+json"
50+
And I add "Content-Type" header equal to "application/ld+json"
51+
And I add "Authorization" header equal to "Basic YWRtaW46a2l0dGVu"
52+
And I send a "POST" request to "/secured_dummies" with body:
53+
"""
54+
{
55+
"title": "Special Title",
56+
"description": "Description",
57+
"owner": "dunglas"
58+
}
59+
"""
60+
Then the response status code should be 201
61+
62+
Scenario: An user retrieve cannot retrieve an item he doesn't own
63+
When I add "Accept" header equal to "application/ld+json"
64+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
65+
And I send a "GET" request to "/secured_dummies/1"
66+
Then the response status code should be 403
67+
And the response should be in JSON
68+
69+
@dropSchema
70+
Scenario: An user can retrieve an item he owns
71+
When I add "Accept" header equal to "application/ld+json"
72+
And I add "Authorization" header equal to "Basic ZHVuZ2xhczprZXZpbg=="
73+
And I send a "GET" request to "/secured_dummies/2"
74+
Then the response status code should be 200

src/Bridge/Doctrine/EventListener/WriteListener.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ public function onKernelView(GetResponseForControllerResultEvent $event)
7979
private function getManager(string $resourceClass, $data)
8080
{
8181
$objectManager = $this->managerRegistry->getManagerForClass($resourceClass);
82-
8382
if (null === $objectManager || !is_object($data)) {
8483
return;
8584
}

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,6 @@
8989
<!-- Event listeners -->
9090

9191
<!-- kernel.request priority must be < 8 to be executed after the Firewall -->
92-
<service id="api_platform.listener.request.deny_access" class="ApiPlatform\Core\EventListener\DenyAccessListener">
93-
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
94-
<argument type="service" id="security.authorization_checker" />
95-
96-
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="7" />
97-
</service>
98-
9992
<service id="api_platform.listener.request.add_format" class="ApiPlatform\Core\EventListener\AddFormatListener">
10093
<argument type="service" id="api_platform.negotiator" />
10194
<argument>%api_platform.formats%</argument>
@@ -118,6 +111,14 @@
118111
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="2" />
119112
</service>
120113

114+
<!-- This listener must be executed only when the current object is available -->
115+
<service id="api_platform.listener.request.deny_access" class="ApiPlatform\Core\EventListener\DenyAccessListener">
116+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
117+
<argument type="service" id="security.authorization_checker" on-invalid="ignore" />
118+
119+
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="1" />
120+
</service>
121+
121122
<service id="api_platform.listener.view.validate" class="ApiPlatform\Core\Bridge\Symfony\Validator\EventListener\ValidateListener">
122123
<argument type="service" id="validator" />
123124
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />

src/EventListener/DenyAccessListener.php

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use ApiPlatform\Core\Exception\RuntimeException;
1515
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1616
use ApiPlatform\Core\Util\RequestAttributesExtractor;
17+
use Symfony\Component\ExpressionLanguage\Expression;
1718
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
1819
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
1920
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
@@ -28,7 +29,7 @@ final class DenyAccessListener
2829
private $resourceMetadataFactory;
2930
private $authorizationChecker;
3031

31-
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, AuthorizationCheckerInterface $authorizationChecker)
32+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, AuthorizationCheckerInterface $authorizationChecker = null)
3233
{
3334
$this->resourceMetadataFactory = $resourceMetadataFactory;
3435
$this->authorizationChecker = $authorizationChecker;
@@ -43,8 +44,10 @@ public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFa
4344
*/
4445
public function onKernelRequest(GetResponseEvent $event)
4546
{
47+
$request = $event->getRequest();
48+
4649
try {
47-
$attributes = RequestAttributesExtractor::extractAttributes($event->getRequest());
50+
$attributes = RequestAttributesExtractor::extractAttributes($request);
4851
} catch (RuntimeException $e) {
4952
return;
5053
}
@@ -61,8 +64,18 @@ public function onKernelRequest(GetResponseEvent $event)
6164
return;
6265
}
6366

64-
foreach ($isGranted as $attribute) {
65-
if ($this->authorizationChecker->isGranted($attribute)) {
67+
if (null === $this->authorizationChecker) {
68+
throw new \LogicException('The "symfony/security" library must be installed to use the "is_granted" attribute.');
69+
}
70+
71+
if (!class_exists(Expression::class)) {
72+
throw new \LogicException('The "symfony/expression-language" library must be installed to use the "is_granted" attribute.');
73+
}
74+
75+
$data = $request->attributes->get('data');
76+
77+
foreach ($isGranted as $expression) {
78+
if ($this->authorizationChecker->isGranted(new Expression($expression), $data)) {
6679
return;
6780
}
6881
}

tests/EventListener/DenyAccessListenerTest.php

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use ApiPlatform\Core\EventListener\DenyAccessListener;
1515
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1616
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
17+
use Prophecy\Argument;
18+
use Symfony\Component\ExpressionLanguage\Expression;
1719
use Symfony\Component\HttpFoundation\Request;
1820
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
1921
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
@@ -65,19 +67,20 @@ public function testNoIsGrantedAttribute()
6567

6668
public function testIsGranted()
6769
{
68-
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']);
70+
$data = new \stdClass();
71+
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get', 'data'=> $data]);
6972

7073
$eventProphecy = $this->prophesize(GetResponseEvent::class);
7174
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
7275
$event = $eventProphecy->reveal();
7376

74-
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['is_granted' => ['ROLE_ADMIN']]);
77+
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['is_granted' => ['has_role("ROLE_ADMIN")']]);
7578

7679
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
7780
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled();
7881

7982
$authorizationCheckerProphecy = $this->prophesize(AuthorizationCheckerInterface::class);
80-
$authorizationCheckerProphecy->isGranted('ROLE_ADMIN')->willReturn(true)->shouldBeCalled();
83+
$authorizationCheckerProphecy->isGranted(Argument::type(Expression::class), $data)->willReturn(true)->shouldBeCalled();
8184

8285
$listener = new DenyAccessListener($resourceMetadataFactoryProphecy->reveal(), $authorizationCheckerProphecy->reveal());
8386
$listener->onKernelRequest($event);
@@ -94,15 +97,35 @@ public function testIsNotGranted()
9497
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
9598
$event = $eventProphecy->reveal();
9699

97-
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['is_granted' => ['ROLE_ADMIN']]);
100+
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['is_granted' => ['has_role("ROLE_ADMIN")']]);
98101

99102
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
100103
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled();
101104

102105
$authorizationCheckerProphecy = $this->prophesize(AuthorizationCheckerInterface::class);
103-
$authorizationCheckerProphecy->isGranted('ROLE_ADMIN')->willReturn(false)->shouldBeCalled();
106+
$authorizationCheckerProphecy->isGranted(Argument::type(Expression::class), null)->willReturn(false)->shouldBeCalled();
104107

105108
$listener = new DenyAccessListener($resourceMetadataFactoryProphecy->reveal(), $authorizationCheckerProphecy->reveal());
106109
$listener->onKernelRequest($event);
107110
}
111+
112+
/**
113+
* @expectedException \LogicException
114+
*/
115+
public function testAuthorizationCheckerNotAvailable()
116+
{
117+
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']);
118+
119+
$eventProphecy = $this->prophesize(GetResponseEvent::class);
120+
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
121+
$event = $eventProphecy->reveal();
122+
123+
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['is_granted' => ['has_role("ROLE_ADMIN")']]);
124+
125+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
126+
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled();
127+
128+
$listener = new DenyAccessListener($resourceMetadataFactoryProphecy->reveal(), null);
129+
$listener->onKernelRequest($event);
130+
}
108131
}

tests/Fixtures/TestBundle/Entity/Secured.php renamed to tests/Fixtures/TestBundle/Entity/SecuredDummy.php

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Fixtures\TestBundle\Entity;
12+
namespace ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity;
1313

1414
use ApiPlatform\Core\Annotation\ApiResource;
1515
use Doctrine\ORM\Mapping as ORM;
@@ -21,18 +21,18 @@
2121
* @author Kévin Dunglas <[email protected]>
2222
*
2323
* @ApiResource(
24-
* attributes={"is_granted"={"ROLE_USER"}},
24+
* attributes={"is_granted"={"has_role('ROLE_USER')"}},
2525
* collectionOperations={
2626
* "get"={"method"="GET"},
27-
* "post"={"method"="POST", "is_granted"={"ROLE_ADMIN"}}
27+
* "post"={"method"="POST", "is_granted"={"has_role('ROLE_ADMIN')"}}
2828
* },
2929
* itemOperations={
30-
* "get"={"method"="GET"}
30+
* "get"={"method"="GET", "is_granted"={"has_role('ROLE_USER') and object.getOwner() == user"}}
3131
* }
3232
* )
3333
* @ORM\Entity
3434
*/
35-
class Secured
35+
class SecuredDummy
3636
{
3737
/**
3838
* @var int
@@ -56,7 +56,15 @@ class Secured
5656
*
5757
* @ORM\Column
5858
*/
59-
private $description;
59+
private $description = '';
60+
61+
/**
62+
* @var string The owner
63+
*
64+
* @ORM\Column
65+
* @Assert\NotBlank
66+
*/
67+
private $owner;
6068

6169
/**
6270
* @return int
@@ -85,4 +93,14 @@ public function setDescription(string $description)
8593
{
8694
$this->description = $description;
8795
}
96+
97+
public function getOwner(): string
98+
{
99+
return $this->owner;
100+
}
101+
102+
public function setOwner(string $owner)
103+
{
104+
$this->owner = $owner;
105+
}
88106
}

tests/Fixtures/app/config/security.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ security:
2727
security: false
2828

2929
default:
30-
provider: in_memory
30+
provider: chain_provider
3131
http_basic: ~
3232
anonymous: ~
3333

0 commit comments

Comments
 (0)