From cad381f56294cdbe603291e897d1f65f09e9c630 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 2 Jul 2018 13:54:43 +0200 Subject: [PATCH] [Security] add "lazy_authentication" mode to firewalls --- .../RegisterForAutoconfigurationPass.php | 40 ++++++++++ .../DependencyInjection/MainConfiguration.php | 1 + .../DependencyInjection/SecurityExtension.php | 7 +- .../Resources/config/collectors.xml | 2 +- .../Resources/config/security.xml | 13 +++- .../Resources/config/security_listeners.xml | 8 ++ .../Security/LazyFirewallContext.php | 58 ++++++++++++++ .../Bundle/SecurityBundle/SecurityBundle.php | 2 + .../Token/Storage/LazyTokenStorage.php | 59 ++++++++++++++ .../Core/Exception/LazyResponseException.php | 34 +++++++++ .../Security/Http/Event/LazyResponseEvent.php | 76 +++++++++++++++++++ .../Http/Firewall/ExceptionListener.php | 3 + .../Http/Firewall/LazyAccessListener.php | 53 +++++++++++++ 13 files changed, 352 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterForAutoconfigurationPass.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php create mode 100644 src/Symfony/Component/Security/Core/Authentication/Token/Storage/LazyTokenStorage.php create mode 100644 src/Symfony/Component/Security/Core/Exception/LazyResponseException.php create mode 100644 src/Symfony/Component/Security/Http/Event/LazyResponseEvent.php create mode 100644 src/Symfony/Component/Security/Http/Firewall/LazyAccessListener.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterForAutoconfigurationPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterForAutoconfigurationPass.php new file mode 100644 index 0000000000000..4c2fe0f5f0db5 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterForAutoconfigurationPass.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Symfony\Bridge\Monolog\Processor\ProcessorInterface; +use Symfony\Component\DependencyInjection\Argument\BoundArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + +/** + * Adds a rule to bind "security.actual_token_storage" to ProcessorInterface instances. + * + * @author Nicolas Grekas + */ +class RegisterForAutoconfigurationPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if ($container->has('security.actual_token_storage')) { + $processorAutoconfiguration = $container->registerForAutoconfiguration(ProcessorInterface::class); + $processorAutoconfiguration->setBindings($processorAutoconfiguration->getBindings() + array( + TokenStorageInterface::class => new BoundArgument(new Reference('security.actual_token_storage'), false), + )); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index f4e06e848eb90..3dd826c400e20 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -196,6 +196,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->scalarNode('entry_point')->end() ->scalarNode('provider')->end() ->booleanNode('stateless')->defaultFalse()->end() + ->booleanNode('lazy_authentication')->defaultFalse()->end() ->scalarNode('context')->cannotBeEmpty()->end() ->booleanNode('logout_on_user_change') ->defaultTrue() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 83ef38b69c983..5a39e23cd27c9 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -204,7 +204,8 @@ private function createFirewalls($config, ContainerBuilder $container) list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId); $contextId = 'security.firewall.map.context.'.$name; - $context = $container->setDefinition($contextId, new ChildDefinition('security.firewall.context')); + $context = new ChildDefinition($firewall['stateless'] || !$firewall['lazy_authentication'] ? 'security.firewall.context' : 'security.firewall.lazy_context'); + $context = $container->setDefinition($contextId, $context); $context ->replaceArgument(0, new IteratorArgument($listeners)) ->replaceArgument(1, $exceptionListener) @@ -374,7 +375,9 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a } // Access listener - $listeners[] = new Reference('security.access_listener'); + if ($firewall['stateless'] || !$firewall['lazy_authentication']) { + $listeners[] = new Reference('security.access_listener'); + } // Exception listener $exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless'])); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml index a8170af900ff9..bf32a2bfa386a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml @@ -9,7 +9,7 @@ - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 9fbdae0bdd46f..6196ccc52e144 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -21,11 +21,14 @@ - + + + + @@ -145,6 +148,14 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 4f598d86ef52f..2605e2107d98a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -242,5 +242,13 @@ + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php new file mode 100644 index 0000000000000..928b9bd0f5b88 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Security; + +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\Security\Core\Exception\LazyResponseException; +use Symfony\Component\Security\Http\Event\LazyResponseEvent; +use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\LazyAccessListener; +use Symfony\Component\Security\Http\Firewall\ListenerInterface; +use Symfony\Component\Security\Http\Firewall\LogoutListener; + +/** + * Lazily calls authentication listeners when actually required by the access listener. + * + * @author Nicolas Grekas + */ +class LazyFirewallContext extends FirewallContext implements ListenerInterface +{ + private $accessListener; + + public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, LazyAccessListener $accessListener) + { + parent::__construct($listeners, $exceptionListener, $logoutListener, $config); + + $this->accessListener = $accessListener; + } + + public function getListeners(): iterable + { + return array($this); + } + + public function handle(GetResponseEvent $event) + { + $this->accessListener->getTokenStorage()->setInitializer(function () use ($event) { + $event = new LazyResponseEvent($event); + foreach (parent::getListeners() as $listener) { + $listener->handle($event); + } + }); + + try { + $this->accessListener->handle($event); + } catch (LazyResponseException $e) { + $event->setResponse($e->getResponse()); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index d7cbf2e08433d..071071e1bf781 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -15,6 +15,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterForAutoconfigurationPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory; @@ -64,5 +65,6 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new AddSecurityVotersPass()); $container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass()); + $container->addCompilerPass(new RegisterForAutoconfigurationPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200); } } diff --git a/src/Symfony/Component/Security/Core/Authentication/Token/Storage/LazyTokenStorage.php b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/LazyTokenStorage.php new file mode 100644 index 0000000000000..c0c16edee16ce --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Token/Storage/LazyTokenStorage.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\Component\Security\Core\Authentication\Token\Storage; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +/** + * Lazily populates a token storage. + * + * @author Nicolas Grekas + * + * @final + */ +class LazyTokenStorage implements TokenStorageInterface +{ + private $storage; + private $initializer; + + public function __construct(TokenStorageInterface $storage) + { + $this->storage = $storage; + } + + public function setInitializer(\Closure $initializer) + { + $this->initializer = $initializer; + } + + /** + * {@inheritdoc} + */ + public function getToken() + { + if ($initializer = $this->initializer) { + $this->initializer = null; + $initializer(); + } + + return $this->storage->getToken(); + } + + /** + * {@inheritdoc} + */ + public function setToken(TokenInterface $token = null) + { + $this->initializer = null; + $this->storage->setToken($token); + } +} diff --git a/src/Symfony/Component/Security/Core/Exception/LazyResponseException.php b/src/Symfony/Component/Security/Core/Exception/LazyResponseException.php new file mode 100644 index 0000000000000..32f816b02f566 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Exception/LazyResponseException.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +use Symfony\Component\HttpFoundation\Response; + +/** + * Wraps a lazily computed response in a signaling exception. + * + * @author Nicolas Grekas + */ +class LazyResponseException extends \Exception implements ExceptionInterface +{ + private $response; + + public function __construct(Response $response) + { + $this->response = $response; + } + + public function getResponse(): Response + { + return $this->response; + } +} diff --git a/src/Symfony/Component/Security/Http/Event/LazyResponseEvent.php b/src/Symfony/Component/Security/Http/Event/LazyResponseEvent.php new file mode 100644 index 0000000000000..2bcbc64da1531 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Event/LazyResponseEvent.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Event; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\Security\Core\Exception\LazyResponseException; + +/** + * Wraps a lazily computed response in a signaling exception. + * + * @author Nicolas Grekas + * + * @final + */ +class LazyResponseEvent extends GetResponseEvent +{ + private $event; + + public function __construct(parent $event) + { + $this->event = $event; + } + + /** + * {@inheritdoc} + */ + public function setResponse(Response $response) + { + $this->stopPropagation(); + $this->event->stopPropagation(); + + throw new LazyResponseException($response); + } + + /** + * {@inheritdoc} + */ + public function getKernel() + { + return $this->event->getKernel(); + } + + /** + * {@inheritdoc} + */ + public function getRequest() + { + return $this->event->getRequest(); + } + + /** + * {@inheritdoc} + */ + public function getRequestType() + { + return $this->event->getRequestType(); + } + + /** + * {@inheritdoc} + */ + public function isMasterRequest() + { + return $this->event->isMasterRequest(); + } +} diff --git a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php index e3009cd074271..7152ce1d1a73d 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php @@ -25,6 +25,7 @@ use Symfony\Component\Security\Core\Exception\AccountStatusException; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException; +use Symfony\Component\Security\Core\Exception\LazyResponseException; use Symfony\Component\Security\Core\Exception\LogoutException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface; @@ -92,6 +93,8 @@ public function onKernelException(GetResponseForExceptionEvent $event) return $this->handleAuthenticationException($event, $exception); } elseif ($exception instanceof AccessDeniedException) { return $this->handleAccessDeniedException($event, $exception); + } elseif ($exception instanceof LazyResponseException) { + return $event->setResponse($exception->getResponse()); } elseif ($exception instanceof LogoutException) { return $this->handleLogoutException($exception); } diff --git a/src/Symfony/Component/Security/Http/Firewall/LazyAccessListener.php b/src/Symfony/Component/Security/Http/Firewall/LazyAccessListener.php new file mode 100644 index 0000000000000..84bc1bcae2e70 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/LazyAccessListener.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Firewall; + +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\LazyTokenStorage; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; +use Symfony\Component\Security\Http\AccessMapInterface; + +/** + * Enforces access control rules while allowing unauthenticated access when no attributes are found. + * + * @author Nicolas Grekas + */ +class LazyAccessListener extends AccessListener +{ + private $tokenStorage; + private $map; + + public function __construct(LazyTokenStorage $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, AuthenticationManagerInterface $authManager) + { + parent::__construct($tokenStorage, $accessDecisionManager, $map, $authManager); + $this->tokenStorage = $tokenStorage; + $this->map = $map; + } + + /** + * {@inheritdoc} + */ + public function handle(GetResponseEvent $event) + { + list($attributes) = $this->map->getPatterns($event->getRequest()); + + if ($attributes) { + return parent::handle($event); + } + } + + public function getTokenStorage(): LazyTokenStorage + { + return $this->tokenStorage; + } +}