Skip to content

[Security] reusing AuthorizationChecker in AccessListener #27121

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ CHANGELOG
* Added support for the new Argon2i password encoder
* added `stateless` option to the `switch_user` listener
* deprecated auto picking the first registered provider when no configured provider on a firewall and ambiguous
* `AccessListener` reuses logic of `AuthorizationCheckerInterface`.
`AccessListener::__construct` now accepts `TokenStorageInterface`, `AccessMapInterface` and `AuthorizationCheckerInterface`,
if old signature is used `E_USER_DEPRECATED` will be triggered

3.3.0
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,8 @@
<service id="security.access_listener" class="Symfony\Component\Security\Http\Firewall\AccessListener">
<tag name="monolog.logger" channel="security" />
<argument type="service" id="security.token_storage" />
<argument type="service" id="security.access.decision_manager" />
<argument type="service" id="security.access_map" />
<argument type="service" id="security.authentication.manager" />
<argument type="service" id="security.authorization_checker"/>
</service>
</services>
</container>
44 changes: 42 additions & 2 deletions src/Symfony/Component/Security/Http/Firewall/AccessListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Http\AccessMapInterface;
Expand All @@ -27,15 +28,42 @@
class AccessListener implements ListenerInterface
{
private $tokenStorage;

/**
* @deprecated since Symfony 4.3
*/
private $accessDecisionManager;
private $map;

/**
* @deprecated since Symfony 4.3
*/
private $authManager;
private $authorizationChecker;

public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, AuthenticationManagerInterface $authManager)
/**
* @param AccessDecisionManagerInterface|AccessMapInterface $map
* @param AccessMapInterface|AuthorizationCheckerInterface $authorizationChecker
* @param AuthenticationManagerInterface|null $authManager
*/
public function __construct(TokenStorageInterface $tokenStorage, $map, $authorizationChecker, $authManager = null)
{
if ($authorizationChecker instanceof AccessMapInterface) {
@trigger_error(sprintf('Signature "%s()" has changed since Symfony 4.2, now it accepts TokenStorageInterface, AccessMapInterface, AuthorizationCheckerInterface.', __METHOD__), E_USER_DEPRECATED);
$accessDecisionManager = $map;
$map = $authorizationChecker;
$authorizationChecker = null;
} elseif (!$authorizationChecker instanceof AuthorizationCheckerInterface) {
throw new \InvalidArgumentException(sprintf('Argument 3 passed to %s() must be an instance of %s or null, %s given.', __METHOD__, AuthorizationCheckerInterface::class, \is_object($authorizationChecker) ? \get_class($authorizationChecker) : \gettype($authorizationChecker)));
} else {
$accessDecisionManager = null;
$authManager = null;
}

$this->tokenStorage = $tokenStorage;
$this->accessDecisionManager = $accessDecisionManager;
$this->map = $map;
$this->authorizationChecker = $authorizationChecker;
$this->accessDecisionManager = $accessDecisionManager;
$this->authManager = $authManager;
}

Expand All @@ -59,6 +87,18 @@ public function handle(GetResponseEvent $event)
return;
}

if (null !== $this->authorizationChecker) {
if (!$this->authorizationChecker->isGranted($attributes, $request)) {
$exception = new AccessDeniedException();
$exception->setAttributes($attributes);
$exception->setSubject($request);

throw $exception;
}

return;
}

if (!$token->isAuthenticated()) {
$token = $this->authManager->authenticate($token);
$this->tokenStorage->setToken($token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,37 @@
class AccessListenerTest extends TestCase
{
/**
* @expectedException \InvalidArgumentException
*/
public function testBadConstructorSignature()
{
$tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();

new AccessListener($tokenStorage, null, null, null);
}

/**
* @deprecated
* @group legacy
* @expectedDeprecation Signature "Symfony\Component\Security\Http\Firewall\AccessListener::__construct()" has changed since Symfony 4.2, now it accepts TokenStorageInterface, AccessMapInterface, AuthorizationCheckerInterface.
*/
public function testOldConstructorSignature()
{
$tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
$accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock();
$accessDecisionManager = $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface')->getMock();
$authManager = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock();

new AccessListener($tokenStorage, $accessDecisionManager, $accessMap, $authManager);
}

/**
* @deprecated
* @group legacy
* @expectedDeprecation Signature "Symfony\Component\Security\Http\Firewall\AccessListener::__construct()" has changed since Symfony 4.2, now it accepts TokenStorageInterface, AccessMapInterface, AuthorizationCheckerInterface.
* @expectedException \Symfony\Component\Security\Core\Exception\AccessDeniedException
*/
public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess()
public function testOldHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess()
{
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock();

Expand Down Expand Up @@ -70,7 +98,56 @@ public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess()
$listener->handle($event);
}

public function testHandleWhenTheTokenIsNotAuthenticated()
/**
* @expectedException \Symfony\Component\Security\Core\Exception\AccessDeniedException
*/
public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccess()
{
$tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
$accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock();
$authorizationChecker = $this->getMockBuilder('\Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface')->getMock();
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock();
$token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock();
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock();
$attributes = ['foo' => 'bar'];

$tokenStorage
->expects($this->any())
->method('getToken')
->will($this->returnValue($token))
;

$accessMap
->expects($this->any())
->method('getPatterns')
->with($this->equalTo($request))
->will($this->returnValue([$attributes, null]))
;

$authorizationChecker
->expects($this->once())
->method('isGranted')
->with($this->equalTo($attributes), $this->equalTo($request))
->will($this->returnValue(false))
;

$listener = new AccessListener($tokenStorage, $accessMap, $authorizationChecker);

$event
->expects($this->any())
->method('getRequest')
->will($this->returnValue($request))
;

$listener->handle($event);
}

/**
* @deprecated
* @group legacy
* @expectedDeprecation Signature "Symfony\Component\Security\Http\Firewall\AccessListener::__construct()" has changed since Symfony 4.2, now it accepts TokenStorageInterface, AccessMapInterface, AuthorizationCheckerInterface.
*/
public function testOldHandleWhenTheTokenIsNotAuthenticated()
{
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock();

Expand Down Expand Up @@ -141,7 +218,12 @@ public function testHandleWhenTheTokenIsNotAuthenticated()
$listener->handle($event);
}

public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
/**
* @deprecated
* @group legacy
* @expectedDeprecation Signature "Symfony\Component\Security\Http\Firewall\AccessListener::__construct()" has changed since Symfony 4.2, now it accepts TokenStorageInterface, AccessMapInterface, AuthorizationCheckerInterface.
*/
public function testOldHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
{
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock();

Expand Down Expand Up @@ -183,10 +265,51 @@ public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
$listener->handle($event);
}

public function testHandleWhenThereIsNoAccessMapEntryMatchingTheRequest()
{
$tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
$authorizationChecker = $this->getMockBuilder('\Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface')->getMock();
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock();
$token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock();
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock();
$accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock();

$tokenStorage
->expects($this->any())
->method('getToken')
->will($this->returnValue($token))
;

$event
->expects($this->any())
->method('getRequest')
->will($this->returnValue($request))
;

$accessMap
->expects($this->any())
->method('getPatterns')
->with($this->equalTo($request))
->will($this->returnValue([null, null]))
;

$authorizationChecker
->expects($this->never())
->method('isGranted')
;

$listener = new AccessListener($tokenStorage, $accessMap, $authorizationChecker);

$listener->handle($event);
}

/**
* @deprecated
* @group legacy
* @expectedDeprecation Signature "Symfony\Component\Security\Http\Firewall\AccessListener::__construct()" has changed since Symfony 4.2, now it accepts TokenStorageInterface, AccessMapInterface, AuthorizationCheckerInterface.
* @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException
*/
public function testHandleWhenTheSecurityTokenStorageHasNoToken()
public function testOldHandleWhenTheSecurityTokenStorageHasNoToken()
{
$tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
$tokenStorage
Expand All @@ -195,14 +318,50 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken()
->will($this->returnValue(null))
;

$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->disableOriginalConstructor()->disableOriginalClone()->getMock();

$accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock();
$accessMap
->expects($this->any())
->method('getPatterns')
->with($this->equalTo($request))
->will($this->returnValue([['foo' => 'bar'], null]))
;

$listener = new AccessListener(
$tokenStorage,
$this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface')->getMock(),
$this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock(),
$accessMap,
$this->getMockBuilder('Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface')->getMock()
);

$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock();
$event
->expects($this->any())
->method('getRequest')
->will($this->returnValue($request))
;

$listener->handle($event);
}

/**
* @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException
*/
public function testHandleWhenTheSecurityTokenStorageHasNoToken()
{
$tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock();
$accessMap = $this->getMockBuilder('Symfony\Component\Security\Http\AccessMapInterface')->getMock();
$authorizationChecker = $this->getMockBuilder('\Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface')->getMock();
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock();

$tokenStorage
->expects($this->any())
->method('getToken')
->will($this->returnValue(null))
;

$listener = new AccessListener($tokenStorage, $accessMap, $authorizationChecker);

$listener->handle($event);
}
Expand Down