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 @@
+ */
+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;
+ }
+}