8000 feature #37336 [Security] Let security factories add firewall listene… · symfony/symfony@0fa01ae · GitHub
[go: up one dir, main page]

Skip to content

Commit 0fa01ae

Browse files
committed
feature #37336 [Security] Let security factories add firewall listeners (scheb)
This PR was merged into the 5.2-dev branch. Discussion ---------- [Security] Let security factories add firewall listeners | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | License | MIT | Doc PR | n/a Hello there, I'm the author of `scheb/two-factor-bundle`, which extends Symfony's security layer with two-factor authentication. I've been closely following the recent changes by @wouterj to rework the security layer with "authenticators" (great work!). While I managed to make my bundle work with authenticators, I see some limitations in the security layer that I'd like to address to make such extensions easier to implement. With the new authenticator-based security system, it is no longer possible to add a authentication listener to the firewall. The only way to do it is a dirty compiler pass, which extends the argument on the `security.firewall.map.context.[firewallName]` service (like I do in: https://github.com/scheb/2fa/blob/ed2ce9804b6a78fe539894e77038ab96a52f5c56/src/bundle/DependencyInjection/Compiler/AccessListenerCompilerPass.php). This is quite ugly and hacky, so I believe there should be an easier and clean way to add firewall-level listeners. This PR adds an interface, which may be implemented by security factories and lets them add additional listeners to the firewall. Why would you want to do that? There are certain use-cases that require extra logic to handle a request within the firewall. For example in my bundle, I need to handle the intermediate state between login and the completion of two-factor authentication. So ideally, I'm able to execute some code from the firewall right before `Symfony\Component\Security\Http\Firewall\AccessListener`. In the old security system, I could handle this in my authentication listener, which I had to implement anyways. With the new authenticator-based system this option is gone. In the ideal world, I could add a firewall listener and tell it to execute between `LogoutListener` and `AccessListener`. This is a draft, so I'd like to hear your opinion on this :) There's another issue, regarding the order of execution, which I'm addressing with #37337. Commits ------- 0a4fcea Add interface to let security factories add their own firewall listeners
2 parents 3267e8e + 0a4fcea commit 0fa01ae

File tree

4 files changed

+96
-0
lines changed

4 files changed

+96
-0
lines changed

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
5.2.0
5+
-----
6+
7+
* Added `FirewallListenerFactoryInterface`, which can be implemented by security factories to add firewall listeners
8+
49
5.1.0
510
-----
611

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\DependencyInjection\Security\Factory;
13+
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
16+
/**
17+
* Can be implemented by a security factory to add a listener to the firewall.
18+
*
19+
* @author Christian Scheb <me@christianscheb.de>
20+
*/
21+
interface FirewallListenerFactoryInterface
22+
{
23+
/**
24+
* Creates the firewall listener services for the provided configuration.
25+
*
26+
* @return string[] The listener service IDs to be used by the firewall
27+
*/
28+
public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array;
29+
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
1515
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\EntryPointFactoryInterface;
16+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface;
1617
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory;
1718
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
1819
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
@@ -570,6 +571,14 @@ private function createAuthenticationListeners(ContainerBuilder $container, stri
570571
$listeners[] = new Reference($listenerId);
571572
$authenticationProviders[] = $provider;
572573
}
574+
575+
if ($factory instanceof FirewallListenerFactoryInterface) {
576+
$firewallListenerIds = $factory->createListeners($container, $id, $firewall[$key]);
577+
foreach ($firewallListenerIds as $firewallListenerId) {
578+
$listeners[] = new Reference($firewallListenerId);
579+
}
580+
}
581+
573582
$hasListeners = true;
574583
}
575584
}

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension;
16+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FirewallListenerFactoryInterface;
17+
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
1618
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
1719
use Symfony\Bundle\SecurityBundle\SecurityBundle;
1820
use Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Fixtures\UserProvider\DummyProvider;
1921
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FirewallEntryPointBundle\Security\EntryPointStub;
2022
use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AppCustomAuthenticator;
23+
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
2124
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
2225
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
2326
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -601,6 +604,29 @@ public function testCompilesWithSessionListenerWithStatefulllFirewallWithAuthent
601604
$this->assertTrue($container->has('security.listener.session.'.$firewallId));
602605
}
603606

607+
public function testConfigureCustomFirewallListener(): void
608+
{
609+
$container = $this->getRawContainer();
610+
/** @var SecurityExtension $extension */
611+
$extension = $container->getExtension('security');
612+
$extension->addSecurityListenerFactory(new TestFirewallListenerFactory());
613+
614+
$container->loadFromExtension('security', [
615+
'firewalls' => [
616+
'main' => [
617+
'custom_listener' => true,
618+
],
619+
],
620+
]);
621+
622+
$container->compile();
623+
624+
/** @var IteratorArgument $listenersIteratorArgument */
625+
$listenersIteratorArgument = $container->getDefinition('security.firewall.map.context.main')->getArgument(0);
626+
$firewallListeners = array_map('strval', $listenersIteratorArgument->getValues());
627+
$this->assertContains('custom_firewall_listener_id', $firewallListeners);
628+
}
629+
604630
protected function getRawContainer()
605631
{
606632
$container = new ContainerBuilder();
@@ -689,3 +715,30 @@ public function supportsRememberMe()
689715
{
690716
}
691717
}
718+
719+
class TestFirewallListenerFactory implements SecurityFactoryInterface, FirewallListenerFactoryInterface
720+
{
721+
public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array
722+
{
723+
return ['custom_firewall_listener_id'];
724+
}
725+
726+
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
727+
{
728+
return ['provider_id', 'listener_id', $defaultEntryPoint];
729+
}
730+
731+
public function getPosition()
732+
{
733+
return 'form';
734+
}
735+
736+
public function getKey()
737+
{
738+
return 'custom_listener';
739+
}
740+
741+
public function addConfiguration(NodeDefinition $builder)
742+
{
743+
}
744+
}

0 commit comments

Comments
 (0)
0