diff --git a/UPGRADE-3.3.md b/UPGRADE-3.3.md index bfa40d86a0f3..08a72c2a09a6 100644 --- a/UPGRADE-3.3.md +++ b/UPGRADE-3.3.md @@ -246,6 +246,9 @@ Security * The `RoleInterface` has been deprecated. Extend the `Symfony\Component\Security\Core\Role\Role` class in your custom role implementations instead. + * The `LogoutUrlGenerator::registerListener()` method will expect a 6th `$context = null` argument in 4.0. + Define the argument when overriding this method. + SecurityBundle -------------- diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index 0987f4dba918..6299176cd543 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -373,6 +373,8 @@ Security * The `RoleInterface` has been removed. Extend the `Symfony\Component\Security\Core\Role\Role` class instead. + + * The `LogoutUrlGenerator::registerListener()` method expects a 6th `$context = null` argument. SecurityBundle -------------- diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 6db601e3ccf9..115f24c66408 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -389,6 +389,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $firewall['logout']['csrf_token_id'], $firewall['logout']['csrf_parameter'], isset($firewall['logout']['csrf_token_generator']) ? new Reference($firewall['logout']['csrf_token_generator']) : null, + false === $firewall['stateless'] && isset($firewall['context']) ? $firewall['context'] : null, )) ; } diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallListener.php new file mode 100644 index 000000000000..a27c422fb8d2 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallListener.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\EventListener; + +use Symfony\Bundle\SecurityBundle\Security\FirewallMap; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\Event\FinishRequestEvent; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\Security\Http\Firewall; +use Symfony\Component\Security\Http\FirewallMapInterface; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; + +/** + * @author Maxime Steinhausser + */ +class FirewallListener extends Firewall +{ + private $map; + private $logoutUrlGenerator; + + public function __construct(FirewallMapInterface $map, EventDispatcherInterface $dispatcher, LogoutUrlGenerator $logoutUrlGenerator) + { + $this->map = $map; + $this->logoutUrlGenerator = $logoutUrlGenerator; + + parent::__construct($map, $dispatcher); + } + + public function onKernelRequest(GetResponseEvent $event) + { + if (!$event->isMasterRequest()) { + return; + } + + if ($this->map instanceof FirewallMap && $config = $this->map->getFirewallConfig($event->getRequest())) { + $this->logoutUrlGenerator->setCurrentFirewall($config->getName(), $config->getContext()); + } + + parent::onKernelRequest($event); + } + + public function onKernelFinishRequest(FinishRequestEvent $event) + { + if ($event->isMasterRequest()) { + $this->logoutUrlGenerator->setCurrentFirewall(null); + } + + parent::onKernelFinishRequest($event); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index ed07c600a5fa..9ec300851df8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -102,10 +102,11 @@ - + + diff --git a/src/Symfony/Component/Security/Http/Logout/LogoutUrlGenerator.php b/src/Symfony/Component/Security/Http/Logout/LogoutUrlGenerator.php index ada733be6b34..bfb7df2954a8 100644 --- a/src/Symfony/Component/Security/Http/Logout/LogoutUrlGenerator.php +++ b/src/Symfony/Component/Security/Http/Logout/LogoutUrlGenerator.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; @@ -28,6 +29,7 @@ class LogoutUrlGenerator private $router; private $tokenStorage; private $listeners = array(); + private $currentFirewall; public function __construct(RequestStack $requestStack = null, UrlGeneratorInterface $router = null, TokenStorageInterface $tokenStorage = null) { @@ -39,15 +41,29 @@ public function __construct(RequestStack $requestStack = null, UrlGeneratorInter /** * Registers a firewall's LogoutListener, allowing its URL to be generated. * - * @param string $key The firewall key - * @param string $logoutPath The path that starts the logout process - * @param string $csrfTokenId The ID of the CSRF token - * @param string $csrfParameter The CSRF token parameter name - * @param CsrfTokenManagerInterface $csrfTokenManager A CsrfTokenManagerInterface instance + * @param string $key The firewall key + * @param string $logoutPath The path that starts the logout process + * @param string $csrfTokenId The ID of the CSRF token + * @param string $csrfParameter The CSRF token parameter name + * @param CsrfTokenManagerInterface|null $csrfTokenManager A CsrfTokenManagerInterface instance + * @param string|null $context The listener context */ - public function registerListener($key, $logoutPath, $csrfTokenId, $csrfParameter, CsrfTokenManagerInterface $csrfTokenManager = null) + public function registerListener($key, $logoutPath, $csrfTokenId, $csrfParameter, CsrfTokenManagerInterface $csrfTokenManager = null/*, $context = null*/) { - $this->listeners[$key] = array($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager); + if (func_num_args() >= 6) { + $context = func_get_arg(5); + } else { + if (__CLASS__ !== get_class($this)) { + $r = new \ReflectionMethod($this, __FUNCTION__); + if (__CLASS__ !== $r->getDeclaringClass()->getName()) { + @trigger_error(sprintf('Method %s() will have a sixth `$context = null` argument in version 4.0. Not defining it is deprecated since 3.3.', get_class($this), __FUNCTION__), E_USER_DEPRECATED); + } + } + + $context = null; + } + + $this->listeners[$key] = array($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager, $context); } /** @@ -74,6 +90,15 @@ public function getLogoutUrl($key = null) return $this->generateLogoutUrl($key, UrlGeneratorInterface::ABSOLUTE_URL); } + /** + * @param string|null $key The current firewall key + * @param string|null $context The current firewall context + */ + public function setCurrentFirewall($key, $context = null) + { + $this->currentFirewall = array($key, $context); + } + /** * Generates the logout URL for the firewall. * @@ -81,28 +106,10 @@ public function getLogoutUrl($key = null) * @param int $referenceType The type of reference (one of the constants in UrlGeneratorInterface) * * @return string The logout URL - * - * @throws \InvalidArgumentException if no LogoutListener is registered for the key or the key could not be found automatically. */ private function generateLogoutUrl($key, $referenceType) { - // Fetch the current provider key from token, if possible - if (null === $key && null !== $this->tokenStorage) { - $token = $this->tokenStorage->getToken(); - if (null !== $token && method_exists($token, 'getProviderKey')) { - $key = $token->getProviderKey(); - } - } - - if (null === $key) { - throw new \InvalidArgumentException('Unable to find the current firewall LogoutListener, please provide the provider key manually.'); - } - - if (!array_key_exists($key, $this->listeners)) { - throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key)); - } - - list($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager) = $this->listeners[$key]; + list($logoutPath, $csrfTokenId, $csrfParameter, $csrfTokenManager) = $this->getListener($key); $parameters = null !== $csrfTokenManager ? array($csrfParameter => (string) $csrfTokenManager->getToken($csrfTokenId)) : array(); @@ -128,4 +135,54 @@ private function generateLogoutUrl($key, $referenceType) return $url; } + + /** + * @param string|null $key The firewall key or null use the current firewall key + * + * @return array The logout listener found + * + * @throws \InvalidArgumentException if no LogoutListener is registered for the key or could not be found automatically. + */ + private function getListener($key) + { + if (null !== $key) { + if (isset($this->listeners[$key])) { + return $this->listeners[$key]; + } + + throw new \InvalidArgumentException(sprintf('No LogoutListener found for firewall key "%s".', $key)); + } + + // Fetch the current provider key from token, if possible + if (null !== $this->tokenStorage) { + $token = $this->tokenStorage->getToken(); + + if ($token instanceof AnonymousToken) { + throw new \InvalidArgumentException('Unable to generate a logout url for an anonymous token.'); + } + + if (null !== $token && method_exists($token, 'getProviderKey')) { + $key = $token->getProviderKey(); + + if (isset($this->listeners[$key])) { + return $this->listeners[$key]; + } + } + } + + // Fetch from injected current firewall information, if possible + list($key, $context) = $this->currentFirewall; + + if (isset($this->listeners[$key])) { + return $this->listeners[$key]; + } + + foreach ($this->listeners as $listener) { + if (isset($listener[4]) && $context === $listener[4]) { + return $listener; + } + } + + throw new \InvalidArgumentException('Unable to find the current firewall LogoutListener, please provide the provider key manually.'); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php b/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php new file mode 100644 index 000000000000..727dde0f81b3 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Logout/LogoutUrlGeneratorTest.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Logout; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; + +/** + * @author Maxime Steinhausser + */ +class LogoutUrlGeneratorTest extends TestCase +{ + /** @var TokenStorage */ + private $tokenStorage; + /** @var LogoutUrlGenerator */ + private $generator; + + protected function setUp() + { + $requestStack = $this->getMockBuilder(RequestStack::class)->getMock(); + $request = $this->getMockBuilder(Request::class)->getMock(); + $requestStack->method('getCurrentRequest')->willReturn($request); + + $this->tokenStorage = new TokenStorage(); + $this->generator = new LogoutUrlGenerator($requestStack, null, $this->tokenStorage); + } + + public function testGetLogoutPath() + { + $this->generator->registerListener('secured_area', '/logout', null, null); + + $this->assertSame('/logout', $this->generator->getLogoutPath('secured_area')); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage No LogoutListener found for firewall key "unregistered_key". + */ + public function testGetLogoutPathWithoutLogoutListenerRegisteredForKeyThrowsException() + { + $this->generator->registerListener('secured_area', '/logout', null, null, null); + + $this->generator->getLogoutPath('unregistered_key'); + } + + public function testGuessFromToken() + { + $this->tokenStorage->setToken(new UsernamePasswordToken('user', 'password', 'secured_area')); + $this->generator->registerListener('secured_area', '/logout', null, null); + + $this->assertSame('/logout', $this->generator->getLogoutPath()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unable to generate a logout url for an anonymous token. + */ + public function testGuessFromAnonymousTokenThrowsException() + { + $this->tokenStorage->setToken(new AnonymousToken('default', 'anon.')); + + $this->generator->getLogoutPath(); + } + + public function testGuessFromCurrentFirewallKey() + { + $this->generator->registerListener('secured_area', '/logout', null, null); + $this->generator->setCurrentFirewall('secured_area'); + + $this->assertSame('/logout', $this->generator->getLogoutPath()); + } + + public function testGuessFromCurrentFirewallContext() + { + $this->generator->registerListener('secured_area', '/logout', null, null, null, 'secured'); + $this->generator->setCurrentFirewall('admin', 'secured'); + + $this->assertSame('/logout', $this->generator->getLogoutPath()); + } + + public function testGuessFromTokenWithoutProviderKeyFallbacksToCurrentFirewall() + { + $this->tokenStorage->setToken($this->getMockBuilder(TokenInterface::class)->getMock()); + $this->generator->registerListener('secured_area', '/logout', null, null); + $this->generator->setCurrentFirewall('secured_area'); + + $this->assertSame('/logout', $this->generator->getLogoutPath()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Unable to find the current firewall LogoutListener, please provide the provider key manually + */ + public function testUnableToGuessThrowsException() + { + $this->generator->registerListener('secured_area', '/logout', null, null); + + $this->generator->getLogoutPath(); + } +}