Skip to content

Commit 09a1c0f

Browse files
[Security] add "anonymous: lazy" mode to firewalls
1 parent 20df3a1 commit 09a1c0f

File tree

12 files changed

+237
-5
lines changed

12 files changed

+237
-5
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')->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($this)), 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/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(\Closure $initializer): void
57+
{
58+
$this->initializer = $initializer;
59+
}
60+
4961
public function reset()
5062
{
5163
$this->setToken(null);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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\Component\Security\Core\Exception;
13+
14+
use Symfony\Component\HttpFoundation\Response;
15+
16+
/**
17+
* Wraps a lazily computed response in a signaling exception.
18+
*
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
class LazyResponseException extends \Exception implements ExceptionInterface
22+
{
23+
private $response;
24+
25+
public function __construct(Response $response)
26+
{
27+
$this->response = $response;
28+
}
29+
30+
public function getResponse(): Response
31+
{
32+
return $this->response;
33+
}
34+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy