8000 Add ability to prioritize firewall listeners · symfony/symfony@7f05dc3 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7f05dc3

Browse files
committed
Add ability to prioritize firewall listeners
1 parent ce8f8a5 commit 7f05dc3

File tree

13 files changed

+215
-29
lines changed

13 files changed

+215
-29
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class UnusedTagsPass implements CompilerPassInterface
7373
'routing.loader',
7474
'routing.route_loader',
7575
'security.expression_language_provider',
76+
'security.firewall_map_context',
7677
'security.remember_me_aware',
7778
'security.voter',
7879
'serializer.encoder',
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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
< 341A code>9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
15+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Definition;
18+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
21+
22+
/**
23+
* Sorts firewall listeners based on the execution order provided by FirewallListenerInterface::getPriority().
24+
*
25+
* @author Christian Scheb <me@christianscheb.de>
26+
*/
27+
class SortFirewallListenersPass implements CompilerPassInterface
28+
{
29+
public function process(ContainerBuilder $container): void
30+
{
31+
$taggedServices = $container->findTaggedServiceIds('security.firewall_map_context');
32+
foreach ($taggedServices as $serviceId => $attributes) {
33+
$firewallContextDefinition = $container->getDefinition($serviceId);
34+
$this->sortFirewallContextListeners($firewallContextDefinition, $container);
35+
}
36+
}
37+
38+
private function sortFirewallContextListeners(Definition $definition, ContainerBuilder $container): void
39+
{
40+
/** @var IteratorArgument $listenerIteratorArgument */
41+
$listenerIteratorArgument = $definition->getArgument(0);
42+
$prioritiesByServiceId = $this->getListenerPriorities($listenerIteratorArgument, $container);
43+
44+
$listeners = $listenerIteratorArgument->getValues();
45+
usort($listeners, function (Reference $a, Reference $b) use ($prioritiesByServiceId) {
46+
return $prioritiesByServiceId[(string) $b] <=> $prioritiesByServiceId[(string) $a];
47+
});
48+
49+
$listenerIteratorArgument->setValues(array_values($listeners));
50+
}
51+
52+
private function getListenerPriorities(IteratorArgument $listeners, ContainerBuilder $container): array
53+
{
54+
$priorities = [];
55+
56+
foreach ($listeners->getValues() as $reference) {
57+
$id = (string) $reference;
58+
$def = $container->getDefinition($id);
59+
60+
// We must assume that the class value has been correctly filled, even if the service is created by a factory
61+
$class = $def->getClass();
62+
63+
if (!$r = $container->getReflectionClass($class)) {
64+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
65+
}
66+
67+
$priority = 0;
68+
if ($r->isSubclassOf(FirewallListenerInterface::class)) {
69+
$priority = $r->getMethod('getPriority')->invoke(null);
70+
}
71+
72+
$priorities[$id] = $priority;
73+
}
74+
75+
return $priorities;
76+
}
77+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
use Symfony\Component\DependencyInjection\Reference;
3636
use Symfony\Component\EventDispatcher\EventDispatcher;
3737
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
38-
use Symfony\Component\Ldap\Entry;
3938
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
4039
use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder;
4140
use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder;
@@ -285,6 +284,7 @@ private function createFirewalls(array $config, ContainerBuilder $container)
285284
->replaceArgument(1, $exceptionListener)
286285
->replaceArgument(2, $logoutListener)
287286
->replaceArgument(3, new Reference($configId))
287+
->addTag('security.firewall_map_context')
288288
;
289289

290290
$contextRefs[$contextId] = new Reference($contextId);
@@ -395,6 +395,7 @@ private function createFirewall(ContainerBuilder $container, string $id, array $
395395
'csrf_token_id' => $firewall['logout']['csrf_token_id'],
396396
'logout_path' => $firewall['logout']['path'],
397397
]);
398+
$listeners[] = new Reference($logoutListenerId);
398399

399400
// add default logout listener
400401
if (isset($firewall['logout']['success_handler'])) {

src/Symfony/Bundle/SecurityBundle/SecurityBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfFeaturesPass;
1818
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterLdapLocatorPass;
1919
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass;
20+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass;
2021
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory;
2122
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory;
2223
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory;
@@ -75,6 +76,7 @@ public function build(ContainerBuilder $container)
7576
$container->addCompilerPass(new RegisterCsrfFeaturesPass());
7677
$container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200);
7778
$container->addCompilerPass(new RegisterLdapLocatorPass());
79+
$container->addCompilerPass(new SortFirewallListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);
7880

7981
$container->addCompilerPass(new AddEventAliasesPass([
8082
AuthenticationSuccessEvent::class => AuthenticationEvents::AUTHENTICATION_SUCCESS,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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\Tests\DependencyInjection\Compiler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\SortFirewallListenersPass;
16+
use Symfony\Bundle\SecurityBundle\Security\FirewallContext;
17+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
18+
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\Security\Http\Firewall\FirewallListenerInterface;
21+
22+
class SortFirewallListenersPassTest extends TestCase
23+
{
24+
public function testSortFirewallListeners()
25+
{
26+
$container = new ContainerBuilder();
27+
28+
$container->register('listener_priority_minus1', FirewallListenerPriorityMinus1::class);
29+
$container->register('listener_priority_1', FirewallListenerPriority1::class);
30+
$container->register('listener_priority_2', FirewallListenerPriority2::class);
31+
$container->register('listener_interface_not_implemented', \stdClass::class);
32+
33+
$firewallContext = $container->register('security.firewall.map.context.main', FirewallContext::class);
34+
$firewallContext->addTag('security.firewall_map_context');
35+
36+
$listeners = new IteratorArgument([
37+
new Reference('listener_priority_minus1'),
38+
new Reference('listener_priority_1'),
39+
new Reference('listener_priority_2'),
40+
new Reference('listener_interface_not_implemented'),
41+
]);
42+
43+
$firewallContext->setArgument(0, $listeners);
44+
45+
$compilerPass = new SortFirewallListenersPass();
46+
$compilerPass->process($container);
47+
48+
$sortedListeners = $firewallContext->getArgument(0);
49+
$expectedSortedlisteners = [
50+
new Reference('listener_priority_2'),
51+
new Reference('listener_priority_1'),
52+
new Reference('listener_interface_not_implemented'),
53+
new Reference('listener_priority_minus1'),
54+
];
55+
$this->assertEquals($expectedSortedlisteners, $sortedListeners->getValues());
56+
}
57+
}
58+
59+
class FirewallListenerPriorityMinus1 implements FirewallListenerInterface
60+
{
61+
public static function getPriority(): int
62+
{
63+
return -1;
64+
}
65+
}
66+
67+
class FirewallListenerPriority1 implements FirewallListenerInterface
68+
{
69+
public static function getPriority(): int
70+
{
71+
return 1;
72+
}
73+
}
74+
75+
class FirewallListenerPriority2 implements FirewallListenerInterface
76+
{
77+
public static function getPriority(): int
78+
{
79+
return 2;
80+
}
81+
}

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ public function testFirewalls()
168168
'security.authentication.listener.rememberme.secure',
169169
'security.authentication.listener.anonymous.secure',
170170
'security.authentication.switchuser_listener.secure',
171+
'security.logout_listener.secure',
171172
'security.access_listener',
172173
],
173174
[

src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
2424
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
2525
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
26+
use Symfony\Component\DependencyInjection\Compiler\ResolveChildDefinitionsPass;
2627
use Symfony\Component\DependencyInjection\ContainerBuilder;
2728
use Symfony\Component\DependencyInjection\Reference;
2829
use Symfony\Component\ExpressionLanguage\Expression;
@@ -671,7 +672,7 @@ protected function getRawContainer()
671672
$bundle = new SecurityBundle();
672673
$bundle->build($container);
673674

674-
$container->getCompilerPassConfig()->setOptimizationPasses([]);
675+
$container->getCompilerPassConfig()->setOptimizationPasses([new ResolveChildDefinitionsPass()]);
675676
$container->getCompilerPassConfig()->setRemovingPasses([]);
676677
$container->getCompilerPassConfig()->setAfterRemovingPasses([]);
677678

@@ -764,11 +765,16 @@ class TestFirewallListenerFactory implements SecurityFactoryInterface, FirewallL
764765
{
765766
public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array
766767
{
768+
$container->register('custom_firewall_listener_id', \stdClass::class);
769+
767770
return ['custom_firewall_listener_id'];
768771
}
769772

770773
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
771774
{
775+
$container->register('provider_id', \stdClass::class);
776+
$container->register('listener_id', \stdClass::class);
777+
772778
return ['provider_id', 'listener_id', $defaultEntryPoint];
773779
}
774780

src/Symfony/Bundle/SecurityBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"symfony/security-core": "^5.1",
2727
"symfony/security-csrf": "^4.4|^5.0",
2828
"symfony/security-guard": "^5.1",
29-
"symfony/security-http": "^5.1,>=5.1.2"
29+
"symfony/security-http": "^5.2"
3030
},
3131
"require-dev": {
3232
"doctrine/doctrine-bundle": "^2.0",

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

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
1616
use Symfony\Component\HttpKernel\Event\RequestEvent;
1717
use Symfony\Component\HttpKernel\KernelEvents;
18-
use Symfony\Component\Security\Http\Firewall\AccessListener;
1918
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2019

2120
/**
@@ -52,36 +51,13 @@ public function onKernelRequest(RequestEvent $event)
5251

5352
$authenticationListeners = $listeners[0];
5453
$exceptionListener = $listeners[1];
55-
$logoutListener = $listeners[2];
5654

5755
if (null !== $exceptionListener) {
5856
$this->exceptionListeners[$event->getRequest()] = $exceptionListener;
5957
$exceptionListener->register($this->dispatcher);
6058
}
6159

62-
$authenticationListeners = function () use ($authenticationListeners, $logoutListener) {
63-
$accessListener = null;
64-
65-
foreach ($authenticationListeners as $listener) {
66-
if ($listener instanceof AccessListener) {
67-
$accessListener = $listener;
68-
69-
continue;
70-
}
71-
72-
yield $listener;
73-
}
74-
75-
if (null !== $logoutListener) {
76-
yield $logoutListener;
77-
}
78-
79-
if (null !== $accessListener) {
80-
yield $accessListener;
81-
}
82-
};
83-
84-
$this->callListeners($event, $authenticationListeners());
60+
$this->callListeners($event, $authenticationListeners);
8561
}
8662

8763
public function onKernelFinishRequest(FinishRequestEvent $event)

src/Symfony/Component/Security/Http/Firewall/AbstractListener.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*
2020
* @author Nicolas Grekas <p@tchwork.com>
2121
*/
22-
abstract class AbstractListener
22+
abstract class AbstractListener implements FirewallListenerInterface
2323
{
2424
final public function __invoke(RequestEvent $event)
2525
{
@@ -39,4 +39,9 @@ abstract public function supports(Request $request): ?bool;
3939
* Does whatever is required to authenticate the request, typically calling $event->setResponse() internally.
4040
*/
4141
abstract public function authenticate(RequestEvent $event);
42+
43+
public static function getPriority(): int
44+
{
45+
return 0; // Default
46+
}
4247
}

src/Symfony/Component/Security/Http/Firewall/AccessListener.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,9 @@ private function createAccessDeniedException(Request $request, array $attributes
122122

123123
return $exception;
124124
}
125+
126+
public static function getPriority(): int
127+
{
128+
return -255;
129+
}
125130
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\Firewall;
13+
14+
/**
15+
* Can be implemented by firewall listeners to define their priority in execution.
16+
*
17+
* @author Christian Scheb <me@christianscheb.de>
18+
*/
19+
interface FirewallListenerInterface
20+
{
21+
/**
22+
* Defines the priority of the listener.
23+
* The higher the number, the earlier a listener is executed.
24+
*/
25+
public static function getPriority(): int;
26+
}

src/Symfony/Component/Security/Http/Firewall/LogoutListener.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,9 @@ protected function requiresLogout(Request $request): bool
142142
{
143143
return isset($this->options['logout_path']) && $this->httpUtils->checkRequestPath($request, $this->options['logout_path']);
144144
}
145+
146+
public static function getPriority(): int
147+
{
148+
return -127;
149+
}
145150
}

0 commit comments

Comments
 (0)
0