diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md index d580cc7a79..1b26220e72 100644 --- a/UPGRADE-5.1.md +++ b/UPGRADE-5.1.md @@ -112,7 +112,7 @@ Routing SecurityBundle -------------- - * Marked the `AbstractFactory`, `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`, + * Marked the `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`, `HttpBasicFactory`, `HttpBasicLdapFactory`, `JsonLoginFactory`, `JsonLoginLdapFactory`, `RememberMeFactory`, `RemoteUserFactory` and `X509Factory` as `@internal`. Instead of extending these classes, create your own implementation based on `SecurityFactoryInterface`. diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 615aceb7dc..ae7c1d9164 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -6,7 +6,7 @@ CHANGELOG * Added XSD for configuration * Added security configuration for priority-based access decision strategy - * Marked the `AbstractFactory`, `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`, `HttpBasicFactory`, `HttpBasicLdapFactory`, `JsonLoginFactory`, `JsonLoginLdapFactory`, `RememberMeFactory`, `RemoteUserFactory` and `X509Factory` as `@internal` + * Marked the `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`, `HttpBasicFactory`, `HttpBasicLdapFactory`, `JsonLoginFactory`, `JsonLoginLdapFactory`, `RememberMeFactory`, `RemoteUserFactory` and `X509Factory` as `@internal` * Renamed method `AbstractFactory#createEntryPoint()` to `AbstractFactory#createDefaultEntryPoint()` 5.0.0 diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index c31e08ba7a..a5d6f7e45e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -23,8 +23,6 @@ use Symfony\Component\DependencyInjection\Reference; * @author Fabien Potencier * @author Lukas Kahwe Smith * @author Johannes M. Schmitt - * - * @internal */ abstract class AbstractFactory implements SecurityFactoryInterface { @@ -67,7 +65,7 @@ abstract class AbstractFactory implements SecurityFactoryInterface } // create entry point if applicable (optional) - $entryPointId = $this->createDefaultEntryPoint($container, $id, $config, $defaultEntryPointId); + $entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPointId); return [$authProviderId, $listenerId, $entryPointId]; } @@ -128,7 +126,7 @@ abstract class AbstractFactory implements SecurityFactoryInterface * * @return string|null the entry point id */ - protected function createDefaultEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId) + protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId) { return $defaultEntryPointId; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php index 0b56e309d5..d7e726b02b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/EntryPointFactoryInterface.php @@ -21,7 +21,10 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; interface EntryPointFactoryInterface { /** - * Creates the entry point and returns the service ID. + * Register the entry point on the container and returns the service ID. + * + * This does not mean that the entry point is also used. This is managed + * by the "entry_point" firewall setting. */ - public function createEntryPoint(ContainerBuilder $container, string $id, array $config): ?string; + public function registerEntryPoint(ContainerBuilder $container, string $id, array $config): ?string; } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 92ce50527d..1f1be87c64 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -92,12 +92,12 @@ class FormLoginFactory extends AbstractFactory implements AuthenticatorFactoryIn return $listenerId; } - protected function createDefaultEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId) + protected function createEntryPoint(ContainerBuilder $container, string $id, array $config, ?string $defaultEntryPointId) { - return $this->createEntryPoint($container, $id, $config); + return $this->registerEntryPoint($container, $id, $config); } - public function createEntryPoint(ContainerBuilder $container, string $id, array $config): string + public function registerEntryPoint(ContainerBuilder $container, string $id, array $config): string { $entryPointId = 'security.authentication.form_entry_point.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php index 283da74373..0b4e12145f 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -113,15 +114,13 @@ class GuardAuthenticationFactory implements SecurityFactoryInterface, Authentica return $authenticatorIds; } - public function createEntryPoint(ContainerBuilder $container, string $id, array $config): ?string + public function registerEntryPoint(ContainerBuilder $container, string $id, array $config): ?string { try { return $this->determineEntryPoint(null, $config); } catch (\LogicException $e) { - // ignore the exception, the new system prefers setting "entry_point" over "guard.entry_point" + throw new InvalidConfigurationException(sprintf('Because you have multiple guard authenticators, you need to set the "entry_point" key to one of your authenticators (%s).', implode(', ', $config['authenticators']))); } - - return null; } private function determineEntryPoint(?string $defaultEntryPointId, array $config): string diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index 5dfe0747d1..1973f83f04 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -38,7 +38,7 @@ class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactory // entry point $entryPointId = $defaultEntryPoint; if (null === $entryPointId) { - $entryPointId = $this->createEntryPoint($container, $id, $config); + $entryPointId = $this->registerEntryPoint($container, $id, $config); } // listener @@ -82,7 +82,7 @@ class HttpBasicFactory implements SecurityFactoryInterface, AuthenticatorFactory ; } - public function createEntryPoint(ContainerBuilder $container, string $id, array $config): string + public function registerEntryPoint(ContainerBuilder $container, string $id, array $config): string { $entryPointId = 'security.authentication.basic_entry_point.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php index 3e0bf5b0e3..d614e9f137 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php @@ -43,7 +43,7 @@ class HttpBasicLdapFactory extends HttpBasicFactory ; // entry point - $entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPoint); + $entryPointId = $this->registerEntryPoint($container, $id, $config, $defaultEntryPoint); if (!empty($config['query_string'])) { if ('' === $config['search_dn'] || '' === $config['search_password']) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index ca4a2abc25..7b5edc7cac 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -33,6 +33,7 @@ use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\Ldap\Entry; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; @@ -443,6 +444,9 @@ class SecurityExtension extends Extension implements PrependExtensionInterface if (!$this->authenticatorManagerEnabled) { $authenticationProviders = array_merge($authenticationProviders, $firewallAuthenticationProviders); } else { + // $configuredEntryPoint is resolved into a service ID and stored in $defaultEntryPoint + $configuredEntryPoint = $defaultEntryPoint; + // authenticator manager $authenticators = array_map(function ($id) { return new Reference($id); @@ -543,7 +547,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface $authenticationProviders[] = $authenticators; } - if ($factory instanceof EntryPointFactoryInterface && ($entryPoint = $factory->createEntryPoint($container, $id, $firewall[$key], null))) { + if ($factory instanceof EntryPointFactoryInterface && ($entryPoint = $factory->registerEntryPoint($container, $id, $firewall[$key]))) { $entryPoints[$key] = $entryPoint; } } else { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php index 291fb1200e..8215aaf9c9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php @@ -178,7 +178,7 @@ class GuardAuthenticationFactoryTest extends TestCase $authenticators = $factory->createAuthenticator($container, $firewallName, $config, $userProviderId); $this->assertEquals('security.authenticator.guard.my_firewall.0', $authenticators[0]); - $entryPointId = $factory->createEntryPoint($container, $firewallName, $config, null); + $entryPointId = $factory->registerEntryPoint($container, $firewallName, $config, null); $this->assertEquals('authenticator123', $entryPointId); $authenticatorDefinition = $container->getDefinition('security.authenticator.guard.my_firewall.0'); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index c395ed1b52..da09e432a0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -16,11 +16,19 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Fixtures\UserProvider\DummyProvider; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FirewallEntryPointBundle\Security\EntryPointStub; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AppCustomAuthenticator; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\AuthenticatorInterface; class SecurityExtensionTest extends TestCase { @@ -413,6 +421,90 @@ class SecurityExtensionTest extends TestCase $this->assertEquals(new Reference('security.user.provider.concrete.second'), $container->getDefinition('security.authentication.switchuser_listener.foobar')->getArgument(1)); } + /** + * @dataProvider provideEntryPointFirewalls + */ + public function testAuthenticatorManagerEnabledEntryPoint(array $firewall, $entryPointId) + { + $container = $this->getRawContainer(); + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'providers' => [ + 'first' => ['id' => 'users'], + ], + + 'firewalls' => [ + 'main' => $firewall, + ], + ]); + + $container->compile(); + + $this->assertEquals($entryPointId, (string) $container->getDefinition('security.firewall.map.config.main')->getArgument(7)); + $this->assertEquals($entryPointId, (string) $container->getDefinition('security.exception_listener.main')->getArgument(4)); + } + + public function provideEntryPointFirewalls() + { + // only one entry point available + yield [['http_basic' => true], 'security.authentication.basic_entry_point.main']; + // explicitly configured by authenticator key + yield [['form_login' => true, 'http_basic' => true, 'entry_point' => 'form_login'], 'security.authentication.form_entry_point.main']; + // explicitly configured another service + yield [['form_login' => true, 'entry_point' => EntryPointStub::class], EntryPointStub::class]; + // no entry point required + yield [['json_login' => true], null]; + + // only one guard authenticator entry point available + yield [[ + 'guard' => ['authenticators' => [AppCustomAuthenticator::class]], + ], AppCustomAuthenticator::class]; + // explicitly configured guard authenticator entry point + yield [[ + 'guard' => [ + 'authenticators' => [AppCustomAuthenticator::class, NullAuthenticator::class], + 'entry_point' => NullAuthenticator::class, + ], + ], NullAuthenticator::class]; + } + + /** + * @dataProvider provideEntryPointRequiredData + */ + public function testEntryPointRequired(array $firewall, $messageRegex) + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessageMatches($messageRegex); + + $container = $this->getRawContainer(); + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + 'providers' => [ + 'first' => ['id' => 'users'], + ], + + 'firewalls' => [ + 'main' => $firewall, + ], + ]); + + $container->compile(); + } + + public function provideEntryPointRequiredData() + { + // more than one entry point available and not explicitly set + yield [ + ['http_basic' => true, 'form_login' => true], + '/^Because you have multiple authenticators in firewall "main", you need to set the "entry_point" key to one of your authenticators/', + ]; + // more than one guard entry point available and not explicitly set + yield [ + ['guard' => ['authenticators' => [AppCustomAuthenticator::class, NullAuthenticator::class]]], + '/^Because you have multiple guard authenticators, you need to set the "entry_point" key to one of your authenticators/', + ]; + } + protected function getRawContainer() { $container = new ContainerBuilder(); @@ -439,3 +531,42 @@ class SecurityExtensionTest extends TestCase return $container; } } + +class NullAuthenticator implements AuthenticatorInterface +{ + public function start(Request $request, AuthenticationException $authException = null) + { + } + + public function supports(Request $request) + { + } + + public function getCredentials(Request $request) + { + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + } + + public function checkCredentials($credentials, UserInterface $user) + { + } + + public function createAuthenticatedToken(UserInterface $user, string $providerKey) + { + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) + { + } + + public function supportsRememberMe() + { + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index b06d8b4c3a..6ef832935e 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -19,7 +19,7 @@ "php": "^7.2.5", "ext-xml": "*", "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", + "symfony/dependency-injection": "^5.1", "symfony/event-dispatcher": "^5.1", "symfony/http-kernel": "^5.0", "symfony/polyfill-php80": "^1.15",