Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 48c698a

Browse filesBrowse files
[Security] add "anonymous: lazy" mode to firewalls
1 parent 3c7172d commit 48c698a
Copy full SHA for 48c698a

File tree

13 files changed

+238
-6
lines changed
Filter options

13 files changed

+238
-6
lines changed

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php
+2-1Lines 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 instanceof AnonymousToken) {
131132
$logoutUrl = $this->logoutUrlGenerator->getLogoutPath();
132133
}
133134
} catch (\Exception $e) {

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php
+5Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+5-2Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
+10Lines 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 -->
+73Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php
+1-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php
+10Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml
+2-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/composer.json
+1-1Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authentication/Token/Storage/TokenStorage.php
+12Lines 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);
+34Lines 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+
* A signaling exception that wraps a lazily computed response.
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+
}
+76Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\Http\Event;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\HttpKernel\Event\RequestEvent;
17+
use Symfony\Component\HttpKernel\HttpKernelInterface;
18+
use Symfony\Component\Security\Core\Exception\LazyResponseException;
19+
20+
/**
21+
* Wraps a lazily computed response in a signaling exception.
22+
*
23+
* @author Nicolas Grekas <p@tchwork.com>
24+
*/
25+
final class LazyResponseEvent extends RequestEvent
26+
{
27+
private $event;
28+
29+
public function __construct(parent $event)
30+
{
31+
$this->event = $event;
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function setResponse(Response $response)
38+
{
39+
$this->stopPropagation();
40+
$this->event->stopPropagation();
41+
42+
throw new LazyResponseException($response);
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public function getKernel(): HttpKernelInterface
49+
{
50+
return $this->event->getKernel();
51+
}
52+
53+
/**
54+
* {@inheritdoc}
55+
*/
56+
public function getRequest(): Request
57+
{
58+
return $this->event->getRequest();
59+
}
60+
61+
/**
62+
* {@inheritdoc}
63+
*/
64+
public function getRequestType(): int
65+
{
66+
return $this->event->getRequestType();
67+
}
68+
69+
/**
70+
* {@inheritdoc}
71+
*/
72+
public function isMasterRequest(): bool
73+
{
74+
return $this->event->isMasterRequest();
75+
}
76+
}

‎src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/Firewall/ExceptionListener.php
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use Symfony\Component\Security\Core\Exception\AccountStatusException;
2727
use Symfony\Component\Security\Core\Exception\AuthenticationException;
2828
use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
29+
use Symfony\Component\Security\Core\Exception\LazyResponseException;
2930
use Symfony\Component\Security\Core\Exception\LogoutException;
3031
use Symfony\Component\Security\Core\Security;
3132
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
@@ -103,6 +104,12 @@ public function onKernelException(GetResponseForExceptionEvent $event)
103104
return;
104105
}
105106

107+
if ($exception instanceof LazyResponseException) {
108+
$event->setResponse($exception->getResponse());
109+
110+
return;
111+
}
112+
106113
if ($exception instanceof LogoutException) {
107114
$this->handleLogoutException($exception);
108115

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.