From 66ed33112cf1b592193104c0ca2296b450b13f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Tue, 23 Sep 2014 13:43:52 +0200 Subject: [PATCH 01/16] Previous PR --- .../Security/SessionRegistry/Schema.php | 48 ++++++ .../SessionRegistry/SessionInformation.php | 42 ++++++ .../SessionRegistryStorage.php | 113 ++++++++++++++ .../Command/InitConcurrentSessionsCommand.php | 76 ++++++++++ .../DependencyInjection/MainConfiguration.php | 28 ++++ .../Security/Factory/AbstractFactory.php | 7 +- .../Security/Factory/FormLoginFactory.php | 4 +- .../Security/Factory/HttpBasicFactory.php | 2 +- .../Security/Factory/HttpDigestFactory.php | 2 +- .../Security/Factory/RememberMeFactory.php | 2 +- .../Factory/SecurityFactoryInterface.php | 2 +- .../Security/Factory/X509Factory.php | 2 +- .../DependencyInjection/SecurityExtension.php | 55 ++++++- .../Resources/config/security_listeners.xml | 12 ++ .../config/security_session_registry.xml | 25 ++++ .../config/security_session_registry_dbal.xml | 25 ++++ .../MaxSessionsExceededException.php | 23 +++ .../Firewall/ConcurrentSessionListener.php | 97 ++++++++++++ .../ConcurrentSessionControlStrategy.php | 138 ++++++++++++++++++ .../Http/Session/SessionInformation.php | 116 +++++++++++++++ .../Security/Http/Session/SessionRegistry.php | 106 ++++++++++++++ .../SessionRegistryStorageInterface.php | 60 ++++++++ 22 files changed, 974 insertions(+), 11 deletions(-) create mode 100644 src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php create mode 100644 src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php create mode 100644 src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml create mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml create mode 100644 src/Symfony/Component/Security/Core/Exception/MaxSessionsExceededException.php create mode 100644 src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php create mode 100644 src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php create mode 100644 src/Symfony/Component/Security/Http/Session/SessionInformation.php create mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistry.php create mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php new file mode 100644 index 0000000000000..355e662b29fba --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Security\SessionRegistry; + +use Doctrine\DBAL\Schema\Schema as BaseSchema; + +/** + * The schema used for the ACL system. + * + * @author Stefan Paschke + */ +final class Schema extends BaseSchema +{ + /** + * Constructor + * + * @param array $options the names for tables + */ + public function __construct(array $options) + { + parent::__construct(); + + $this->addSessionInformationTable($options); + } + + /** + * Adds the session_information table to the schema + */ + protected function addSessionInformationTable(array $options) + { + $table = $this->createTable($options['session_information_table_name']); + $table->addColumn('session_id', 'string'); + $table->addColumn('username', 'string'); + $table->addColumn('expired', 'datetime', array('unsigned' => true, 'notnull' => false)); + $table->addColumn('last_request', 'datetime', array('unsigned' => true, 'notnull' => false)); + $table->setPrimaryKey(array('session_id')); + $table->addUniqueIndex(array('session_id')); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php new file mode 100644 index 0000000000000..f9eb8eb0ed957 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Security\SessionRegistry; + +use Symfony\Component\Security\Http\Session\SessionInformation as BaseSessionInformation; + +/** + * SessionInformation. + * + * Allows to persist SessionInformation using Doctrine DBAL. + * + * @author Stefan Paschke + */ +class SessionInformation extends BaseSessionInformation +{ + public function __construct($sessionId, $username, \DateTime $lastRequest = null, \DateTime $expired = null) + { + parent::__construct($sessionId, $username); + + if (null !== $lastRequest) { + $this->setLastRequest($lastRequest); + } + + if (null !== $expired) { + $this->setExpired($expired); + } + } + + public function getExpired() + { + return parent::getExpired(); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php new file mode 100644 index 0000000000000..742713a1789fc --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -0,0 +1,113 @@ +connection = $connection; + $this->options = $options; + } + + /** + * not implemented + */ + public function getUsers() + { + throw new \BadMethodCallException("Not implemented."); + } + + /** + * Obtains the maintained information for one session. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation a SessionInformation object. + */ + public function getSessionInformation($sessionId) + { + $statement = $this->connection->executeQuery( + 'SELECT * FROM '.$this->options['session_information_table_name'].' WHERE session_id = :session_id', + array('session_id' => $sessionId) + ); + + $data = $statement->fetch(\PDO::FETCH_ASSOC); + + return $data ? $this->instantiateSessionInformationFromResultSet($data) : null; + } + + /** + * Obtains the maintained information for one user. + * + * @param string $username The user identifier. + * @param boolean $includeExpiredSessions. + * @return array An array of SessionInformation objects. + */ + public function getSessionInformations($username, $includeExpiredSessions = false) + { + $sessionInformations = array(); + + $statement = $this->connection->executeQuery( + 'SELECT * + FROM '.$this->options['session_information_table_name'].' + WHERE username = :username'.($includeExpiredSessions ? '' : ' AND expired IS NULL ').' + ORDER BY last_request DESC', + array('username' => $username) + ); + + while ($data = $statement->fetch(\PDO::FETCH_ASSOC)) + { + $sessionInformations[] = $this->instantiateSessionInformationFromResultSet($data); + } + + return $sessionInformations; + } + + /** + * Adds information for one session. + * + * @param string $sessionId the session identifier key. + * @param SessionInformation a SessionInformation object. + */ + public function setSessionInformation(SessionInformation $sessionInformation) + { + $statement = $this->connection->prepare( + 'INSERT INTO '.$this->options['session_information_table_name'].' + (session_id, username, last_request, expired) VALUES(:session_id, :username, :last_request, :expired) + ON DUPLICATE KEY + UPDATE username=:username, last_request=:last_request, expired=:expired'); + + $statement->bindValue('session_id', $sessionInformation->getSessionId()); + $statement->bindValue('username', $sessionInformation->getUsername()); + $statement->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); + $statement->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); + $statement->execute(); + } + + /** + * Deletes the maintained information of one session. + * + * @param string $sessionId the session identifier key. + */ + public function removeSessionInformation($sessionId) + { + $this->connection->delete($this->options['session_information_table_name'], array('session_id' => $sessionId)); + } + + private function instantiateSessionInformationFromResultSet($data) + { + return new $this->options['session_information_class_name']( + $data['session_id'], + $data['username'], + (null == $data['last_request']) ? null : new \DateTime($data['last_request']), + (null == $data['expired']) ? null : new \DateTime($data['expired']) + ); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php new file mode 100644 index 0000000000000..ea20546d78bda --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Bridge\Doctrine\Security\SessionRegistry\Schema; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Installs the database schema required by the concurrent session Doctrine implementation + * + * @author Stefan Paschke + */ +class InitConcurrentSessionsCommand extends ContainerAwareCommand +{ + /** + * @see Command + */ + protected function configure() + { + parent::configure(); + + $this + ->setName('init:concurrent-session') + ->setDescription('Executes the SQL needed to generate the database schema required by the concurrent sessions feature.') + ->setHelp(<<init:concurrent-session command executes the SQL needed to +generate the database schema required by the concurrent session Doctrine implementation: + +./app/console init:concurrent-session + +You can also output the SQL instead of executing it: + +./app/console init:concurrent-session --dump-sql +EOT + ); + } + + /** + * @see Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $connection = $this->getContainer()->get('security.session_registry.dbal.connection'); + $sm = $connection->getSchemaManager(); + $tableNames = $sm->listTableNames(); + $tables = array( + 'session_information_table_name' => $this->getContainer()->getParameter('security.session_registry.dbal.session_information_table_name'), + ); + + foreach ($tables as $table) { + if (in_array($table, $tableNames, true)) { + $output->writeln(sprintf('The table "%s" already exists. Aborting.', $table)); + + return; + } + } + + $schema = new Schema($tables); + foreach ($schema->toSql($connection->getDatabasePlatform()) as $sql) { + $connection->exec($sql); + } + + $output->writeln('concurrent session tables have been initialized successfully.'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 3f25c3da03e4a..886fb2f68719e 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -79,10 +79,31 @@ public function getConfigTreeBuilder() $this->addFirewallsSection($rootNode, $this->factories); $this->addAccessControlSection($rootNode); $this->addRoleHierarchySection($rootNode); + $this->addSessionRegistrySection($rootNode); return $tb; } + private function addSessionRegistrySection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('session_registry') + ->children() + ->scalarNode('connection')->end() + ->arrayNode('tables') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('session_information')->defaultValue('cs_session_information')->end() + ->end() + ->end() + ->scalarNode('session_registry_storage')->end() + ->end() + ->end() + ->end() + ; + } + private function addAclSection(ArrayNodeDefinition $rootNode) { $rootNode @@ -288,6 +309,13 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end() ->end() ->end() + ->arrayNode('session_concurrency') + ->canBeUnset() + ->children() + ->scalarNode('max_sessions')->defaultNull()->end() + ->scalarNode('expiration_url')->defaultValue('/')->end() + ->end() + ->end() ; $abstractFactoryKeys = array(); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index 00f0c3a0e1dcc..dab748ba808f4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -47,13 +47,13 @@ abstract class AbstractFactory implements SecurityFactoryInterface 'failure_path_parameter' => '_failure_path', ); - public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId) + public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId, $sessionStrategy) { // authentication provider $authProviderId = $this->createAuthProvider($container, $id, $config, $userProviderId); // authentication listener - $listenerId = $this->createListener($container, $id, $config, $userProviderId); + $listenerId = $this->createListener($container, $id, $config, $userProviderId, $sessionStrategy); // add remember-me aware tag if requested if ($this->isRememberMeAware($config)) { @@ -153,10 +153,11 @@ protected function isRememberMeAware($config) return $config['remember_me']; } - protected function createListener($container, $id, $config, $userProvider) + protected function createListener($container, $id, $config, $userProvider, $sessionStrategy) { $listenerId = $this->getListenerId(); $listener = new DefinitionDecorator($listenerId); + $listener->replaceArgument(2, $sessionStrategy); $listener->replaceArgument(4, $id); $listener->replaceArgument(5, new Reference($this->createAuthenticationSuccessHandler($container, $id, $config))); $listener->replaceArgument(6, new Reference($this->createAuthenticationFailureHandler($container, $id, $config))); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index b674c47e15bf0..9007ab9a79072 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -71,9 +71,9 @@ protected function createAuthProvider(ContainerBuilder $container, $id, $config, return $provider; } - protected function createListener($container, $id, $config, $userProvider) + protected function createListener($container, $id, $config, $userProvider, $sessionStrategy) { - $listenerId = parent::createListener($container, $id, $config, $userProvider); + $listenerId = parent::createListener($container, $id, $config, $userProvider, $sessionStrategy); $container ->getDefinition($listenerId) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index c87f68c38ff6b..62fe22ca3115a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -24,7 +24,7 @@ */ class HttpBasicFactory implements SecurityFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy) { $provider = 'security.authentication.provider.dao.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php index 3a49b5dcdc583..5c1e9c334463c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php @@ -24,7 +24,7 @@ */ class HttpDigestFactory implements SecurityFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy) { $provider = 'security.authentication.provider.dao.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index 7aa4f5baa03eb..ee16392c58a56 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -29,7 +29,7 @@ class RememberMeFactory implements SecurityFactoryInterface 'remember_me_parameter' => '_remember_me', ); - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy) { // authentication provider $authProviderId = 'security.authentication.provider.rememberme.'.$id; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php index 5e8773b742c13..d39c842f7835a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php @@ -21,7 +21,7 @@ */ interface SecurityFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint); + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy); public function getPosition(); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php index 19446d4a95017..aa523d0aa73b2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php @@ -25,7 +25,7 @@ */ class X509Factory implements SecurityFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy) { $providerId = 'security.authentication.provider.pre_authenticated.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 1f20fc7596414..04215ee890a8b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -71,6 +71,10 @@ public function load(array $configs, ContainerBuilder $container) $container->removeDefinition('security.access.expression_voter'); } + if (isset($config['session_registry'])) { + $this->sessionRegistryLoad($config['session_registry'], $container, $loader); + } + // set some global scalars $container->setParameter('security.access.denied_url', $config['access_denied_url']); $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']); @@ -159,6 +163,30 @@ private function configureDbalAclProvider(array $config, ContainerBuilder $conta $container->setParameter('security.acl.dbal.sid_table_name', $config['tables']['security_identity']); } + private function sessionRegistryLoad($config, ContainerBuilder $container, $loader) + { + $loader->load('security_session_registry.xml'); + + if (isset($config['session_registry_storage'])) { + $container->setAlias('security.authentication.session_registry_storage', $config['session_registry_storage']); + + return; + } + + $this->configureDbalSessionRegistryStorage($config, $container, $loader); + } + + private function configureDbalSessionRegistryStorage($config, ContainerBuilder $container, $loader) + { + $loader->load('security_session_registry_dbal.xml'); + + if (isset($config['connection'])) { + $container->setAlias('security.session_registry.dbal.connection', sprintf('doctrine.dbal.%s_connection', $config['connection'])); + } + + $container->setParameter('security.session_registry.dbal.session_information_table_name', $config['tables']['session_information']); + } + /** * Loads the web configuration. * @@ -364,6 +392,11 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a // Access listener $listeners[] = new Reference('security.access_listener'); + // Concurrent session listener + if (isset($firewall['session_concurrency'])) { + $listeners[] = new Reference($this->createConcurrentSessionListener($container, $id, $firewall['session_concurrency'])); + } + // Determine default entry point if (isset($firewall['entry_point'])) { $defaultEntryPoint = $firewall['entry_point']; @@ -394,6 +427,16 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut $hasListeners = false; $defaultEntryPoint = null; + if (isset($firewall['session_concurrency']['max_sessions'])) { + $sessionStrategyId = 'security.authentication.concurrent_session_strategy.'.$id; + $container->setDefinition($sessionStrategyId, new DefinitionDecorator('security.authentication.concurrent_session_strategy')) + ->replaceArgument(1, $firewall['session_concurrency']['max_sessions']); + + $sessionStrategy = new Reference($sessionStrategyId); + } else { + $sessionStrategy = $container->get('security.authentication.session_strategy'); + } + foreach ($this->listenerPositions as $position) { foreach ($this->factories[$position] as $factory) { $key = str_replace('-', '_', $factory->getKey()); @@ -401,7 +444,7 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut if (isset($firewall[$key])) { $userProvider = isset($firewall[$key]['provider']) ? $this->getUserProviderId($firewall[$key]['provider']) : $defaultProvider; - list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); + list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint, $sessionStrategy); $listeners[] = new Reference($listenerId); $authenticationProviders[] = $provider; @@ -627,6 +670,16 @@ private function createExpression($container, $expression) return $this->expressions[$id] = new Reference($id); } + private function createConcurrentSessionListener($container, $id, $config) + { + $concurrentSessionListenerId = 'security.authentication.concurrentsession_listener.'.$id; + $listener = $container->setDefinition($concurrentSessionListenerId, new DefinitionDecorator('security.authentication.concurrentsession_listener')); + $listener->replaceArgument(3, $config['expiration_url']); + $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); + + return $concurrentSessionListenerId; + } + private function createRequestMatcher($container, $path = null, $host = null, $methods = array(), $ip = null, array $attributes = array()) { $serialized = serialize(array($path, $host, $methods, $ip, $attributes)); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 7d3ba1a6f322c..e3b7a6777a953 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -28,6 +28,8 @@ Symfony\Component\Security\Http\Firewall\SwitchUserListener + Symfony\Component\Security\Http\Firewall\ConcurrentSessionListener + Symfony\Component\Security\Http\Firewall\LogoutListener Symfony\Component\Security\Http\Logout\SessionLogoutHandler Symfony\Component\Security\Http\Logout\CookieClearingLogoutHandler @@ -257,6 +259,16 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml new file mode 100644 index 0000000000000..93eaa73eec193 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml @@ -0,0 +1,25 @@ + + + + + + Symfony\Component\Security\Http\Session\ConcurrentSessionControlStrategy + Symfony\Component\Security\Http\Session\SessionRegistry + Symfony\Component\Security\Http\Session\SessionInformation + + + + + + + %security.authentication.session_strategy.strategy% + + + + + %security.authentication.session_information.class% + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml new file mode 100644 index 0000000000000..42c8d03a8f9b1 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml @@ -0,0 +1,25 @@ + + + + + + Symfony\Bridge\Doctrine\Security\SessionRegistry\SessionRegistryStorage + Symfony\Bridge\Doctrine\Security\SessionRegistry\SessionInformation + + + + + + + + + + %security.session_registry.dbal.session_information_table_name% + %security.authentication.session_information.class% + + + + + diff --git a/src/Symfony/Component/Security/Core/Exception/MaxSessionsExceededException.php b/src/Symfony/Component/Security/Core/Exception/MaxSessionsExceededException.php new file mode 100644 index 0000000000000..6d3759a7a0c06 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Exception/MaxSessionsExceededException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Exception; + +/** + * This exception is thrown when the user has exceeded the allowed number of sessions, and the + * ConcurrentSessionControlStrategy is set to limit the number by disallowing opening new sessions. + * (By default, the ConcurrentSessionControlStrategy will expire the user's oldest existing session) + * + * @author Stefan Paschke + */ +class MaxSessionsExceededException extends AuthenticationException +{ +} diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php new file mode 100644 index 0000000000000..95dbbf795eb15 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Firewall; + +use Symfony\Component\Security\Core\SecurityContextInterface; +use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; +use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; +use Symfony\Component\Security\Http\Session\SessionRegistry; +use Symfony\Component\Security\Http\HttpUtils; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\Log\LoggerInterface; + +class ConcurrentSessionListener implements ListenerInterface +{ + private $securityContext; + private $httpUtils; + private $sessionRegistry; + private $targetUrl; + private $logger; + private $handlers; + private $successHandler; + + public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, SessionRegistry $sessionRegistry, $targetUrl = '/', LogoutSuccessHandlerInterface $successHandler = null, LoggerInterface $logger = null) + { + $this->securityContext = $securityContext; + $this->httpUtils = $httpUtils; + $this->sessionRegistry = $sessionRegistry; + $this->targetUrl = $targetUrl; + $this->successHandler = $successHandler; + $this->logger = $logger; + $this->handlers = array(); + } + + /** + * Adds a logout handler + * + * @param LogoutHandlerInterface $handler + */ + public function addHandler(LogoutHandlerInterface $handler) + { + $this->handlers[] = $handler; + } + + /** + * Handles the number of simultaneous sessions for a single user. + * + * @param GetResponseEvent $event A GetResponseEvent instance + */ + public function handle(GetResponseEvent $event) + { + $request = $event->getRequest(); + + $session = $request->hasSession() ? $request->getSession() : null; + + if (null === $session || null === $token = $this->securityContext->getToken()) { + return; + } else { + if ($sessionInformation = $this->sessionRegistry->getSessionInformation($session->getId())) { + if ($sessionInformation->isExpired()) { + if (null !== $this->successHandler) { + $response = $this->successHandler->onLogoutSuccess($request); + + if (!$response instanceof Response) { + throw new \RuntimeException('Logout Success Handler did not return a Response.'); + } + } else { + $response = $this->httpUtils->createRedirectResponse($request, $this->targetUrl); + } + + foreach ($this->handlers as $handler) { + $handler->logout($request, $response, $token); + } + + $this->securityContext->setToken(null); + + $event->setResponse($response); + } else { + $sessionInformation->refreshLastRequest(); + $this->sessionRegistry->setSessionInformation($sessionInformation); + } + } else { + // sessionInformation was lost, try to recover by recreating it + $this->sessionRegistry->registerNewSession($session->getId(), $token->getUser()); + } + } + } +} diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php new file mode 100644 index 0000000000000..36909330248f3 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * ConcurrentSessionControlStrategy. + * + * Strategy which handles concurrent session-control, in addition to the functionality provided by the base class. + * When invoked following an authentication, it will check whether the user in question should be allowed to proceed, + * by comparing the number of sessions they already have active with the configured maximumSessions value. + * The SessionRegistry is used as the source of data on authenticated users and session data. + * + * @author Stefan Paschke + */ +class ConcurrentSessionControlStrategy extends SessionAuthenticationStrategy +{ + protected $registry; + protected $alwaysCreateSession; + protected $exceptionIfMaximumExceeded = false; + protected $maximumSessions; + + public function __construct(SessionRegistry $registry, $maximumSessions, $sessionAuthenticationStrategy) + { + parent::__construct($sessionAuthenticationStrategy); + $this->registry = $registry; + $this->setMaximumSessions($maximumSessions); + } + + /** + * {@inheritDoc} + */ + public function onAuthentication(Request $request, TokenInterface $token) + { + $user = $token->getUser(); + $originalSessionId = $request->getSession()->getId(); + + parent::onAuthentication($request, $token); + + if ($originalSessionId != $request->getSession()->getId()) { + $this->onSessionChange($originalSessionId, $request->getSession()->getId()); + } + + $sessions = $this->registry->getAllSessions($user); + $maxSessions = $this->getMaximumSessionsForThisUser($user); + + if (count($sessions) >= $maxSessions && $this->alwaysCreateSession !== true) { + if ($this->exceptionIfMaximumExceeded) { + throw new MaxSessionsExceededException(sprintf('Maximum of sessions (%s) exceeded', $maxSessions)); + } + + $this->allowableSessionsExceeded($sessions, $maxSessions, $this->registry); + } + + $this->registry->registerNewSession($request->getSession()->getID(), $user); + } + + /** + * Sets a boolean flag that allows to bypass allowableSessionsExceeded(). + * + * param boolean $alwaysCreateSession + */ + public function setAlwaysCreateSession($alwaysCreateSession) + { + $this->alwaysCreateSession = $alwaysCreateSession; + } + + /** + * Sets a boolean flag that causes a RuntimeException to be thrown if the number of sessions is exceeded. + * + * @param boolean $exceptionIfMaximumExceeded + */ + public function setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded) + { + $this->exceptionIfMaximumExceeded = $exceptionIfMaximumExceeded; + } + + /** + * Sets the maxSessions property. + * + * @param $maximumSessions + */ + public function setMaximumSessions($maximumSessions) + { + $this->maximumSessions = $maximumSessions; + } + + /** + * Allows subclasses to customise behaviour when too many sessions are detected. + * + * @param array $sessions + * @param integer $allowableSessions + * @param SessionRegistry $registry + */ + protected function allowableSessionsExceeded($sessions, $allowableSessions, SessionRegistry $registry) + { + // remove oldest sessions from registry + for ($i = $allowableSessions - 1; $i < count($sessions); $i++) { + $sessions[$i]->expireNow(); + $registry->setSessionInformation($sessions[$i]); + } + } + + /** + * Method intended for use by subclasses to override the maximum number of sessions that are permitted for a particular authentication. + * + * @param UserInterface $user + * @return integer + */ + protected function getMaximumSessionsForThisUser(UserInterface $user) + { + return $this->maximumSessions; + } + + /** + * Called when the session has been changed and the old attributes have been migrated to the new session. + * + * @param string $originalSessionId + * @param string $newSessionId + * @param TokenInterface $token + */ + protected function onSessionChange($originalSessionId, $newSessionId) + { + $this->registry->removeSessionInformation($originalSessionId); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php new file mode 100644 index 0000000000000..81b47f3974e8d --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.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\Http\Session; + +/** + * SessionInformation. + * + * Represents a record of a session. This is primarily used for concurrent session support. + * + * @author Stefan Paschke + */ +class SessionInformation +{ + protected $sessionId; + protected $username; + protected $expired; + protected $lastRequest; + + public function __construct($sessionId, $username) + { + $this->setSessionId($sessionId); + $this->setUsername($username); + } + + /** + * Sets the session informations expired date to the current date and time. + * + */ + public function expireNow() + { + $this->setExpired(new \DateTime()); + } + + /** + * Obtain the last request date. + * + * @return DateTime the last request date and time. + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + /** + * Obtains the username. + * + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * Obtain the session identifier. + * + * @return string $sessionId the session identifier key. + */ + public function getSessionId() + { + return $this->sessionId; + } + + /** + * Return wether this session is expired. + * + * @return boolean + */ + public function isExpired() + { + return $this->getExpired() && $this->getExpired()->getTimestamp() < time(); + } + + /** + * Set the last request date to the current date and time. + * + */ + public function refreshLastRequest() + { + $this->lastRequest = new \DateTime(); + } + + protected function getExpired() + { + return $this->expired; + } + + protected function setExpired(\DateTime $expired) + { + $this->expired = $expired; + } + + protected function setLastRequest(\DateTime $lastRequest) + { + $this->lastRequest = $lastRequest; + } + + private function setSessionId($sessionId) + { + $this->sessionId = $sessionId; + } + + private function setUsername($username) + { + $this->username = $username; + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php new file mode 100644 index 0000000000000..ab34177c31a93 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * SessionRegistry. + * + * Maintains a registry of SessionInformation instances. + * + * @author Stefan Paschke + */ +class SessionRegistry +{ + protected $sessionRegistryStorage; + protected $sessionInformationClass; + + public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage, $sessionInformationClass) + { + $this->sessionRegistryStorage = $sessionRegistryStorage; + $this->sessionInformationClass = $sessionInformationClass; + } + + /** + * Obtains all the users for which session information is stored. + * + * @return array An array of UserInterface objects. + */ + public function getAllUsers() + { + return $this->sessionRegistryStorage->getUsers(); + } + + /** + * Obtains all the known sessions for the specified user. + * + * @param UserInterface $user the specified user. + * @param boolean $includeExpiredSessions. + * @return array An array of SessionInformation objects. + */ + public function getAllSessions(UserInterface $user, $includeExpiredSessions = false) + { + return $this->sessionRegistryStorage->getSessionInformations($user->getUsername(), $includeExpiredSessions); + } + + /** + * {@inheritDoc} + */ + public function getSessionInformation($sessionId) + { + return $this->sessionRegistryStorage->getSessionInformation($sessionId); + } + + /** + * {@inheritDoc} + */ + public function setSessionInformation(SessionInformation $sessionInformation) + { + $this->sessionRegistryStorage->setSessionInformation($sessionInformation); + } + + /** + * Updates the given sessionId so its last request time is equal to the present date and time. + * + * @param string $sessionId the session identifier key. + */ + public function refreshLastRequest($sessionId) + { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + $sessionInformation->refreshLastRequest(); + $this->setSessionInformation($sessionInformation); + } + } + + /** + * Registers a new session for the specified user. + * + * @param string $sessionId the session identifier key. + * @param UserInterface $user the specified user. + */ + public function registerNewSession($sessionId, UserInterface $user) + { + $sessionInformation = new $this->sessionInformationClass($sessionId, $user->getUsername()); + $sessionInformation->refreshLastRequest(); + + $this->setSessionInformation($sessionInformation); + } + + /** + * {@inheritDoc} + */ + public function removeSessionInformation($sessionId) + { + $this->sessionRegistryStorage->removeSessionInformation($sessionId); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php new file mode 100644 index 0000000000000..6c43c7d864a9c --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.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\Http\Session; + +/** + * SessionRegistryStorageInterface. + * + * Stores the SessionInformation instances maintained in the SessionRegistry. + * + * @author Stefan Paschke + */ +interface SessionRegistryStorageInterface +{ + /** + * Obtains all the users for which session information is stored. + * + * @return array An array of UserInterface objects. + */ + function getUsers(); + + /** + * Obtains the session information for the specified sessionId. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation $sessionInformation + */ + function getSessionInformation($sessionId); + + /** + * Obtains the maintained information for one user. + * + * @param string $username The user identifier. + * @param boolean $includeExpiredSessions. + * @return array An array of SessionInformation objects. + */ + function getSessionInformations($username, $includeExpiredSessions); + + /** + * Sets a SessionInformation object. + * + * @param SessionInformation $sessionInformation + */ + function setSessionInformation(SessionInformation $sessionInformation); + + /** + * Deletes the maintained information of one session. + * + * @param string $sessionId the session identifier key. + */ + function removeSessionInformation($sessionId); +} From 90eba3473132105c46149511b11c2e4bbda8d9a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Wed, 24 Sep 2014 08:43:29 +0200 Subject: [PATCH 02/16] BC Concurrent Sessions Control --- .../Security/SessionRegistry/Schema.php | 11 +- .../SessionRegistry/SessionInformation.php | 42 --- .../SessionRegistryStorage.php | 131 +++++-- .../Command/InitConcurrentSessionsCommand.php | 23 +- .../DependencyInjection/MainConfiguration.php | 8 +- .../Security/Factory/AbstractFactory.php | 14 +- .../Security/Factory/FormLoginFactory.php | 4 +- .../Security/Factory/HttpBasicFactory.php | 2 +- .../Security/Factory/HttpDigestFactory.php | 2 +- .../Security/Factory/RememberMeFactory.php | 2 +- .../Factory/SecurityFactoryInterface.php | 2 +- .../Security/Factory/X509Factory.php | 2 +- .../DependencyInjection/SecurityExtension.php | 156 ++++++--- .../Resources/config/security_listeners.xml | 6 +- .../config/security_session_concurrency.xml | 35 ++ .../config/security_session_registry.xml | 25 -- .../config/security_session_registry_dbal.xml | 7 +- .../Functional/SessionConcurrencyTest.php | 87 +++++ .../StandardFormLogin/session_concurrency.yml | 21 ++ .../session_concurrency_expiration.yml | 14 + ...istener.php => ExpiredSessionListener.php} | 64 ++-- ...CompositeSessionAuthenticationStrategy.php | 55 +++ ...ntSessionControlAuthenticationStrategy.php | 120 +++++++ .../ConcurrentSessionControlStrategy.php | 138 -------- .../MockFileSessionRegistryStorage.php | 94 +++++ .../RegisterSessionAuthenticationStrategy.php | 42 +++ .../Http/Session/SessionInformation.php | 13 +- .../Security/Http/Session/SessionRegistry.php | 68 ++-- .../SessionRegistryStorageInterface.php | 29 +- .../Firewall/ExpiredSessionListenerTest.php | 321 ++++++++++++++++++ ...ositeSessionAuthenticationStrategyTest.php | 52 +++ ...ssionControlAuthenticationStrategyTest.php | 151 ++++++++ ...isterSessionAuthenticationStrategyTest.php | 59 ++++ .../Tests/Session/SessionInformationTest.php | 59 ++++ .../Tests/Session/SessionRegistryTest.php | 92 +++++ 35 files changed, 1539 insertions(+), 412 deletions(-) delete mode 100644 src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml delete mode 100644 src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionConcurrencyTest.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency.yml create mode 100644 src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency_expiration.yml rename src/Symfony/Component/Security/Http/Firewall/{ConcurrentSessionListener.php => ExpiredSessionListener.php} (56%) create mode 100644 src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php create mode 100644 src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php delete mode 100644 src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php create mode 100644 src/Symfony/Component/Security/Http/Session/MockFileSessionRegistryStorage.php create mode 100644 src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Session/RegisterSessionAuthenticationStrategyTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php index 355e662b29fba..05ac55320376e 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php @@ -17,27 +17,28 @@ * The schema used for the ACL system. * * @author Stefan Paschke + * @author Antonio J. García Lagar */ final class Schema extends BaseSchema { /** * Constructor * - * @param array $options the names for tables + * @param string $table */ - public function __construct(array $options) + public function __construct($table) { parent::__construct(); - $this->addSessionInformationTable($options); + $this->addSessionInformationTable($table); } /** * Adds the session_information table to the schema */ - protected function addSessionInformationTable(array $options) + protected function addSessionInformationTable($table) { - $table = $this->createTable($options['session_information_table_name']); + $table = $this->createTable($table); $table->addColumn('session_id', 'string'); $table->addColumn('username', 'string'); $table->addColumn('expired', 'datetime', array('unsigned' => true, 'notnull' => false)); diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php deleted file mode 100644 index f9eb8eb0ed957..0000000000000 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php +++ /dev/null @@ -1,42 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Doctrine\Security\SessionRegistry; - -use Symfony\Component\Security\Http\Session\SessionInformation as BaseSessionInformation; - -/** - * SessionInformation. - * - * Allows to persist SessionInformation using Doctrine DBAL. - * - * @author Stefan Paschke - */ -class SessionInformation extends BaseSessionInformation -{ - public function __construct($sessionId, $username, \DateTime $lastRequest = null, \DateTime $expired = null) - { - parent::__construct($sessionId, $username); - - if (null !== $lastRequest) { - $this->setLastRequest($lastRequest); - } - - if (null !== $expired) { - $this->setExpired($expired); - } - } - - public function getExpired() - { - return parent::getExpired(); - } -} diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index 742713a1789fc..646e52fc8ae77 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -2,39 +2,35 @@ namespace Symfony\Bridge\Doctrine\Security\SessionRegistry; -use Doctrine\DBAL\Driver\Connection; +use Doctrine\DBAL\Connection; use Symfony\Component\Security\Http\Session\SessionInformation; use Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface; +/** + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ class SessionRegistryStorage implements SessionRegistryStorageInterface { protected $connection; - protected $options; + protected $table; - public function __construct(Connection $connection, array $options) + public function __construct(Connection $connection, $table) { - $this->connection = $connection; - $this->options = $options; - } - - /** - * not implemented - */ - public function getUsers() - { - throw new \BadMethodCallException("Not implemented."); + $this->connection = $connection; + $this->table = $table; } /** * Obtains the maintained information for one session. * - * @param string $sessionId the session identifier key. + * @param string $sessionId the session identifier key. * @return SessionInformation a SessionInformation object. */ public function getSessionInformation($sessionId) { $statement = $this->connection->executeQuery( - 'SELECT * FROM '.$this->options['session_information_table_name'].' WHERE session_id = :session_id', + 'SELECT * FROM '.$this->table.' WHERE session_id = :session_id', array('session_id' => $sessionId) ); @@ -46,9 +42,9 @@ public function getSessionInformation($sessionId) /** * Obtains the maintained information for one user. * - * @param string $username The user identifier. - * @param boolean $includeExpiredSessions. - * @return array An array of SessionInformation objects. + * @param string $username The user identifier. + * @param boolean $includeExpiredSessions. + * @return array An array of SessionInformation objects. */ public function getSessionInformations($username, $includeExpiredSessions = false) { @@ -56,14 +52,13 @@ public function getSessionInformations($username, $includeExpiredSessions = fals $statement = $this->connection->executeQuery( 'SELECT * - FROM '.$this->options['session_information_table_name'].' + FROM '.$this->table.' WHERE username = :username'.($includeExpiredSessions ? '' : ' AND expired IS NULL ').' ORDER BY last_request DESC', array('username' => $username) ); - while ($data = $statement->fetch(\PDO::FETCH_ASSOC)) - { + while ($data = $statement->fetch(\PDO::FETCH_ASSOC)) { $sessionInformations[] = $this->instantiateSessionInformationFromResultSet($data); } @@ -78,17 +73,52 @@ public function getSessionInformations($username, $includeExpiredSessions = fals */ public function setSessionInformation(SessionInformation $sessionInformation) { - $statement = $this->connection->prepare( - 'INSERT INTO '.$this->options['session_information_table_name'].' - (session_id, username, last_request, expired) VALUES(:session_id, :username, :last_request, :expired) - ON DUPLICATE KEY - UPDATE username=:username, last_request=:last_request, expired=:expired'); - - $statement->bindValue('session_id', $sessionInformation->getSessionId()); - $statement->bindValue('username', $sessionInformation->getUsername()); - $statement->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); - $statement->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); - $statement->execute(); + $mergeSql = $this->getMergeSql(); + + if (null !== $mergeSql) { + $mergeStmt = $this->pdo->prepare($mergeSql); + $mergeStmt->bindValue('session_id', $sessionInformation->getSessionId()); + $mergeStmt->bindValue('username', $sessionInformation->getUsername()); + $mergeStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); + $mergeStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); + $mergeStmt->execute(); + + return true; + } + + $updateStmt = $this->pdo->prepare( + "UPDATE $this->table SET username=:username, last_request=:last_request, expired=:expired WHERE session_id = :session_id" + ); + $mergeStmt->bindValue('session_id', $sessionInformation->getSessionId()); + $mergeStmt->bindValue('username', $sessionInformation->getUsername()); + $mergeStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); + $mergeStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); + $updateStmt->execute(); + + // When MERGE is not supported, like in Postgres, we have to use this approach that can result in + // duplicate key errors when the same sessioninfo is written simultaneously. We can just catch such an + // error and re-execute the update. This is similar to a serializable transaction with retry logic + // on serialization failures but without the overhead and without possible false positives due to + // longer gap locking. + if (!$updateStmt->rowCount()) { + try { + $insertStmt = $this->pdo->prepare( + "INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired)" + ); + $insertStmt->bindValue('session_id', $sessionInformation->getSessionId()); + $insertStmt->bindValue('username', $sessionInformation->getUsername()); + $insertStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); + $insertStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); + $insertStmt->execute(); + } catch (\PDOException $e) { + // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys + if (0 === strpos($e->getCode(), '23')) { + $updateStmt->execute(); + } else { + throw $e; + } + } + } } /** @@ -98,16 +128,45 @@ public function setSessionInformation(SessionInformation $sessionInformation) */ public function removeSessionInformation($sessionId) { - $this->connection->delete($this->options['session_information_table_name'], array('session_id' => $sessionId)); + $this->connection->delete($this->table, array('session_id' => $sessionId)); } private function instantiateSessionInformationFromResultSet($data) { - return new $this->options['session_information_class_name']( + return new SessionInformation( $data['session_id'], $data['username'], - (null == $data['last_request']) ? null : new \DateTime($data['last_request']), - (null == $data['expired']) ? null : new \DateTime($data['expired']) + null === $data['last_request'] ? null : new \DateTime($data['last_request']), + null === $data['expired'] ? null : new \DateTime($data['expired']) ); } + + /** + * Returns a merge/upsert (i.e. insert or update) SQL query when supported by the database. + * + * @return string|null The SQL string or null when not supported + */ + private function getMergeSql() + { + switch ($this->connection->getDriver()->getName()) { + case 'pdo_mysql': + return "INSERT INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " . + "ON DUPLICATE KEY UPDATE username = VALUES(username), last_request = VALUES(last_request), expired = VALUES(expired)"; + case 'pdo_oracle': + // DUAL is Oracle specific dummy table + return "MERGE INTO $this->table USING DUAL ON (session_id= :session_id) " . + "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " . + "WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired"; + case 'pdo_sqlsrv': + if (version_compare($this->connection->getWrappedConnection()->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>=')) { + // MERGE is only available since SQL Server 2008 and must be terminated by semicolon + // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx + return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON (session_id = :session_id) " . + "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " . + "WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired;"; + } + case 'pdo_sqlite': + return "INSERT OR REPLACE INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired)"; + } + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php index ea20546d78bda..fa6203290350c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php @@ -20,6 +20,7 @@ * Installs the database schema required by the concurrent session Doctrine implementation * * @author Stefan Paschke + * @author Antonio J. García Lagar */ class InitConcurrentSessionsCommand extends ContainerAwareCommand { @@ -28,8 +29,6 @@ class InitConcurrentSessionsCommand extends ContainerAwareCommand */ protected function configure() { - parent::configure(); - $this ->setName('init:concurrent-session') ->setDescription('Executes the SQL needed to generate the database schema required by the concurrent sessions feature.') @@ -38,10 +37,6 @@ protected function configure() generate the database schema required by the concurrent session Doctrine implementation: ./app/console init:concurrent-session - -You can also output the SQL instead of executing it: - -./app/console init:concurrent-session --dump-sql EOT ); } @@ -54,23 +49,19 @@ protected function execute(InputInterface $input, OutputInterface $output) $connection = $this->getContainer()->get('security.session_registry.dbal.connection'); $sm = $connection->getSchemaManager(); $tableNames = $sm->listTableNames(); - $tables = array( - 'session_information_table_name' => $this->getContainer()->getParameter('security.session_registry.dbal.session_information_table_name'), - ); + $table = $this->getContainer()->getParameter('security.session_registry.dbal.session_information_table_name'); - foreach ($tables as $table) { - if (in_array($table, $tableNames, true)) { - $output->writeln(sprintf('The table "%s" already exists. Aborting.', $table)); + if (in_array($table, $tableNames, true)) { + $output->writeln(sprintf('The table "%s" already exists. Aborting.', $table)); - return; - } + return; } - $schema = new Schema($tables); + $schema = new Schema($table); foreach ($schema->toSql($connection->getDatabasePlatform()) as $sql) { $connection->exec($sql); } - $output->writeln('concurrent session tables have been initialized successfully.'); + $output->writeln('concurrent session table have been initialized successfully.'); } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 886fb2f68719e..aafb23d630a16 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -91,12 +91,7 @@ private function addSessionRegistrySection(ArrayNodeDefinition $rootNode) ->arrayNode('session_registry') ->children() ->scalarNode('connection')->end() - ->arrayNode('tables') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('session_information')->defaultValue('cs_session_information')->end() - ->end() - ->end() + ->scalarNode('table')->defaultValue('cs_session_information')->end() ->scalarNode('session_registry_storage')->end() ->end() ->end() @@ -313,6 +308,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->canBeUnset() ->children() ->scalarNode('max_sessions')->defaultNull()->end() + ->booleanNode('error_if_maximum_exceeded')->defaultTrue()->end() ->scalarNode('expiration_url')->defaultValue('/')->end() ->end() ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index dab748ba808f4..5db1656993e85 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -47,13 +47,13 @@ abstract class AbstractFactory implements SecurityFactoryInterface 'failure_path_parameter' => '_failure_path', ); - public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId, $sessionStrategy) + public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId) { // authentication provider $authProviderId = $this->createAuthProvider($container, $id, $config, $userProviderId); // authentication listener - $listenerId = $this->createListener($container, $id, $config, $userProviderId, $sessionStrategy); + $listenerId = $this->createListener($container, $id, $config, $userProviderId); // add remember-me aware tag if requested if ($this->isRememberMeAware($config)) { @@ -153,11 +153,17 @@ protected function isRememberMeAware($config) return $config['remember_me']; } - protected function createListener($container, $id, $config, $userProvider, $sessionStrategy) + protected function createListener($container, $id, $config, $userProvider) { $listenerId = $this->getListenerId(); $listener = new DefinitionDecorator($listenerId); - $listener->replaceArgument(2, $sessionStrategy); + + //Check for custom session authentication strategy + $sessionAuthenticationStrategyId = 'security.authentication.session_strategy.'.$id; + if ($container->hasDefinition($sessionAuthenticationStrategyId) || $container->hasAlias($sessionAuthenticationStrategyId)) { + $listener->replaceArgument(2, new Reference($sessionAuthenticationStrategyId)); + } + $listener->replaceArgument(4, $id); $listener->replaceArgument(5, new Reference($this->createAuthenticationSuccessHandler($container, $id, $config))); $listener->replaceArgument(6, new Reference($this->createAuthenticationFailureHandler($container, $id, $config))); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 9007ab9a79072..b674c47e15bf0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -71,9 +71,9 @@ protected function createAuthProvider(ContainerBuilder $container, $id, $config, return $provider; } - protected function createListener($container, $id, $config, $userProvider, $sessionStrategy) + protected function createListener($container, $id, $config, $userProvider) { - $listenerId = parent::createListener($container, $id, $config, $userProvider, $sessionStrategy); + $listenerId = parent::createListener($container, $id, $config, $userProvider); $container ->getDefinition($listenerId) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index 62fe22ca3115a..c87f68c38ff6b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -24,7 +24,7 @@ */ class HttpBasicFactory implements SecurityFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy) + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { $provider = 'security.authentication.provider.dao.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php index 5c1e9c334463c..3a49b5dcdc583 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php @@ -24,7 +24,7 @@ */ class HttpDigestFactory implements SecurityFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy) + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { $provider = 'security.authentication.provider.dao.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index ee16392c58a56..7aa4f5baa03eb 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -29,7 +29,7 @@ class RememberMeFactory implements SecurityFactoryInterface 'remember_me_parameter' => '_remember_me', ); - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy) + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { // authentication provider $authProviderId = 'security.authentication.provider.rememberme.'.$id; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php index d39c842f7835a..5e8773b742c13 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php @@ -21,7 +21,7 @@ */ interface SecurityFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy); + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint); public function getPosition(); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php index aa523d0aa73b2..19446d4a95017 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php @@ -25,7 +25,7 @@ */ class X509Factory implements SecurityFactoryInterface { - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy) + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) { $providerId = 'security.authentication.provider.pre_authenticated.'.$id; $container diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 04215ee890a8b..857f21300fba3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -165,7 +165,7 @@ private function configureDbalAclProvider(array $config, ContainerBuilder $conta private function sessionRegistryLoad($config, ContainerBuilder $container, $loader) { - $loader->load('security_session_registry.xml'); + $loader->load('security_session_concurrency.xml'); if (isset($config['session_registry_storage'])) { $container->setAlias('security.authentication.session_registry_storage', $config['session_registry_storage']); @@ -184,7 +184,7 @@ private function configureDbalSessionRegistryStorage($config, ContainerBuilder $ $container->setAlias('security.session_registry.dbal.connection', sprintf('doctrine.dbal.%s_connection', $config['connection'])); } - $container->setParameter('security.session_registry.dbal.session_information_table_name', $config['tables']['session_information']); + $container->setParameter('security.session_registry.dbal.session_information_table_name', $config['table']); } /** @@ -332,14 +332,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a )); $listeners[] = new Reference($listenerId); - // add logout success handler - if (isset($firewall['logout']['success_handler'])) { - $logoutSuccessHandlerId = $firewall['logout']['success_handler']; - } else { - $logoutSuccessHandlerId = 'security.logout.success_handler.'.$id; - $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new DefinitionDecorator('security.logout.success_handler')); - $logoutSuccessHandler->replaceArgument(1, $firewall['logout']['target']); - } + $logoutSuccessHandlerId = $this->createLogoutSuccessHandler($container, $id, $firewall['logout']); $listener->replaceArgument(2, new Reference($logoutSuccessHandlerId)); // add CSRF provider @@ -347,24 +340,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $listener->addArgument(new Reference($firewall['logout']['csrf_token_generator'])); } - // add session logout handler - if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) { - $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); - } - - // add cookie logout handler - if (count($firewall['logout']['delete_cookies']) > 0) { - $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id; - $cookieHandler = $container->setDefinition($cookieHandlerId, new DefinitionDecorator('security.logout.handler.cookie_clearing')); - $cookieHandler->addArgument($firewall['logout']['delete_cookies']); - - $listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId))); - } - - // add custom handlers - foreach ($firewall['logout']['handlers'] as $handlerId) { - $listener->addMethodCall('addHandler', array(new Reference($handlerId))); - } + $this->addLogoutHandlers($container, $listenerId, $id, $firewall); // register with LogoutUrlHelper $container @@ -392,9 +368,9 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a // Access listener $listeners[] = new Reference('security.access_listener'); - // Concurrent session listener + // Expired session listener if (isset($firewall['session_concurrency'])) { - $listeners[] = new Reference($this->createConcurrentSessionListener($container, $id, $firewall['session_concurrency'])); + $listeners[] = new Reference($this->createExpiredSessionListener($container, $id, $firewall)); } // Determine default entry point @@ -427,14 +403,8 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut $hasListeners = false; $defaultEntryPoint = null; - if (isset($firewall['session_concurrency']['max_sessions'])) { - $sessionStrategyId = 'security.authentication.concurrent_session_strategy.'.$id; - $container->setDefinition($sessionStrategyId, new DefinitionDecorator('security.authentication.concurrent_session_strategy')) - ->replaceArgument(1, $firewall['session_concurrency']['max_sessions']); - - $sessionStrategy = new Reference($sessionStrategyId); - } else { - $sessionStrategy = $container->get('security.authentication.session_strategy'); + if (isset($firewall['session_concurrency'])) { + $this->createConcurrentSessionAuthenticationStrategy($container, $id, $firewall['session_concurrency']); } foreach ($this->listenerPositions as $position) { @@ -444,7 +414,7 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut if (isset($firewall[$key])) { $userProvider = isset($firewall[$key]['provider']) ? $this->getUserProviderId($firewall[$key]['provider']) : $defaultProvider; - list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint, $sessionStrategy); + list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); $listeners[] = new Reference($listenerId); $authenticationProviders[] = $provider; @@ -670,14 +640,20 @@ private function createExpression($container, $expression) return $this->expressions[$id] = new Reference($id); } - private function createConcurrentSessionListener($container, $id, $config) + private function createExpiredSessionListener($container, $id, $config) { - $concurrentSessionListenerId = 'security.authentication.concurrentsession_listener.'.$id; - $listener = $container->setDefinition($concurrentSessionListenerId, new DefinitionDecorator('security.authentication.concurrentsession_listener')); - $listener->replaceArgument(3, $config['expiration_url']); - $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); + $expiredSessionListenerId = 'security.authentication.expiredsession_listener.'.$id; + $listener = $container->setDefinition($expiredSessionListenerId, new DefinitionDecorator('security.authentication.expiredsession_listener')); - return $concurrentSessionListenerId; + $listener->replaceArgument(3, $config['session_concurrency']['expiration_url']); + + if (isset($config['logout'])) { + $logoutSuccessHandlerId = $this->createLogoutSuccessHandler($container, $id, $config); + $listener->replaceArgument(4, new Reference($logoutSuccessHandlerId)); + $this->addLogoutHandlers($container, $expiredSessionListenerId, $id, $config); + } + + return $expiredSessionListenerId; } private function createRequestMatcher($container, $path = null, $host = null, $methods = array(), $ip = null, array $attributes = array()) @@ -750,4 +726,94 @@ private function getExpressionLanguage() return $this->expressionLanguage; } + + private function createConcurrentSessionAuthenticationStrategy($container, $id, $config) + { + $sessionStrategyId = 'security.authentication.session_strategy.'.$id; + + if (isset($config['max_sessions'])) { + $concurrentSessionControlStrategyId = 'security.authentication.session_strategy.concurrent_control.'.$id; + $container->setDefinition( + $concurrentSessionControlStrategyId, + new DefinitionDecorator( + 'security.authentication.session_strategy.concurrent_control' + ) + )->replaceArgument(1, $config['max_sessions']) + ->replaceArgument(2, $config['error_if_maximum_exceeded']); + + $fixationSessionStrategyId = 'security.authentication.session_strategy.fixation.'.$id; + $container->setAlias( + $fixationSessionStrategyId, + 'security.authentication.session_strategy' + ); + + $registerSessionStrategyId = 'security.authentication.session_strategy.register.'.$id; + $container->setDefinition( + $registerSessionStrategyId, + new DefinitionDecorator( + 'security.authentication.session_strategy.register' + ) + ); + + $container->setDefinition( + $sessionStrategyId, + new DefinitionDecorator( + 'security.authentication.session_strategy.composite' + ) + )->replaceArgument( + 0, + array( + new Reference($concurrentSessionControlStrategyId), + new Reference($fixationSessionStrategyId), + new Reference($registerSessionStrategyId) + ) + ); + } else { + $container->setAlias( + $sessionStrategyId, + 'security.authentication.session_strategy' + ); + } + + return $sessionStrategyId; + } + + private function createLogoutSuccessHandler($container, $id, $config) + { + // add logout success handler + if (isset($config['success_handler'])) { + $logoutSuccessHandlerId = $config['success_handler']; + } else { + $logoutSuccessHandlerId = 'security.logout.success_handler.'.$id; + if (!$container->hasDefinition($logoutSuccessHandlerId)) { + $logoutSuccessHandler = $container->setDefinition($logoutSuccessHandlerId, new DefinitionDecorator('security.logout.success_handler')); + $logoutSuccessHandler->replaceArgument(1, $config['target']); + } + } + return $logoutSuccessHandlerId; + } + + private function addLogoutHandlers($container, $listenerId, $id, $config) + { + $listener = $container->findDefinition($listenerId); + + // add session logout handler + if (true === $config['logout']['invalidate_session'] && false === $config['stateless']) { + $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); + } + + // add cookie logout handler + if (count($config['logout']['delete_cookies']) > 0) { + $cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id; + $cookieHandler = $container->setDefinition($cookieHandlerId, new DefinitionDecorator('security.logout.handler.cookie_clearing')); + $cookieHandler->addArgument($config['logout']['delete_cookies']); + + $listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId))); + } + + // add custom handlers + foreach ($config['logout']['handlers'] as $handlerId) { + $listener->addMethodCall('addHandler', array(new Reference($handlerId))); + } + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index e3b7a6777a953..0097bd1f5ef68 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -28,7 +28,7 @@ Symfony\Component\Security\Http\Firewall\SwitchUserListener - Symfony\Component\Security\Http\Firewall\ConcurrentSessionListener + Symfony\Component\Security\Http\Firewall\ExpiredSessionListener Symfony\Component\Security\Http\Firewall\LogoutListener Symfony\Component\Security\Http\Logout\SessionLogoutHandler @@ -259,13 +259,13 @@ - + - + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml new file mode 100644 index 0000000000000..70347005a1f85 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml @@ -0,0 +1,35 @@ + + + + + + Symfony\Component\Security\Http\Session\ConcurrentSessionControlAuthenticationStrategy + Symfony\Component\Security\Http\Session\RegisterSessionAuthenticationStrategy + Symfony\Component\Security\Http\Session\CompositeSessionAuthenticationStrategy + Symfony\Component\Security\Http\Session\SessionRegistry + Symfony\Component\Security\Http\Session\SessionInformation + + + + + + + + + + + + + + + + + + + + %security.authentication.session_information.class% + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml deleted file mode 100644 index 93eaa73eec193..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Symfony\Component\Security\Http\Session\ConcurrentSessionControlStrategy - Symfony\Component\Security\Http\Session\SessionRegistry - Symfony\Component\Security\Http\Session\SessionInformation - - - - - - - %security.authentication.session_strategy.strategy% - - - - - %security.authentication.session_information.class% - - - diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml index 42c8d03a8f9b1..bcee305f19d61 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml @@ -6,19 +6,14 @@ Symfony\Bridge\Doctrine\Security\SessionRegistry\SessionRegistryStorage - Symfony\Bridge\Doctrine\Security\SessionRegistry\SessionInformation - - - %security.session_registry.dbal.session_information_table_name% - %security.authentication.session_information.class% - + %security.session_registry.dbal.session_information_table_name% diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionConcurrencyTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionConcurrencyTest.php new file mode 100644 index 0000000000000..37c59d3225b6f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SessionConcurrencyTest.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +/** + * @author Antonio J. García Lagar + * @group functional + */ +class SessionConcurrencyTest extends WebTestCase +{ + public function testLoginWorksWhenConcurrentSessionsLesserThanMaximun() + { + $client = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'session_concurrency.yml')); + $client->insulate(); + $form = $client->request('GET', '/login')->selectButton('login')->form(); + $form['_username'] = 'johannes'; + $form['_password'] = 'test'; + $client->submit($form); + + $this->assertRedirect($client->getResponse(), '/profile'); + } + + public function testLoginFailsWhenConcurrentSessionsGreaterOrEqualThanMaximun() + { + $client1 = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'session_concurrency.yml')); + $client1->insulate(); + $form1 = $client1->request('GET', '/login')->selectButton('login')->form(); + $form1['_username'] = 'johannes'; + $form1['_password'] = 'test'; + $client1->submit($form1); + + $client2 = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'session_concurrency.yml')); + $client2->insulate(); + $form2 = $client2->request('GET', '/login')->selectButton('login')->form(); + $form2['_username'] = 'johannes'; + $form2['_password'] = 'test'; + $client2->submit($form2); + + $this->assertRedirect($client2->getResponse(), '/login'); + } + + public function testOldSessionExpiresConcurrentSessionsGreaterOrEqualThanMaximun() + { + $client1 = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'session_concurrency_expiration.yml')); + $client1->insulate(); + $form1 = $client1->request('GET', '/login')->selectButton('login')->form(); + $form1['_username'] = 'johannes'; + $form1['_password'] = 'test'; + $client1->submit($form1); + $this->assertRedirect($client1->getResponse(), '/profile'); + + $client2 = $this->createClient(array('test_case' => 'StandardFormLogin', 'root_config' => 'session_concurrency_expiration.yml')); + $client2->insulate(); + $form2 = $client2->request('GET', '/login')->selectButton('login')->form(); + $form2['_username'] = 'johannes'; + $form2['_password'] = 'test'; + $client2->submit($form2); + + $this->assertRedirect($client2->getResponse(), '/profile'); + + $client1->request('GET', '/profile'); + $this->assertRedirect($client1->getResponse(), '/expired'); + } + + protected function setUp() + { + parent::setUp(); + + $this->deleteTmpDir('StandardFormLogin'); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->deleteTmpDir('StandardFormLogin'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency.yml new file mode 100644 index 0000000000000..edef156195f20 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency.yml @@ -0,0 +1,21 @@ +imports: + - { resource: ./config.yml } + +services: + my_session_registry_storage: + class: Symfony\Component\Security\Http\Session\MockFileSessionRegistryStorage + arguments: + - %kernel.cache_dir%/session_registry + +security: + firewalls: + default: + form_login: + check_path: /login_check + default_target_path: /profile + anonymous: ~ + session_concurrency: + max_sessions: 1 + + session_registry: + session_registry_storage: my_session_registry_storage diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency_expiration.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency_expiration.yml new file mode 100644 index 0000000000000..f47e7ea1dd14b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/session_concurrency_expiration.yml @@ -0,0 +1,14 @@ +imports: + - { resource: ./session_concurrency.yml } + +security: + firewalls: + default: + form_login: + check_path: /login_check + default_target_path: /profile + anonymous: ~ + session_concurrency: + max_sessions: 1 + error_if_maximum_exceeded: false + expiration_url: /expired diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php similarity index 56% rename from src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php rename to src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php index 95dbbf795eb15..90cadd3a5d040 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php @@ -3,7 +3,7 @@ /* * This file is part of the Symfony package. * - * (c) Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,24 +11,28 @@ namespace Symfony\Component\Security\Http\Firewall; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Log\LoggerInterface; use Symfony\Component\Security\Core\SecurityContextInterface; use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; use Symfony\Component\Security\Http\Session\SessionRegistry; use Symfony\Component\Security\Http\HttpUtils; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\HttpKernel\Log\LoggerInterface; -class ConcurrentSessionListener implements ListenerInterface +/** + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ +class ExpiredSessionListener implements ListenerInterface { private $securityContext; private $httpUtils; private $sessionRegistry; private $targetUrl; + private $successHandler; private $logger; private $handlers; - private $successHandler; public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, SessionRegistry $sessionRegistry, $targetUrl = '/', LogoutSuccessHandlerInterface $successHandler = null, LoggerInterface $logger = null) { @@ -55,43 +59,47 @@ public function addHandler(LogoutHandlerInterface $handler) * Handles the number of simultaneous sessions for a single user. * * @param GetResponseEvent $event A GetResponseEvent instance + * @throws \RuntimeException if the successHandler exists and do not return a response */ public function handle(GetResponseEvent $event) { $request = $event->getRequest(); - $session = $request->hasSession() ? $request->getSession() : null; + $session = $request->getSession(); if (null === $session || null === $token = $this->securityContext->getToken()) { return; - } else { - if ($sessionInformation = $this->sessionRegistry->getSessionInformation($session->getId())) { - if ($sessionInformation->isExpired()) { - if (null !== $this->successHandler) { - $response = $this->successHandler->onLogoutSuccess($request); - - if (!$response instanceof Response) { - throw new \RuntimeException('Logout Success Handler did not return a Response.'); - } - } else { - $response = $this->httpUtils->createRedirectResponse($request, $this->targetUrl); - } + } - foreach ($this->handlers as $handler) { - $handler->logout($request, $response, $token); - } + if ($sessionInformation = $this->sessionRegistry->getSessionInformation($session->getId())) { + if ($sessionInformation->isExpired()) { - $this->securityContext->setToken(null); + if (null !== $this->logger) { + $this->logger->info(sprintf("Logging out expired session for username '%s'", $token->getUsername())); + } - $event->setResponse($response); + if (null !== $this->successHandler) { + $response = $this->successHandler->onLogoutSuccess($request); + if (!$response instanceof Response) { + throw new \RuntimeException('Logout Success Handler did not return a Response.'); + } } else { - $sessionInformation->refreshLastRequest(); - $this->sessionRegistry->setSessionInformation($sessionInformation); + $response = $this->httpUtils->createRedirectResponse($request, $this->targetUrl); } + + foreach ($this->handlers as $handler) { + $handler->logout($request, $response, $token); + } + + $this->securityContext->setToken(null); + + $event->setResponse($response); } else { - // sessionInformation was lost, try to recover by recreating it - $this->sessionRegistry->registerNewSession($session->getId(), $token->getUser()); + $this->sessionRegistry->refreshLastRequest($session->getId()); } + } else { + // sessionInformation was lost, try to recover by recreating it + $this->sessionRegistry->registerNewSession($session->getId(), $token->getUsername()); } } } diff --git a/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php new file mode 100644 index 0000000000000..e61ab2a1eccef --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * A session authentication strategy that accepts multiple + * SessionAuthenticationStrategyInterface implementations to delegate to. Each + * SessionAuthenticationStrategyInterface is invoked in turn. The invocations are + * short circuited if any exception, (i.e. SessionAuthenticationException) is + * thrown. + * + * @author Antonio J. García Lagar + */ +class CompositeSessionAuthenticationStrategy implements SessionAuthenticationStrategyInterface +{ + /** + * @var array + */ + private $delegateStrategies = array(); + + public function __construct(array $delegateStrategies) + { + foreach ($delegateStrategies as $strategy) { + $this->addDelegateStrategy($strategy); + } + } + + /** + * {@inheritdoc} + */ + public function onAuthentication(Request $request, TokenInterface $token) + { + foreach ($this->delegateStrategies as $strategy) { + /* @var $strategy SessionAuthenticationStrategyInterface */ + $strategy->onAuthentication($request, $token); + } + } + + private function addDelegateStrategy(SessionAuthenticationStrategyInterface $strategy) + { + $this->delegateStrategies[] = $strategy; + } +} diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php new file mode 100644 index 0000000000000..857e66d7f9908 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\MaxSessionsExceededException; + +/** + * ConcurrentSessionControlAuthenticationStrategy. + * + * Strategy which handles concurrent session-control, in addition to the functionality provided by the base class. + * When invoked following an authentication, it will check whether the user in question should be allowed to proceed, + * by comparing the number of sessions they already have active with the configured maximumSessions value. + * The SessionRegistry is used as the source of data on authenticated users and session data. + * + * @author Stefan Paschke + * @author Antonio J. García Lagar + */ +class ConcurrentSessionControlAuthenticationStrategy implements SessionAuthenticationStrategyInterface +{ + protected $registry; + protected $exceptionIfMaximumExceeded; + protected $maximumSessions; + + public function __construct(SessionRegistry $registry, $maximumSessions, $exceptionIfMaximumExceeded = true) + { + $this->registry = $registry; + $this->setMaximumSessions($maximumSessions); + $this->setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded); + } + + /** + * {@inheritdoc} + */ + public function onAuthentication(Request $request, TokenInterface $token) + { + $username = $token->getUsername(); + + $sessions = $this->registry->getAllSessions($username); + $sessionCount = count($sessions); + $maxSessions = $this->getMaximumSessionsForThisUser($username); + + if ($sessionCount < $maxSessions) { + return; + } + + if ($sessionCount == $maxSessions) { + foreach ($sessions as $sessionInfo) { + /* @var $sessionInfo SessionInformation */ + if ($sessionInfo->getSessionId() == $request->getSession()->getId()) { + return; + } + } + } + + $this->allowableSessionsExceeded($sessions, $maxSessions, $this->registry); + } + + /** + * Sets a boolean flag that causes a RuntimeException to be thrown if the number of sessions is exceeded. + * + * @param bool $exceptionIfMaximumExceeded + */ + public function setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded) + { + $this->exceptionIfMaximumExceeded = (bool) $exceptionIfMaximumExceeded; + } + + /** + * Sets the maxSessions property. + * + * @param $maximumSessions + */ + public function setMaximumSessions($maximumSessions) + { + $this->maximumSessions = (integer) $maximumSessions; + } + + /** + * Allows subclasses to customise behaviour when too many sessions are detected. + * + * @param array $orderedSessions Array of SessionInformation ordered from + * newest to oldest + * @param integer $allowableSessions + * @param SessionRegistry $registry + */ + protected function allowableSessionsExceeded($orderedSessions, $allowableSessions, SessionRegistry $registry) + { + if ($this->exceptionIfMaximumExceeded) { + throw new MaxSessionsExceededException(sprintf('Maximum number of sessions (%s) exceeded', $allowableSessions)); + } + + // Expire oldest session + $orderedSessionsVector = array_values($orderedSessions); + for ($i = $allowableSessions - 1, $countSessions = count($orderedSessionsVector); $i < $countSessions; $i++) { + $registry->expireNow($orderedSessionsVector[$i]->getSessionId()); + } + } + + /** + * Method intended for use by subclasses to override the maximum number of sessions that are permitted for a particular authentication. + * + * @param string $username + * @return integer + */ + protected function getMaximumSessionsForThisUser($username) + { + return $this->maximumSessions; + } +} diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php deleted file mode 100644 index 36909330248f3..0000000000000 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php +++ /dev/null @@ -1,138 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Security\Http\Session; - -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Security\Core\User\UserInterface; - -/** - * ConcurrentSessionControlStrategy. - * - * Strategy which handles concurrent session-control, in addition to the functionality provided by the base class. - * When invoked following an authentication, it will check whether the user in question should be allowed to proceed, - * by comparing the number of sessions they already have active with the configured maximumSessions value. - * The SessionRegistry is used as the source of data on authenticated users and session data. - * - * @author Stefan Paschke - */ -class ConcurrentSessionControlStrategy extends SessionAuthenticationStrategy -{ - protected $registry; - protected $alwaysCreateSession; - protected $exceptionIfMaximumExceeded = false; - protected $maximumSessions; - - public function __construct(SessionRegistry $registry, $maximumSessions, $sessionAuthenticationStrategy) - { - parent::__construct($sessionAuthenticationStrategy); - $this->registry = $registry; - $this->setMaximumSessions($maximumSessions); - } - - /** - * {@inheritDoc} - */ - public function onAuthentication(Request $request, TokenInterface $token) - { - $user = $token->getUser(); - $originalSessionId = $request->getSession()->getId(); - - parent::onAuthentication($request, $token); - - if ($originalSessionId != $request->getSession()->getId()) { - $this->onSessionChange($originalSessionId, $request->getSession()->getId()); - } - - $sessions = $this->registry->getAllSessions($user); - $maxSessions = $this->getMaximumSessionsForThisUser($user); - - if (count($sessions) >= $maxSessions && $this->alwaysCreateSession !== true) { - if ($this->exceptionIfMaximumExceeded) { - throw new MaxSessionsExceededException(sprintf('Maximum of sessions (%s) exceeded', $maxSessions)); - } - - $this->allowableSessionsExceeded($sessions, $maxSessions, $this->registry); - } - - $this->registry->registerNewSession($request->getSession()->getID(), $user); - } - - /** - * Sets a boolean flag that allows to bypass allowableSessionsExceeded(). - * - * param boolean $alwaysCreateSession - */ - public function setAlwaysCreateSession($alwaysCreateSession) - { - $this->alwaysCreateSession = $alwaysCreateSession; - } - - /** - * Sets a boolean flag that causes a RuntimeException to be thrown if the number of sessions is exceeded. - * - * @param boolean $exceptionIfMaximumExceeded - */ - public function setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded) - { - $this->exceptionIfMaximumExceeded = $exceptionIfMaximumExceeded; - } - - /** - * Sets the maxSessions property. - * - * @param $maximumSessions - */ - public function setMaximumSessions($maximumSessions) - { - $this->maximumSessions = $maximumSessions; - } - - /** - * Allows subclasses to customise behaviour when too many sessions are detected. - * - * @param array $sessions - * @param integer $allowableSessions - * @param SessionRegistry $registry - */ - protected function allowableSessionsExceeded($sessions, $allowableSessions, SessionRegistry $registry) - { - // remove oldest sessions from registry - for ($i = $allowableSessions - 1; $i < count($sessions); $i++) { - $sessions[$i]->expireNow(); - $registry->setSessionInformation($sessions[$i]); - } - } - - /** - * Method intended for use by subclasses to override the maximum number of sessions that are permitted for a particular authentication. - * - * @param UserInterface $user - * @return integer - */ - protected function getMaximumSessionsForThisUser(UserInterface $user) - { - return $this->maximumSessions; - } - - /** - * Called when the session has been changed and the old attributes have been migrated to the new session. - * - * @param string $originalSessionId - * @param string $newSessionId - * @param TokenInterface $token - */ - protected function onSessionChange($originalSessionId, $newSessionId) - { - $this->registry->removeSessionInformation($originalSessionId); - } -} diff --git a/src/Symfony/Component/Security/Http/Session/MockFileSessionRegistryStorage.php b/src/Symfony/Component/Security/Http/Session/MockFileSessionRegistryStorage.php new file mode 100644 index 0000000000000..6bc0e8faa50b1 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/MockFileSessionRegistryStorage.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +/** + * MockFileSessionRegistryStorage mocks the session registry for functional tests. + * + * @author Antonio J. García Lagar + */ +class MockFileSessionRegistryStorage implements SessionRegistryStorageInterface +{ + private $savePath; + + /** + * @param string $savePath + */ + public function __construct($savePath = null) + { + if (null === $savePath) { + $savePath = sys_get_temp_dir(); + } + + if (!is_dir($savePath)) { + mkdir($savePath, 0777, true); + } + + $this->savePath = $savePath; + } + + /** + * {@inheritdoc} + */ + public function getSessionInformation($sessionId) + { + $filename = $this->getFilePath($sessionId); + if (file_exists($filename)) { + return $this->fileToSessionInfo($filename); + } + } + + /** + * {@inheritdoc} + */ + public function getSessionInformations($username, $includeExpiredSessions = false) + { + $result = array(); + + foreach (glob($this->getFilePath('*')) as $filename) { + $sessionInfo = $this->fileToSessionInfo($filename); + if ($sessionInfo->getUsername() == $username && ($includeExpiredSessions || !$sessionInfo->isExpired())) { + $result[] = $sessionInfo; + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function setSessionInformation(SessionInformation $sessionInformation) + { + file_put_contents($this->getFilePath($sessionInformation->getSessionId()), serialize($sessionInformation)); + } + + /** + * {@inheritdoc} + */ + public function removeSessionInformation($sessionId) + { + if (isset($this->sessionInformations[$sessionId])) { + unset($this->sessionInformations[$sessionId]); + } + } + + private function getFilePath($sessionId) + { + return $this->savePath.'/'.$sessionId.'.mocksessinfo'; + } + + private function fileToSessionInfo($filename) + { + return unserialize(file_get_contents($filename)); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php new file mode 100644 index 0000000000000..1f7a191ad2227 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Session; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Strategy used to register a user with the SessionRegistry after + * successful authentication. + * + * @author Antonio J. García Lagar + */ +class RegisterSessionAuthenticationStrategy implements SessionAuthenticationStrategyInterface +{ + /** + * @var SessionRegistry + */ + private $registry; + + public function __construct(SessionRegistry $registry) + { + $this->registry = $registry; + } + + /** + * {@inheritdoc} + */ + public function onAuthentication(Request $request, TokenInterface $token) + { + $this->registry->registerNewSession($request->getSession()->getId(), $token->getUsername()); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 81b47f3974e8d..5075e0a12eb35 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -3,7 +3,7 @@ /* * This file is part of the Symfony package. * - * (c) Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -25,10 +25,15 @@ class SessionInformation protected $expired; protected $lastRequest; - public function __construct($sessionId, $username) + public function __construct($sessionId, $username, \DateTime $lastRequest, \DateTime $expired = null) { $this->setSessionId($sessionId); $this->setUsername($username); + $this->setLastRequest($lastRequest); + + if (null !== $expired) { + $this->setExpired($expired); + } } /** @@ -73,11 +78,11 @@ public function getSessionId() /** * Return wether this session is expired. * - * @return boolean + * @return bool */ public function isExpired() { - return $this->getExpired() && $this->getExpired()->getTimestamp() < time(); + return null !== $this->getExpired() && $this->getExpired()->getTimestamp() < microtime(true); } /** diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index ab34177c31a93..225e125cab0a8 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -3,7 +3,7 @@ /* * This file is part of the Symfony package. * - * (c) Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -11,50 +11,40 @@ namespace Symfony\Component\Security\Http\Session; -use Symfony\Component\Security\Core\User\UserInterface; - /** * SessionRegistry. * * Maintains a registry of SessionInformation instances. * * @author Stefan Paschke + * @author Antonio J. García Lagar */ class SessionRegistry { protected $sessionRegistryStorage; - protected $sessionInformationClass; - public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage, $sessionInformationClass) + public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage) { $this->sessionRegistryStorage = $sessionRegistryStorage; - $this->sessionInformationClass = $sessionInformationClass; - } - - /** - * Obtains all the users for which session information is stored. - * - * @return array An array of UserInterface objects. - */ - public function getAllUsers() - { - return $this->sessionRegistryStorage->getUsers(); } /** * Obtains all the known sessions for the specified user. * - * @param UserInterface $user the specified user. - * @param boolean $includeExpiredSessions. - * @return array An array of SessionInformation objects. + * @param string $username the specified user. + * @param boolean $includeExpiredSessions + * @return array An array of SessionInformation objects. */ - public function getAllSessions(UserInterface $user, $includeExpiredSessions = false) + public function getAllSessions($username, $includeExpiredSessions = false) { - return $this->sessionRegistryStorage->getSessionInformations($user->getUsername(), $includeExpiredSessions); + return $this->sessionRegistryStorage->getSessionInformations($username, $includeExpiredSessions); } /** - * {@inheritDoc} + * Obtains the session information for the specified sessionId. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation $sessionInformation */ public function getSessionInformation($sessionId) { @@ -62,9 +52,11 @@ public function getSessionInformation($sessionId) } /** - * {@inheritDoc} + * Sets a SessionInformation object. + * + * @param SessionInformation $sessionInformation */ - public function setSessionInformation(SessionInformation $sessionInformation) + private function setSessionInformation(SessionInformation $sessionInformation) { $this->sessionRegistryStorage->setSessionInformation($sessionInformation); } @@ -83,21 +75,37 @@ public function refreshLastRequest($sessionId) } /** - * Registers a new session for the specified user. + * Expires the given sessionId. * * @param string $sessionId the session identifier key. - * @param UserInterface $user the specified user. */ - public function registerNewSession($sessionId, UserInterface $user) + public function expireNow($sessionId) + { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + $sessionInformation->expireNow(); + $this->setSessionInformation($sessionInformation); + } + } + + /** + * Registers a new session for the specified user. + * + * @param string $sessionId the session identifier key. + * @param string $username the specified user. + * @param \DateTime $lastRequest + */ + public function registerNewSession($sessionId, $username, \DateTime $lastRequest = null) { - $sessionInformation = new $this->sessionInformationClass($sessionId, $user->getUsername()); - $sessionInformation->refreshLastRequest(); + $lastRequest = ($lastRequest) ?: new \DateTime(); + $sessionInformation = new SessionInformation($sessionId, $username, $lastRequest); $this->setSessionInformation($sessionInformation); } /** - * {@inheritDoc} + * Deletes the maintained information of one session. + * + * @param string $sessionId the session identifier key. */ public function removeSessionInformation($sessionId) { diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php index 6c43c7d864a9c..37c48031911db 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -3,7 +3,7 @@ /* * This file is part of the Symfony package. * - * (c) Fabien Potencier + * (c) Fabien Potencier * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -17,44 +17,39 @@ * Stores the SessionInformation instances maintained in the SessionRegistry. * * @author Stefan Paschke + * @author Antonio J. García Lagar */ interface SessionRegistryStorageInterface { - /** - * Obtains all the users for which session information is stored. - * - * @return array An array of UserInterface objects. - */ - function getUsers(); - /** * Obtains the session information for the specified sessionId. * - * @param string $sessionId the session identifier key. + * @param string $sessionId the session identifier key. * @return SessionInformation $sessionInformation */ - function getSessionInformation($sessionId); + public function getSessionInformation($sessionId); /** - * Obtains the maintained information for one user. + * Obtains the maintained information for one user ordered from newest to + * oldest * - * @param string $username The user identifier. - * @param boolean $includeExpiredSessions. - * @return array An array of SessionInformation objects. + * @param string $username The user identifier. + * @param bool $includeExpiredSessions + * @return array An array of SessionInformation objects. */ - function getSessionInformations($username, $includeExpiredSessions); + public function getSessionInformations($username, $includeExpiredSessions = false); /** * Sets a SessionInformation object. * * @param SessionInformation $sessionInformation */ - function setSessionInformation(SessionInformation $sessionInformation); + public function setSessionInformation(SessionInformation $sessionInformation); /** * Deletes the maintained information of one session. * * @param string $sessionId the session identifier key. */ - function removeSessionInformation($sessionId); + public function removeSessionInformation($sessionId); } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php new file mode 100644 index 0000000000000..0e5cb14f4a462 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php @@ -0,0 +1,321 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Firewall; + +use Symfony\Component\Security\Http\Firewall\ExpiredSessionListener; + +/** + * @author Antonio J. García Lagar + */ +class ExpiredSessionListenerTest extends \PHPUnit_Framework_TestCase +{ + public function testHandleWhenNoSession() + { + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('hasSession') + ->will($this->returnValue(false)) + ; + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)) + ; + + $listener = new ExpiredSessionListener( + $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'), + $this->getHttpUtils(), + $this->getSessionRegistry() + ); + + $this->assertNull($listener->handle($event)); + } + + public function testHandleWhenNoToken() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('hasSession') + ->will($this->returnValue(true)) + ; + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)) + ; + + $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue(null)) + ; + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)) + ; + + $listener = new ExpiredSessionListener( + $securityContext, + $this->getHttpUtils(), + $this->getSessionRegistry() + ); + + $this->assertNull($listener->handle($event)); + } + + public function testHandleWhenSessionInformationIsExpired() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session + ->expects($this->any()) + ->method('getId') + ->will($this->returnValue('foo')) + ; + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('hasSession') + ->will($this->returnValue(true)) + ; + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)) + ; + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token + ->expects($this->any()) + ->method('getUsername') + ->will($this->returnValue('foobar')) + ; + + $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)) + ; + + $sessionInformation = $this->getSessionInformation(); + $sessionInformation + ->expects($this->once()) + ->method('isExpired') + ->will($this->returnValue(true)) + ; + + $sessionRegistry = $this->getSessionRegistry(); + $sessionRegistry + ->expects($this->once()) + ->method('getSessionInformation') + ->with($this->equalTo('foo')) + ->will($this->returnValue($sessionInformation)) + ; + + $response = $this->getMock('Symfony\Component\HttpFoundation\Response'); + + $httpUtils = $this->getHttpUtils(); + $httpUtils + ->expects($this->once()) + ->method('createRedirectResponse') + ->with($this->identicalTo($request), $this->equalTo('/')) + ->will($this->returnValue($response)) + ; + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)) + ; + $event + ->expects($this->once()) + ->method('setResponse') + ->with($this->identicalTo($response)) + ; + + $listener = new ExpiredSessionListener( + $securityContext, + $httpUtils, + $sessionRegistry + ); + + $this->assertNull($listener->handle($event)); + } + + public function testHandleWhenSessionInformationIsNotExpired() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session + ->expects($this->any()) + ->method('getId') + ->will($this->returnValue('foo')) + ; + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('hasSession') + ->will($this->returnValue(true)) + ; + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)) + ; + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token + ->expects($this->any()) + ->method('getUsername') + ->will($this->returnValue('foobar')) + ; + + $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext + ->expects($this->once()) + ->method('getToken') + ->will($this->returnValue($token)) + ; + + $sessionInformation = $this->getSessionInformation(); + $sessionInformation + ->expects($this->once()) + ->method('isExpired') + ->will($this->returnValue(false)) + ; + + $sessionRegistry = $this->getSessionRegistry(); + $sessionRegistry + ->expects($this->once()) + ->method('getSessionInformation') + ->with($this->equalTo('foo')) + ->will($this->returnValue($sessionInformation)) + ; + $sessionRegistry + ->expects($this->once()) + ->method('refreshLastRequest') + ->with($this->equalTo('foo')) + ; + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)) + ; + + $listener = new ExpiredSessionListener( + $securityContext, + $this->getHttpUtils(), + $sessionRegistry + ); + + $this->assertNull($listener->handle($event)); + } + + public function testHandleWhenNoSessionInformationIsRegistered() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session + ->expects($this->any()) + ->method('getId') + ->will($this->returnValue('foo')) + ; + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $request + ->expects($this->any()) + ->method('hasSession') + ->will($this->returnValue(true)) + ; + $request + ->expects($this->any()) + ->method('getSession') + ->will($this->returnValue($session)) + ; + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token + ->expects($this->once()) + ->method('getUsername') + ->will($this->returnValue('foobar')) + ; + + $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext + ->expects($this->any()) + ->method('getToken') + ->will($this->returnValue($token)) + ; + + $sessionRegistry = $this->getSessionRegistry(); + $sessionRegistry + ->expects($this->once()) + ->method('getSessionInformation') + ->with($this->equalTo('foo')) + ->will($this->returnValue(null)) + ; + $sessionRegistry + ->expects($this->once()) + ->method('registerNewSession') + ->with($this->equalTo('foo'), $this->equalTo('foobar')) + ; + + $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); + $event + ->expects($this->any()) + ->method('getRequest') + ->will($this->returnValue($request)) + ; + + $listener = new ExpiredSessionListener( + $securityContext, + $this->getHttpUtils(), + $sessionRegistry + ); + + $this->assertNull($listener->handle($event)); + } + + private function getHttpUtils() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\HttpUtils') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getSessionRegistry() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionRegistry') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getSessionInformation() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionInformation') + ->disableOriginalConstructor() + ->getMock(); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php new file mode 100644 index 0000000000000..3e4f148060627 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\CompositeSessionAuthenticationStrategy; + +/** + * @author Antonio J. García Lagar + */ +class CompositeSessionAuthenticationStrategyTest extends \PHPUnit_Framework_TestCase +{ + public function testAuthenticationDelegation() + { + $strategies = array( + $this->getDelegateAuthenticationStrategy(), + $this->getDelegateAuthenticationStrategy(), + $this->getDelegateAuthenticationStrategy() + ); + + $request = $this->getRequest(); + + $strategy = new CompositeSessionAuthenticationStrategy($strategies); + $strategy->onAuthentication($request, $this->getToken()); + } + + private function getRequest() + { + return $this->getMock('Symfony\Component\HttpFoundation\Request'); + } + + private function getToken() + { + return $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + } + + private function getDelegateAuthenticationStrategy() + { + $strategy = $this->getMock('Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface'); + $strategy->expects($this->once())->method('onAuthentication'); + + return $strategy; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php new file mode 100644 index 0000000000000..4f50bd1d3091c --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\ConcurrentSessionControlAuthenticationStrategy; + +/** + * @author Antonio J. García Lagar + */ +class ConcurrentSessionControlAuthenticationStrategyTest extends \PHPUnit_Framework_TestCase +{ + public function testSessionsCountLesserThanAllowed() + { + $request = $this->getRequest($this->getSession()); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once()) + ->method('getAllSessions') + ->will($this->returnValue(array( + $this->getSessionInformation(), + $this->getSessionInformation() + ))); + + $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 3); + $this->assertNull($strategy->onAuthentication($request, $this->getToken())); + } + + public function testSessionsCountEqualsThanAllowedWithRegisteredSession() + { + $request = $this->getRequest($this->getSession('bar')); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once()) + ->method('getAllSessions') + ->will($this->returnValue(array( + $this->getSessionInformation('bar'), + $this->getSessionInformation('foo') + ))); + + $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 2); + $this->assertNull($strategy->onAuthentication($request, $this->getToken())); + } + + /** + * @expectedException Symfony\Component\Security\Core\Exception\MaxSessionsExceededException + * @expectedExceptionMessage Maximum number of sessions (2) exceeded + */ + public function testSessionsCountEqualsThanAllowedWithUnregisteredSession() + { + $request = $this->getRequest($this->getSession('foobar')); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once()) + ->method('getAllSessions') + ->will($this->returnValue(array( + $this->getSessionInformation('bar'), + $this->getSessionInformation('foo') + ))); + + $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 2); + $this->assertNull($strategy->onAuthentication($request, $this->getToken())); + } + + public function testExpiresOldSessionsWhenNoExceptionIsThrownIfMaximunExceeded() + { + $request = $this->getRequest($this->getSession('foobar')); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once()) + ->method('getAllSessions') + ->will($this->returnValue(array( + $this->getSessionInformation('foo'), + $this->getSessionInformation('bar'), + $this->getSessionInformation('barfoo') + ))); + + $registry->expects($this->at(1)) + ->method('expireNow') + ->with($this->equalTo('bar')) + ; + $registry->expects($this->at(2)) + ->method('expireNow') + ->with($this->equalTo('barfoo')) + ; + + $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 2, false); + $this->assertNull($strategy->onAuthentication($request, $this->getToken())); + } + + private function getSession($sessionId = null) + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + if (null !== $sessionId) { + $session->expects($this->any())->method('getId')->will($this->returnValue($sessionId)); + } + + return $session; + } + + private function getRequest($session = null) + { + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + + if (null !== $session) { + $request->expects($this->any())->method('getSession')->will($this->returnValue($session)); + } + + return $request; + } + + private function getToken() + { + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token->expects($this->any())->method('getUsername')->will($this->returnValue('foo')); + + return $token; + } + + private function getSessionRegistry() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionRegistry') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getSessionInformation($sessionId = null, $username = null) + { + $sessionInfo = $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionInformation') + ->disableOriginalConstructor() + ->getMock(); + + if (null !== $sessionId) { + $sessionInfo->expects($this->any())->method('getSessionId')->will($this->returnValue($sessionId)); + } + + if (null !== $username) { + $sessionInfo->expects($this->any())->method('getUsername')->will($this->returnValue($username)); + } + + return $sessionInfo; + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/RegisterSessionAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/RegisterSessionAuthenticationStrategyTest.php new file mode 100644 index 0000000000000..bbdce8f50ea5f --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/RegisterSessionAuthenticationStrategyTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\RegisterSessionAuthenticationStrategy; + +/** + * @author Antonio J. García Lagar + */ +class RegisterSessionAuthenticationStrategyTest extends \PHPUnit_Framework_TestCase +{ + public function testRegisterSession() + { + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\SessionInterface'); + $session->expects($this->any())->method('getId')->will($this->returnValue('bar')); + $request = $this->getRequest($session); + + $registry = $this->getSessionRegistry(); + $registry->expects($this->once())->method('registerNewSession')->with($this->equalTo('bar'), $this->equalTo('foo')); + + $strategy = new RegisterSessionAuthenticationStrategy($registry); + $strategy->onAuthentication($request, $this->getToken()); + } + + private function getRequest($session = null) + { + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + + if (null !== $session) { + $request->expects($this->any())->method('getSession')->will($this->returnValue($session)); + } + + return $request; + } + + private function getToken() + { + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token->expects($this->any())->method('getUsername')->will($this->returnValue('foo')); + + return $token; + } + + private function getSessionRegistry() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionRegistry') + ->disableOriginalConstructor() + ->getMock(); + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php new file mode 100644 index 0000000000000..99d4db27fed6a --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\SessionInformation; + +/** + * @author Antonio J. García Lagar + */ +class SessionInformationTest extends \PHPUnit_Framework_TestCase +{ + public function testExpiration() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertFalse($sessionInfo->isExpired()); + $sessionInfo->expireNow(); + + $this->assertTrue($sessionInfo->isExpired()); + } + + public function testRefreshLastRequest() + { + $sessionInfo = $this->getSessionInformation(); + $lastRequest = $sessionInfo->getLastRequest(); + $this->assertInstanceOf('DateTime', $lastRequest); + $sessionInfo->refreshLastRequest(); + $this->assertGreaterThanOrEqual($lastRequest, $sessionInfo->getLastRequest()); + } + + public function testGetSessionId() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertEquals('foo', $sessionInfo->getSessionId()); + } + + public function testGetUsername() + { + $sessionInfo = $this->getSessionInformation(); + $this->assertEquals('bar', $sessionInfo->getUsername()); + } + + /** + * @return \Symfony\Component\Security\Http\Session\SessionInformation + */ + private function getSessionInformation() + { + return new SessionInformation('foo', 'bar', new \DateTime()); + } + +} diff --git a/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php new file mode 100644 index 0000000000000..0f5b801b43239 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Session; + +use Symfony\Component\Security\Http\Session\SessionRegistry; + +/** + * @author Antonio J. García Lagar + */ +class SessionRegistryTest extends \PHPUnit_Framework_TestCase +{ + + public function testGetAllSessions() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('getSessionInformations')->with('foo', true); + $registry = $this->getSessionRegistry($storage); + $registry->getAllSessions('foo', true); + } + + public function testGetSessionInformation() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('getSessionInformation')->with('foobar'); + $registry = $this->getSessionRegistry($storage); + $registry->getSessionInformation('foobar'); + } + + public function testRefreshLastRequest() + { + $sessionInformation = $this->getSessionInformation(); + $sessionInformation->expects($this->once())->method('refreshLastRequest'); + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->any())->method('getSessionInformation')->with('foobar')->will($this->returnValue($sessionInformation)); + $storage->expects($this->once())->method('setSessionInformation')->with($sessionInformation); + $registry = $this->getSessionRegistry($storage); + $registry->refreshLastRequest('foobar'); + } + + public function testExpireNow() + { + $sessionInformation = $this->getSessionInformation(); + $sessionInformation->expects($this->once())->method('expireNow'); + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->any())->method('getSessionInformation')->with('foobar')->will($this->returnValue($sessionInformation)); + $storage->expects($this->once())->method('setSessionInformation')->with($this->identicalTo($sessionInformation)); + $registry = $this->getSessionRegistry($storage); + $registry->expireNow('foobar'); + } + + public function testRegisterNewSession() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('setSessionInformation')->with($this->isInstanceOf('Symfony\Component\Security\Http\Session\SessionInformation')); + $registry = $this->getSessionRegistry($storage); + $registry->registerNewSession('foo', 'bar', new \DateTime()); + } + + public function testRemoveSessionInformation() + { + $storage = $this->getSessionRegistryStorage(); + $storage->expects($this->once())->method('removeSessionInformation')->with('foobar'); + $registry = $this->getSessionRegistry($storage); + $registry->removeSessionInformation('foobar'); + } + + private function getSessionRegistryStorage() + { + return $this->getMock('Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface'); + } + + private function getSessionInformation() + { + return $this->getMockBuilder('Symfony\Component\Security\Http\Session\SessionInformation') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getSessionRegistry($storage) + { + return new SessionRegistry($storage, 'Symfony\Component\Security\Http\Session\SessionInformation'); + } +} From 82c5f715f406e72958fb542c45f8794ddc464627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Wed, 24 Sep 2014 10:25:31 +0200 Subject: [PATCH 03/16] CS fix thanks to fabbot.io --- .../SessionRegistryStorage.php | 10 +- .../DependencyInjection/MainConfiguration.php | 39 +++----- .../Security/Factory/AbstractFactory.php | 6 +- .../DependencyInjection/SecurityExtension.php | 39 +++----- .../Http/Firewall/ExpiredSessionListener.php | 1 - .../Firewall/ExpiredSessionListenerTest.php | 99 +++++++------------ ...ositeSessionAuthenticationStrategyTest.php | 2 +- ...ssionControlAuthenticationStrategyTest.php | 14 ++- .../Tests/Session/SessionInformationTest.php | 1 - .../Tests/Session/SessionRegistryTest.php | 1 - 10 files changed, 74 insertions(+), 138 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index 646e52fc8ae77..18858fa6d34b1 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -150,19 +150,19 @@ private function getMergeSql() { switch ($this->connection->getDriver()->getName()) { case 'pdo_mysql': - return "INSERT INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " . + return "INSERT INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) ". "ON DUPLICATE KEY UPDATE username = VALUES(username), last_request = VALUES(last_request), expired = VALUES(expired)"; case 'pdo_oracle': // DUAL is Oracle specific dummy table - return "MERGE INTO $this->table USING DUAL ON (session_id= :session_id) " . - "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " . + return "MERGE INTO $this->table USING DUAL ON (session_id= :session_id) ". + "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) ". "WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired"; case 'pdo_sqlsrv': if (version_compare($this->connection->getWrappedConnection()->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>=')) { // MERGE is only available since SQL Server 2008 and must be terminated by semicolon // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx - return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON (session_id = :session_id) " . - "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) " . + return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON (session_id = :session_id) ". + "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) ". "WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired;"; } case 'pdo_sqlite': diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index aafb23d630a16..a3c3a3e46adbe 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -70,8 +70,7 @@ public function getConfigTreeBuilder() ->booleanNode('allow_if_equal_granted_denied')->defaultTrue()->end() ->end() ->end() - ->end() - ; + ->end(); $this->addAclSection($rootNode); $this->addEncodersSection($rootNode); @@ -95,8 +94,7 @@ private function addSessionRegistrySection(ArrayNodeDefinition $rootNode) ->scalarNode('session_registry_storage')->end() ->end() ->end() - ->end() - ; + ->end(); } private function addAclSection(ArrayNodeDefinition $rootNode) @@ -135,8 +133,7 @@ private function addAclSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() - ->end() - ; + ->end(); } private function addRoleHierarchySection(ArrayNodeDefinition $rootNode) @@ -156,8 +153,7 @@ private function addRoleHierarchySection(ArrayNodeDefinition $rootNode) ->prototype('scalar')->end() ->end() ->end() - ->end() - ; + ->end(); } private function addAccessControlSection(ArrayNodeDefinition $rootNode) @@ -196,8 +192,7 @@ private function addAccessControlSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() - ->end() - ; + ->end(); } private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $factories) @@ -211,8 +206,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->disallowNewKeysInSubsequentConfigs() ->useAttributeAsKey('name') ->prototype('array') - ->children() - ; + ->children(); $firewallNodeBuilder ->scalarNode('pattern')->end() @@ -311,16 +305,14 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->booleanNode('error_if_maximum_exceeded')->defaultTrue()->end() ->scalarNode('expiration_url')->defaultValue('/')->end() ->end() - ->end() - ; + ->end(); $abstractFactoryKeys = array(); foreach ($factories as $factoriesAtPosition) { foreach ($factoriesAtPosition as $factory) { $name = str_replace('-', '_', $factory->getKey()); $factoryNode = $firewallNodeBuilder->arrayNode($name) - ->canBeUnset() - ; + ->canBeUnset(); if ($factory instanceof AbstractFactory) { $abstractFactoryKeys[] = $name; @@ -350,8 +342,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto return $firewall; }) - ->end() - ; + ->end(); } private function addProvidersSection(ArrayNodeDefinition $rootNode) @@ -375,8 +366,7 @@ private function addProvidersSection(ArrayNodeDefinition $rootNode) ->isRequired() ->requiresAtLeastOneElement() ->useAttributeAsKey('name') - ->prototype('array') - ; + ->prototype('array'); $providerNodeBuilder ->children() @@ -393,8 +383,7 @@ private function addProvidersSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() - ->end() - ; + ->end(); foreach ($this->userProviderFactories as $factory) { $name = str_replace('-', '_', $factory->getKey()); @@ -411,8 +400,7 @@ private function addProvidersSection(ArrayNodeDefinition $rootNode) ->validate() ->ifTrue(function ($v) {return count($v) === 0;}) ->thenInvalid('You must set a provider definition for the provider.') - ->end() - ; + ->end(); } private function addEncodersSection(ArrayNodeDefinition $rootNode) @@ -451,7 +439,6 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->end() - ->end() - ; + ->end(); } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index 5db1656993e85..5d73d47d12833 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -59,8 +59,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProviderI if ($this->isRememberMeAware($config)) { $container ->getDefinition($listenerId) - ->addTag('security.remember_me_aware', array('id' => $id, 'provider' => $userProviderId)) - ; + ->addTag('security.remember_me_aware', array('id' => $id, 'provider' => $userProviderId)); } // create entry point if applicable (optional) @@ -77,8 +76,7 @@ public function addConfiguration(NodeDefinition $node) ->scalarNode('provider')->end() ->booleanNode('remember_me')->defaultTrue()->end() ->scalarNode('success_handler')->end() - ->scalarNode('failure_handler')->end() - ; + ->scalarNode('failure_handler')->end(); foreach (array_merge($this->options, $this->defaultSuccessHandlerOptions, $this->defaultFailureHandlerOptions) as $name => $default) { if (is_bool($default)) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 857f21300fba3..4d26b8bb604d0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -83,8 +83,7 @@ public function load(array $configs, ContainerBuilder $container) ->getDefinition('security.access.decision_manager') ->addArgument($config['access_decision_manager']['strategy']) ->addArgument($config['access_decision_manager']['allow_if_all_abstain']) - ->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']) - ; + ->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']); $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); $container->setParameter('security.authentication.hide_user_not_found', $config['hide_user_not_found']); @@ -151,8 +150,7 @@ private function configureDbalAclProvider(array $config, ContainerBuilder $conta 'connection' => $config['connection'], 'event' => 'postGenerateSchema', 'lazy' => true, - )) - ; + )); $container->getDefinition('security.acl.cache.doctrine')->addArgument($config['cache']['prefix']); @@ -264,8 +262,7 @@ private function createFirewalls($config, ContainerBuilder $container) $context = $container->setDefinition($contextId, new DefinitionDecorator('security.firewall.context')); $context ->replaceArgument(0, $listeners) - ->replaceArgument(1, $exceptionListener) - ; + ->replaceArgument(1, $exceptionListener); $map[$contextId] = $matcher; } $mapDef->replaceArgument(1, $map); @@ -276,8 +273,7 @@ private function createFirewalls($config, ContainerBuilder $container) }, array_values(array_unique($authenticationProviders))); $container ->getDefinition('security.authentication.manager') - ->replaceArgument(0, $authenticationProviders) - ; + ->replaceArgument(0, $authenticationProviders); } private function createFirewall(ContainerBuilder $container, $id, $firewall, &$authenticationProviders, $providerIds) @@ -351,8 +347,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $firewall['logout']['csrf_token_id'], $firewall['logout']['csrf_parameter'], isset($firewall['logout']['csrf_token_generator']) ? new Reference($firewall['logout']['csrf_token_generator']) : null, - )) - ; + )); } // Authentication listeners @@ -428,16 +423,14 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut $listenerId = 'security.authentication.listener.anonymous.'.$id; $container ->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.anonymous')) - ->replaceArgument(1, $firewall['anonymous']['key']) - ; + ->replaceArgument(1, $firewall['anonymous']['key']); $listeners[] = new Reference($listenerId); $providerId = 'security.authentication.provider.anonymous.'.$id; $container ->setDefinition($providerId, new DefinitionDecorator('security.authentication.provider.anonymous')) - ->replaceArgument(0, $firewall['anonymous']['key']) - ; + ->replaceArgument(0, $firewall['anonymous']['key']); $authenticationProviders[] = $providerId; $hasListeners = true; @@ -459,8 +452,7 @@ private function createEncoders($encoders, ContainerBuilder $container) $container ->getDefinition('security.encoder_factory.generic') - ->setArguments(array($encoderMap)) - ; + ->setArguments(array($encoderMap)); } private function createEncoder($config, ContainerBuilder $container) @@ -555,8 +547,7 @@ private function createUserDaoProvider($name, $provider, ContainerBuilder $conta $container ->setDefinition($name, new DefinitionDecorator('security.user.provider.chain')) - ->addArgument($providers) - ; + ->addArgument($providers); return $name; } @@ -566,8 +557,7 @@ private function createUserDaoProvider($name, $provider, ContainerBuilder $conta $container ->setDefinition($name, new DefinitionDecorator('security.user.provider.entity')) ->addArgument($provider['entity']['class']) - ->addArgument($provider['entity']['property']) - ; + ->addArgument($provider['entity']['property']); return $name; } @@ -579,8 +569,7 @@ private function createUserDaoProvider($name, $provider, ContainerBuilder $conta $container ->setDefinition($userId, new DefinitionDecorator('security.user.provider.in_memory.user')) - ->setArguments(array($username, (string) $user['password'], $user['roles'])) - ; + ->setArguments(array($username, (string) $user['password'], $user['roles'])); $definition->addMethodCall('createUser', array(new Reference($userId))); } @@ -678,8 +667,7 @@ private function createRequestMatcher($container, $path = null, $host = null, $m $container ->register($id, '%security.matcher.class%') ->setPublic(false) - ->setArguments($arguments) - ; + ->setArguments($arguments); return $this->requestMatchers[$id] = new Reference($id); } @@ -765,7 +753,7 @@ private function createConcurrentSessionAuthenticationStrategy($container, $id, array( new Reference($concurrentSessionControlStrategyId), new Reference($fixationSessionStrategyId), - new Reference($registerSessionStrategyId) + new Reference($registerSessionStrategyId), ) ); } else { @@ -790,6 +778,7 @@ private function createLogoutSuccessHandler($container, $id, $config) $logoutSuccessHandler->replaceArgument(1, $config['target']); } } + return $logoutSuccessHandlerId; } diff --git a/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php index 90cadd3a5d040..dd3528be9edca 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php @@ -73,7 +73,6 @@ public function handle(GetResponseEvent $event) if ($sessionInformation = $this->sessionRegistry->getSessionInformation($session->getId())) { if ($sessionInformation->isExpired()) { - if (null !== $this->logger) { $this->logger->info(sprintf("Logging out expired session for username '%s'", $token->getUsername())); } diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php index 0e5cb14f4a462..e3507af086e56 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php @@ -24,15 +24,13 @@ public function testHandleWhenNoSession() $request ->expects($this->any()) ->method('hasSession') - ->will($this->returnValue(false)) - ; + ->will($this->returnValue(false)); $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); $event ->expects($this->any()) ->method('getRequest') - ->will($this->returnValue($request)) - ; + ->will($this->returnValue($request)); $listener = new ExpiredSessionListener( $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'), @@ -51,27 +49,23 @@ public function testHandleWhenNoToken() $request ->expects($this->any()) ->method('hasSession') - ->will($this->returnValue(true)) - ; + ->will($this->returnValue(true)); $request ->expects($this->any()) ->method('getSession') - ->will($this->returnValue($session)) - ; + ->will($this->returnValue($session)); $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); $securityContext ->expects($this->once()) ->method('getToken') - ->will($this->returnValue(null)) - ; + ->will($this->returnValue(null)); $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); $event ->expects($this->any()) ->method('getRequest') - ->will($this->returnValue($request)) - ; + ->will($this->returnValue($request)); $listener = new ExpiredSessionListener( $securityContext, @@ -88,49 +82,42 @@ public function testHandleWhenSessionInformationIsExpired() $session ->expects($this->any()) ->method('getId') - ->will($this->returnValue('foo')) - ; + ->will($this->returnValue('foo')); $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); $request ->expects($this->any()) ->method('hasSession') - ->will($this->returnValue(true)) - ; + ->will($this->returnValue(true)); $request ->expects($this->any()) ->method('getSession') - ->will($this->returnValue($session)) - ; + ->will($this->returnValue($session)); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $token ->expects($this->any()) ->method('getUsername') - ->will($this->returnValue('foobar')) - ; + ->will($this->returnValue('foobar')); $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); $securityContext ->expects($this->once()) ->method('getToken') - ->will($this->returnValue($token)) - ; + ->will($this->returnValue($token)); $sessionInformation = $this->getSessionInformation(); $sessionInformation ->expects($this->once()) ->method('isExpired') - ->will($this->returnValue(true)) - ; + ->will($this->returnValue(true)); $sessionRegistry = $this->getSessionRegistry(); $sessionRegistry ->expects($this->once()) ->method('getSessionInformation') ->with($this->equalTo('foo')) - ->will($this->returnValue($sessionInformation)) - ; + ->will($this->returnValue($sessionInformation)); $response = $this->getMock('Symfony\Component\HttpFoundation\Response'); @@ -139,20 +126,17 @@ public function testHandleWhenSessionInformationIsExpired() ->expects($this->once()) ->method('createRedirectResponse') ->with($this->identicalTo($request), $this->equalTo('/')) - ->will($this->returnValue($response)) - ; + ->will($this->returnValue($response)); $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); $event ->expects($this->any()) ->method('getRequest') - ->will($this->returnValue($request)) - ; + ->will($this->returnValue($request)); $event ->expects($this->once()) ->method('setResponse') - ->with($this->identicalTo($response)) - ; + ->with($this->identicalTo($response)); $listener = new ExpiredSessionListener( $securityContext, @@ -169,61 +153,52 @@ public function testHandleWhenSessionInformationIsNotExpired() $session ->expects($this->any()) ->method('getId') - ->will($this->returnValue('foo')) - ; + ->will($this->returnValue('foo')); $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); $request ->expects($this->any()) ->method('hasSession') - ->will($this->returnValue(true)) - ; + ->will($this->returnValue(true)); $request ->expects($this->any()) ->method('getSession') - ->will($this->returnValue($session)) - ; + ->will($this->returnValue($session)); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $token ->expects($this->any()) ->method('getUsername') - ->will($this->returnValue('foobar')) - ; + ->will($this->returnValue('foobar')); $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); $securityContext ->expects($this->once()) ->method('getToken') - ->will($this->returnValue($token)) - ; + ->will($this->returnValue($token)); $sessionInformation = $this->getSessionInformation(); $sessionInformation ->expects($this->once()) ->method('isExpired') - ->will($this->returnValue(false)) - ; + ->will($this->returnValue(false)); $sessionRegistry = $this->getSessionRegistry(); $sessionRegistry ->expects($this->once()) ->method('getSessionInformation') ->with($this->equalTo('foo')) - ->will($this->returnValue($sessionInformation)) - ; + ->will($this->returnValue($sessionInformation)); $sessionRegistry ->expects($this->once()) ->method('refreshLastRequest') - ->with($this->equalTo('foo')) - ; + ->with($this->equalTo('foo')); $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); $event ->expects($this->any()) ->method('getRequest') - ->will($this->returnValue($request)) - ; + ->will($this->returnValue($request)); $listener = new ExpiredSessionListener( $securityContext, @@ -240,54 +215,46 @@ public function testHandleWhenNoSessionInformationIsRegistered() $session ->expects($this->any()) ->method('getId') - ->will($this->returnValue('foo')) - ; + ->will($this->returnValue('foo')); $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); $request ->expects($this->any()) ->method('hasSession') - ->will($this->returnValue(true)) - ; + ->will($this->returnValue(true)); $request ->expects($this->any()) ->method('getSession') - ->will($this->returnValue($session)) - ; + ->will($this->returnValue($session)); $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); $token ->expects($this->once()) ->method('getUsername') - ->will($this->returnValue('foobar')) - ; + ->will($this->returnValue('foobar')); $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); $securityContext ->expects($this->any()) ->method('getToken') - ->will($this->returnValue($token)) - ; + ->will($this->returnValue($token)); $sessionRegistry = $this->getSessionRegistry(); $sessionRegistry ->expects($this->once()) ->method('getSessionInformation') ->with($this->equalTo('foo')) - ->will($this->returnValue(null)) - ; + ->will($this->returnValue(null)); $sessionRegistry ->expects($this->once()) ->method('registerNewSession') - ->with($this->equalTo('foo'), $this->equalTo('foobar')) - ; + ->with($this->equalTo('foo'), $this->equalTo('foobar')); $event = $this->getMock('Symfony\Component\HttpKernel\Event\GetResponseEvent', array(), array(), '', false); $event ->expects($this->any()) ->method('getRequest') - ->will($this->returnValue($request)) - ; + ->will($this->returnValue($request)); $listener = new ExpiredSessionListener( $securityContext, diff --git a/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php index 3e4f148060627..15d7ac3ecf71b 100644 --- a/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Session/CompositeSessionAuthenticationStrategyTest.php @@ -23,7 +23,7 @@ public function testAuthenticationDelegation() $strategies = array( $this->getDelegateAuthenticationStrategy(), $this->getDelegateAuthenticationStrategy(), - $this->getDelegateAuthenticationStrategy() + $this->getDelegateAuthenticationStrategy(), ); $request = $this->getRequest(); diff --git a/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php b/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php index 4f50bd1d3091c..d9126506778b4 100644 --- a/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Session/ConcurrentSessionControlAuthenticationStrategyTest.php @@ -27,7 +27,7 @@ public function testSessionsCountLesserThanAllowed() ->method('getAllSessions') ->will($this->returnValue(array( $this->getSessionInformation(), - $this->getSessionInformation() + $this->getSessionInformation(), ))); $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 3); @@ -43,7 +43,7 @@ public function testSessionsCountEqualsThanAllowedWithRegisteredSession() ->method('getAllSessions') ->will($this->returnValue(array( $this->getSessionInformation('bar'), - $this->getSessionInformation('foo') + $this->getSessionInformation('foo'), ))); $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 2); @@ -63,7 +63,7 @@ public function testSessionsCountEqualsThanAllowedWithUnregisteredSession() ->method('getAllSessions') ->will($this->returnValue(array( $this->getSessionInformation('bar'), - $this->getSessionInformation('foo') + $this->getSessionInformation('foo'), ))); $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 2); @@ -80,17 +80,15 @@ public function testExpiresOldSessionsWhenNoExceptionIsThrownIfMaximunExceeded() ->will($this->returnValue(array( $this->getSessionInformation('foo'), $this->getSessionInformation('bar'), - $this->getSessionInformation('barfoo') + $this->getSessionInformation('barfoo'), ))); $registry->expects($this->at(1)) ->method('expireNow') - ->with($this->equalTo('bar')) - ; + ->with($this->equalTo('bar')); $registry->expects($this->at(2)) ->method('expireNow') - ->with($this->equalTo('barfoo')) - ; + ->with($this->equalTo('barfoo')); $strategy = new ConcurrentSessionControlAuthenticationStrategy($registry, 2, false); $this->assertNull($strategy->onAuthentication($request, $this->getToken())); diff --git a/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php index 99d4db27fed6a..c876c7adb58b3 100644 --- a/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionInformationTest.php @@ -55,5 +55,4 @@ private function getSessionInformation() { return new SessionInformation('foo', 'bar', new \DateTime()); } - } diff --git a/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php b/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php index 0f5b801b43239..aadcecda896fd 100644 --- a/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Session/SessionRegistryTest.php @@ -18,7 +18,6 @@ */ class SessionRegistryTest extends \PHPUnit_Framework_TestCase { - public function testGetAllSessions() { $storage = $this->getSessionRegistryStorage(); From 5772a75e016717f4e4e87ad2fd6d901057bb50da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Wed, 24 Sep 2014 10:26:00 +0200 Subject: [PATCH 04/16] PHP Types fix thanks to fabbot.io --- .../Security/SessionRegistry/SessionRegistryStorage.php | 2 +- .../ConcurrentSessionControlAuthenticationStrategy.php | 6 +++--- .../Component/Security/Http/Session/SessionRegistry.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index 18858fa6d34b1..bfb5c9f092051 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -43,7 +43,7 @@ public function getSessionInformation($sessionId) * Obtains the maintained information for one user. * * @param string $username The user identifier. - * @param boolean $includeExpiredSessions. + * @param bool $includeExpiredSessions. * @return array An array of SessionInformation objects. */ public function getSessionInformations($username, $includeExpiredSessions = false) diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php index 857e66d7f9908..f48088f435ec8 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php @@ -83,7 +83,7 @@ public function setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded) */ public function setMaximumSessions($maximumSessions) { - $this->maximumSessions = (integer) $maximumSessions; + $this->maximumSessions = (int) $maximumSessions; } /** @@ -91,7 +91,7 @@ public function setMaximumSessions($maximumSessions) * * @param array $orderedSessions Array of SessionInformation ordered from * newest to oldest - * @param integer $allowableSessions + * @param int $allowableSessions * @param SessionRegistry $registry */ protected function allowableSessionsExceeded($orderedSessions, $allowableSessions, SessionRegistry $registry) @@ -111,7 +111,7 @@ protected function allowableSessionsExceeded($orderedSessions, $allowableSession * Method intended for use by subclasses to override the maximum number of sessions that are permitted for a particular authentication. * * @param string $username - * @return integer + * @return int */ protected function getMaximumSessionsForThisUser($username) { diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index 225e125cab0a8..15841b85d2953 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -32,7 +32,7 @@ public function __construct(SessionRegistryStorageInterface $sessionRegistryStor * Obtains all the known sessions for the specified user. * * @param string $username the specified user. - * @param boolean $includeExpiredSessions + * @param bool $includeExpiredSessions * @return array An array of SessionInformation objects. */ public function getAllSessions($username, $includeExpiredSessions = false) From c6b1488fdc5c51f2afcfd89cadf6605ca82d0f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Fri, 26 Sep 2014 10:47:03 +0200 Subject: [PATCH 05/16] Depend on TokenStorageInterface instead of SecurityContextInterface --- .../Resources/config/security_listeners.xml | 2 +- .../Http/Firewall/ExpiredSessionListener.php | 12 ++++++------ .../Tests/Firewall/ExpiredSessionListenerTest.php | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 0097bd1f5ef68..a46647173aa79 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -261,7 +261,7 @@ - + diff --git a/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php index dd3528be9edca..a130b08062bd6 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php @@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Log\LoggerInterface; -use Symfony\Component\Security\Core\SecurityContextInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface; use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface; use Symfony\Component\Security\Http\Session\SessionRegistry; @@ -26,7 +26,7 @@ */ class ExpiredSessionListener implements ListenerInterface { - private $securityContext; + private $tokenStorage; private $httpUtils; private $sessionRegistry; private $targetUrl; @@ -34,9 +34,9 @@ class ExpiredSessionListener implements ListenerInterface private $logger; private $handlers; - public function __construct(SecurityContextInterface $securityContext, HttpUtils $httpUtils, SessionRegistry $sessionRegistry, $targetUrl = '/', LogoutSuccessHandlerInterface $successHandler = null, LoggerInterface $logger = null) + public function __construct(TokenStorageInterface $tokenStorage, HttpUtils $httpUtils, SessionRegistry $sessionRegistry, $targetUrl = '/', LogoutSuccessHandlerInterface $successHandler = null, LoggerInterface $logger = null) { - $this->securityContext = $securityContext; + $this->tokenStorage = $tokenStorage; $this->httpUtils = $httpUtils; $this->sessionRegistry = $sessionRegistry; $this->targetUrl = $targetUrl; @@ -67,7 +67,7 @@ public function handle(GetResponseEvent $event) $session = $request->getSession(); - if (null === $session || null === $token = $this->securityContext->getToken()) { + if (null === $session || null === $token = $this->tokenStorage->getToken()) { return; } @@ -90,7 +90,7 @@ public function handle(GetResponseEvent $event) $handler->logout($request, $response, $token); } - $this->securityContext->setToken(null); + $this->tokenStorage->setToken(null); $event->setResponse($response); } else { diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php index e3507af086e56..55bc7b28b9a38 100644 --- a/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Firewall/ExpiredSessionListenerTest.php @@ -33,7 +33,7 @@ public function testHandleWhenNoSession() ->will($this->returnValue($request)); $listener = new ExpiredSessionListener( - $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'), + $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'), $this->getHttpUtils(), $this->getSessionRegistry() ); @@ -55,7 +55,7 @@ public function testHandleWhenNoToken() ->method('getSession') ->will($this->returnValue($session)); - $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); $securityContext ->expects($this->once()) ->method('getToken') @@ -100,7 +100,7 @@ public function testHandleWhenSessionInformationIsExpired() ->method('getUsername') ->will($this->returnValue('foobar')); - $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); $securityContext ->expects($this->once()) ->method('getToken') @@ -171,7 +171,7 @@ public function testHandleWhenSessionInformationIsNotExpired() ->method('getUsername') ->will($this->returnValue('foobar')); - $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); $securityContext ->expects($this->once()) ->method('getToken') @@ -233,7 +233,7 @@ public function testHandleWhenNoSessionInformationIsRegistered() ->method('getUsername') ->will($this->returnValue('foobar')); - $securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'); $securityContext ->expects($this->any()) ->method('getToken') From 9c3aebb59726a7c0e882d1d7be8cf81ed9812adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Fri, 26 Sep 2014 10:50:37 +0200 Subject: [PATCH 06/16] Mark as private several methods and properties --- .../Doctrine/Security/SessionRegistry/Schema.php | 2 +- .../SessionRegistry/SessionRegistryStorage.php | 4 ++-- .../Security/Http/Session/SessionInformation.php | 14 +++++++------- .../Security/Http/Session/SessionRegistry.php | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php index 05ac55320376e..a44e995d18624 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php @@ -36,7 +36,7 @@ public function __construct($table) /** * Adds the session_information table to the schema */ - protected function addSessionInformationTable($table) + private function addSessionInformationTable($table) { $table = $this->createTable($table); $table->addColumn('session_id', 'string'); diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index bfb5c9f092051..256e48bb33507 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -12,8 +12,8 @@ */ class SessionRegistryStorage implements SessionRegistryStorageInterface { - protected $connection; - protected $table; + private $connection; + private $table; public function __construct(Connection $connection, $table) { diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 5075e0a12eb35..e6c560ebb81fd 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -20,10 +20,10 @@ */ class SessionInformation { - protected $sessionId; - protected $username; - protected $expired; - protected $lastRequest; + private $sessionId; + private $username; + private $expired; + private $lastRequest; public function __construct($sessionId, $username, \DateTime $lastRequest, \DateTime $expired = null) { @@ -94,17 +94,17 @@ public function refreshLastRequest() $this->lastRequest = new \DateTime(); } - protected function getExpired() + private function getExpired() { return $this->expired; } - protected function setExpired(\DateTime $expired) + private function setExpired(\DateTime $expired) { $this->expired = $expired; } - protected function setLastRequest(\DateTime $lastRequest) + private function setLastRequest(\DateTime $lastRequest) { $this->lastRequest = $lastRequest; } diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index 15841b85d2953..0e7529448329f 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -21,7 +21,7 @@ */ class SessionRegistry { - protected $sessionRegistryStorage; + private $sessionRegistryStorage; public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage) { From 2b5e2ed5d1d0dd0d38b86f8f556b74844b23da5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Fri, 26 Sep 2014 10:53:06 +0200 Subject: [PATCH 07/16] Rename exceptionIfMaximunExceeded to errorIfMaximunExceeded to follow the Spring framework name. Rename allowableSessionsExceeded to allowedSessionsExceeded. --- ...entSessionControlAuthenticationStrategy.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php index f48088f435ec8..6a6709dcd13d3 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php @@ -29,14 +29,14 @@ class ConcurrentSessionControlAuthenticationStrategy implements SessionAuthenticationStrategyInterface { protected $registry; - protected $exceptionIfMaximumExceeded; + protected $errorIfMaximumExceeded; protected $maximumSessions; - public function __construct(SessionRegistry $registry, $maximumSessions, $exceptionIfMaximumExceeded = true) + public function __construct(SessionRegistry $registry, $maximumSessions, $errorIfMaximumExceeded = true) { $this->registry = $registry; $this->setMaximumSessions($maximumSessions); - $this->setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded); + $this->setErrorIfMaximumExceeded($errorIfMaximumExceeded); } /** @@ -63,17 +63,17 @@ public function onAuthentication(Request $request, TokenInterface $token) } } - $this->allowableSessionsExceeded($sessions, $maxSessions, $this->registry); + $this->allowedSessionsExceeded($sessions, $maxSessions, $this->registry); } /** * Sets a boolean flag that causes a RuntimeException to be thrown if the number of sessions is exceeded. * - * @param bool $exceptionIfMaximumExceeded + * @param bool $errorIfMaximumExceeded */ - public function setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded) + public function setErrorIfMaximumExceeded($errorIfMaximumExceeded) { - $this->exceptionIfMaximumExceeded = (bool) $exceptionIfMaximumExceeded; + $this->errorIfMaximumExceeded = (bool) $errorIfMaximumExceeded; } /** @@ -94,9 +94,9 @@ public function setMaximumSessions($maximumSessions) * @param int $allowableSessions * @param SessionRegistry $registry */ - protected function allowableSessionsExceeded($orderedSessions, $allowableSessions, SessionRegistry $registry) + protected function allowedSessionsExceeded($orderedSessions, $allowableSessions, SessionRegistry $registry) { - if ($this->exceptionIfMaximumExceeded) { + if ($this->errorIfMaximumExceeded) { throw new MaxSessionsExceededException(sprintf('Maximum number of sessions (%s) exceeded', $allowableSessions)); } From 5e22c79ebf783372c42818d136b1e93b938c85ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Fri, 26 Sep 2014 10:54:02 +0200 Subject: [PATCH 08/16] Remove the useless unique index for the primary key. --- src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php index a44e995d18624..6e17212d1131e 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php @@ -44,6 +44,5 @@ private function addSessionInformationTable($table) $table->addColumn('expired', 'datetime', array('unsigned' => true, 'notnull' => false)); $table->addColumn('last_request', 'datetime', array('unsigned' => true, 'notnull' => false)); $table->setPrimaryKey(array('session_id')); - $table->addUniqueIndex(array('session_id')); } } From 5664571e6c5d5982a4c28746e02c3e8199eac411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Fri, 26 Sep 2014 10:55:05 +0200 Subject: [PATCH 09/16] Control to avoid an error when the request has no session. --- .../Http/Session/RegisterSessionAuthenticationStrategy.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php index 1f7a191ad2227..8d426f03820b9 100644 --- a/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/RegisterSessionAuthenticationStrategy.php @@ -37,6 +37,8 @@ public function __construct(SessionRegistry $registry) */ public function onAuthentication(Request $request, TokenInterface $token) { - $this->registry->registerNewSession($request->getSession()->getId(), $token->getUsername()); + if ($session = $request->getSession()) { + $this->registry->registerNewSession($session->getId(), $token->getUsername()); + } } } From f458bd44176f24bdaadf8445fb7516d044b21db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Fri, 26 Sep 2014 10:56:44 +0200 Subject: [PATCH 10/16] Fix phpdoc --- .../Security/SessionRegistry/Schema.php | 6 ++--- .../SessionRegistryStorage.php | 17 +++++++------ .../Resources/config/security_listeners.xml | 2 +- .../Http/Firewall/ExpiredSessionListener.php | 2 +- ...CompositeSessionAuthenticationStrategy.php | 11 ++++----- ...ntSessionControlAuthenticationStrategy.php | 15 ++++++------ .../Http/Session/SessionInformation.php | 8 +++---- .../Security/Http/Session/SessionRegistry.php | 24 +++++++++---------- .../SessionRegistryStorageInterface.php | 10 ++++---- 9 files changed, 49 insertions(+), 46 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php index 6e17212d1131e..0fb2c14a2ddba 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php @@ -22,9 +22,7 @@ final class Schema extends BaseSchema { /** - * Constructor - * - * @param string $table + * @param string $table The name of the table to create. */ public function __construct($table) { @@ -35,6 +33,8 @@ public function __construct($table) /** * Adds the session_information table to the schema + * + * @param string $table The name of the table to create. */ private function addSessionInformationTable($table) { diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index 256e48bb33507..ff38a3957436d 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -15,6 +15,10 @@ class SessionRegistryStorage implements SessionRegistryStorageInterface private $connection; private $table; + /** + * @param Connection $connection The DB connection + * @param string $table The table name to store session information + */ public function __construct(Connection $connection, $table) { $this->connection = $connection; @@ -22,7 +26,7 @@ public function __construct(Connection $connection, $table) } /** - * Obtains the maintained information for one session. + * Gets the stored information for the given session. * * @param string $sessionId the session identifier key. * @return SessionInformation a SessionInformation object. @@ -40,11 +44,11 @@ public function getSessionInformation($sessionId) } /** - * Obtains the maintained information for one user. + * Gets the stored sessions information for the given username. * - * @param string $username The user identifier. - * @param bool $includeExpiredSessions. - * @return array An array of SessionInformation objects. + * @param string $username The user identifier. + * @param bool $includeExpiredSessions If true, expired sessions information is included. + * @return SessionInformations[] An array of SessionInformation objects. */ public function getSessionInformations($username, $includeExpiredSessions = false) { @@ -68,7 +72,6 @@ public function getSessionInformations($username, $includeExpiredSessions = fals /** * Adds information for one session. * - * @param string $sessionId the session identifier key. * @param SessionInformation a SessionInformation object. */ public function setSessionInformation(SessionInformation $sessionInformation) @@ -122,7 +125,7 @@ public function setSessionInformation(SessionInformation $sessionInformation) } /** - * Deletes the maintained information of one session. + * Deletes stored information of one session. * * @param string $sessionId the session identifier key. */ diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index a46647173aa79..7b28ffa6cff09 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -264,7 +264,7 @@ - + diff --git a/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php index a130b08062bd6..2be5d619bfe09 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ExpiredSessionListener.php @@ -58,7 +58,7 @@ public function addHandler(LogoutHandlerInterface $handler) /** * Handles the number of simultaneous sessions for a single user. * - * @param GetResponseEvent $event A GetResponseEvent instance + * @param GetResponseEvent $event A GetResponseEvent instance * @throws \RuntimeException if the successHandler exists and do not return a response */ public function handle(GetResponseEvent $event) diff --git a/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php index e61ab2a1eccef..fc3ac355cedeb 100644 --- a/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/CompositeSessionAuthenticationStrategy.php @@ -16,17 +16,17 @@ /** * A session authentication strategy that accepts multiple - * SessionAuthenticationStrategyInterface implementations to delegate to. Each - * SessionAuthenticationStrategyInterface is invoked in turn. The invocations are - * short circuited if any exception, (i.e. SessionAuthenticationException) is - * thrown. + * SessionAuthenticationStrategyInterface implementations to delegate to. + * + * Each SessionAuthenticationStrategyInterface is invoked in turn. The + * invocations are short circuited if any exception is thrown. * * @author Antonio J. García Lagar */ class CompositeSessionAuthenticationStrategy implements SessionAuthenticationStrategyInterface { /** - * @var array + * @var SessionAuthenticationStrategyInterface[] */ private $delegateStrategies = array(); @@ -43,7 +43,6 @@ public function __construct(array $delegateStrategies) public function onAuthentication(Request $request, TokenInterface $token) { foreach ($this->delegateStrategies as $strategy) { - /* @var $strategy SessionAuthenticationStrategyInterface */ $strategy->onAuthentication($request, $token); } } diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php index 6a6709dcd13d3..eddfd4f34abd3 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php @@ -16,12 +16,13 @@ use Symfony\Component\Security\Core\Exception\MaxSessionsExceededException; /** - * ConcurrentSessionControlAuthenticationStrategy. + * Strategy which handles concurrent session-control. * - * Strategy which handles concurrent session-control, in addition to the functionality provided by the base class. - * When invoked following an authentication, it will check whether the user in question should be allowed to proceed, - * by comparing the number of sessions they already have active with the configured maximumSessions value. - * The SessionRegistry is used as the source of data on authenticated users and session data. + * When invoked following an authentication, it will check whether the user in + * question should be allowed to proceed, by comparing the number of sessions + * they already have active with the configured maximumSessions value. + * The SessionRegistry is used as the source of data on authenticated users and + * session data. * * @author Stefan Paschke * @author Antonio J. García Lagar @@ -87,7 +88,7 @@ public function setMaximumSessions($maximumSessions) } /** - * Allows subclasses to customise behaviour when too many sessions are detected. + * Allows subclasses to customize behavior when too many sessions are detected. * * @param array $orderedSessions Array of SessionInformation ordered from * newest to oldest @@ -110,7 +111,7 @@ protected function allowedSessionsExceeded($orderedSessions, $allowableSessions, /** * Method intended for use by subclasses to override the maximum number of sessions that are permitted for a particular authentication. * - * @param string $username + * @param string $username * @return int */ protected function getMaximumSessionsForThisUser($username) diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index e6c560ebb81fd..ebb22d285060e 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -37,7 +37,7 @@ public function __construct($sessionId, $username, \DateTime $lastRequest, \Date } /** - * Sets the session informations expired date to the current date and time. + * Sets the session informations expiration date to the current date and time. * */ public function expireNow() @@ -56,7 +56,7 @@ public function getLastRequest() } /** - * Obtains the username. + * Gets the username. * * @return string */ @@ -66,7 +66,7 @@ public function getUsername() } /** - * Obtain the session identifier. + * Gets the session identifier key. * * @return string $sessionId the session identifier key. */ @@ -76,7 +76,7 @@ public function getSessionId() } /** - * Return wether this session is expired. + * Return whether this session is expired. * * @return bool */ diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index 0e7529448329f..5ff6a53348467 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -14,7 +14,7 @@ /** * SessionRegistry. * - * Maintains a registry of SessionInformation instances. + * Stores a registry of SessionInformation instances. * * @author Stefan Paschke * @author Antonio J. García Lagar @@ -29,11 +29,11 @@ public function __construct(SessionRegistryStorageInterface $sessionRegistryStor } /** - * Obtains all the known sessions for the specified user. + * Returns all the sessions stored for the given user ordered from newest to oldest. * - * @param string $username the specified user. - * @param bool $includeExpiredSessions - * @return array An array of SessionInformation objects. + * @param string $username the given user. + * @param bool $includeExpiredSessions + * @return SessionInformation[] An array of SessionInformation objects. */ public function getAllSessions($username, $includeExpiredSessions = false) { @@ -41,10 +41,10 @@ public function getAllSessions($username, $includeExpiredSessions = false) } /** - * Obtains the session information for the specified sessionId. + * Obtains the session information for the given sessionId. * - * @param string $sessionId the session identifier key. - * @return SessionInformation $sessionInformation + * @param string $sessionId the session identifier key. + * @return SessionInformation|null $sessionInformation */ public function getSessionInformation($sessionId) { @@ -88,22 +88,22 @@ public function expireNow($sessionId) } /** - * Registers a new session for the specified user. + * Registers a new session for the given user. * * @param string $sessionId the session identifier key. - * @param string $username the specified user. + * @param string $username the given user. * @param \DateTime $lastRequest */ public function registerNewSession($sessionId, $username, \DateTime $lastRequest = null) { - $lastRequest = ($lastRequest) ?: new \DateTime(); + $lastRequest = $lastRequest ?: new \DateTime(); $sessionInformation = new SessionInformation($sessionId, $username, $lastRequest); $this->setSessionInformation($sessionInformation); } /** - * Deletes the maintained information of one session. + * Deletes the stored information of one session. * * @param string $sessionId the session identifier key. */ diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php index 37c48031911db..b9a04995f57cd 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -24,8 +24,8 @@ interface SessionRegistryStorageInterface /** * Obtains the session information for the specified sessionId. * - * @param string $sessionId the session identifier key. - * @return SessionInformation $sessionInformation + * @param string $sessionId the session identifier key. + * @return SessionInformation|null $sessionInformation */ public function getSessionInformation($sessionId); @@ -33,9 +33,9 @@ public function getSessionInformation($sessionId); * Obtains the maintained information for one user ordered from newest to * oldest * - * @param string $username The user identifier. - * @param bool $includeExpiredSessions - * @return array An array of SessionInformation objects. + * @param string $username The user identifier. + * @param bool $includeExpiredSessions + * @return SessionInformation[] An array of SessionInformation objects. */ public function getSessionInformations($username, $includeExpiredSessions = false); From f92286a184705b2d1605491214ec2a4ac8b79bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Fri, 26 Sep 2014 11:00:36 +0200 Subject: [PATCH 11/16] Several improvements and error fixes to DBAL related code. --- .../SessionRegistryStorage.php | 67 ++++++++++++------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index ff38a3957436d..6dc31807704f3 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -3,6 +3,7 @@ namespace Symfony\Bridge\Doctrine\Security\SessionRegistry; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\DBALException; use Symfony\Component\Security\Http\Session\SessionInformation; use Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface; @@ -66,6 +67,8 @@ public function getSessionInformations($username, $includeExpiredSessions = fals $sessionInformations[] = $this->instantiateSessionInformationFromResultSet($data); } + $statement->closeCursor(); + return $sessionInformations; } @@ -79,23 +82,30 @@ public function setSessionInformation(SessionInformation $sessionInformation) $mergeSql = $this->getMergeSql(); if (null !== $mergeSql) { - $mergeStmt = $this->pdo->prepare($mergeSql); - $mergeStmt->bindValue('session_id', $sessionInformation->getSessionId()); - $mergeStmt->bindValue('username', $sessionInformation->getUsername()); - $mergeStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); - $mergeStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); - $mergeStmt->execute(); + $this->connection->executeQuery( + $mergeSql, + array( + 'session_id' => $sessionInformation->getSessionId(), + 'username' => $sessionInformation->getUsername(), + 'last_request' => $sessionInformation->getLastRequest(), + 'expired' => $sessionInformation->getExpired() + ), + array( + 'last_request' => 'datetime', + 'expired' => 'datetime' + ) + ); return true; } - $updateStmt = $this->pdo->prepare( + $updateStmt = $this->connection->prepare( "UPDATE $this->table SET username=:username, last_request=:last_request, expired=:expired WHERE session_id = :session_id" ); - $mergeStmt->bindValue('session_id', $sessionInformation->getSessionId()); - $mergeStmt->bindValue('username', $sessionInformation->getUsername()); - $mergeStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); - $mergeStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); + $updateStmt->bindValue('session_id', $sessionInformation->getSessionId()); + $updateStmt->bindValue('username', $sessionInformation->getUsername()); + $updateStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); + $updateStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); $updateStmt->execute(); // When MERGE is not supported, like in Postgres, we have to use this approach that can result in @@ -105,17 +115,22 @@ public function setSessionInformation(SessionInformation $sessionInformation) // longer gap locking. if (!$updateStmt->rowCount()) { try { - $insertStmt = $this->pdo->prepare( - "INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired)" + $this->connection->insert( + $this->table, + array( + 'session_id' => $sessionInformation->getSessionId(), + 'username' => $sessionInformation->getUsername(), + 'last_request' => $sessionInformation->getLastRequest(), + 'expired' => $sessionInformation->getExpired() + ), + array( + 'last_request' => 'datetime', + 'expired' => 'datetime' + ) ); - $insertStmt->bindValue('session_id', $sessionInformation->getSessionId()); - $insertStmt->bindValue('username', $sessionInformation->getUsername()); - $insertStmt->bindValue('last_request', $sessionInformation->getLastRequest(), 'datetime'); - $insertStmt->bindValue('expired', $sessionInformation->getExpired(), 'datetime'); - $insertStmt->execute(); - } catch (\PDOException $e) { + } catch (DBALException $e) { // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys - if (0 === strpos($e->getCode(), '23')) { + if ($e->getPrevious() instanceof \PDOException && 0 === strpos($e->getPrevious()->getCode(), '23')) { $updateStmt->execute(); } else { throw $e; @@ -151,24 +166,24 @@ private function instantiateSessionInformationFromResultSet($data) */ private function getMergeSql() { - switch ($this->connection->getDriver()->getName()) { - case 'pdo_mysql': + switch ($this->connection->getDatabasePlatform()->getName()) { + case 'mysql': return "INSERT INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) ". "ON DUPLICATE KEY UPDATE username = VALUES(username), last_request = VALUES(last_request), expired = VALUES(expired)"; - case 'pdo_oracle': + case 'oracle': // DUAL is Oracle specific dummy table return "MERGE INTO $this->table USING DUAL ON (session_id= :session_id) ". "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) ". "WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired"; - case 'pdo_sqlsrv': - if (version_compare($this->connection->getWrappedConnection()->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>=')) { + case 'mssql': + if ($this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SQLServer2008Platform || $this->connection->getDatabasePlatform() instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform) { // MERGE is only available since SQL Server 2008 and must be terminated by semicolon // It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx return "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON (session_id = :session_id) ". "WHEN NOT MATCHED THEN INSERT (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired) ". "WHEN MATCHED THEN UPDATE SET username = :username, last_request = :last_request, expired = :expired;"; } - case 'pdo_sqlite': + case 'sqlite': return "INSERT OR REPLACE INTO $this->table (session_id, username, last_request, expired) VALUES (:session_id, :username, :last_request, :expired)"; } } From 52b7ba42c0390d70d897ff7ae43a411d158910bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Fri, 26 Sep 2014 11:13:01 +0200 Subject: [PATCH 12/16] CS fix thanks to fabbot.io --- .../Security/SessionRegistry/SessionRegistryStorage.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index 6dc31807704f3..7f16b6d84814c 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -88,11 +88,11 @@ public function setSessionInformation(SessionInformation $sessionInformation) 'session_id' => $sessionInformation->getSessionId(), 'username' => $sessionInformation->getUsername(), 'last_request' => $sessionInformation->getLastRequest(), - 'expired' => $sessionInformation->getExpired() + 'expired' => $sessionInformation->getExpired(), ), array( 'last_request' => 'datetime', - 'expired' => 'datetime' + 'expired' => 'datetime', ) ); @@ -121,11 +121,11 @@ public function setSessionInformation(SessionInformation $sessionInformation) 'session_id' => $sessionInformation->getSessionId(), 'username' => $sessionInformation->getUsername(), 'last_request' => $sessionInformation->getLastRequest(), - 'expired' => $sessionInformation->getExpired() + 'expired' => $sessionInformation->getExpired(), ), array( 'last_request' => 'datetime', - 'expired' => 'datetime' + 'expired' => 'datetime', ) ); } catch (DBALException $e) { From 4ca3a472bcf4c32a6ca5bcf7420e4abb20415006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Mon, 10 Nov 2014 18:08:03 +0100 Subject: [PATCH 13/16] Remove aligned '=>' and '=' --- .../SessionRegistry/SessionRegistryStorage.php | 18 +++++++++--------- ...entSessionControlAuthenticationStrategy.php | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index 7f16b6d84814c..1d60a91b21fc0 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -23,7 +23,7 @@ class SessionRegistryStorage implements SessionRegistryStorageInterface public function __construct(Connection $connection, $table) { $this->connection = $connection; - $this->table = $table; + $this->table = $table; } /** @@ -85,14 +85,14 @@ public function setSessionInformation(SessionInformation $sessionInformation) $this->connection->executeQuery( $mergeSql, array( - 'session_id' => $sessionInformation->getSessionId(), - 'username' => $sessionInformation->getUsername(), + 'session_id' => $sessionInformation->getSessionId(), + 'username' => $sessionInformation->getUsername(), 'last_request' => $sessionInformation->getLastRequest(), - 'expired' => $sessionInformation->getExpired(), + 'expired' => $sessionInformation->getExpired(), ), array( 'last_request' => 'datetime', - 'expired' => 'datetime', + 'expired' => 'datetime', ) ); @@ -118,14 +118,14 @@ public function setSessionInformation(SessionInformation $sessionInformation) $this->connection->insert( $this->table, array( - 'session_id' => $sessionInformation->getSessionId(), - 'username' => $sessionInformation->getUsername(), + 'session_id' => $sessionInformation->getSessionId(), + 'username' => $sessionInformation->getUsername(), 'last_request' => $sessionInformation->getLastRequest(), - 'expired' => $sessionInformation->getExpired(), + 'expired' => $sessionInformation->getExpired(), ), array( 'last_request' => 'datetime', - 'expired' => 'datetime', + 'expired' => 'datetime', ) ); } catch (DBALException $e) { diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php index eddfd4f34abd3..cd3592c7c2ee0 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlAuthenticationStrategy.php @@ -47,9 +47,9 @@ public function onAuthentication(Request $request, TokenInterface $token) { $username = $token->getUsername(); - $sessions = $this->registry->getAllSessions($username); - $sessionCount = count($sessions); - $maxSessions = $this->getMaximumSessionsForThisUser($username); + $sessions = $this->registry->getAllSessions($username); + $sessionCount = count($sessions); + $maxSessions = $this->getMaximumSessionsForThisUser($username); if ($sessionCount < $maxSessions) { return; From 6224ba0c2d1a3fdfe1ecf9be6987ad5c832a50fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Mon, 10 Nov 2014 19:41:32 +0100 Subject: [PATCH 14/16] Set the max_sessions config option as integer. --- .../SecurityBundle/DependencyInjection/MainConfiguration.php | 2 +- .../SecurityBundle/DependencyInjection/SecurityExtension.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index a3c3a3e46adbe..be6bac44b5d7c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -301,7 +301,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->arrayNode('session_concurrency') ->canBeUnset() ->children() - ->scalarNode('max_sessions')->defaultNull()->end() + ->integerNode('max_sessions')->defaultValue(0)->min(0)->end() ->booleanNode('error_if_maximum_exceeded')->defaultTrue()->end() ->scalarNode('expiration_url')->defaultValue('/')->end() ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 4d26b8bb604d0..8b095a6b0b0ad 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -719,7 +719,7 @@ private function createConcurrentSessionAuthenticationStrategy($container, $id, { $sessionStrategyId = 'security.authentication.session_strategy.'.$id; - if (isset($config['max_sessions'])) { + if (isset($config['max_sessions']) && $config['max_sessions'] > 0) { $concurrentSessionControlStrategyId = 'security.authentication.session_strategy.concurrent_control.'.$id; $container->setDefinition( $concurrentSessionControlStrategyId, From 81c01deb2f18f5faa45d28b4d4dd0ecbae31e169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Mon, 10 Nov 2014 19:41:57 +0100 Subject: [PATCH 15/16] Makes the session registry service public. --- .../Resources/config/security_session_concurrency.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml index 70347005a1f85..b7331bf89b5ad 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml @@ -27,7 +27,7 @@ - + %security.authentication.session_information.class% From c0edefd16504bbc9b5d4a35ec7dc8dabef5f948a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20J=2E=20Garc=C3=ADa=20Lagar?= Date: Tue, 11 Nov 2014 20:27:08 +0100 Subject: [PATCH 16/16] Adds a new logout handler to removes the session information from session registry on logout. --- .../DependencyInjection/SecurityExtension.php | 5 ++ .../config/security_session_concurrency.xml | 6 +++ .../Logout/SessionRegistryLogoutHandler.php | 51 +++++++++++++++++++ .../SessionRegistryLogoutHandlerTest.php | 48 +++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 src/Symfony/Component/Security/Http/Logout/SessionRegistryLogoutHandler.php create mode 100644 src/Symfony/Component/Security/Http/Tests/Logout/SessionRegistryLogoutHandlerTest.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 8b095a6b0b0ad..1c0d5f4e3a0fb 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -786,6 +786,11 @@ private function addLogoutHandlers($container, $listenerId, $id, $config) { $listener = $container->findDefinition($listenerId); + // add session registry logout handler + if (isset($config['session_concurrency'])) { + $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session_registry'))); + } + // add session logout handler if (true === $config['logout']['invalidate_session'] && false === $config['stateless']) { $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml index b7331bf89b5ad..8855be55ac393 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_concurrency.xml @@ -10,6 +10,7 @@ Symfony\Component\Security\Http\Session\CompositeSessionAuthenticationStrategy Symfony\Component\Security\Http\Session\SessionRegistry Symfony\Component\Security\Http\Session\SessionInformation + Symfony\Component\Security\Http\Logout\SessionRegistryLogoutHandler @@ -31,5 +32,10 @@ %security.authentication.session_information.class% + + + + + diff --git a/src/Symfony/Component/Security/Http/Logout/SessionRegistryLogoutHandler.php b/src/Symfony/Component/Security/Http/Logout/SessionRegistryLogoutHandler.php new file mode 100644 index 0000000000000..760e67ec42f63 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Logout/SessionRegistryLogoutHandler.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Logout; + +use Symfony\Component\Security\Http\Session\SessionRegistry; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; + +/** + * Handler for removing session information from the session registry. + * + * @author Antonio J. García Lagar + */ +class SessionRegistryLogoutHandler +{ + private $registry; + + /** + * Constructor + * + * @param SessionRegistry $registry + */ + public function __construct(SessionRegistry $registry) + { + $this->registry = $registry; + } + + /** + * Remove current session information from the session registry + * + * @param Request $request + * @param Response $response + * @param TokenInterface $token + */ + public function logout(Request $request, Response $response, TokenInterface $token) + { + if (null !== $session = $request->getSession()) { + $this->registry->removeSessionInformation($session->getId()); + } + } +} diff --git a/src/Symfony/Component/Security/Http/Tests/Logout/SessionRegistryLogoutHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/Logout/SessionRegistryLogoutHandlerTest.php new file mode 100644 index 0000000000000..32f9b6cc83563 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Tests/Logout/SessionRegistryLogoutHandlerTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Http\Tests\Logout; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Http\Logout\SessionRegistryLogoutHandler; + +class SessionRegistryLogoutHandlerTest extends \PHPUnit_Framework_TestCase +{ + public function testLogout() + { + $registry = $this->getMock('Symfony\Component\Security\Http\Session\SessionRegistry', array(), array(), '', false); + $handler = new SessionRegistryLogoutHandler($registry); + + $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); + $response = new Response(); + $session = $this->getMock('Symfony\Component\HttpFoundation\Session\Session', array(), array(), '', false); + + $request + ->expects($this->once()) + ->method('getSession') + ->will($this->returnValue($session)) + ; + + $session + ->expects($this->once()) + ->method('getId') + ->will($this->returnValue('foobar')) + ; + + $registry + ->expects($this->once()) + ->method('removeSessionInformation') + ->with($this->equalTo('foobar')) + ; + + $handler->logout($request, $response, $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')); + } +}