diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
index a1522fefc0b5d..b2b3366445158 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php
@@ -215,6 +215,11 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto
->prototype('scalar')->end()
->end()
->booleanNode('security')->defaultTrue()->end()
+ ->arrayNode('user_checkers')
+ ->defaultValue(array('security.user_checker'))
+ ->info('A list of user checkers reserved for this firewall.')
+ ->prototype('scalar')->end()
+ ->end()
->scalarNode('request_matcher')->end()
->scalarNode('access_denied_url')->end()
->scalarNode('access_denied_handler')->end()
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php
index b674c47e15bf0..3de9f0ac51f8a 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php
@@ -65,6 +65,7 @@ protected function createAuthProvider(ContainerBuilder $container, $id, $config,
$container
->setDefinition($provider, new DefinitionDecorator('security.authentication.provider.dao'))
->replaceArgument(0, new Reference($userProviderId))
+ ->replaceArgument(1, new Reference('security.chain_user_checker.'.$id))
->replaceArgument(2, $id)
;
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php
index 7aa4f5baa03eb..4144d655637f0 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php
@@ -35,6 +35,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider,
$authProviderId = 'security.authentication.provider.rememberme.'.$id;
$container
->setDefinition($authProviderId, new DefinitionDecorator('security.authentication.provider.rememberme'))
+ ->replaceArgument(0, new Reference('security.chain_user_checker.'.$id))
->addArgument($config['key'])
->addArgument($id)
;
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php
index 01ac91ce2ce9d..c4141fd13c15c 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RemoteUserFactory.php
@@ -30,6 +30,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider,
$container
->setDefinition($providerId, new DefinitionDecorator('security.authentication.provider.pre_authenticated'))
->replaceArgument(0, new Reference($userProvider))
+ ->replaceArgument(1, new Reference('security.chain_user_checker.'.$id))
->addArgument($id)
;
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php
index f8ca5509d039d..cf486b71d8199 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php
@@ -29,6 +29,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider,
$container
->setDefinition($providerId, new DefinitionDecorator('security.authentication.provider.pre_authenticated'))
->replaceArgument(0, new Reference($userProvider))
+ ->replaceArgument(1, new Reference('security.chain_user_checker.'.$id))
->addArgument($id)
;
diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
index 106083643bbaf..8eb0251a385a0 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php
@@ -14,6 +14,7 @@
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\UserProviderFactoryInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
+use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@@ -99,17 +100,17 @@ public function load(array $configs, ContainerBuilder $container)
// add some required classes for compilation
$this->addClassesToCompile(array(
- 'Symfony\\Component\\Security\\Http\\Firewall',
- 'Symfony\\Component\\Security\\Core\\SecurityContext',
- 'Symfony\\Component\\Security\\Core\\User\\UserProviderInterface',
- 'Symfony\\Component\\Security\\Core\\Authentication\\AuthenticationProviderManager',
- 'Symfony\\Component\\Security\\Core\\Authentication\\Token\\Storage\\TokenStorage',
- 'Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager',
- 'Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationChecker',
- 'Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface',
- 'Symfony\\Bundle\\SecurityBundle\\Security\\FirewallMap',
- 'Symfony\\Bundle\\SecurityBundle\\Security\\FirewallContext',
- 'Symfony\\Component\\HttpFoundation\\RequestMatcher',
+ 'Symfony\Component\Security\Http\Firewall',
+ 'Symfony\Component\Security\Core\SecurityContext',
+ 'Symfony\Component\Security\Core\User\UserProviderInterface',
+ 'Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager',
+ 'Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage',
+ 'Symfony\Component\Security\Core\Authorization\AccessDecisionManager',
+ 'Symfony\Component\Security\Core\Authorization\AuthorizationChecker',
+ 'Symfony\Component\Security\Core\Authorization\Voter\VoterInterface',
+ 'Symfony\Bundle\SecurityBundle\Security\FirewallMap',
+ 'Symfony\Bundle\SecurityBundle\Security\FirewallContext',
+ 'Symfony\Component\HttpFoundation\RequestMatcher',
));
}
@@ -230,7 +231,6 @@ private function createFirewalls($config, ContainerBuilder $container)
$map = $authenticationProviders = array();
foreach ($firewalls as $name => $firewall) {
list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds);
-
$contextId = 'security.firewall.map.context.'.$name;
$context = $container->setDefinition($contextId, new DefinitionDecorator('security.firewall.context'));
$context
@@ -369,6 +369,17 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a
// Exception listener
$exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint));
+ $userCheckers = array();
+
+ foreach ($firewall['user_checkers'] as $userChecker) {
+ $userCheckers[] = new Reference($userChecker);
+ }
+
+ $chainUserChecker = new Definition('Symfony\Component\Security\Core\User\ChainUserChecker', array($userCheckers));
+ $chainUserChecker->setPublic(false);
+
+ $container->setDefinition('security.chain_user_checker.'.$id, $chainUserChecker);
+
return array($matcher, $listeners, $exceptionListener);
}
@@ -576,6 +587,7 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv
$switchUserListenerId = 'security.authentication.switchuser_listener.'.$id;
$listener = $container->setDefinition($switchUserListenerId, new DefinitionDecorator('security.authentication.switchuser_listener'));
$listener->replaceArgument(1, new Reference($userProvider));
+ $listener->replaceArgument(2, new Reference('security.chain_user_checker.'.$id));
$listener->replaceArgument(3, $id);
$listener->replaceArgument(6, $config['parameter']);
$listener->replaceArgument(7, $config['role']);
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
index 5f139ca6e1157..42b1421452c48 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php
@@ -93,6 +93,13 @@ public function testFirewalls()
'security.authentication.listener.anonymous.host',
'security.access_listener',
),
+ array(
+ 'security.channel_listener',
+ 'security.context_listener.1',
+ 'security.authentication.listener.basic.with_user_checkers',
+ 'security.authentication.listener.anonymous.with_user_checkers',
+ 'security.access_listener',
+ ),
), $listeners);
}
@@ -233,6 +240,40 @@ public function testRememberMeThrowExceptions()
$this->assertFalse($service->getArgument(5));
}
+ public function testUserCheckerConfig()
+ {
+ $definition = $this->getContainer('container1')->getDefinition('security.chain_user_checker.with_user_checkers');
+
+ $this->assertCount(1, $definition->getArguments());
+
+ $userCheckers = $definition->getArgument(0);
+ $this->assertCount(2, $userCheckers);
+ $this->assertEquals('app.user_checker1', $userCheckers[0]);
+ $this->assertEquals('app.user_checker2', $userCheckers[1]);
+ }
+
+ public function testUserCheckerConfigWithDefaultChecker()
+ {
+ $definition = $this->getContainer('container1')->getDefinition('security.chain_user_checker.host');
+
+ $this->assertCount(1, $definition->getArguments());
+
+ $userCheckers = $definition->getArgument(0);
+ $this->assertCount(1, $userCheckers);
+ $this->assertEquals('security.user_checker', $userCheckers[0]);
+ }
+
+ public function testUserCheckerConfigWithNoCheckers()
+ {
+ $definition = $this->getContainer('container1')->getDefinition('security.chain_user_checker.secure');
+
+ $this->assertCount(1, $definition->getArguments());
+
+ $userCheckers = $definition->getArgument(0);
+
+ $this->assertEmpty($userCheckers);
+ }
+
protected function getContainer($file)
{
$container = new ContainerBuilder();
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php
index b16a46ff03457..b523be0bf0c0d 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/container1.php
@@ -72,6 +72,7 @@
'remote_user' => true,
'logout' => true,
'remember_me' => array('key' => 'TheKey'),
+ 'user_checkers' => array(),
),
'host' => array(
'pattern' => '/test',
@@ -80,6 +81,14 @@
'anonymous' => true,
'http_basic' => true,
),
+ 'with_user_checkers' => array(
+ 'user_checkers' => array(
+ 'app.user_checker1',
+ 'app.user_checker2',
+ ),
+ 'anonymous' => true,
+ 'http_basic' => true,
+ ),
),
'access_control' => array(
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml
index 1a56aa88fda07..18d4dc4f82fc8 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/container1.xml
@@ -55,6 +55,7 @@
+
@@ -64,6 +65,13 @@
+
+
+
+ app.user_checker1
+ app.user_checker2
+
+
ROLE_USER
ROLE_USER,ROLE_ADMIN,ROLE_ALLOWED_TO_SWITCH
ROLE_USER,ROLE_ADMIN
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml
index 93c231ea235f1..16de8c3f740c3 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/container1.yml
@@ -56,6 +56,8 @@ security:
logout: true
remember_me:
key: TheKey
+ user_checkers:
+
host:
pattern: /test
host: foo\.example\.org
@@ -63,6 +65,13 @@ security:
anonymous: true
http_basic: true
+ with_user_checkers:
+ anonymous: ~
+ http_basic: ~
+ user_checkers:
+ - "app.user_checker1"
+ - "app.user_checker2"
+
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php
index 402b321968739..82866ba6954b0 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php
@@ -46,7 +46,7 @@ public function testNoConfigForProvider()
$processor = new Processor();
$configuration = new MainConfiguration(array(), array());
- $config = $processor->processConfiguration($configuration, array($config));
+ $processor->processConfiguration($configuration, array($config));
}
/**
@@ -65,7 +65,7 @@ public function testManyConfigForProvider()
$processor = new Processor();
$configuration = new MainConfiguration(array(), array());
- $config = $processor->processConfiguration($configuration, array($config));
+ $processor->processConfiguration($configuration, array($config));
}
public function testCsrfAliases()
@@ -108,8 +108,38 @@ public function testCsrfOriginalAndAliasValueCausesException()
);
$config = array_merge(static::$minimalConfig, $config);
+ $processor = new Processor();
+ $configuration = new MainConfiguration(array(), array());
+ $processor->processConfiguration($configuration, array($config));
+ }
+
+ public function testDefaultUserCheckers()
+ {
+ $processor = new Processor();
+ $configuration = new MainConfiguration(array(), array());
+ $processedConfig = $processor->processConfiguration($configuration, array(static::$minimalConfig));
+
+ $this->assertEquals(array('security.user_checker'), $processedConfig['firewalls']['stub']['user_checkers']);
+ }
+
+ public function testUserCheckers()
+ {
+ $config = array(
+ 'firewalls' => array(
+ 'stub' => array(
+ 'user_checkers' => array(
+ 'security.dummy_checker',
+ 'app.henk_checker',
+ ),
+ ),
+ ),
+ );
+ $config = array_merge(static::$minimalConfig, $config);
+
$processor = new Processor();
$configuration = new MainConfiguration(array(), array());
$processedConfig = $processor->processConfiguration($configuration, array($config));
+
+ $this->assertEquals(array('security.dummy_checker', 'app.henk_checker'), $processedConfig['firewalls']['stub']['user_checkers']);
}
}
diff --git a/src/Symfony/Component/Security/Core/Tests/User/ChainUserCheckerTest.php b/src/Symfony/Component/Security/Core/Tests/User/ChainUserCheckerTest.php
new file mode 100644
index 0000000000000..0a6b1db1779ac
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Tests/User/ChainUserCheckerTest.php
@@ -0,0 +1,116 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Tests\User;
+
+use Symfony\Component\Security\Core\Exception\AuthenticationException;
+use Symfony\Component\Security\Core\User\ChainUserChecker;
+
+class ChainUserCheckerTest extends \PHPUnit_Framework_TestCase
+{
+ const USER_CHECKER_INTERFACE = 'Symfony\Component\Security\Core\User\UserCheckerInterface';
+ const USER_INTERFACE = 'Symfony\Component\Security\Core\User\UserInterface';
+
+ public function testDefaultsWithoutFailures()
+ {
+ $user = $this->getMock(self::USER_INTERFACE);
+ $checkers = array(
+ $chained1 = $this->getMock(self::USER_CHECKER_INTERFACE),
+ $chained2 = $this->getMock(self::USER_CHECKER_INTERFACE),
+ );
+
+ $chained1
+ ->expects($this->once())
+ ->method('checkPreAuth')
+ ->with($user);
+
+ $chained2
+ ->expects($this->once())
+ ->method('checkPreAuth')
+ ->with($user);
+
+ $chained1
+ ->expects($this->once())
+ ->method('checkPostAuth')
+ ->with($user);
+
+ $chained2
+ ->expects($this->once())
+ ->method('checkPostAuth')
+ ->with($user);
+
+ $chainUserChecker = new ChainUserChecker($checkers);
+
+ $chainUserChecker->checkPreAuth($user);
+ $chainUserChecker->checkPostAuth($user);
+ }
+
+ /**
+ * @dataProvider methodProvider
+ * @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationException
+ */
+ public function testWithFailures($method)
+ {
+ $user = $this->getMock(self::USER_INTERFACE);
+ $checkers = array(
+ $chained1 = $this->getMock(self::USER_CHECKER_INTERFACE),
+ $chained2 = $this->getMock(self::USER_CHECKER_INTERFACE),
+ );
+
+ $chained1
+ ->expects($this->once())
+ ->method($method)
+ ->with($user)
+ ->willThrowException(new AuthenticationException());
+
+ $chained2
+ ->expects($this->never())
+ ->method($method)
+ ->with($user);
+
+ $chainUserChecker = new ChainUserChecker($checkers);
+
+ $chainUserChecker->$method($user);
+ }
+
+ /**
+ * @dataProvider methodProvider
+ * @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationException
+ */
+ public function testWithFailuresOnLastToEnsureSequence($method)
+ {
+ $user = $this->getMock(self::USER_INTERFACE);
+ $checkers = array(
+ $chained1 = $this->getMock(self::USER_CHECKER_INTERFACE),
+ $chained2 = $this->getMock(self::USER_CHECKER_INTERFACE),
+ );
+
+ $chained1
+ ->expects($this->once())
+ ->method($method)
+ ->with($user);
+
+ $chained2
+ ->expects($this->once())
+ ->method($method)
+ ->with($user)
+ ->willThrowException(new AuthenticationException());
+
+ $chainUserChecker = new ChainUserChecker($checkers);
+
+ $chainUserChecker->$method($user);
+ }
+
+ public function methodProvider()
+ {
+ return array(array('checkPreAuth'), array('checkPostAuth'));
+ }
+}
diff --git a/src/Symfony/Component/Security/Core/User/ChainUserChecker.php b/src/Symfony/Component/Security/Core/User/ChainUserChecker.php
new file mode 100644
index 0000000000000..5ad8c1ae62556
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/User/ChainUserChecker.php
@@ -0,0 +1,60 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\User;
+
+/**
+ * Supports multiple user checkers.
+ *
+ * This user checker is a collection of other user checkers
+ * and triggers each user checker in the sequence provided.
+ *
+ * @author Iltar van der Berg
+ */
+final class ChainUserChecker implements UserCheckerInterface
+{
+ /**
+ * @var UserCheckerInterface[]
+ */
+ private $userCheckers;
+
+ /**
+ * @param UserCheckerInterface[] $userCheckers
+ */
+ public function __construct(array $userCheckers)
+ {
+ $this->userCheckers = $userCheckers;
+ }
+
+ /**
+ * checkPreAuth on all available UserCheckers.
+ *
+ * {@inheritdoc}
+ */
+ public function checkPreAuth(UserInterface $user)
+ {
+ foreach ($this->userCheckers as $userChecker) {
+ $userChecker->checkPreAuth($user);
+ }
+ }
+
+ /**
+ * checkPostAuth on all available UserCheckers.
+ *
+ * {@inheritdoc}
+ */
+ public function checkPostAuth(UserInterface $user)
+ {
+ foreach ($this->userCheckers as $userChecker) {
+ $userChecker->checkPostAuth($user);
+ }
+ }
+}