bug #36661 [SecurityBundle] Fixed entry point service ID resolving and multiple guard entry points (wouterj)

This PR was squashed before being merged into the 5.1-dev branch.

Discussion
----------

[SecurityBundle] Fixed entry point service ID resolving and multiple guard entry points

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | -
| License       | MIT
| Doc PR        | n/a

@fabpot I am not able to reproduce [the error you reported](https://github.com/symfony/symfony/pull/36575#issuecomment-622272051) in any of my demo applications or in the tests introduced in this PR. The error indicates that no entry point is configured in your application, can you maybe try out this patch (given it now makes a hard error when more than one guard is used)? If it still doesn't work, can you maybe share your firewall configuration?

---

_build failures are unrelated_

Commits
-------

c75659350e Do not make AbstractFactory internal and revert method rename
6870a18803 Fixed entry point resolving and guard entry point configuration
This commit is contained in:
Fabien Potencier 2020-05-03 08:36:54 +02:00
commit 28bb74cd50
12 changed files with 156 additions and 21 deletions

View File

@ -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`.

View File

@ -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

View File

@ -23,8 +23,6 @@ use Symfony\Component\DependencyInjection\Reference;
* @author Fabien Potencier <fabien@symfony.com>
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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']) {

View File

@ -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 {

View File

@ -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');

View File

@ -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()
{
}
}

View File

@ -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",