Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #938 from dunglas/security
Allow to configure auth access from the resource class
- Loading branch information
Showing
9 changed files
with
423 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
Feature: Authorization checking | ||
In order to use the API | ||
As a client software developer | ||
I need to be authorized to access a given resource. | ||
|
||
@createSchema | ||
Scenario: An anonymous user retrieve a secured resource | ||
When I add "Accept" header equal to "application/ld+json" | ||
And I send a "GET" request to "/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 "/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 "/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 "/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 "/secured_dummies" with body: | ||
""" | ||
{ | ||
"title": "Special Title", | ||
"description": "Description", | ||
"owner": "dunglas" | ||
} | ||
""" | ||
Then the response status code should be 201 | ||
|
||
Scenario: An user retrieve cannot retrieve an item he 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 "/secured_dummies/1" | ||
Then the response status code should be 403 | ||
And the response should be in JSON | ||
|
||
@dropSchema | ||
Scenario: An user can retrieve an item he 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 "/secured_dummies/2" | ||
Then the response status code should be 200 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<?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. | ||
*/ | ||
|
||
namespace ApiPlatform\Core\EventListener; | ||
|
||
use ApiPlatform\Core\Exception\RuntimeException; | ||
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; | ||
use ApiPlatform\Core\Util\RequestAttributesExtractor; | ||
use Symfony\Component\ExpressionLanguage\Expression; | ||
use Symfony\Component\HttpKernel\Event\GetResponseEvent; | ||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; | ||
use Symfony\Component\Security\Core\Exception\AccessDeniedException; | ||
|
||
/** | ||
* Denies access to the current resource if the logged user doesn't have sufficient permissions. | ||
* | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
*/ | ||
final class DenyAccessListener | ||
{ | ||
private $resourceMetadataFactory; | ||
private $authorizationChecker; | ||
|
||
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, AuthorizationCheckerInterface $authorizationChecker = null) | ||
{ | ||
$this->resourceMetadataFactory = $resourceMetadataFactory; | ||
$this->authorizationChecker = $authorizationChecker; | ||
} | ||
|
||
/** | ||
* Sets the applicable format to the HttpFoundation Request. | ||
* | ||
* @param GetResponseEvent $event | ||
* | ||
* @throws AccessDeniedException | ||
*/ | ||
public function onKernelRequest(GetResponseEvent $event) | ||
{ | ||
$request = $event->getRequest(); | ||
|
||
try { | ||
$attributes = RequestAttributesExtractor::extractAttributes($request); | ||
} catch (RuntimeException $e) { | ||
return; | ||
} | ||
|
||
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']); | ||
|
||
if (isset($attributes['collection_operation_name'])) { | ||
$isGranted = $resourceMetadata->getCollectionOperationAttribute($attributes['collection_operation_name'], 'is_granted', null, true); | ||
} else { | ||
$isGranted = $resourceMetadata->getItemOperationAttribute($attributes['item_operation_name'], 'is_granted', null, true); | ||
} | ||
|
||
if (null === $isGranted) { | ||
return; | ||
} | ||
|
||
if (null === $this->authorizationChecker) { | ||
throw new \LogicException(sprintf('The "symfony/security" library must be installed to use the "is_granted" attribute on class "%s".', $attributes['resource_class'])); | ||
} | ||
|
||
if (!class_exists(Expression::class)) { | ||
throw new \LogicException(sprintf('The "symfony/expression-language" library must be installed to use the "is_granted" attribute on class "%s".', $attributes['resource_class'])); | ||
} | ||
|
||
if (!$this->authorizationChecker->isGranted(new Expression($isGranted), $request->attributes->get('data'))) { | ||
throw new AccessDeniedException(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
<?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. | ||
*/ | ||
|
||
namespace ApiPlatform\Core\Tests\EventListener; | ||
|
||
use ApiPlatform\Core\EventListener\DenyAccessListener; | ||
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; | ||
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; | ||
use Prophecy\Argument; | ||
use Symfony\Component\ExpressionLanguage\Expression; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Event\GetResponseEvent; | ||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; | ||
|
||
/** | ||
* @author Kévin Dunglas <dunglas@gmail.com> | ||
*/ | ||
class DenyAccessListenerTest extends \PHPUnit_Framework_TestCase | ||
{ | ||
public function testNoResourceClass() | ||
{ | ||
$request = new Request(); | ||
|
||
$eventProphecy = $this->prophesize(GetResponseEvent::class); | ||
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); | ||
$event = $eventProphecy->reveal(); | ||
|
||
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); | ||
$resourceMetadataFactoryProphecy->create()->shouldNotBeCalled(); | ||
$resourceMetadataFactory = $resourceMetadataFactoryProphecy->reveal(); | ||
|
||
$authorizationCheckerProphecy = $this->prophesize(AuthorizationCheckerInterface::class); | ||
$authorizationCheckerProphecy->isGranted()->shouldNotBeCalled(); | ||
$authorizationChecker = $authorizationCheckerProphecy->reveal(); | ||
|
||
$listener = new DenyAccessListener($resourceMetadataFactory, $authorizationChecker); | ||
$listener->onKernelRequest($event); | ||
} | ||
|
||
public function testNoIsGrantedAttribute() | ||
{ | ||
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); | ||
|
||
$eventProphecy = $this->prophesize(GetResponseEvent::class); | ||
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); | ||
$event = $eventProphecy->reveal(); | ||
|
||
$resourceMetadata = new ResourceMetadata(); | ||
|
||
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); | ||
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled(); | ||
|
||
$authorizationCheckerProphecy = $this->prophesize(AuthorizationCheckerInterface::class); | ||
$authorizationCheckerProphecy->isGranted()->shouldNotBeCalled(); | ||
|
||
$listener = new DenyAccessListener($resourceMetadataFactoryProphecy->reveal(), $authorizationCheckerProphecy->reveal()); | ||
$listener->onKernelRequest($event); | ||
} | ||
|
||
public function testIsGranted() | ||
{ | ||
$data = new \stdClass(); | ||
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get', 'data' => $data]); | ||
|
||
$eventProphecy = $this->prophesize(GetResponseEvent::class); | ||
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); | ||
$event = $eventProphecy->reveal(); | ||
|
||
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['is_granted' => 'has_role("ROLE_ADMIN")']); | ||
|
||
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); | ||
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled(); | ||
|
||
$authorizationCheckerProphecy = $this->prophesize(AuthorizationCheckerInterface::class); | ||
$authorizationCheckerProphecy->isGranted(Argument::type(Expression::class), $data)->willReturn(true)->shouldBeCalled(); | ||
|
||
$listener = new DenyAccessListener($resourceMetadataFactoryProphecy->reveal(), $authorizationCheckerProphecy->reveal()); | ||
$listener->onKernelRequest($event); | ||
} | ||
|
||
/** | ||
* @expectedException \Symfony\Component\Security\Core\Exception\AccessDeniedException | ||
*/ | ||
public function testIsNotGranted() | ||
{ | ||
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); | ||
|
||
$eventProphecy = $this->prophesize(GetResponseEvent::class); | ||
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); | ||
$event = $eventProphecy->reveal(); | ||
|
||
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['is_granted' => 'has_role("ROLE_ADMIN")']); | ||
|
||
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); | ||
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled(); | ||
|
||
$authorizationCheckerProphecy = $this->prophesize(AuthorizationCheckerInterface::class); | ||
$authorizationCheckerProphecy->isGranted(Argument::type(Expression::class), null)->willReturn(false)->shouldBeCalled(); | ||
|
||
$listener = new DenyAccessListener($resourceMetadataFactoryProphecy->reveal(), $authorizationCheckerProphecy->reveal()); | ||
$listener->onKernelRequest($event); | ||
} | ||
|
||
/** | ||
* @expectedException \LogicException | ||
*/ | ||
public function testAuthorizationCheckerNotAvailable() | ||
{ | ||
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']); | ||
|
||
$eventProphecy = $this->prophesize(GetResponseEvent::class); | ||
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled(); | ||
$event = $eventProphecy->reveal(); | ||
|
||
$resourceMetadata = new ResourceMetadata(null, null, null, null, null, ['is_granted' => 'has_role("ROLE_ADMIN")']); | ||
|
||
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); | ||
$resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata)->shouldBeCalled(); | ||
|
||
$listener = new DenyAccessListener($resourceMetadataFactoryProphecy->reveal(), null); | ||
$listener->onKernelRequest($event); | ||
} | ||
} |
Oops, something went wrong.