Skip to content
This repository has been archived by the owner on Feb 24, 2023. It is now read-only.

Commit

Permalink
Changing how IsGranted works so that it can use all controller args as
Browse files Browse the repository at this point in the history
subject
  • Loading branch information
weaverryan committed Oct 2, 2017
1 parent 74f1f17 commit 0a2d6b3
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 27 deletions.
41 changes: 30 additions & 11 deletions EventListener/IsGrantedListener.php
Expand Up @@ -13,7 +13,8 @@

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
Expand All @@ -26,14 +27,16 @@
*/
class IsGrantedListener implements EventSubscriberInterface
{
private $argumentMetadataFactory;
private $authChecker;

public function __construct(AuthorizationCheckerInterface $authChecker = null)
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory, AuthorizationCheckerInterface $authChecker = null)
{
$this->argumentMetadataFactory = $argumentMetadataFactory;
$this->authChecker = $authChecker;
}

public function onKernelController(FilterControllerEvent $event)
public function onKernelControllerArguments(FilterControllerArgumentsEvent $event)
{
$request = $event->getRequest();
/** @var $configurations IsGranted[] */
Expand All @@ -42,20 +45,19 @@ public function onKernelController(FilterControllerEvent $event)
}

if (null === $this->authChecker) {
throw new \LogicException('To use the @IsGranted tag, you need to install symfony/security-bundle.');
throw new \LogicException('To use the @IsGranted tag, you need to install symfony/security-bundle and configure your security system.');
}

$arguments = $this->getArguments($event);

foreach ($configurations as $configuration) {
$subject = null;
if ($configuration->getSubject()) {
if (!$request->attributes->has($configuration->getSubject())) {
$allAttributes = $request->attributes->all();
// remove one attribute we know is noise in the error message
unset($allAttributes['_is_granted']);
throw new \RuntimeException(sprintf('Could not find the subject "%s" for the @IsGranted annotation. Available attributes are: %s', $configuration->getSubject(), implode(', ', array_keys($allAttributes))));
if (!isset($arguments[$configuration->getSubject()])) {
throw new \RuntimeException(sprintf('Could not find the subject "%s" for the @IsGranted annotation. Try adding a "$%s" argument to your controller method.', $configuration->getSubject(), $configuration->getSubject()));
}

$subject = $request->attributes->get($configuration->getSubject());
$subject = $arguments[$configuration->getSubject()];
}

if (!$this->authChecker->isGranted($configuration->getAttributes(), $subject)) {
Expand All @@ -72,6 +74,23 @@ public function onKernelController(FilterControllerEvent $event)
}
}

private function getArguments(FilterControllerArgumentsEvent $event)
{
$namedArguments = $event->getRequest()->attributes->all();
$argumentMetadatas = $this->argumentMetadataFactory->createArgumentMetadata($event->getController());

// loop over each argument value and its name from the metadata
foreach ($event->getArguments() as $index => $argument) {
if (!isset($argumentMetadatas[$index])) {
throw new \LogicException(sprintf('Could not find any argument metadata for argument %d of the controller.', $index));
}

$namedArguments[$argumentMetadatas[$index]->getName()] = $argument;
}

return $namedArguments;
}

private function getIsGrantedString(IsGranted $isGranted)
{
$attributes = array_map(function ($attribute) {
Expand All @@ -95,6 +114,6 @@ private function getIsGrantedString(IsGranted $isGranted)
*/
public static function getSubscribedEvents()
{
return array(KernelEvents::CONTROLLER => 'onKernelController');
return array(KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments');
}
}
1 change: 1 addition & 0 deletions Resources/config/security.xml
Expand Up @@ -17,6 +17,7 @@
<service id="Sensio\Bundle\FrameworkExtraBundle\Security\ExpressionLanguage" class="Sensio\Bundle\FrameworkExtraBundle\Security\ExpressionLanguage" public="false" />

<service id="Sensio\Bundle\FrameworkExtraBundle\EventListener\IsGrantedListener" class="Sensio\Bundle\FrameworkExtraBundle\EventListener\IsGrantedListener" public="false">
<argument type="service" id="argument_metadata_factory" />
<argument type="service" id="security.authorization_checker" on-invalid="null" />
<tag name="kernel.event_subscriber" />
</service>
Expand Down
67 changes: 51 additions & 16 deletions Tests/EventListener/IsGrantedListenerTest.php
Expand Up @@ -15,7 +15,9 @@
use Sensio\Bundle\FrameworkExtraBundle\EventListener\IsGrantedListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerArgumentsEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
Expand All @@ -27,9 +29,9 @@ class IsGrantedListenerTest extends \PHPUnit_Framework_TestCase
*/
public function testExceptionIfSecurityNotInstalled()
{
$listener = new IsGrantedListener();
$listener = new IsGrantedListener($this->createArgumentMetadataFactory(array()));
$request = $this->createRequest(new IsGranted(array()));
$listener->onKernelController($this->createFilterControllerEvent($request));
$listener->onKernelControllerArguments($this->createFilterControllerEvent($request, array()));
}

public function testNothingHappensWithNoConfig()
Expand All @@ -38,9 +40,9 @@ public function testNothingHappensWithNoConfig()
$authChecker->expects($this->never())
->method('isGranted');

$listener = new IsGrantedListener($authChecker);
$listener = new IsGrantedListener($this->createArgumentMetadataFactory(array()), $authChecker);
$request = $this->createRequest();
$listener->onKernelController($this->createFilterControllerEvent($request));
$listener->onKernelControllerArguments($this->createFilterControllerEvent($request, array()));
}

public function testIsGrantedCalledCorrectly()
Expand All @@ -52,11 +54,33 @@ public function testIsGrantedCalledCorrectly()
->with('ROLE_ADMIN', 'bar')
->will($this->returnValue(true));

$listener = new IsGrantedListener($authChecker);
$listener = new IsGrantedListener($this->createArgumentMetadataFactory(array()), $authChecker);
$isGranted = new IsGranted(array('attributes' => 'ROLE_ADMIN', 'subject' => 'foo'));
$request = $this->createRequest($isGranted);
$request->attributes->set('foo', 'bar');
$listener->onKernelController($this->createFilterControllerEvent($request));
$listener->onKernelControllerArguments($this->createFilterControllerEvent($request, array()));
}

public function testIsGrantedSubjectFromArguments()
{
$authChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
// createRequest() puts 2 IsGranted annotations into the config
$authChecker->expects($this->exactly(2))
->method('isGranted')
// the subject => arg2name will eventually resolve to the 2nd argument, which has this value
->with('ROLE_ADMIN', 'arg2Value')
->will($this->returnValue(true));

// create metadata for 2 named args for the controller
$arg1Metadata = new ArgumentMetadata('arg1Name', 'string', false, false, null);
$arg2Metadata = new ArgumentMetadata('arg2Name', 'string', false, false, null);
$listener = new IsGrantedListener($this->createArgumentMetadataFactory(array($arg1Metadata, $arg2Metadata)), $authChecker);
$isGranted = new IsGranted(array('attributes' => 'ROLE_ADMIN', 'subject' => 'arg2Name'));
$request = $this->createRequest($isGranted);

// the 2 resolved arguments to the controller
$arguments = array('arg1Value', 'arg2Value');
$listener->onKernelControllerArguments($this->createFilterControllerEvent($request, $arguments));
}

/**
Expand All @@ -66,10 +90,10 @@ public function testExceptionWhenMissingSubjectAttribute()
{
$authChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();

$listener = new IsGrantedListener($authChecker);
$listener = new IsGrantedListener($this->createArgumentMetadataFactory(array()), $authChecker);
$isGranted = new IsGranted(array('attributes' => 'ROLE_ADMIN', 'subject' => 'non_existent'));
$request = $this->createRequest($isGranted);
$listener->onKernelController($this->createFilterControllerEvent($request));
$listener->onKernelControllerArguments($this->createFilterControllerEvent($request, array()));
}

/**
Expand All @@ -82,7 +106,7 @@ public function testAccessDeniedMessages(array $attributes, $subject, $expectedM
->method('isGranted')
->will($this->returnValue(false));

$listener = new IsGrantedListener($authChecker);
$listener = new IsGrantedListener($this->createArgumentMetadataFactory(array()), $authChecker);
$isGranted = new IsGranted(array('attributes' => $attributes, 'subject' => $subject));
$request = $this->createRequest($isGranted);

Expand All @@ -93,7 +117,7 @@ public function testAccessDeniedMessages(array $attributes, $subject, $expectedM

$this->setExpectedException(AccessDeniedException::class, $expectedMessage);

$listener->onKernelController($this->createFilterControllerEvent($request));
$listener->onKernelControllerArguments($this->createFilterControllerEvent($request, array()));
}

public function getAccessDeniedMessageTests()
Expand All @@ -110,15 +134,15 @@ public function getAccessDeniedMessageTests()
public function testNotFoundHttpException()
{
$request = $this->createRequest(new IsGranted(array('attributes' => 'ROLE_ADMIN', 'statusCode' => 404, 'message' => 'Not found')));
$event = $this->createFilterControllerEvent($request);
$event = $this->createFilterControllerEvent($request, array());

$authChecker = $this->getMockBuilder(AuthorizationCheckerInterface::class)->getMock();
$authChecker->expects($this->any())
->method('isGranted')
->will($this->returnValue(false));

$listener = new IsGrantedListener($authChecker);
$listener->onKernelController($event);
$listener = new IsGrantedListener($this->createArgumentMetadataFactory(array()), $authChecker);
$listener->onKernelControllerArguments($event);
}

private function createRequest(IsGranted $isGranted = null)
Expand All @@ -131,8 +155,19 @@ private function createRequest(IsGranted $isGranted = null)
));
}

private function createFilterControllerEvent(Request $request)
private function createFilterControllerEvent(Request $request, array $arguments)
{
return new FilterControllerArgumentsEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), function () { return new Response(); }, $arguments, $request, null);
}

private function createArgumentMetadataFactory(array $argumentMetadatas)
{
return new FilterControllerEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), function () { return new Response(); }, $request, null);
$factory = $this->getMockBuilder(ArgumentMetadataFactoryInterface::class)->getMock();

$factory->expects($this->any())
->method('createArgumentMetadata')
->will($this->returnValue($argumentMetadatas));

return $factory;
}
}
47 changes: 47 additions & 0 deletions Tests/Fixtures/FooBundle/Controller/IsGrantedController.php
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Tests\Fixtures\FooBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class IsGrantedController
{
/**
* @Route("/is_granted/anonymous")
* @IsGranted("IS_AUTHENTICATED_ANONYMOUSLY")
*/
public function someAction()
{
return new Response('yay1');
}

/**
* @Route("/is_granted/request/attribute/args/{a}")
* @IsGranted("ISGRANTED_VOTER", subject="a")
*/
public function some2Action($a)
{
return new Response('yay2');
}

/**
* @Route("/is_granted/resolved/args")
* @IsGranted("ISGRANTED_VOTER", subject="foo")
*/
public function some3Action(Request $foo)
{
return new Response('yay3');
}
}
41 changes: 41 additions & 0 deletions Tests/Fixtures/FooBundle/Security/IsGrantedVoter.php
@@ -0,0 +1,41 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Tests\Fixtures\FooBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

/**
* Used in the function test IsGrantedTest.
*/
class IsGrantedVoter extends Voter
{
protected function supports($attribute, $subject)
{
return 'ISGRANTED_VOTER' === $attribute;
}

protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
// we use these specific conditions in the test
if ('allow_access' === $subject) {
return true;
}

if ($subject instanceof Request) {
return true;
}

return false;
}
}
1 change: 1 addition & 0 deletions Tests/Fixtures/TestKernel.php
Expand Up @@ -26,6 +26,7 @@ public function registerBundles()
new \Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new \Symfony\Bundle\TwigBundle\TwigBundle(),
new \Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new \Symfony\Bundle\SecurityBundle\SecurityBundle(),
new \Tests\Fixtures\FooBundle\FooBundle(),
new \Tests\Fixtures\ActionArgumentsBundle\ActionArgumentsBundle(),
);
Expand Down
28 changes: 28 additions & 0 deletions Tests/Fixtures/config/config.yml
Expand Up @@ -27,3 +27,31 @@ services:
test.action_arguments:
class: Tests\Fixtures\ActionArgumentsBundle\Controller\ActionArgumentsController
public: true

test.is_granted_voter:
class: Tests\Fixtures\FooBundle\Security\IsGrantedVoter
public: false
tags:
- { name: security.voter }

security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext

providers:
in_memory:
memory:
users:
johannes: { password: test, roles: [ROLE_USER] }

firewalls:
# This firewall doesn't make sense in combination with the rest of the
# configuration file, but it's here for testing purposes (do not use
# this file in a real world scenario though)
login_form:
pattern: ^/login$
security: false

default:
form_login: ~
anonymous: ~

0 comments on commit 0a2d6b3

Please sign in to comment.