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); + } + } +}