Skip to content

Commit 2a766b1

Browse files
[Security] add "anonymous: lazy" mode to firewalls
1 parent 3c7172d commit 2a766b1

File tree

13 files changed

+238
-6
lines changed

13 files changed

+238
-6
lines changed

src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\HttpFoundation\Response;
1818
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
1919
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
20+
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
2021
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
2122
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
2223
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
@@ -127,7 +128,7 @@ public function collect(Request $request, Response $response, \Exception $except
127128

128129
$logoutUrl = null;
129130
try {
130-
if (null !== $this->logoutUrlGenerator) {
131+
if (null !== $this->logoutUrlGenerator && $token && !$token instanceof AnonymousToken) {
131132
$logoutUrl = $this->logoutUrlGenerator->getLogoutPath();
132133
}
133134
} catch (\Exception $e) {

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ public function getKey()
5555
public function addConfiguration(NodeDefinition $builder)
5656
{
5757
$builder
58+
->beforeNormalization()
59+
->ifTrue(function ($v) { return 'lazy' === $v; })
60+
->then(function ($v) { return ['lazy' => true]; })
61+
->end()
5862
->children()
63+
->booleanNode('lazy')->defaultFalse()->end()
5964
->scalarNode('secret')->defaultNull()->end()
6065
->end()
6166
;

src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,8 @@ private function createFirewalls(array $config, ContainerBuilder $container)
243243
list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId);
244244

245245
$contextId = 'security.firewall.map.context.'.$name;
246-
$context = $container->setDefinition($contextId, new ChildDefinition('security.firewall.context'));
246+
$context = new ChildDefinition($firewall['stateless'] || empty($firewall['anonymous']['lazy']) ? 'security.firewall.context' : 'security.firewall.lazy_context');
247+
$context = $container->setDefinition($contextId, $context);
247248
$context
248249
->replaceArgument(0, new IteratorArgument($listeners))
249250
->replaceArgument(1, $exceptionListener)
@@ -409,7 +410,9 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
409410
}
410411

411412
// Access listener
412-
$listeners[] = new Reference('security.access_listener');
413+
if ($firewall['stateless'] || empty($firewall['anonymous']['lazy'])) {
414+
$listeners[] = new Reference('security.access_listener');
415+
}
413416

414417
// Exception listener
415418
$exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless']));

src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,16 @@
151151
<argument /> <!-- FirewallConfig -->
152152
</service>
153153

154+
<service id="security.firewall.lazy_context" class="Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext" abstract="true">
155+
<argument type="collection" />
156+
<argument type="service" id="security.exception_listener" />
157+
<argument /> <!-- LogoutListener -->
158+
<argument /> <!-- FirewallConfig -->
159+
<argument type="service" id="security.access_listener" />
160+
<argument type="service" id="security.untracked_token_storage" />
161+
<argument type="service" id="security.access_map" />
162+
</service>
163+
154164
<service id="security.firewall.config" class="Symfony\Bundle\SecurityBundle\Security\FirewallConfig" abstract="true">
155165
<argument /> <!-- name -->
156166
<argument /> <!-- user_checker -->
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Security;
13+
14+
use Symfony\Component\HttpKernel\Event\RequestEvent;
15+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
16+
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
17+
use Symfony\Component\Security\Core\Exception\LazyResponseException;
18+
use Symfony\Component\Security\Http\AccessMapInterface;
19+
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
20+
use Symfony\Component\Security\Http\Firewall\AccessListener;
21+
use Symfony\Component\Security\Http\Firewall\ExceptionListener;
22+
use Symfony\Component\Security\Http\Firewall\LogoutListener;
23+
24+
/**
25+
* Lazily calls authentication listeners when actually required by the access listener.
26+
*
27+
* @author Nicolas Grekas <p@tchwork.com>
28+
*/
29+
class LazyFirewallContext extends FirewallContext
30+
{
31+
private $accessListener;
32+
private $tokenStorage;
33+
private $map;
34+
35+
public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, AccessListener $accessListener, TokenStorage $tokenStorage, AccessMapInterface $map)
36+
{
37+
parent::__construct($listeners, $exceptionListener, $logoutListener, $config);
38+
39+
$this->accessListener = $accessListener;
40+
$this->tokenStorage = $tokenStorage;
41+
$this->map = $map;
42+
}
43+
44+
public function getListeners(): iterable
45+
{
46+
return [$this];
47+
}
48+
49+
public function __invoke(RequestEvent $event)
50+
{
51+
$this->tokenStorage->setInitializer(function () use ($event) {
52+
$event = new LazyResponseEvent($event);
53+
foreach (parent::getListeners() as $listener) {
54+
if (\is_callable($listener)) {
55+
$listener($event);
56+
} else {
57+
@trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($listener)), E_USER_DEPRECATED);
58+
$listener->handle($event);
59+
}
60+
}
61+
});
62+
63+
try {
64+
[$attributes] = $this->map->getPatterns($event->getRequest());
65+
66+
if ($attributes && [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY] !== $attributes) {
67+
($this->accessListener)($event);
68+
}
69+
} catch (LazyResponseException $e) {
70+
$event->setResponse($e->getResponse());
71+
}
72+
}
73+
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,6 @@ public function profileAction()
5959

6060
public function homepageAction()
6161
{
62-
return new Response('<html><body>Homepage</body></html>');
62+
return (new Response('<html><body>Homepage</body></html>'))->setPublic();
6363
}
6464
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ public function testInvalidIpsInAccessControl()
129129
$client->request('GET', '/unprotected_resource');
130130
}
131131

132+
public function testPublicHomepage()
133+
{
134+
$client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml']);
135+
$client->request('GET', '/en/');
136+
137+
$this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse());
138+
$this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public'));
139+
$this->assertSame(0, self::$container->get('session')->getUsageIndex());
140+
}
141+
132142
private function assertAllowed($client, $path)
133143
{
134144
$client->request('GET', $path);

src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ security:
2727
check_path: /login_check
2828
default_target_path: /profile
2929
logout: ~
30-
anonymous: ~
30+
anonymous: lazy
3131

3232
# This firewall is here just to check its the logout functionality
3333
second_area:
@@ -38,6 +38,7 @@ security:
3838
path: /second/logout
3939

4040
access_control:
41+
- { path: ^/en/$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
4142
- { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
4243
- { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
4344
- { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY }

src/Symfony/Bundle/SecurityBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"symfony/security-core": "^4.4",
2525
"symfony/security-csrf": "^4.2|^5.0",
2626
"symfony/security-guard": "^4.2|^5.0",
27-
"symfony/security-http": "^4.3"
27+
"symfony/security-http": "^4.4"
2828
},
2929
"require-dev": {
3030
"symfony/asset": "^3.4|^4.0|^5.0",

src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,18 @@
2525
class TokenStorage implements TokenStorageInterface, ResetInterface
2626
{
2727
private $token;
28+
private $initializer;
2829

2930
/**
3031
* {@inheritdoc}
3132
*/
3233
public function getToken()
3334
{
35+
if ($initializer = $this->initializer) {
36+
$this->initializer = null;
37+
$initializer();
38+
}
39+
3440
return $this->token;
3541
}
3642

@@ -43,9 +49,15 @@ public function setToken(TokenInterface $token = null)
4349
@trigger_error(sprintf('Not implementing the "%s::getRoleNames()" method in "%s" is deprecated since Symfony 4.3.', TokenInterface::class, \get_class($token)), E_USER_DEPRECATED);
4450
}
4551

52+
$this->initializer = null;
4653
$this->token = $token;
4754
}
4855

56+
public function setInitializer(?callable $initializer): void
57+
{
58+
$this->initializer = $initializer;
59+
}
60+
4961
public function reset()
5062
{
5163
$this->setToken(null);

0 commit comments

Comments
 (0)