From c0b33f88c0d7fcfd06495fc577f732b458f1221b Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Wed, 4 May 2011 17:25:59 +0200 Subject: [PATCH 01/30] added listener, strategy, registry, configuration for concurrent sessions --- .../DependencyInjection/MainConfiguration.php | 7 + .../DependencyInjection/SecurityExtension.php | 27 +++ .../Resources/config/security.xml | 19 +++ .../Resources/config/security_listeners.xml | 11 ++ .../Firewall/ConcurrentSessionListener.php | 95 +++++++++++ .../ConcurrentSessionControlStrategy.php | 150 +++++++++++++++++ .../Http/Session/SessionInformation.php | 95 +++++++++++ .../Session/SessionInformationIterator.php | 157 ++++++++++++++++++ .../Security/Http/Session/SessionRegistry.php | 132 +++++++++++++++ .../Http/Session/SessionRegistryInterface.php | 52 ++++++ .../SessionRegistryStorageInterface.php | 82 +++++++++ 11 files changed, 827 insertions(+) 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/SessionInformationIterator.php create mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistry.php create mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php create mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 6884cfafff414..cc5fe5aa75df3 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -230,6 +230,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('target_url')->defaultValue('/')->end() + ->end() + ->end() ; foreach ($factories as $factoriesAtPosition) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 6588cd138bbe1..983ba94b6099c 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -71,6 +71,19 @@ public function load(array $configs, ContainerBuilder $container) ; $container->setParameter('security.access.always_authenticate_before_granting', $config['always_authenticate_before_granting']); + if (isset($config['firewalls'])) { + foreach ($config['firewalls'] as $firewallconfig) { + if (!empty($firewallconfig['session_concurrency']['max_sessions'])) { + $container->getDefinition('security.authentication.concurrent_session_strategy') + ->replaceArgument(1, $firewallconfig['session_concurrency']['max_sessions']) + ->replaceArgument(2, $config['session_fixation_strategy']); + $container->setDefinition('security.authentication.session_strategy', $container->getDefinition('security.authentication.concurrent_session_strategy')); + + break; + } + } + } + $this->createFirewalls($config, $container); $this->createAuthorization($config, $container); $this->createRoleHierarchy($config, $container); @@ -304,6 +317,11 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider)); } + // 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']; @@ -521,6 +539,15 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv return $switchUserListenerId; } + private function createConcurrentSessionListener($container, $id, $config) + { + $concurrentSessionListenerId = 'security.authentication.concurrentsession_listener'.$id; + $listener = $container->setDefinition($concurrentSessionListenerId, new DefinitionDecorator('security.authentication.concurrentsession_listener')); + $listener->replaceArgument(2, $config['target_url']); + + return $concurrentSessionListenerId; + } + private function createRequestMatcher($container, $path = null, $host = null, $methods = null, $ip = null, array $attributes = array()) { $serialized = serialize(array($path, $host, $methods, $ip, $attributes)); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 276a68880c142..be8d223bc7d07 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -25,6 +25,11 @@ Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy + Symfony\Component\Security\Http\Session\ConcurrentSessionControlStrategy + Symfony\Component\Security\Http\Session\SessionRegistry + Symfony\Component\Security\Http\Session\SessionInformation + Symfony\Component\Security\Http\Session\SessionInformationIterator + Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -61,6 +66,20 @@ + + + + + + + + + %security.authentication.session_information.class% + %security.authentication.session_information_iterator.class% + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 246fb85a439b4..e7fe191a4afed 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -24,6 +24,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 @@ -171,6 +173,15 @@ + + + + + + + + + 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..46540aa200469 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -0,0 +1,95 @@ + + * + * 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\Session\SessionRegistry; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\Log\LoggerInterface; + +class ConcurrentSessionListener implements ListenerInterface +{ + private $securityContext; + private $sessionRegistry; + private $targetUrl; + private $logger; + private $handlers; + private $successHandler; + + public function __construct(SecurityContextInterface $securityContext, SessionRegistry $sessionRegistry, $targetUrl = '/', LogoutSuccessHandlerInterface $successHandler = null, LoggerInterface $logger = null) + { + $this->securityContext = $securityContext; + $this->sessionRegistry = $sessionRegistry; + $this->targetUrl = $targetUrl; + $this->successHandler = $successHandler; + $this->logger = $logger; + $this->handlers = array(); + } + + /** + * Adds a logout handler + * + * @param LogoutHandlerInterface $handler + * @return void + */ + public function addHandler(LogoutHandlerInterface $handler) + { + $this->handlers[] = $handler; + } + + public function register(EventDispatcherInterface $dispatcher) + { + $dispatcher->connect('core.security', array($this, 'handle'), 0); + } + + public function unregister(EventDispatcherInterface $dispatcher) + { + } + + public function handle(GetResponseEvent $event) + { + $request = $event->getRequest(); + + $session = $request->hasSession() ? $request->getSession() : null; + + if (null !== $session && null !== $token = $this->securityContext->getToken()) { + 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 = new RedirectResponse(0 !== strpos($this->targetUrl, 'http') ? $request->getUriForPath($this->targetUrl) : $this->targetUrl, 302); + } + + foreach ($this->handlers as $handler) { + $handler->logout($request, $response, $token); + } + + $this->securityContext->setToken(null); + + $event->setResponse($response); + } else { + $sessionInformation->refreshLastRequest(); + $this->sessionRegistry->setSessionInformation($sessionInformation); + } + } + } + + } +} 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..be605114f07db --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php @@ -0,0 +1,150 @@ + + * + * 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); + } + + /** + * Called when a user is newly authenticated. + * + * @param Request $request + * @param TokenInterface $token + * @return void + */ + 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(), $token); + } + + $sessions = $this->registry->getAllSessions($user); + $maxSessions = $this->getMaximumSessionsForThisUser($user); + + if ($sessions->count() >= $maxSessions && $this->alwaysCreateSession !== true) { + if ($this->exceptionIfMaximumExceeded) { + throw new \RuntimeException(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 + * @return void + */ + 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 + * @return void + */ + public function setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded) + { + $this->exceptionIfMaximumExceeded = $exceptionIfMaximumExceeded; + } + + /** + * Sets the maxSessions property. + * + * @param $maximumSessions + * @return void + */ + public function setMaximumSessions($maximumSessions) + { + $this->maximumSessions = $maximumSessions; + } + + /** + * Allows subclasses to customise behaviour when too many sessions are detected. + * + * @param SessionInformationIterator $sessions + * @param integer $allowableSessions + * @param SessionRegistry $registry + * @return void + */ + protected function allowableSessionsExceeded(SessionInformationIterator $sessions, $allowableSessions, SessionRegistry $registry) + { + // remove oldest sessions from registry + $count = 0; + $sessions->sort(); + + for ($i = $allowableSessions - 1; $i < $sessions->count(); $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 + * @return void + */ + protected function onSessionChange($originalSessionId, $newSessionId, TokenInterface $token) + { + $this->registry->removeSessionInformation($originalSessionId, $token->getUser()); + } +} 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..2e119ebaa49dc --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -0,0 +1,95 @@ + + * + * 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; + +/** + * SessionInformation. + * + * Represents a record of a session. This is primarily used for concurrent session support. + * + * @author Stefan Paschke + */ +class SessionInformation +{ + protected $sessionId; + protected $user; + protected $expired; + protected $lastRequest; + + public function __construct($sessionId, UserInterface $user) + { + $this->sessionId = $sessionId; + $this->user = $user; + } + + /** + * Sets the session informations expired date to the current date and time. + * + * @return void + */ + public function expireNow() + { + $this->expired = time(); + } + + /** + * Obtain the last request date. + * + * @return integer UNIX Timestamp of the last request date and time. + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + /** + * Obtains the user. + * + * @return UserInterface + */ + public function getUser() + { + return $this->user; + } + + /** + * 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->expired && $this->expired < time(); + } + + /** + * Set the last request date to the current date and time. + * + * @return void + */ + public function refreshLastRequest() + { + $this->lastRequest = time(); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php new file mode 100644 index 0000000000000..9e019489a2a9c --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php @@ -0,0 +1,157 @@ + + * + * 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; + +/** + * SessionInformationIterator. + * + * @author Stefan Paschke + */ +class SessionInformationIterator implements \Iterator, \ArrayAccess +{ + protected $sessions = array(); + protected $position = 0; + + /** + * Iterator interface. + * + * @return void + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Iterator interface. + * + * @return SessionInformation + */ + public function current() + { + return $this->sessions[$this->position]; + } + + /** + * Iterator interface. + * + * @return integer + */ + public function key() + { + return $this->position; + } + + /** + * Iterator interface. + * + * @return void + */ + public function next() + { + $this->position++; + } + + /** + * Iterator interface. + * + * @return boolean + */ + public function valid() + { + return isset($this->sessions[$this->position]); + } + + /** + * ArrayAccess interface. + * + * @return boolean + */ + public function offsetExists($offset) + { + return isset($this->sessions[$offset]); + } + + /** + * ArrayAccess interface. + * + * @return SessionInformation + */ + public function offsetGet($offset) + { + return $this->sessions[$offset]; + } + + /** + * ArrayAccess interface. + * + * @param integer $offset + * @param SessionInformation $sessionInformation + * @return void + */ + public function offsetSet($offset, $sessionInformation) + { + if ($sessionInformation instanceof SessionInformation) { + $this->sessions[$offset] = $sessionInformation; + } + } + + /** + * ArrayAccess interface. + * + * @param integer $offset + * @return void + */ + public function offsetUnset($offset) + { + if ($this->offsetExists($offset)) { + unset($this->sessions[$offset]); + } + } + + /** + * Adds a SessionInformation object to the iterator. + * + * @param SessionInformation $sessionInformation + * @return void + */ + public function add(SessionInformation $sessionInformation) + { + $this->sessions[] = $sessionInformation; + } + + /** + * Sorts the session informations by the last request date. + * + * @return void + */ + function sort() + { + $sessionsSorted = array(); + foreach ($this->sessions as $session) { + $sessionsSorted[$session->getLastRequest()] = $session; + } + krsort($sessionsSorted); + + $this->sessions = array_values($sessionsSorted); + } + + /** + * Returns the number of session information objects. + * + * @return integer + */ + function count() + { + return count($this->sessions); + } +} 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..fd0297d18bccb --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -0,0 +1,132 @@ + + * + * 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; + protected $sessionInformationIteratorClass; + + public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage, $sessionInformationClass, $sessionInformationIteratorClass) + { + $this->sessionRegistryStorage = $sessionRegistryStorage; + $this->sessionInformationClass = $sessionInformationClass; + $this->sessionInformationIteratorClass = $sessionInformationIteratorClass; + } + + /** + * 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 SessionInformationIterator $sessions the known sessions. + */ + public function getAllSessions(UserInterface $user, $includeExpiredSessions = false) + { + $sessions = new $this->sessionInformationIteratorClass(); + + foreach ($this->sessionRegistryStorage->getSessionIds($user) as $sessionId) { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + if ($includeExpiredSessions === true || $sessionInformation->isExpired() === false) { + $sessions->add($sessionInformation); + } + } + } + + return $sessions; + } + + /** + * Obtains the session information for the specified sessionId. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation $sessionInformation + */ + public function getSessionInformation($sessionId) + { + return $this->sessionRegistryStorage->getSessionInformation($sessionId); + } + + /** + * Sets a SessionInformation object. + * + * @param SessionInformation $sessionInformation + * @return void + */ + public function setSessionInformation(SessionInformation $sessionInformation) + { + $this->sessionRegistryStorage->setSessionInformation($sessionInformation->getSessionId(), $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. + * @return void + */ + public function refreshLastRequest($sessionId) + { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + $sessionInformation->refreshLastRequest(); + $this->sessionRegistryStorage->setSessionInformation($sessionInformation); + } + } + + /** + * Registers a new session for the specified user. + * + * @param string $sessionId the session identifier key. + * @param UserInterface $user the specified user. + * @return void + */ + public function registerNewSession($sessionId, UserInterface $user) + { + $sessionInformation = new $this->sessionInformationClass($sessionId, $user); + $sessionInformation->refreshLastRequest(); + + $this->sessionRegistryStorage->setSessionInformation($sessionId, $sessionInformation); + $this->sessionRegistryStorage->addSessionId($sessionId, $user); + } + + /** + * Deletes all the session information being maintained for the specified sessionId. + * + * @param string $sessionId the session identifier key. + * @param UserInterface $user the specified user. + * @return void + */ + public function removeSessionInformation($sessionId, UserInterface $user) + { + $this->sessionRegistryStorage->removeSessionInformation($sessionId); + $this->sessionRegistryStorage->removeSessionId($sessionId, $user); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php new file mode 100644 index 0000000000000..2b60cfc4dab61 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.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\Session; + +/** + * SessionRegistryInterface. + * + * The SessionRegistry is used as the source of data on authenticated users and session data. + * + * @author Stefan Paschke + */ +interface SessionRegistryInterface +{ + /** + * Obtains all the known users in the SessionRegistry. + */ + function getAllUsers(); + + /** + * Obtains all the known sessions for the specified user. + */ + function getAllSessions($user, $includeExpiredSessions = false); + + /** + * Obtains the session information for the specified sessionId. + */ + function getSessionInformation($sessionId); + + /** + * Updates the given sessionId so its last request time is equal to the present date and time. + */ + function refreshLastRequest($sessionId); + + /** + * Registers a new session for the specified principal. + */ + function registerNewSession($sessionId, $user); + + /** + * Deletes all the session information being maintained for the specified sessionId. + */ + function removeSessionInformation($sessionId, $user); +} 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..3ece57269d73c --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -0,0 +1,82 @@ + + * + * 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; + +/** + * 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 all the known session IDs for the specified user. + * + * @param UserInterface $user + * @return array an array ob session identifiers. + */ + function getSessionIds(UserInterface $user); + + /** + * Adds one session ID to the specified users array + * + * @param string $sessionId the session identifier key. + * @param UserInterface $user + * @return void + */ + function addSessionId($sessionId, UserInterface $user); + + /** + * Removes one session ID from the specified users array. + * + * @param string $sessionId the session identifier key. + * @param UserInterface $user + * @return void + */ + function removeSessionId($sessionId, UserInterface $user); + + /** + * Obtains the maintained information for one session. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation a SessionInformation object. + */ + function getSessionInformation($sessionId); + + /** + * Adds information for one session. + * + * @param string $sessionId the session identifier key. + * @param SessionInformation a SessionInformation object. + * @return void + */ + function setSessionInformation($sessionId, SessionInformation $sessionInformation); + + /** + * Deletes the maintained information of one session. + * + * @param string $sessionId the session identifier key. + * @return void + */ + function removeSessionInformation($sessionId); +} From 0f3d7c42fd982987d7639b48b8edb4ad317775cf Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 5 May 2011 12:00:04 +0200 Subject: [PATCH 02/30] added SessionLogoutHandler to setup of ConcurrentSessionListener --- .../SecurityBundle/DependencyInjection/MainConfiguration.php | 2 +- .../SecurityBundle/DependencyInjection/SecurityExtension.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index cc5fe5aa75df3..5139645316b5d 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -234,7 +234,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->canBeUnset() ->children() ->scalarNode('max_sessions')->defaultNull()->end() - ->scalarNode('target_url')->defaultValue('/')->end() + ->scalarNode('expiration_url')->defaultValue('/')->end() ->end() ->end() ; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 983ba94b6099c..6074d9ce5af22 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -541,9 +541,10 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv private function createConcurrentSessionListener($container, $id, $config) { - $concurrentSessionListenerId = 'security.authentication.concurrentsession_listener'.$id; + $concurrentSessionListenerId = 'security.authentication.concurrentsession_listener.'.$id; $listener = $container->setDefinition($concurrentSessionListenerId, new DefinitionDecorator('security.authentication.concurrentsession_listener')); - $listener->replaceArgument(2, $config['target_url']); + $listener->replaceArgument(2, $config['expiration_url']); + $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); return $concurrentSessionListenerId; } From e9bcd84c810f7e67c9620be820d1ad16f47768c4 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 5 May 2011 14:16:28 +0200 Subject: [PATCH 03/30] removed uneeded interface --- .../Http/Session/SessionRegistryInterface.php | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php deleted file mode 100644 index 2b60cfc4dab61..0000000000000 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php +++ /dev/null @@ -1,52 +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; - -/** - * SessionRegistryInterface. - * - * The SessionRegistry is used as the source of data on authenticated users and session data. - * - * @author Stefan Paschke - */ -interface SessionRegistryInterface -{ - /** - * Obtains all the known users in the SessionRegistry. - */ - function getAllUsers(); - - /** - * Obtains all the known sessions for the specified user. - */ - function getAllSessions($user, $includeExpiredSessions = false); - - /** - * Obtains the session information for the specified sessionId. - */ - function getSessionInformation($sessionId); - - /** - * Updates the given sessionId so its last request time is equal to the present date and time. - */ - function refreshLastRequest($sessionId); - - /** - * Registers a new session for the specified principal. - */ - function registerNewSession($sessionId, $user); - - /** - * Deletes all the session information being maintained for the specified sessionId. - */ - function removeSessionInformation($sessionId, $user); -} From 507befaef2539fe1d2ba19396de5cbbc7c139663 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 5 May 2011 15:26:22 +0200 Subject: [PATCH 04/30] removed security.authentication.session_registry_storage service from Component --- .../Bundle/SecurityBundle/Resources/config/security.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index be8d223bc7d07..7b7f020a1b7c2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -29,7 +29,6 @@ Symfony\Component\Security\Http\Session\SessionRegistry Symfony\Component\Security\Http\Session\SessionInformation Symfony\Component\Security\Http\Session\SessionInformationIterator - Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -78,8 +77,6 @@ %security.authentication.session_information_iterator.class% - - From 5559ef8df95975fc40e26866675eea2c2dafc136 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 5 May 2011 16:00:10 +0200 Subject: [PATCH 05/30] removed unneeded methods --- .../Http/Firewall/ConcurrentSessionListener.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php index 46540aa200469..5d0f2ff834cb9 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -49,15 +49,11 @@ public function addHandler(LogoutHandlerInterface $handler) $this->handlers[] = $handler; } - public function register(EventDispatcherInterface $dispatcher) - { - $dispatcher->connect('core.security', array($this, 'handle'), 0); - } - - public function unregister(EventDispatcherInterface $dispatcher) - { - } - + /** + * Handles the number of simultaneous sessions for a single user. + * + * @param GetResponseEvent $event A GetResponseEvent instance + */ public function handle(GetResponseEvent $event) { $request = $event->getRequest(); From 455197f1e6e982f0c35506a9ef6a78813938639e Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Tue, 17 May 2011 10:05:16 +0200 Subject: [PATCH 06/30] prepare Doctrine implementation of SessionRegistryStorage --- .../Command/InitConcurrentSessionsCommand.php | 67 ++++++++ .../ConcurrentSessionControlStrategy.php | 6 +- .../Security/Http/Session/Dbal/Schema.php | 145 ++++++++++++++++++ .../Http/Session/SessionInformation.php | 16 +- .../Security/Http/Session/SessionRegistry.php | 22 +-- .../SessionRegistryStorageInterface.php | 32 +--- 6 files changed, 233 insertions(+), 55 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php create mode 100644 src/Symfony/Component/Security/Http/Session/Dbal/Schema.php diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php new file mode 100644 index 0000000000000..9ace314a27a7c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php @@ -0,0 +1,67 @@ + + * + * 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\Command; +use Symfony\Component\Security\Http\Session\Dbal\Schema; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Doctrine\DBAL\DriverManager; + +/** + * Installs the tables required by the concurrent session system + * + * @author Stefan Paschke + */ +class InitConcurrentSessionsCommand extends Command +{ + /** + * @see Command + */ + protected function configure() + { + $this + ->setName('init:concurrent-session') + ; + } + + /** + * @see Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $connection = $this->container->get('security.concurrent-session.dbal.connection'); + $sm = $connection->getSchemaManager(); + $tableNames = $sm->listTableNames(); + $tables = array( + 'class_table_name' => $this->container->getParameter('security.acl.dbal.class_table_name'), + 'sid_table_name' => $this->container->getParameter('security.acl.dbal.sid_table_name'), + 'oid_table_name' => $this->container->getParameter('security.acl.dbal.oid_table_name'), + 'oid_ancestors_table_name' => $this->container->getParameter('security.acl.dbal.oid_ancestors_table_name'), + 'entry_table_name' => $this->container->getParameter('security.acl.dbal.entry_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/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php index be605114f07db..b1f3b51b87a33 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php @@ -54,7 +54,7 @@ public function onAuthentication(Request $request, TokenInterface $token) parent::onAuthentication($request, $token); if ($originalSessionId != $request->getSession()->getId()) { - $this->onSessionChange($originalSessionId, $request->getSession()->getId(), $token); + $this->onSessionChange($originalSessionId, $request->getSession()->getId()); } $sessions = $this->registry->getAllSessions($user); @@ -143,8 +143,8 @@ protected function getMaximumSessionsForThisUser(UserInterface $user) * @param TokenInterface $token * @return void */ - protected function onSessionChange($originalSessionId, $newSessionId, TokenInterface $token) + protected function onSessionChange($originalSessionId, $newSessionId) { - $this->registry->removeSessionInformation($originalSessionId, $token->getUser()); + $this->registry->removeSessionInformation($originalSessionId); } } diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php new file mode 100644 index 0000000000000..ac616d9e10d95 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php @@ -0,0 +1,145 @@ + + * + * 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\Dbal; + +use Doctrine\DBAL\Schema\Schema as BaseSchema; + +/** + * The schema used for the concurrent session system. + * + * @author Stefan Paschke + */ +final class Schema extends BaseSchema +{ + protected $options; + + /** + * Constructor + * + * @param array $options the names for tables + * @return void + */ + public function __construct(array $options) + { + parent::__construct(); + + $this->options = $options; + + $this->addClassTable(); + $this->addSecurityIdentitiesTable(); + $this->addObjectIdentitiesTable(); + $this->addObjectIdentityAncestorsTable(); + $this->addEntryTable(); + } + + /** + * Adds the class table to the schema + * + * @return void + */ + protected function addClassTable() + { + $table = $this->createTable($this->options['class_table_name']); + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('class_type', 'string', array('length' => 200)); + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('class_type')); + } + + /** + * Adds the entry table to the schema + * + * @return void + */ + protected function addEntryTable() + { + $table = $this->createTable($this->options['entry_table_name']); + + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('class_id', 'integer', array('unsigned' => true)); + $table->addColumn('object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); + $table->addColumn('field_name', 'string', array('length' => 50, 'notnull' => false)); + $table->addColumn('ace_order', 'smallint', array('unsigned' => true)); + $table->addColumn('security_identity_id', 'integer', array('unsigned' => true)); + $table->addColumn('mask', 'integer'); + $table->addColumn('granting', 'boolean'); + $table->addColumn('granting_strategy', 'string', array('length' => 30)); + $table->addColumn('audit_success', 'boolean'); + $table->addColumn('audit_failure', 'boolean'); + + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('class_id', 'object_identity_id', 'field_name', 'ace_order')); + $table->addIndex(array('class_id', 'object_identity_id', 'security_identity_id')); + + $table->addForeignKeyConstraint($this->getTable($this->options['class_table_name']), array('class_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + $table->addForeignKeyConstraint($this->getTable($this->options['oid_table_name']), array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + $table->addForeignKeyConstraint($this->getTable($this->options['sid_table_name']), array('security_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + } + + /** + * Adds the object identity table to the schema + * + * @return void + */ + protected function addObjectIdentitiesTable() + { + $table = $this->createTable($this->options['oid_table_name']); + + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('class_id', 'integer', array('unsigned' => true)); + $table->addColumn('object_identifier', 'string', array('length' => 100)); + $table->addColumn('parent_object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); + $table->addColumn('entries_inheriting', 'boolean'); + + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('object_identifier', 'class_id')); + $table->addIndex(array('parent_object_identity_id')); + + $table->addForeignKeyConstraint($table, array('parent_object_identity_id'), array('id'), array('onDelete' => 'RESTRICT', 'onUpdate' => 'RESTRICT')); + } + + /** + * Adds the object identity relation table to the schema + * + * @return void + */ + protected function addObjectIdentityAncestorsTable() + { + $table = $this->createTable($this->options['oid_ancestors_table_name']); + + $table->addColumn('object_identity_id', 'integer', array('unsigned' => true)); + $table->addColumn('ancestor_id', 'integer', array('unsigned' => true)); + + $table->setPrimaryKey(array('object_identity_id', 'ancestor_id')); + + $oidTable = $this->getTable($this->options['oid_table_name']); + $table->addForeignKeyConstraint($oidTable, array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + $table->addForeignKeyConstraint($oidTable, array('ancestor_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + } + + /** + * Adds the security identity table to the schema + * + * @return void + */ + protected function addSecurityIdentitiesTable() + { + $table = $this->createTable($this->options['sid_table_name']); + + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('identifier', 'string', array('length' => 200)); + $table->addColumn('username', 'boolean'); + + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('identifier', 'username')); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 2e119ebaa49dc..961055a742662 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Http\Session; -use Symfony\Component\Security\Core\User\UserInterface; - /** * SessionInformation. * @@ -23,14 +21,14 @@ class SessionInformation { protected $sessionId; - protected $user; + protected $username; protected $expired; protected $lastRequest; - public function __construct($sessionId, UserInterface $user) + public function __construct($sessionId, $username) { $this->sessionId = $sessionId; - $this->user = $user; + $this->username = $username; } /** @@ -54,13 +52,13 @@ public function getLastRequest() } /** - * Obtains the user. + * Obtains the username. * - * @return UserInterface + * @return string */ - public function getUser() + public function getUsername() { - return $this->user; + return $this->username; } /** diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index fd0297d18bccb..10f9fbaa9d8c1 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -52,17 +52,7 @@ public function getAllUsers() */ public function getAllSessions(UserInterface $user, $includeExpiredSessions = false) { - $sessions = new $this->sessionInformationIteratorClass(); - - foreach ($this->sessionRegistryStorage->getSessionIds($user) as $sessionId) { - if ($sessionInformation = $this->getSessionInformation($sessionId)) { - if ($includeExpiredSessions === true || $sessionInformation->isExpired() === false) { - $sessions->add($sessionInformation); - } - } - } - - return $sessions; + return $this->sessionRegistryStorage->getSessionInformations($user->getUsername(), $includeExpiredSessions); } /** @@ -84,7 +74,7 @@ public function getSessionInformation($sessionId) */ public function setSessionInformation(SessionInformation $sessionInformation) { - $this->sessionRegistryStorage->setSessionInformation($sessionInformation->getSessionId(), $sessionInformation); + $this->sessionRegistryStorage->setSessionInformation($sessionInformation); } /** @@ -110,11 +100,10 @@ public function refreshLastRequest($sessionId) */ public function registerNewSession($sessionId, UserInterface $user) { - $sessionInformation = new $this->sessionInformationClass($sessionId, $user); + $sessionInformation = new $this->sessionInformationClass($sessionId, $user->getUsername()); $sessionInformation->refreshLastRequest(); - $this->sessionRegistryStorage->setSessionInformation($sessionId, $sessionInformation); - $this->sessionRegistryStorage->addSessionId($sessionId, $user); + $this->sessionRegistryStorage->setSessionInformation($sessionInformation); } /** @@ -124,9 +113,8 @@ public function registerNewSession($sessionId, UserInterface $user) * @param UserInterface $user the specified user. * @return void */ - public function removeSessionInformation($sessionId, UserInterface $user) + public function removeSessionInformation($sessionId) { $this->sessionRegistryStorage->removeSessionInformation($sessionId); - $this->sessionRegistryStorage->removeSessionId($sessionId, $user); } } diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php index 3ece57269d73c..cbcfea2b795d3 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Http\Session; -use Symfony\Component\Security\Core\User\UserInterface; - /** * SessionRegistryStorageInterface. * @@ -30,38 +28,20 @@ interface SessionRegistryStorageInterface function getUsers(); /** - * Obtains all the known session IDs for the specified user. - * - * @param UserInterface $user - * @return array an array ob session identifiers. - */ - function getSessionIds(UserInterface $user); - - /** - * Adds one session ID to the specified users array - * - * @param string $sessionId the session identifier key. - * @param UserInterface $user - * @return void - */ - function addSessionId($sessionId, UserInterface $user); - - /** - * Removes one session ID from the specified users array. + * Obtains the maintained information for one session. * * @param string $sessionId the session identifier key. - * @param UserInterface $user - * @return void + * @return SessionInformation a SessionInformation object. */ - function removeSessionId($sessionId, UserInterface $user); + function getSessionInformation($sessionId); /** - * Obtains the maintained information for one session. + * Obtains the maintained information for one user. * * @param string $sessionId the session identifier key. * @return SessionInformation a SessionInformation object. */ - function getSessionInformation($sessionId); + function getSessionInformations($username, $includeExpiredSessions); /** * Adds information for one session. @@ -70,7 +50,7 @@ function getSessionInformation($sessionId); * @param SessionInformation a SessionInformation object. * @return void */ - function setSessionInformation($sessionId, SessionInformation $sessionInformation); + function setSessionInformation(SessionInformation $sessionInformation); /** * Deletes the maintained information of one session. From aadb2a758e8f429f4e88c478f28df77af319c5d7 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 2 Jun 2011 19:29:54 +0200 Subject: [PATCH 07/30] default Doctrine implementation of SessionInformation --- .../Command/InitConcurrentSessionsCommand.php | 50 +++--- .../Doctrine/DoctrineSessionInformation.php | 55 +++++++ .../DoctrineSessionRegistryStorage.php | 85 ++++++++++ .../Resources/config/security.xml | 7 +- .../Security/Http/Session/Dbal/Schema.php | 145 ------------------ .../Http/Session/SessionInformation.php | 7 +- .../Session/SessionInformationIterator.php | 2 +- 7 files changed, 172 insertions(+), 179 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php delete mode 100644 src/Symfony/Component/Security/Http/Session/Dbal/Schema.php diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php index 9ace314a27a7c..942b578f566cf 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php @@ -11,27 +11,40 @@ namespace Symfony\Bundle\SecurityBundle\Command; -use Symfony\Bundle\FrameworkBundle\Command\Command; -use Symfony\Component\Security\Http\Session\Dbal\Schema; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Doctrine\DBAL\DriverManager; +use Symfony\Component\Console\Output\Output; +use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand; +use Symfony\Bundle\DoctrineBundle\Command\Proxy\DoctrineCommandHelper; /** - * Installs the tables required by the concurrent session system + * Installs the database schema required by the concurrent session Doctrine implementation * * @author Stefan Paschke */ -class InitConcurrentSessionsCommand extends Command +class InitConcurrentSessionsCommand extends CreateCommand { /** * @see Command */ protected function configure() { + parent::configure(); + $this ->setName('init:concurrent-session') - ; + ->setDescription('Executes the SQL needed to generate the database schema reqired 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 + ); } /** @@ -39,29 +52,8 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $connection = $this->container->get('security.concurrent-session.dbal.connection'); - $sm = $connection->getSchemaManager(); - $tableNames = $sm->listTableNames(); - $tables = array( - 'class_table_name' => $this->container->getParameter('security.acl.dbal.class_table_name'), - 'sid_table_name' => $this->container->getParameter('security.acl.dbal.sid_table_name'), - 'oid_table_name' => $this->container->getParameter('security.acl.dbal.oid_table_name'), - 'oid_ancestors_table_name' => $this->container->getParameter('security.acl.dbal.oid_ancestors_table_name'), - 'entry_table_name' => $this->container->getParameter('security.acl.dbal.entry_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); - } + DoctrineCommandHelper::setApplicationEntityManager($this->getApplication(), 'security'); - $output->writeln('Concurrent session tables have been initialized successfully.'); + parent::execute($input, $output); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php new file mode 100644 index 0000000000000..c9a999ef6b509 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.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\Bundle\SecurityBundle\Doctrine; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Symfony\Component\Security\Http\Session\SessionInformation; + +/** + * DoctrineSessionInformation. + * + * Allows to persist SessionInformation using Doctrine. + * + * @author Stefan Paschke + */ +class DoctrineSessionInformation extends SessionInformation +{ + public static function loadMetadata(ClassMetadata $metadata) + { + $metadata->setTableName('session_information'); + + $metadata->mapField(array( + 'id' => true, + 'fieldName' => 'sessionId', + 'columnName' => 'session_id', + 'type' => 'string' + )); + + $metadata->mapField(array( + 'fieldName' => 'username', + 'type' => 'string' + )); + + $metadata->mapField(array( + 'fieldName' => 'expired', + 'type' => 'datetime', + 'nullable' => true + )); + + $metadata->mapField(array( + 'fieldName' => 'lastRequest', + 'columnName' => 'last_request', + 'type' => 'datetime', + 'nullable' => true + )); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php new file mode 100644 index 0000000000000..447f38783fe8d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php @@ -0,0 +1,85 @@ +em = $em; + + $driver = new StaticPHPDriver(__DIR__); + $this->em->getConfiguration()->setMetadataDriverImpl($driver); + } + + /** + * 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 DoctrineSessionInformation a SessionInformation object. + */ + public function getSessionInformation($sessionId) + { + return $this->em->find('Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation', $sessionId); + } + + /** + * Obtains the maintained information for one user. + * + * @param string $sessionId the session identifier key. + * @return SessionInformationIterator a SessionInformationIterator object. + */ + public function getSessionInformations($username, $includeExpiredSessions) + { + $sessions = new SessionInformationIterator(); + + foreach ($this->em->getRepository('Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation')->findBy(array('username' => $username)) as $sessionInformation) { + $sessions->add($sessionInformation); + } + + return $sessions; + } + + /** + * Adds information for one session. + * + * @param string $sessionId the session identifier key. + * @param DoctrineSessionInformation a SessionInformation object. + * @return void + */ + public function setSessionInformation(SessionInformation $sessionInformation) + { + $this->em->persist($sessionInformation); + $this->em->flush(); + } + + /** + * Deletes the maintained information of one session. + * + * @param string $sessionId the session identifier key. + * @return void + */ + public function removeSessionInformation($sessionId) + { + if (null !== $sessionInformation = $this->getSessionInformation($sessionId)) { + $this->em->remove($sessionInformation); + $this->em->flush(); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 34eb73c396515..686ae0d6b881a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -27,7 +27,8 @@ Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy Symfony\Component\Security\Http\Session\ConcurrentSessionControlStrategy Symfony\Component\Security\Http\Session\SessionRegistry - Symfony\Component\Security\Http\Session\SessionInformation + Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionRegistryStorage + Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation Symfony\Component\Security\Http\Session\SessionInformationIterator Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -77,6 +78,10 @@ %security.authentication.session_information_iterator.class% + + + + diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php deleted file mode 100644 index ac616d9e10d95..0000000000000 --- a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php +++ /dev/null @@ -1,145 +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\Dbal; - -use Doctrine\DBAL\Schema\Schema as BaseSchema; - -/** - * The schema used for the concurrent session system. - * - * @author Stefan Paschke - */ -final class Schema extends BaseSchema -{ - protected $options; - - /** - * Constructor - * - * @param array $options the names for tables - * @return void - */ - public function __construct(array $options) - { - parent::__construct(); - - $this->options = $options; - - $this->addClassTable(); - $this->addSecurityIdentitiesTable(); - $this->addObjectIdentitiesTable(); - $this->addObjectIdentityAncestorsTable(); - $this->addEntryTable(); - } - - /** - * Adds the class table to the schema - * - * @return void - */ - protected function addClassTable() - { - $table = $this->createTable($this->options['class_table_name']); - $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); - $table->addColumn('class_type', 'string', array('length' => 200)); - $table->setPrimaryKey(array('id')); - $table->addUniqueIndex(array('class_type')); - } - - /** - * Adds the entry table to the schema - * - * @return void - */ - protected function addEntryTable() - { - $table = $this->createTable($this->options['entry_table_name']); - - $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); - $table->addColumn('class_id', 'integer', array('unsigned' => true)); - $table->addColumn('object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); - $table->addColumn('field_name', 'string', array('length' => 50, 'notnull' => false)); - $table->addColumn('ace_order', 'smallint', array('unsigned' => true)); - $table->addColumn('security_identity_id', 'integer', array('unsigned' => true)); - $table->addColumn('mask', 'integer'); - $table->addColumn('granting', 'boolean'); - $table->addColumn('granting_strategy', 'string', array('length' => 30)); - $table->addColumn('audit_success', 'boolean'); - $table->addColumn('audit_failure', 'boolean'); - - $table->setPrimaryKey(array('id')); - $table->addUniqueIndex(array('class_id', 'object_identity_id', 'field_name', 'ace_order')); - $table->addIndex(array('class_id', 'object_identity_id', 'security_identity_id')); - - $table->addForeignKeyConstraint($this->getTable($this->options['class_table_name']), array('class_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - $table->addForeignKeyConstraint($this->getTable($this->options['oid_table_name']), array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - $table->addForeignKeyConstraint($this->getTable($this->options['sid_table_name']), array('security_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - } - - /** - * Adds the object identity table to the schema - * - * @return void - */ - protected function addObjectIdentitiesTable() - { - $table = $this->createTable($this->options['oid_table_name']); - - $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); - $table->addColumn('class_id', 'integer', array('unsigned' => true)); - $table->addColumn('object_identifier', 'string', array('length' => 100)); - $table->addColumn('parent_object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); - $table->addColumn('entries_inheriting', 'boolean'); - - $table->setPrimaryKey(array('id')); - $table->addUniqueIndex(array('object_identifier', 'class_id')); - $table->addIndex(array('parent_object_identity_id')); - - $table->addForeignKeyConstraint($table, array('parent_object_identity_id'), array('id'), array('onDelete' => 'RESTRICT', 'onUpdate' => 'RESTRICT')); - } - - /** - * Adds the object identity relation table to the schema - * - * @return void - */ - protected function addObjectIdentityAncestorsTable() - { - $table = $this->createTable($this->options['oid_ancestors_table_name']); - - $table->addColumn('object_identity_id', 'integer', array('unsigned' => true)); - $table->addColumn('ancestor_id', 'integer', array('unsigned' => true)); - - $table->setPrimaryKey(array('object_identity_id', 'ancestor_id')); - - $oidTable = $this->getTable($this->options['oid_table_name']); - $table->addForeignKeyConstraint($oidTable, array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - $table->addForeignKeyConstraint($oidTable, array('ancestor_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - } - - /** - * Adds the security identity table to the schema - * - * @return void - */ - protected function addSecurityIdentitiesTable() - { - $table = $this->createTable($this->options['sid_table_name']); - - $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); - $table->addColumn('identifier', 'string', array('length' => 200)); - $table->addColumn('username', 'boolean'); - - $table->setPrimaryKey(array('id')); - $table->addUniqueIndex(array('identifier', 'username')); - } -} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 961055a742662..33ca7375e3969 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -29,6 +29,7 @@ public function __construct($sessionId, $username) { $this->sessionId = $sessionId; $this->username = $username; + $this->refreshLastRequest(); } /** @@ -38,7 +39,7 @@ public function __construct($sessionId, $username) */ public function expireNow() { - $this->expired = time(); + $this->expired = new \DateTime(); } /** @@ -78,7 +79,7 @@ public function getSessionId() */ public function isExpired() { - return $this->expired && $this->expired < time(); + return $this->expired && $this->expired->getTimestamp() < time(); } /** @@ -88,6 +89,6 @@ public function isExpired() */ public function refreshLastRequest() { - $this->lastRequest = time(); + $this->lastRequest = new \DateTime(); } } diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php index 9e019489a2a9c..db6a3beb54799 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php @@ -138,7 +138,7 @@ function sort() { $sessionsSorted = array(); foreach ($this->sessions as $session) { - $sessionsSorted[$session->getLastRequest()] = $session; + $sessionsSorted[$session->getLastRequest()->getTimestamp()] = $session; } krsort($sessionsSorted); From 33d83b4d8facceaab40b27946cd376dc2241f128 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 2 Jun 2011 22:05:37 +0200 Subject: [PATCH 08/30] removed SessionInformationIterator class --- .../DoctrineSessionRegistryStorage.php | 21 +-- .../Resources/config/security.xml | 2 - .../ConcurrentSessionControlStrategy.php | 11 +- .../Session/SessionInformationIterator.php | 157 ------------------ .../Security/Http/Session/SessionRegistry.php | 6 +- .../SessionRegistryStorageInterface.php | 5 +- 6 files changed, 18 insertions(+), 184 deletions(-) delete mode 100644 src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php index 447f38783fe8d..4439f4358d7c2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php +++ b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php @@ -5,7 +5,6 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\StaticPHPDriver; use Symfony\Component\Security\Http\Session\SessionInformation; -use Symfony\Component\Security\Http\Session\SessionInformationIterator; use Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface; class DoctrineSessionRegistryStorage implements SessionRegistryStorageInterface @@ -36,24 +35,22 @@ public function getUsers() */ public function getSessionInformation($sessionId) { - return $this->em->find('Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation', $sessionId); + return $this->em->find('Security:DoctrineSessionInformation', $sessionId); } /** * Obtains the maintained information for one user. * - * @param string $sessionId the session identifier key. - * @return SessionInformationIterator a SessionInformationIterator object. + * @param string $username The user identifier. + * @param boolean $includeExpiredSessions. + * @return array An array of SessionInformation objects. */ - public function getSessionInformations($username, $includeExpiredSessions) + public function getSessionInformations($username, $includeExpiredSessions = false) { - $sessions = new SessionInformationIterator(); - - foreach ($this->em->getRepository('Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation')->findBy(array('username' => $username)) as $sessionInformation) { - $sessions->add($sessionInformation); - } - - return $sessions; + $query = $this->em->createQuery( + 'SELECT si FROM Security:DoctrineSessionInformation si'.($includeExpiredSessions ? '' : ' WHERE si.expired IS NULL').' ORDER BY si.lastRequest DESC' + ); + return $query->getResult(); } /** diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index bcfc0dac0c713..ab61589e34870 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -29,7 +29,6 @@ Symfony\Component\Security\Http\Session\SessionRegistry Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionRegistryStorage Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation - Symfony\Component\Security\Http\Session\SessionInformationIterator Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -75,7 +74,6 @@ %security.authentication.session_information.class% - %security.authentication.session_information_iterator.class% diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php index b1f3b51b87a33..9087e289c2b96 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php @@ -60,7 +60,7 @@ public function onAuthentication(Request $request, TokenInterface $token) $sessions = $this->registry->getAllSessions($user); $maxSessions = $this->getMaximumSessionsForThisUser($user); - if ($sessions->count() >= $maxSessions && $this->alwaysCreateSession !== true) { + if (count($sessions) >= $maxSessions && $this->alwaysCreateSession !== true) { if ($this->exceptionIfMaximumExceeded) { throw new \RuntimeException(sprintf('Maximum of sessions (%s) exceeded', $maxSessions)); } @@ -107,18 +107,15 @@ public function setMaximumSessions($maximumSessions) /** * Allows subclasses to customise behaviour when too many sessions are detected. * - * @param SessionInformationIterator $sessions + * @param array $sessions * @param integer $allowableSessions * @param SessionRegistry $registry * @return void */ - protected function allowableSessionsExceeded(SessionInformationIterator $sessions, $allowableSessions, SessionRegistry $registry) + protected function allowableSessionsExceeded($sessions, $allowableSessions, SessionRegistry $registry) { // remove oldest sessions from registry - $count = 0; - $sessions->sort(); - - for ($i = $allowableSessions - 1; $i < $sessions->count(); $i++) { + for ($i = $allowableSessions - 1; $i < count($sessions); $i++) { $sessions[$i]->expireNow(); $registry->setSessionInformation($sessions[$i]); } diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php deleted file mode 100644 index db6a3beb54799..0000000000000 --- a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php +++ /dev/null @@ -1,157 +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; - -/** - * SessionInformationIterator. - * - * @author Stefan Paschke - */ -class SessionInformationIterator implements \Iterator, \ArrayAccess -{ - protected $sessions = array(); - protected $position = 0; - - /** - * Iterator interface. - * - * @return void - */ - public function rewind() - { - $this->position = 0; - } - - /** - * Iterator interface. - * - * @return SessionInformation - */ - public function current() - { - return $this->sessions[$this->position]; - } - - /** - * Iterator interface. - * - * @return integer - */ - public function key() - { - return $this->position; - } - - /** - * Iterator interface. - * - * @return void - */ - public function next() - { - $this->position++; - } - - /** - * Iterator interface. - * - * @return boolean - */ - public function valid() - { - return isset($this->sessions[$this->position]); - } - - /** - * ArrayAccess interface. - * - * @return boolean - */ - public function offsetExists($offset) - { - return isset($this->sessions[$offset]); - } - - /** - * ArrayAccess interface. - * - * @return SessionInformation - */ - public function offsetGet($offset) - { - return $this->sessions[$offset]; - } - - /** - * ArrayAccess interface. - * - * @param integer $offset - * @param SessionInformation $sessionInformation - * @return void - */ - public function offsetSet($offset, $sessionInformation) - { - if ($sessionInformation instanceof SessionInformation) { - $this->sessions[$offset] = $sessionInformation; - } - } - - /** - * ArrayAccess interface. - * - * @param integer $offset - * @return void - */ - public function offsetUnset($offset) - { - if ($this->offsetExists($offset)) { - unset($this->sessions[$offset]); - } - } - - /** - * Adds a SessionInformation object to the iterator. - * - * @param SessionInformation $sessionInformation - * @return void - */ - public function add(SessionInformation $sessionInformation) - { - $this->sessions[] = $sessionInformation; - } - - /** - * Sorts the session informations by the last request date. - * - * @return void - */ - function sort() - { - $sessionsSorted = array(); - foreach ($this->sessions as $session) { - $sessionsSorted[$session->getLastRequest()->getTimestamp()] = $session; - } - krsort($sessionsSorted); - - $this->sessions = array_values($sessionsSorted); - } - - /** - * Returns the number of session information objects. - * - * @return integer - */ - function count() - { - return count($this->sessions); - } -} diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index 10f9fbaa9d8c1..c1c25afda65d1 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -24,13 +24,11 @@ class SessionRegistry { protected $sessionRegistryStorage; protected $sessionInformationClass; - protected $sessionInformationIteratorClass; - public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage, $sessionInformationClass, $sessionInformationIteratorClass) + public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage, $sessionInformationClass) { $this->sessionRegistryStorage = $sessionRegistryStorage; $this->sessionInformationClass = $sessionInformationClass; - $this->sessionInformationIteratorClass = $sessionInformationIteratorClass; } /** @@ -48,7 +46,7 @@ public function getAllUsers() * * @param UserInterface $user the specified user. * @param boolean $includeExpiredSessions. - * @return SessionInformationIterator $sessions the known sessions. + * @return array An array of SessionInformation objects. */ public function getAllSessions(UserInterface $user, $includeExpiredSessions = false) { diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php index cbcfea2b795d3..35cd277a06677 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -38,8 +38,9 @@ function getSessionInformation($sessionId); /** * Obtains the maintained information for one user. * - * @param string $sessionId the session identifier key. - * @return SessionInformation a SessionInformation object. + * @param string $username The user identifier. + * @param boolean $includeExpiredSessions. + * @return array An array of SessionInformation objects. */ function getSessionInformations($username, $includeExpiredSessions); From 624b01f756585aded16799f7c4291bd37fd7911c Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 2 Jun 2011 22:26:35 +0200 Subject: [PATCH 09/30] forgot query condition --- .../Doctrine/DoctrineSessionRegistryStorage.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php index 4439f4358d7c2..54f3dae4aae5e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php +++ b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php @@ -48,8 +48,10 @@ public function getSessionInformation($sessionId) public function getSessionInformations($username, $includeExpiredSessions = false) { $query = $this->em->createQuery( - 'SELECT si FROM Security:DoctrineSessionInformation si'.($includeExpiredSessions ? '' : ' WHERE si.expired IS NULL').' ORDER BY si.lastRequest DESC' + 'SELECT si FROM Security:DoctrineSessionInformation si WHERE si.username = ?1'.($includeExpiredSessions ? '' : ' AND si.expired IS NULL').' ORDER BY si.lastRequest DESC' ); + $query->setParameter(1, $username); + return $query->getResult(); } From bc182f9c0670a9e262f3e8e8d3814cd7f58dc65f Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 22 Dec 2011 12:08:14 +0100 Subject: [PATCH 10/30] added DBAL default implementation of Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface --- .../Command/InitConcurrentSessionsCommand.php | 31 +++-- .../DependencyInjection/MainConfiguration.php | 20 +++ .../DependencyInjection/SecurityExtension.php | 29 +++++ .../Doctrine/DoctrineSessionInformation.php | 55 --------- .../DoctrineSessionRegistryStorage.php | 84 ------------- .../Resources/config/security.xml | 19 --- .../config/security_session_registry.xml | 25 ++++ .../config/security_session_registry_dbal.xml | 25 ++++ .../Session/Dbal/DbalSessionInformation.php | 37 ++++++ .../Security/Http/Session/Dbal/Schema.php | 52 ++++++++ .../Session/Dbal/SessionRegistryStorage.php | 115 ++++++++++++++++++ .../Http/Session/SessionInformation.php | 34 +++++- 12 files changed, 356 insertions(+), 170 deletions(-) delete mode 100644 src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php delete mode 100644 src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.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/Http/Session/Dbal/DbalSessionInformation.php create mode 100644 src/Symfony/Component/Security/Http/Session/Dbal/Schema.php create mode 100644 src/Symfony/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php index 942b578f566cf..9106130e9dbde 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php @@ -11,18 +11,17 @@ namespace Symfony\Bundle\SecurityBundle\Command; +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Security\Http\Session\Dbal\Schema; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Output\Output; -use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand; -use Symfony\Bundle\DoctrineBundle\Command\Proxy\DoctrineCommandHelper; /** * Installs the database schema required by the concurrent session Doctrine implementation * * @author Stefan Paschke */ -class InitConcurrentSessionsCommand extends CreateCommand +class InitConcurrentSessionsCommand extends ContainerAwareCommand { /** * @see Command @@ -33,7 +32,7 @@ protected function configure() $this ->setName('init:concurrent-session') - ->setDescription('Executes the SQL needed to generate the database schema reqired by the concurrent sessions feature.') + ->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: @@ -52,8 +51,26 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - DoctrineCommandHelper::setApplicationEntityManager($this->getApplication(), 'security'); + $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); + } - parent::execute($input, $output); + $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 f051e7f13e0cb..30eba54855172 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -79,10 +79,30 @@ 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() + ->end() + ->end() + ->end() + ; + } + private function addAclSection(ArrayNodeDefinition $rootNode) { $rootNode diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index b7ad0204e623c..d3a171436c055 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -62,6 +62,10 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('templating_twig.xml'); $loader->load('collectors.xml'); + if (isset($config['session_registry'])) { + $this->sessionRegistryLoad($config['session_registry'], $container); + } + // set some global scalars $container->setParameter('security.access.denied_url', $config['access_denied_url']); $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']); @@ -158,6 +162,31 @@ 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 = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('security_session_registry.xml'); + + if (isset($config['session_registry_storage'])) { + // FIXME: define custom 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. * diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php deleted file mode 100644 index c9a999ef6b509..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Doctrine; - -use Doctrine\ORM\Mapping\ClassMetadata; -use Symfony\Component\Security\Http\Session\SessionInformation; - -/** - * DoctrineSessionInformation. - * - * Allows to persist SessionInformation using Doctrine. - * - * @author Stefan Paschke - */ -class DoctrineSessionInformation extends SessionInformation -{ - public static function loadMetadata(ClassMetadata $metadata) - { - $metadata->setTableName('session_information'); - - $metadata->mapField(array( - 'id' => true, - 'fieldName' => 'sessionId', - 'columnName' => 'session_id', - 'type' => 'string' - )); - - $metadata->mapField(array( - 'fieldName' => 'username', - 'type' => 'string' - )); - - $metadata->mapField(array( - 'fieldName' => 'expired', - 'type' => 'datetime', - 'nullable' => true - )); - - $metadata->mapField(array( - 'fieldName' => 'lastRequest', - 'columnName' => 'last_request', - 'type' => 'datetime', - 'nullable' => true - )); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php deleted file mode 100644 index 54f3dae4aae5e..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php +++ /dev/null @@ -1,84 +0,0 @@ -em = $em; - - $driver = new StaticPHPDriver(__DIR__); - $this->em->getConfiguration()->setMetadataDriverImpl($driver); - } - - /** - * 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 DoctrineSessionInformation a SessionInformation object. - */ - public function getSessionInformation($sessionId) - { - return $this->em->find('Security:DoctrineSessionInformation', $sessionId); - } - - /** - * 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) - { - $query = $this->em->createQuery( - 'SELECT si FROM Security:DoctrineSessionInformation si WHERE si.username = ?1'.($includeExpiredSessions ? '' : ' AND si.expired IS NULL').' ORDER BY si.lastRequest DESC' - ); - $query->setParameter(1, $username); - - return $query->getResult(); - } - - /** - * Adds information for one session. - * - * @param string $sessionId the session identifier key. - * @param DoctrineSessionInformation a SessionInformation object. - * @return void - */ - public function setSessionInformation(SessionInformation $sessionInformation) - { - $this->em->persist($sessionInformation); - $this->em->flush(); - } - - /** - * Deletes the maintained information of one session. - * - * @param string $sessionId the session identifier key. - * @return void - */ - public function removeSessionInformation($sessionId) - { - if (null !== $sessionInformation = $this->getSessionInformation($sessionId)) { - $this->em->remove($sessionInformation); - $this->em->flush(); - } - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 74c47456cff00..0c723a02a9972 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -24,10 +24,6 @@ Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy - Symfony\Component\Security\Http\Session\ConcurrentSessionControlStrategy - Symfony\Component\Security\Http\Session\SessionRegistry - Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionRegistryStorage - Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -72,21 +68,6 @@ %security.authentication.session_strategy.strategy% - - - - - - - - - %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 new file mode 100644 index 0000000000000..3ca7f881fba0e --- /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_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..217b3b9cbd5c8 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml @@ -0,0 +1,25 @@ + + + + + + Symfony\Component\Security\Http\Session\Dbal\SessionRegistryStorage + Symfony\Component\Security\Http\Session\Dbal\DbalSessionInformation + + + + + + + + + + %security.session_registry.dbal.session_information_table_name% + %security.authentication.session_information.class% + + + + + diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php b/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php new file mode 100644 index 0000000000000..bdbdac5495a0a --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php @@ -0,0 +1,37 @@ + + * + * 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\Dbal; + +use Symfony\Component\Security\Http\Session\SessionInformation; + +/** + * DbalSessionInformation. + * + * Allows to persist SessionInformation using Doctrine DBAL. + * + * @author Stefan Paschke + */ +class DbalSessionInformation extends SessionInformation +{ + 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); + } + } +} diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php new file mode 100644 index 0000000000000..268991ebba90c --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.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\Session\Dbal; + +use Doctrine\DBAL\Schema\Schema as BaseSchema; + +/** + * The schema used for the ACL system. + * + * @author Stefan Paschke + */ +final class Schema extends BaseSchema +{ + protected $options; + + /** + * Constructor + * + * @param array $options the names for tables + */ + public function __construct(array $options) + { + parent::__construct(); + + $this->options = $options; + + $this->addSessionInformationTable(); + } + + /** + * Adds the session_information table to the schema + */ + protected function addSessionInformationTable() + { + $table = $this->createTable($this->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/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php b/src/Symfony/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php new file mode 100644 index 0000000000000..85a5201218050 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php @@ -0,0 +1,115 @@ +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. + * @return void + */ + 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. + * @return void + */ + 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/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 33ca7375e3969..5833dc679f035 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -27,9 +27,8 @@ class SessionInformation public function __construct($sessionId, $username) { - $this->sessionId = $sessionId; - $this->username = $username; - $this->refreshLastRequest(); + $this->setSessionId($sessionId); + $this->setUsername($username); } /** @@ -39,7 +38,7 @@ public function __construct($sessionId, $username) */ public function expireNow() { - $this->expired = new \DateTime(); + $this->setExpired(new \DateTime()); } /** @@ -79,7 +78,7 @@ public function getSessionId() */ public function isExpired() { - return $this->expired && $this->expired->getTimestamp() < time(); + return $this->getExpired() && $this->getExpired()->getTimestamp() < time(); } /** @@ -91,4 +90,29 @@ 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; + } } From d0c826aae004b02f7ca537203c0f5921d1bdbae9 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 22 Dec 2011 14:06:24 +0100 Subject: [PATCH 11/30] recreate SessionInformation if they were lost from the registry --- .../Security/Http/Firewall/ConcurrentSessionListener.php | 3 +++ .../Security/Http/Session/Dbal/DbalSessionInformation.php | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php index 5d0f2ff834cb9..1481b4fa29caa 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -84,6 +84,9 @@ public function handle(GetResponseEvent $event) $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/Dbal/DbalSessionInformation.php b/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php index bdbdac5495a0a..7f0bcb74d7b8f 100644 --- a/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php @@ -34,4 +34,9 @@ public function __construct($sessionId, $username, \DateTime $lastRequest = null $this->setExpired($expired); } } + + public function getExpired() + { + return parent::getExpired(); + } } From c9f08a433cf289041ef657d949cf13924820e6ab Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 22 Dec 2011 20:08:57 +0100 Subject: [PATCH 12/30] minor fixes --- .../SecurityBundle/DependencyInjection/MainConfiguration.php | 1 + .../SecurityBundle/DependencyInjection/SecurityExtension.php | 2 +- .../Security/Http/Firewall/ConcurrentSessionListener.php | 1 - .../Component/Security/Http/Session/SessionInformation.php | 2 +- .../Component/Security/Http/Session/SessionRegistry.php | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 30eba54855172..a6dfb28e467c4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -97,6 +97,7 @@ private function addSessionRegistrySection(ArrayNodeDefinition $rootNode) ->scalarNode('session_information')->defaultValue('cs_session_information')->end() ->end() ->end() + ->scalarNode('session_registry_storage')->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index d3a171436c055..fdbfeffc10058 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -168,7 +168,7 @@ private function sessionRegistryLoad($config, ContainerBuilder $container) $loader->load('security_session_registry.xml'); if (isset($config['session_registry_storage'])) { - // FIXME: define custom session registry storage + $container->setAlias('security.authentication.session_registry_storage', $config['session_registry_storage']); return; } diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php index 1481b4fa29caa..a172de15a944d 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -89,6 +89,5 @@ public function handle(GetResponseEvent $event) $this->sessionRegistry->registerNewSession($session->getId(), $token->getUser()); } } - } } diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 5833dc679f035..4a686770a06df 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -44,7 +44,7 @@ public function expireNow() /** * Obtain the last request date. * - * @return integer UNIX Timestamp of the last request date and time. + * @return DateTime the last request date and time. */ public function getLastRequest() { diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index c1c25afda65d1..281c88e03c99f 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -85,7 +85,7 @@ public function refreshLastRequest($sessionId) { if ($sessionInformation = $this->getSessionInformation($sessionId)) { $sessionInformation->refreshLastRequest(); - $this->sessionRegistryStorage->setSessionInformation($sessionInformation); + $this->setSessionInformation($sessionInformation); } } @@ -101,7 +101,7 @@ public function registerNewSession($sessionId, UserInterface $user) $sessionInformation = new $this->sessionInformationClass($sessionId, $user->getUsername()); $sessionInformation->refreshLastRequest(); - $this->sessionRegistryStorage->setSessionInformation($sessionInformation); + $this->setSessionInformation($sessionInformation); } /** From 13813da7a9430937013f710c952b9a98e26fe97d Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Wed, 4 May 2011 17:25:59 +0200 Subject: [PATCH 13/30] added listener, strategy, registry, configuration for concurrent sessions --- .../DependencyInjection/MainConfiguration.php | 7 + .../DependencyInjection/SecurityExtension.php | 27 +++ .../Resources/config/security.xml | 19 +++ .../Resources/config/security_listeners.xml | 11 ++ .../Firewall/ConcurrentSessionListener.php | 95 +++++++++++ .../ConcurrentSessionControlStrategy.php | 150 +++++++++++++++++ .../Http/Session/SessionInformation.php | 95 +++++++++++ .../Session/SessionInformationIterator.php | 157 ++++++++++++++++++ .../Security/Http/Session/SessionRegistry.php | 132 +++++++++++++++ .../Http/Session/SessionRegistryInterface.php | 52 ++++++ .../SessionRegistryStorageInterface.php | 82 +++++++++ 11 files changed, 827 insertions(+) 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/SessionInformationIterator.php create mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistry.php create mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php create mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index ff94ac193fb0b..5ee310a6d5f38 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -242,6 +242,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('target_url')->defaultValue('/')->end() + ->end() + ->end() ; $abstractFactoryKeys = array(); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index bf13395365941..eab5c97f66e68 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -75,6 +75,19 @@ public function load(array $configs, ContainerBuilder $container) $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']); + if (isset($config['firewalls'])) { + foreach ($config['firewalls'] as $firewallconfig) { + if (!empty($firewallconfig['session_concurrency']['max_sessions'])) { + $container->getDefinition('security.authentication.concurrent_session_strategy') + ->replaceArgument(1, $firewallconfig['session_concurrency']['max_sessions']) + ->replaceArgument(2, $config['session_fixation_strategy']); + $container->setDefinition('security.authentication.session_strategy', $container->getDefinition('security.authentication.concurrent_session_strategy')); + + break; + } + } + } + $this->createFirewalls($config, $container); $this->createAuthorization($config, $container); $this->createRoleHierarchy($config, $container); @@ -318,6 +331,11 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider)); } + // 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']; @@ -545,6 +563,15 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv return $switchUserListenerId; } + private function createConcurrentSessionListener($container, $id, $config) + { + $concurrentSessionListenerId = 'security.authentication.concurrentsession_listener'.$id; + $listener = $container->setDefinition($concurrentSessionListenerId, new DefinitionDecorator('security.authentication.concurrentsession_listener')); + $listener->replaceArgument(2, $config['target_url']); + + return $concurrentSessionListenerId; + } + private function createRequestMatcher($container, $path = null, $host = null, $methods = null, $ip = null, array $attributes = array()) { $serialized = serialize(array($path, $host, $methods, $ip, $attributes)); diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 0c723a02a9972..e275bc9dbae9a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -24,6 +24,11 @@ Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy + Symfony\Component\Security\Http\Session\ConcurrentSessionControlStrategy + Symfony\Component\Security\Http\Session\SessionRegistry + Symfony\Component\Security\Http\Session\SessionInformation + Symfony\Component\Security\Http\Session\SessionInformationIterator + Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -68,6 +73,20 @@ %security.authentication.session_strategy.strategy% + + + + + + + + + %security.authentication.session_information.class% + %security.authentication.session_information_iterator.class% + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 430e419606094..304cf623515a1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -24,6 +24,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 @@ -175,6 +177,15 @@ + + + + + + + + + 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..46540aa200469 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -0,0 +1,95 @@ + + * + * 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\Session\SessionRegistry; +use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\Log\LoggerInterface; + +class ConcurrentSessionListener implements ListenerInterface +{ + private $securityContext; + private $sessionRegistry; + private $targetUrl; + private $logger; + private $handlers; + private $successHandler; + + public function __construct(SecurityContextInterface $securityContext, SessionRegistry $sessionRegistry, $targetUrl = '/', LogoutSuccessHandlerInterface $successHandler = null, LoggerInterface $logger = null) + { + $this->securityContext = $securityContext; + $this->sessionRegistry = $sessionRegistry; + $this->targetUrl = $targetUrl; + $this->successHandler = $successHandler; + $this->logger = $logger; + $this->handlers = array(); + } + + /** + * Adds a logout handler + * + * @param LogoutHandlerInterface $handler + * @return void + */ + public function addHandler(LogoutHandlerInterface $handler) + { + $this->handlers[] = $handler; + } + + public function register(EventDispatcherInterface $dispatcher) + { + $dispatcher->connect('core.security', array($this, 'handle'), 0); + } + + public function unregister(EventDispatcherInterface $dispatcher) + { + } + + public function handle(GetResponseEvent $event) + { + $request = $event->getRequest(); + + $session = $request->hasSession() ? $request->getSession() : null; + + if (null !== $session && null !== $token = $this->securityContext->getToken()) { + 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 = new RedirectResponse(0 !== strpos($this->targetUrl, 'http') ? $request->getUriForPath($this->targetUrl) : $this->targetUrl, 302); + } + + foreach ($this->handlers as $handler) { + $handler->logout($request, $response, $token); + } + + $this->securityContext->setToken(null); + + $event->setResponse($response); + } else { + $sessionInformation->refreshLastRequest(); + $this->sessionRegistry->setSessionInformation($sessionInformation); + } + } + } + + } +} 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..be605114f07db --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php @@ -0,0 +1,150 @@ + + * + * 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); + } + + /** + * Called when a user is newly authenticated. + * + * @param Request $request + * @param TokenInterface $token + * @return void + */ + 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(), $token); + } + + $sessions = $this->registry->getAllSessions($user); + $maxSessions = $this->getMaximumSessionsForThisUser($user); + + if ($sessions->count() >= $maxSessions && $this->alwaysCreateSession !== true) { + if ($this->exceptionIfMaximumExceeded) { + throw new \RuntimeException(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 + * @return void + */ + 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 + * @return void + */ + public function setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded) + { + $this->exceptionIfMaximumExceeded = $exceptionIfMaximumExceeded; + } + + /** + * Sets the maxSessions property. + * + * @param $maximumSessions + * @return void + */ + public function setMaximumSessions($maximumSessions) + { + $this->maximumSessions = $maximumSessions; + } + + /** + * Allows subclasses to customise behaviour when too many sessions are detected. + * + * @param SessionInformationIterator $sessions + * @param integer $allowableSessions + * @param SessionRegistry $registry + * @return void + */ + protected function allowableSessionsExceeded(SessionInformationIterator $sessions, $allowableSessions, SessionRegistry $registry) + { + // remove oldest sessions from registry + $count = 0; + $sessions->sort(); + + for ($i = $allowableSessions - 1; $i < $sessions->count(); $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 + * @return void + */ + protected function onSessionChange($originalSessionId, $newSessionId, TokenInterface $token) + { + $this->registry->removeSessionInformation($originalSessionId, $token->getUser()); + } +} 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..2e119ebaa49dc --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -0,0 +1,95 @@ + + * + * 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; + +/** + * SessionInformation. + * + * Represents a record of a session. This is primarily used for concurrent session support. + * + * @author Stefan Paschke + */ +class SessionInformation +{ + protected $sessionId; + protected $user; + protected $expired; + protected $lastRequest; + + public function __construct($sessionId, UserInterface $user) + { + $this->sessionId = $sessionId; + $this->user = $user; + } + + /** + * Sets the session informations expired date to the current date and time. + * + * @return void + */ + public function expireNow() + { + $this->expired = time(); + } + + /** + * Obtain the last request date. + * + * @return integer UNIX Timestamp of the last request date and time. + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + /** + * Obtains the user. + * + * @return UserInterface + */ + public function getUser() + { + return $this->user; + } + + /** + * 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->expired && $this->expired < time(); + } + + /** + * Set the last request date to the current date and time. + * + * @return void + */ + public function refreshLastRequest() + { + $this->lastRequest = time(); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php new file mode 100644 index 0000000000000..9e019489a2a9c --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php @@ -0,0 +1,157 @@ + + * + * 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; + +/** + * SessionInformationIterator. + * + * @author Stefan Paschke + */ +class SessionInformationIterator implements \Iterator, \ArrayAccess +{ + protected $sessions = array(); + protected $position = 0; + + /** + * Iterator interface. + * + * @return void + */ + public function rewind() + { + $this->position = 0; + } + + /** + * Iterator interface. + * + * @return SessionInformation + */ + public function current() + { + return $this->sessions[$this->position]; + } + + /** + * Iterator interface. + * + * @return integer + */ + public function key() + { + return $this->position; + } + + /** + * Iterator interface. + * + * @return void + */ + public function next() + { + $this->position++; + } + + /** + * Iterator interface. + * + * @return boolean + */ + public function valid() + { + return isset($this->sessions[$this->position]); + } + + /** + * ArrayAccess interface. + * + * @return boolean + */ + public function offsetExists($offset) + { + return isset($this->sessions[$offset]); + } + + /** + * ArrayAccess interface. + * + * @return SessionInformation + */ + public function offsetGet($offset) + { + return $this->sessions[$offset]; + } + + /** + * ArrayAccess interface. + * + * @param integer $offset + * @param SessionInformation $sessionInformation + * @return void + */ + public function offsetSet($offset, $sessionInformation) + { + if ($sessionInformation instanceof SessionInformation) { + $this->sessions[$offset] = $sessionInformation; + } + } + + /** + * ArrayAccess interface. + * + * @param integer $offset + * @return void + */ + public function offsetUnset($offset) + { + if ($this->offsetExists($offset)) { + unset($this->sessions[$offset]); + } + } + + /** + * Adds a SessionInformation object to the iterator. + * + * @param SessionInformation $sessionInformation + * @return void + */ + public function add(SessionInformation $sessionInformation) + { + $this->sessions[] = $sessionInformation; + } + + /** + * Sorts the session informations by the last request date. + * + * @return void + */ + function sort() + { + $sessionsSorted = array(); + foreach ($this->sessions as $session) { + $sessionsSorted[$session->getLastRequest()] = $session; + } + krsort($sessionsSorted); + + $this->sessions = array_values($sessionsSorted); + } + + /** + * Returns the number of session information objects. + * + * @return integer + */ + function count() + { + return count($this->sessions); + } +} 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..fd0297d18bccb --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -0,0 +1,132 @@ + + * + * 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; + protected $sessionInformationIteratorClass; + + public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage, $sessionInformationClass, $sessionInformationIteratorClass) + { + $this->sessionRegistryStorage = $sessionRegistryStorage; + $this->sessionInformationClass = $sessionInformationClass; + $this->sessionInformationIteratorClass = $sessionInformationIteratorClass; + } + + /** + * 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 SessionInformationIterator $sessions the known sessions. + */ + public function getAllSessions(UserInterface $user, $includeExpiredSessions = false) + { + $sessions = new $this->sessionInformationIteratorClass(); + + foreach ($this->sessionRegistryStorage->getSessionIds($user) as $sessionId) { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + if ($includeExpiredSessions === true || $sessionInformation->isExpired() === false) { + $sessions->add($sessionInformation); + } + } + } + + return $sessions; + } + + /** + * Obtains the session information for the specified sessionId. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation $sessionInformation + */ + public function getSessionInformation($sessionId) + { + return $this->sessionRegistryStorage->getSessionInformation($sessionId); + } + + /** + * Sets a SessionInformation object. + * + * @param SessionInformation $sessionInformation + * @return void + */ + public function setSessionInformation(SessionInformation $sessionInformation) + { + $this->sessionRegistryStorage->setSessionInformation($sessionInformation->getSessionId(), $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. + * @return void + */ + public function refreshLastRequest($sessionId) + { + if ($sessionInformation = $this->getSessionInformation($sessionId)) { + $sessionInformation->refreshLastRequest(); + $this->sessionRegistryStorage->setSessionInformation($sessionInformation); + } + } + + /** + * Registers a new session for the specified user. + * + * @param string $sessionId the session identifier key. + * @param UserInterface $user the specified user. + * @return void + */ + public function registerNewSession($sessionId, UserInterface $user) + { + $sessionInformation = new $this->sessionInformationClass($sessionId, $user); + $sessionInformation->refreshLastRequest(); + + $this->sessionRegistryStorage->setSessionInformation($sessionId, $sessionInformation); + $this->sessionRegistryStorage->addSessionId($sessionId, $user); + } + + /** + * Deletes all the session information being maintained for the specified sessionId. + * + * @param string $sessionId the session identifier key. + * @param UserInterface $user the specified user. + * @return void + */ + public function removeSessionInformation($sessionId, UserInterface $user) + { + $this->sessionRegistryStorage->removeSessionInformation($sessionId); + $this->sessionRegistryStorage->removeSessionId($sessionId, $user); + } +} diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php new file mode 100644 index 0000000000000..2b60cfc4dab61 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.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\Session; + +/** + * SessionRegistryInterface. + * + * The SessionRegistry is used as the source of data on authenticated users and session data. + * + * @author Stefan Paschke + */ +interface SessionRegistryInterface +{ + /** + * Obtains all the known users in the SessionRegistry. + */ + function getAllUsers(); + + /** + * Obtains all the known sessions for the specified user. + */ + function getAllSessions($user, $includeExpiredSessions = false); + + /** + * Obtains the session information for the specified sessionId. + */ + function getSessionInformation($sessionId); + + /** + * Updates the given sessionId so its last request time is equal to the present date and time. + */ + function refreshLastRequest($sessionId); + + /** + * Registers a new session for the specified principal. + */ + function registerNewSession($sessionId, $user); + + /** + * Deletes all the session information being maintained for the specified sessionId. + */ + function removeSessionInformation($sessionId, $user); +} 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..3ece57269d73c --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -0,0 +1,82 @@ + + * + * 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; + +/** + * 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 all the known session IDs for the specified user. + * + * @param UserInterface $user + * @return array an array ob session identifiers. + */ + function getSessionIds(UserInterface $user); + + /** + * Adds one session ID to the specified users array + * + * @param string $sessionId the session identifier key. + * @param UserInterface $user + * @return void + */ + function addSessionId($sessionId, UserInterface $user); + + /** + * Removes one session ID from the specified users array. + * + * @param string $sessionId the session identifier key. + * @param UserInterface $user + * @return void + */ + function removeSessionId($sessionId, UserInterface $user); + + /** + * Obtains the maintained information for one session. + * + * @param string $sessionId the session identifier key. + * @return SessionInformation a SessionInformation object. + */ + function getSessionInformation($sessionId); + + /** + * Adds information for one session. + * + * @param string $sessionId the session identifier key. + * @param SessionInformation a SessionInformation object. + * @return void + */ + function setSessionInformation($sessionId, SessionInformation $sessionInformation); + + /** + * Deletes the maintained information of one session. + * + * @param string $sessionId the session identifier key. + * @return void + */ + function removeSessionInformation($sessionId); +} From 8ef6923d28ddea98afcb53448ed2fe8776f7f084 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 5 May 2011 12:00:04 +0200 Subject: [PATCH 14/30] added SessionLogoutHandler to setup of ConcurrentSessionListener --- .../SecurityBundle/DependencyInjection/MainConfiguration.php | 2 +- .../SecurityBundle/DependencyInjection/SecurityExtension.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 5ee310a6d5f38..f051e7f13e0cb 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -246,7 +246,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->canBeUnset() ->children() ->scalarNode('max_sessions')->defaultNull()->end() - ->scalarNode('target_url')->defaultValue('/')->end() + ->scalarNode('expiration_url')->defaultValue('/')->end() ->end() ->end() ; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index eab5c97f66e68..bb9315398fe52 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -565,9 +565,10 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv private function createConcurrentSessionListener($container, $id, $config) { - $concurrentSessionListenerId = 'security.authentication.concurrentsession_listener'.$id; + $concurrentSessionListenerId = 'security.authentication.concurrentsession_listener.'.$id; $listener = $container->setDefinition($concurrentSessionListenerId, new DefinitionDecorator('security.authentication.concurrentsession_listener')); - $listener->replaceArgument(2, $config['target_url']); + $listener->replaceArgument(2, $config['expiration_url']); + $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); return $concurrentSessionListenerId; } From f4f0a8c419107252105a0234554fc840431c7331 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 5 May 2011 14:16:28 +0200 Subject: [PATCH 15/30] removed uneeded interface --- .../Http/Session/SessionRegistryInterface.php | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php deleted file mode 100644 index 2b60cfc4dab61..0000000000000 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryInterface.php +++ /dev/null @@ -1,52 +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; - -/** - * SessionRegistryInterface. - * - * The SessionRegistry is used as the source of data on authenticated users and session data. - * - * @author Stefan Paschke - */ -interface SessionRegistryInterface -{ - /** - * Obtains all the known users in the SessionRegistry. - */ - function getAllUsers(); - - /** - * Obtains all the known sessions for the specified user. - */ - function getAllSessions($user, $includeExpiredSessions = false); - - /** - * Obtains the session information for the specified sessionId. - */ - function getSessionInformation($sessionId); - - /** - * Updates the given sessionId so its last request time is equal to the present date and time. - */ - function refreshLastRequest($sessionId); - - /** - * Registers a new session for the specified principal. - */ - function registerNewSession($sessionId, $user); - - /** - * Deletes all the session information being maintained for the specified sessionId. - */ - function removeSessionInformation($sessionId, $user); -} From e11a540b3d3f225ded0a2af93cc0a08098a4fa4a Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 5 May 2011 15:26:22 +0200 Subject: [PATCH 16/30] removed security.authentication.session_registry_storage service from Component --- .../Bundle/SecurityBundle/Resources/config/security.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index e275bc9dbae9a..f04813c568841 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -28,7 +28,6 @@ Symfony\Component\Security\Http\Session\SessionRegistry Symfony\Component\Security\Http\Session\SessionInformation Symfony\Component\Security\Http\Session\SessionInformationIterator - Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -85,8 +84,6 @@ %security.authentication.session_information_iterator.class% - - From a3d0a59167f7688c955f1027f776ab90b0643c8a Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 5 May 2011 16:00:10 +0200 Subject: [PATCH 17/30] removed unneeded methods --- .../Http/Firewall/ConcurrentSessionListener.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php index 46540aa200469..5d0f2ff834cb9 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -49,15 +49,11 @@ public function addHandler(LogoutHandlerInterface $handler) $this->handlers[] = $handler; } - public function register(EventDispatcherInterface $dispatcher) - { - $dispatcher->connect('core.security', array($this, 'handle'), 0); - } - - public function unregister(EventDispatcherInterface $dispatcher) - { - } - + /** + * Handles the number of simultaneous sessions for a single user. + * + * @param GetResponseEvent $event A GetResponseEvent instance + */ public function handle(GetResponseEvent $event) { $request = $event->getRequest(); From c800408c269a9b82bf699ec1c8947d1198ebfb2a Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Tue, 17 May 2011 10:05:16 +0200 Subject: [PATCH 18/30] prepare Doctrine implementation of SessionRegistryStorage --- .../Command/InitConcurrentSessionsCommand.php | 67 ++++++++ .../ConcurrentSessionControlStrategy.php | 6 +- .../Security/Http/Session/Dbal/Schema.php | 145 ++++++++++++++++++ .../Http/Session/SessionInformation.php | 16 +- .../Security/Http/Session/SessionRegistry.php | 22 +-- .../SessionRegistryStorageInterface.php | 32 +--- 6 files changed, 233 insertions(+), 55 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php create mode 100644 src/Symfony/Component/Security/Http/Session/Dbal/Schema.php diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php new file mode 100644 index 0000000000000..9ace314a27a7c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php @@ -0,0 +1,67 @@ + + * + * 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\Command; +use Symfony\Component\Security\Http\Session\Dbal\Schema; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Doctrine\DBAL\DriverManager; + +/** + * Installs the tables required by the concurrent session system + * + * @author Stefan Paschke + */ +class InitConcurrentSessionsCommand extends Command +{ + /** + * @see Command + */ + protected function configure() + { + $this + ->setName('init:concurrent-session') + ; + } + + /** + * @see Command + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $connection = $this->container->get('security.concurrent-session.dbal.connection'); + $sm = $connection->getSchemaManager(); + $tableNames = $sm->listTableNames(); + $tables = array( + 'class_table_name' => $this->container->getParameter('security.acl.dbal.class_table_name'), + 'sid_table_name' => $this->container->getParameter('security.acl.dbal.sid_table_name'), + 'oid_table_name' => $this->container->getParameter('security.acl.dbal.oid_table_name'), + 'oid_ancestors_table_name' => $this->container->getParameter('security.acl.dbal.oid_ancestors_table_name'), + 'entry_table_name' => $this->container->getParameter('security.acl.dbal.entry_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/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php index be605114f07db..b1f3b51b87a33 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php @@ -54,7 +54,7 @@ public function onAuthentication(Request $request, TokenInterface $token) parent::onAuthentication($request, $token); if ($originalSessionId != $request->getSession()->getId()) { - $this->onSessionChange($originalSessionId, $request->getSession()->getId(), $token); + $this->onSessionChange($originalSessionId, $request->getSession()->getId()); } $sessions = $this->registry->getAllSessions($user); @@ -143,8 +143,8 @@ protected function getMaximumSessionsForThisUser(UserInterface $user) * @param TokenInterface $token * @return void */ - protected function onSessionChange($originalSessionId, $newSessionId, TokenInterface $token) + protected function onSessionChange($originalSessionId, $newSessionId) { - $this->registry->removeSessionInformation($originalSessionId, $token->getUser()); + $this->registry->removeSessionInformation($originalSessionId); } } diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php new file mode 100644 index 0000000000000..ac616d9e10d95 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php @@ -0,0 +1,145 @@ + + * + * 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\Dbal; + +use Doctrine\DBAL\Schema\Schema as BaseSchema; + +/** + * The schema used for the concurrent session system. + * + * @author Stefan Paschke + */ +final class Schema extends BaseSchema +{ + protected $options; + + /** + * Constructor + * + * @param array $options the names for tables + * @return void + */ + public function __construct(array $options) + { + parent::__construct(); + + $this->options = $options; + + $this->addClassTable(); + $this->addSecurityIdentitiesTable(); + $this->addObjectIdentitiesTable(); + $this->addObjectIdentityAncestorsTable(); + $this->addEntryTable(); + } + + /** + * Adds the class table to the schema + * + * @return void + */ + protected function addClassTable() + { + $table = $this->createTable($this->options['class_table_name']); + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('class_type', 'string', array('length' => 200)); + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('class_type')); + } + + /** + * Adds the entry table to the schema + * + * @return void + */ + protected function addEntryTable() + { + $table = $this->createTable($this->options['entry_table_name']); + + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('class_id', 'integer', array('unsigned' => true)); + $table->addColumn('object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); + $table->addColumn('field_name', 'string', array('length' => 50, 'notnull' => false)); + $table->addColumn('ace_order', 'smallint', array('unsigned' => true)); + $table->addColumn('security_identity_id', 'integer', array('unsigned' => true)); + $table->addColumn('mask', 'integer'); + $table->addColumn('granting', 'boolean'); + $table->addColumn('granting_strategy', 'string', array('length' => 30)); + $table->addColumn('audit_success', 'boolean'); + $table->addColumn('audit_failure', 'boolean'); + + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('class_id', 'object_identity_id', 'field_name', 'ace_order')); + $table->addIndex(array('class_id', 'object_identity_id', 'security_identity_id')); + + $table->addForeignKeyConstraint($this->getTable($this->options['class_table_name']), array('class_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + $table->addForeignKeyConstraint($this->getTable($this->options['oid_table_name']), array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + $table->addForeignKeyConstraint($this->getTable($this->options['sid_table_name']), array('security_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + } + + /** + * Adds the object identity table to the schema + * + * @return void + */ + protected function addObjectIdentitiesTable() + { + $table = $this->createTable($this->options['oid_table_name']); + + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('class_id', 'integer', array('unsigned' => true)); + $table->addColumn('object_identifier', 'string', array('length' => 100)); + $table->addColumn('parent_object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); + $table->addColumn('entries_inheriting', 'boolean'); + + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('object_identifier', 'class_id')); + $table->addIndex(array('parent_object_identity_id')); + + $table->addForeignKeyConstraint($table, array('parent_object_identity_id'), array('id'), array('onDelete' => 'RESTRICT', 'onUpdate' => 'RESTRICT')); + } + + /** + * Adds the object identity relation table to the schema + * + * @return void + */ + protected function addObjectIdentityAncestorsTable() + { + $table = $this->createTable($this->options['oid_ancestors_table_name']); + + $table->addColumn('object_identity_id', 'integer', array('unsigned' => true)); + $table->addColumn('ancestor_id', 'integer', array('unsigned' => true)); + + $table->setPrimaryKey(array('object_identity_id', 'ancestor_id')); + + $oidTable = $this->getTable($this->options['oid_table_name']); + $table->addForeignKeyConstraint($oidTable, array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + $table->addForeignKeyConstraint($oidTable, array('ancestor_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); + } + + /** + * Adds the security identity table to the schema + * + * @return void + */ + protected function addSecurityIdentitiesTable() + { + $table = $this->createTable($this->options['sid_table_name']); + + $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); + $table->addColumn('identifier', 'string', array('length' => 200)); + $table->addColumn('username', 'boolean'); + + $table->setPrimaryKey(array('id')); + $table->addUniqueIndex(array('identifier', 'username')); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 2e119ebaa49dc..961055a742662 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Http\Session; -use Symfony\Component\Security\Core\User\UserInterface; - /** * SessionInformation. * @@ -23,14 +21,14 @@ class SessionInformation { protected $sessionId; - protected $user; + protected $username; protected $expired; protected $lastRequest; - public function __construct($sessionId, UserInterface $user) + public function __construct($sessionId, $username) { $this->sessionId = $sessionId; - $this->user = $user; + $this->username = $username; } /** @@ -54,13 +52,13 @@ public function getLastRequest() } /** - * Obtains the user. + * Obtains the username. * - * @return UserInterface + * @return string */ - public function getUser() + public function getUsername() { - return $this->user; + return $this->username; } /** diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index fd0297d18bccb..10f9fbaa9d8c1 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -52,17 +52,7 @@ public function getAllUsers() */ public function getAllSessions(UserInterface $user, $includeExpiredSessions = false) { - $sessions = new $this->sessionInformationIteratorClass(); - - foreach ($this->sessionRegistryStorage->getSessionIds($user) as $sessionId) { - if ($sessionInformation = $this->getSessionInformation($sessionId)) { - if ($includeExpiredSessions === true || $sessionInformation->isExpired() === false) { - $sessions->add($sessionInformation); - } - } - } - - return $sessions; + return $this->sessionRegistryStorage->getSessionInformations($user->getUsername(), $includeExpiredSessions); } /** @@ -84,7 +74,7 @@ public function getSessionInformation($sessionId) */ public function setSessionInformation(SessionInformation $sessionInformation) { - $this->sessionRegistryStorage->setSessionInformation($sessionInformation->getSessionId(), $sessionInformation); + $this->sessionRegistryStorage->setSessionInformation($sessionInformation); } /** @@ -110,11 +100,10 @@ public function refreshLastRequest($sessionId) */ public function registerNewSession($sessionId, UserInterface $user) { - $sessionInformation = new $this->sessionInformationClass($sessionId, $user); + $sessionInformation = new $this->sessionInformationClass($sessionId, $user->getUsername()); $sessionInformation->refreshLastRequest(); - $this->sessionRegistryStorage->setSessionInformation($sessionId, $sessionInformation); - $this->sessionRegistryStorage->addSessionId($sessionId, $user); + $this->sessionRegistryStorage->setSessionInformation($sessionInformation); } /** @@ -124,9 +113,8 @@ public function registerNewSession($sessionId, UserInterface $user) * @param UserInterface $user the specified user. * @return void */ - public function removeSessionInformation($sessionId, UserInterface $user) + public function removeSessionInformation($sessionId) { $this->sessionRegistryStorage->removeSessionInformation($sessionId); - $this->sessionRegistryStorage->removeSessionId($sessionId, $user); } } diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php index 3ece57269d73c..cbcfea2b795d3 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -11,8 +11,6 @@ namespace Symfony\Component\Security\Http\Session; -use Symfony\Component\Security\Core\User\UserInterface; - /** * SessionRegistryStorageInterface. * @@ -30,38 +28,20 @@ interface SessionRegistryStorageInterface function getUsers(); /** - * Obtains all the known session IDs for the specified user. - * - * @param UserInterface $user - * @return array an array ob session identifiers. - */ - function getSessionIds(UserInterface $user); - - /** - * Adds one session ID to the specified users array - * - * @param string $sessionId the session identifier key. - * @param UserInterface $user - * @return void - */ - function addSessionId($sessionId, UserInterface $user); - - /** - * Removes one session ID from the specified users array. + * Obtains the maintained information for one session. * * @param string $sessionId the session identifier key. - * @param UserInterface $user - * @return void + * @return SessionInformation a SessionInformation object. */ - function removeSessionId($sessionId, UserInterface $user); + function getSessionInformation($sessionId); /** - * Obtains the maintained information for one session. + * Obtains the maintained information for one user. * * @param string $sessionId the session identifier key. * @return SessionInformation a SessionInformation object. */ - function getSessionInformation($sessionId); + function getSessionInformations($username, $includeExpiredSessions); /** * Adds information for one session. @@ -70,7 +50,7 @@ function getSessionInformation($sessionId); * @param SessionInformation a SessionInformation object. * @return void */ - function setSessionInformation($sessionId, SessionInformation $sessionInformation); + function setSessionInformation(SessionInformation $sessionInformation); /** * Deletes the maintained information of one session. From 9c8bd2172d54cfa0d556fc183006bcf0a713e24f Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 2 Jun 2011 19:29:54 +0200 Subject: [PATCH 19/30] default Doctrine implementation of SessionInformation --- .../Command/InitConcurrentSessionsCommand.php | 50 +++--- .../Doctrine/DoctrineSessionInformation.php | 55 +++++++ .../DoctrineSessionRegistryStorage.php | 85 ++++++++++ .../Resources/config/security.xml | 7 +- .../Security/Http/Session/Dbal/Schema.php | 145 ------------------ .../Http/Session/SessionInformation.php | 7 +- .../Session/SessionInformationIterator.php | 2 +- 7 files changed, 172 insertions(+), 179 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php create mode 100644 src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php delete mode 100644 src/Symfony/Component/Security/Http/Session/Dbal/Schema.php diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php index 9ace314a27a7c..942b578f566cf 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php @@ -11,27 +11,40 @@ namespace Symfony\Bundle\SecurityBundle\Command; -use Symfony\Bundle\FrameworkBundle\Command\Command; -use Symfony\Component\Security\Http\Session\Dbal\Schema; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Doctrine\DBAL\DriverManager; +use Symfony\Component\Console\Output\Output; +use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand; +use Symfony\Bundle\DoctrineBundle\Command\Proxy\DoctrineCommandHelper; /** - * Installs the tables required by the concurrent session system + * Installs the database schema required by the concurrent session Doctrine implementation * * @author Stefan Paschke */ -class InitConcurrentSessionsCommand extends Command +class InitConcurrentSessionsCommand extends CreateCommand { /** * @see Command */ protected function configure() { + parent::configure(); + $this ->setName('init:concurrent-session') - ; + ->setDescription('Executes the SQL needed to generate the database schema reqired 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 + ); } /** @@ -39,29 +52,8 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - $connection = $this->container->get('security.concurrent-session.dbal.connection'); - $sm = $connection->getSchemaManager(); - $tableNames = $sm->listTableNames(); - $tables = array( - 'class_table_name' => $this->container->getParameter('security.acl.dbal.class_table_name'), - 'sid_table_name' => $this->container->getParameter('security.acl.dbal.sid_table_name'), - 'oid_table_name' => $this->container->getParameter('security.acl.dbal.oid_table_name'), - 'oid_ancestors_table_name' => $this->container->getParameter('security.acl.dbal.oid_ancestors_table_name'), - 'entry_table_name' => $this->container->getParameter('security.acl.dbal.entry_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); - } + DoctrineCommandHelper::setApplicationEntityManager($this->getApplication(), 'security'); - $output->writeln('Concurrent session tables have been initialized successfully.'); + parent::execute($input, $output); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php new file mode 100644 index 0000000000000..c9a999ef6b509 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.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\Bundle\SecurityBundle\Doctrine; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Symfony\Component\Security\Http\Session\SessionInformation; + +/** + * DoctrineSessionInformation. + * + * Allows to persist SessionInformation using Doctrine. + * + * @author Stefan Paschke + */ +class DoctrineSessionInformation extends SessionInformation +{ + public static function loadMetadata(ClassMetadata $metadata) + { + $metadata->setTableName('session_information'); + + $metadata->mapField(array( + 'id' => true, + 'fieldName' => 'sessionId', + 'columnName' => 'session_id', + 'type' => 'string' + )); + + $metadata->mapField(array( + 'fieldName' => 'username', + 'type' => 'string' + )); + + $metadata->mapField(array( + 'fieldName' => 'expired', + 'type' => 'datetime', + 'nullable' => true + )); + + $metadata->mapField(array( + 'fieldName' => 'lastRequest', + 'columnName' => 'last_request', + 'type' => 'datetime', + 'nullable' => true + )); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php new file mode 100644 index 0000000000000..447f38783fe8d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php @@ -0,0 +1,85 @@ +em = $em; + + $driver = new StaticPHPDriver(__DIR__); + $this->em->getConfiguration()->setMetadataDriverImpl($driver); + } + + /** + * 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 DoctrineSessionInformation a SessionInformation object. + */ + public function getSessionInformation($sessionId) + { + return $this->em->find('Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation', $sessionId); + } + + /** + * Obtains the maintained information for one user. + * + * @param string $sessionId the session identifier key. + * @return SessionInformationIterator a SessionInformationIterator object. + */ + public function getSessionInformations($username, $includeExpiredSessions) + { + $sessions = new SessionInformationIterator(); + + foreach ($this->em->getRepository('Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation')->findBy(array('username' => $username)) as $sessionInformation) { + $sessions->add($sessionInformation); + } + + return $sessions; + } + + /** + * Adds information for one session. + * + * @param string $sessionId the session identifier key. + * @param DoctrineSessionInformation a SessionInformation object. + * @return void + */ + public function setSessionInformation(SessionInformation $sessionInformation) + { + $this->em->persist($sessionInformation); + $this->em->flush(); + } + + /** + * Deletes the maintained information of one session. + * + * @param string $sessionId the session identifier key. + * @return void + */ + public function removeSessionInformation($sessionId) + { + if (null !== $sessionInformation = $this->getSessionInformation($sessionId)) { + $this->em->remove($sessionInformation); + $this->em->flush(); + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index f04813c568841..9e684bd710da8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -26,7 +26,8 @@ Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy Symfony\Component\Security\Http\Session\ConcurrentSessionControlStrategy Symfony\Component\Security\Http\Session\SessionRegistry - Symfony\Component\Security\Http\Session\SessionInformation + Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionRegistryStorage + Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation Symfony\Component\Security\Http\Session\SessionInformationIterator Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -84,6 +85,10 @@ %security.authentication.session_information_iterator.class% + + + + diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php deleted file mode 100644 index ac616d9e10d95..0000000000000 --- a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php +++ /dev/null @@ -1,145 +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\Dbal; - -use Doctrine\DBAL\Schema\Schema as BaseSchema; - -/** - * The schema used for the concurrent session system. - * - * @author Stefan Paschke - */ -final class Schema extends BaseSchema -{ - protected $options; - - /** - * Constructor - * - * @param array $options the names for tables - * @return void - */ - public function __construct(array $options) - { - parent::__construct(); - - $this->options = $options; - - $this->addClassTable(); - $this->addSecurityIdentitiesTable(); - $this->addObjectIdentitiesTable(); - $this->addObjectIdentityAncestorsTable(); - $this->addEntryTable(); - } - - /** - * Adds the class table to the schema - * - * @return void - */ - protected function addClassTable() - { - $table = $this->createTable($this->options['class_table_name']); - $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); - $table->addColumn('class_type', 'string', array('length' => 200)); - $table->setPrimaryKey(array('id')); - $table->addUniqueIndex(array('class_type')); - } - - /** - * Adds the entry table to the schema - * - * @return void - */ - protected function addEntryTable() - { - $table = $this->createTable($this->options['entry_table_name']); - - $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); - $table->addColumn('class_id', 'integer', array('unsigned' => true)); - $table->addColumn('object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); - $table->addColumn('field_name', 'string', array('length' => 50, 'notnull' => false)); - $table->addColumn('ace_order', 'smallint', array('unsigned' => true)); - $table->addColumn('security_identity_id', 'integer', array('unsigned' => true)); - $table->addColumn('mask', 'integer'); - $table->addColumn('granting', 'boolean'); - $table->addColumn('granting_strategy', 'string', array('length' => 30)); - $table->addColumn('audit_success', 'boolean'); - $table->addColumn('audit_failure', 'boolean'); - - $table->setPrimaryKey(array('id')); - $table->addUniqueIndex(array('class_id', 'object_identity_id', 'field_name', 'ace_order')); - $table->addIndex(array('class_id', 'object_identity_id', 'security_identity_id')); - - $table->addForeignKeyConstraint($this->getTable($this->options['class_table_name']), array('class_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - $table->addForeignKeyConstraint($this->getTable($this->options['oid_table_name']), array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - $table->addForeignKeyConstraint($this->getTable($this->options['sid_table_name']), array('security_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - } - - /** - * Adds the object identity table to the schema - * - * @return void - */ - protected function addObjectIdentitiesTable() - { - $table = $this->createTable($this->options['oid_table_name']); - - $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); - $table->addColumn('class_id', 'integer', array('unsigned' => true)); - $table->addColumn('object_identifier', 'string', array('length' => 100)); - $table->addColumn('parent_object_identity_id', 'integer', array('unsigned' => true, 'notnull' => false)); - $table->addColumn('entries_inheriting', 'boolean'); - - $table->setPrimaryKey(array('id')); - $table->addUniqueIndex(array('object_identifier', 'class_id')); - $table->addIndex(array('parent_object_identity_id')); - - $table->addForeignKeyConstraint($table, array('parent_object_identity_id'), array('id'), array('onDelete' => 'RESTRICT', 'onUpdate' => 'RESTRICT')); - } - - /** - * Adds the object identity relation table to the schema - * - * @return void - */ - protected function addObjectIdentityAncestorsTable() - { - $table = $this->createTable($this->options['oid_ancestors_table_name']); - - $table->addColumn('object_identity_id', 'integer', array('unsigned' => true)); - $table->addColumn('ancestor_id', 'integer', array('unsigned' => true)); - - $table->setPrimaryKey(array('object_identity_id', 'ancestor_id')); - - $oidTable = $this->getTable($this->options['oid_table_name']); - $table->addForeignKeyConstraint($oidTable, array('object_identity_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - $table->addForeignKeyConstraint($oidTable, array('ancestor_id'), array('id'), array('onDelete' => 'CASCADE', 'onUpdate' => 'CASCADE')); - } - - /** - * Adds the security identity table to the schema - * - * @return void - */ - protected function addSecurityIdentitiesTable() - { - $table = $this->createTable($this->options['sid_table_name']); - - $table->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => 'auto')); - $table->addColumn('identifier', 'string', array('length' => 200)); - $table->addColumn('username', 'boolean'); - - $table->setPrimaryKey(array('id')); - $table->addUniqueIndex(array('identifier', 'username')); - } -} \ No newline at end of file diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 961055a742662..33ca7375e3969 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -29,6 +29,7 @@ public function __construct($sessionId, $username) { $this->sessionId = $sessionId; $this->username = $username; + $this->refreshLastRequest(); } /** @@ -38,7 +39,7 @@ public function __construct($sessionId, $username) */ public function expireNow() { - $this->expired = time(); + $this->expired = new \DateTime(); } /** @@ -78,7 +79,7 @@ public function getSessionId() */ public function isExpired() { - return $this->expired && $this->expired < time(); + return $this->expired && $this->expired->getTimestamp() < time(); } /** @@ -88,6 +89,6 @@ public function isExpired() */ public function refreshLastRequest() { - $this->lastRequest = time(); + $this->lastRequest = new \DateTime(); } } diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php index 9e019489a2a9c..db6a3beb54799 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php @@ -138,7 +138,7 @@ function sort() { $sessionsSorted = array(); foreach ($this->sessions as $session) { - $sessionsSorted[$session->getLastRequest()] = $session; + $sessionsSorted[$session->getLastRequest()->getTimestamp()] = $session; } krsort($sessionsSorted); From 2efbf11e078848504edabbdc0d39e0bd7d152953 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 2 Jun 2011 22:05:37 +0200 Subject: [PATCH 20/30] removed SessionInformationIterator class --- .../DoctrineSessionRegistryStorage.php | 21 +-- .../Resources/config/security.xml | 2 - .../ConcurrentSessionControlStrategy.php | 11 +- .../Session/SessionInformationIterator.php | 157 ------------------ .../Security/Http/Session/SessionRegistry.php | 6 +- .../SessionRegistryStorageInterface.php | 5 +- 6 files changed, 18 insertions(+), 184 deletions(-) delete mode 100644 src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php index 447f38783fe8d..4439f4358d7c2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php +++ b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php @@ -5,7 +5,6 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\StaticPHPDriver; use Symfony\Component\Security\Http\Session\SessionInformation; -use Symfony\Component\Security\Http\Session\SessionInformationIterator; use Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface; class DoctrineSessionRegistryStorage implements SessionRegistryStorageInterface @@ -36,24 +35,22 @@ public function getUsers() */ public function getSessionInformation($sessionId) { - return $this->em->find('Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation', $sessionId); + return $this->em->find('Security:DoctrineSessionInformation', $sessionId); } /** * Obtains the maintained information for one user. * - * @param string $sessionId the session identifier key. - * @return SessionInformationIterator a SessionInformationIterator object. + * @param string $username The user identifier. + * @param boolean $includeExpiredSessions. + * @return array An array of SessionInformation objects. */ - public function getSessionInformations($username, $includeExpiredSessions) + public function getSessionInformations($username, $includeExpiredSessions = false) { - $sessions = new SessionInformationIterator(); - - foreach ($this->em->getRepository('Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation')->findBy(array('username' => $username)) as $sessionInformation) { - $sessions->add($sessionInformation); - } - - return $sessions; + $query = $this->em->createQuery( + 'SELECT si FROM Security:DoctrineSessionInformation si'.($includeExpiredSessions ? '' : ' WHERE si.expired IS NULL').' ORDER BY si.lastRequest DESC' + ); + return $query->getResult(); } /** diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 9e684bd710da8..74c47456cff00 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -28,7 +28,6 @@ Symfony\Component\Security\Http\Session\SessionRegistry Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionRegistryStorage Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation - Symfony\Component\Security\Http\Session\SessionInformationIterator Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -82,7 +81,6 @@ %security.authentication.session_information.class% - %security.authentication.session_information_iterator.class% diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php index b1f3b51b87a33..9087e289c2b96 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php @@ -60,7 +60,7 @@ public function onAuthentication(Request $request, TokenInterface $token) $sessions = $this->registry->getAllSessions($user); $maxSessions = $this->getMaximumSessionsForThisUser($user); - if ($sessions->count() >= $maxSessions && $this->alwaysCreateSession !== true) { + if (count($sessions) >= $maxSessions && $this->alwaysCreateSession !== true) { if ($this->exceptionIfMaximumExceeded) { throw new \RuntimeException(sprintf('Maximum of sessions (%s) exceeded', $maxSessions)); } @@ -107,18 +107,15 @@ public function setMaximumSessions($maximumSessions) /** * Allows subclasses to customise behaviour when too many sessions are detected. * - * @param SessionInformationIterator $sessions + * @param array $sessions * @param integer $allowableSessions * @param SessionRegistry $registry * @return void */ - protected function allowableSessionsExceeded(SessionInformationIterator $sessions, $allowableSessions, SessionRegistry $registry) + protected function allowableSessionsExceeded($sessions, $allowableSessions, SessionRegistry $registry) { // remove oldest sessions from registry - $count = 0; - $sessions->sort(); - - for ($i = $allowableSessions - 1; $i < $sessions->count(); $i++) { + for ($i = $allowableSessions - 1; $i < count($sessions); $i++) { $sessions[$i]->expireNow(); $registry->setSessionInformation($sessions[$i]); } diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php b/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php deleted file mode 100644 index db6a3beb54799..0000000000000 --- a/src/Symfony/Component/Security/Http/Session/SessionInformationIterator.php +++ /dev/null @@ -1,157 +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; - -/** - * SessionInformationIterator. - * - * @author Stefan Paschke - */ -class SessionInformationIterator implements \Iterator, \ArrayAccess -{ - protected $sessions = array(); - protected $position = 0; - - /** - * Iterator interface. - * - * @return void - */ - public function rewind() - { - $this->position = 0; - } - - /** - * Iterator interface. - * - * @return SessionInformation - */ - public function current() - { - return $this->sessions[$this->position]; - } - - /** - * Iterator interface. - * - * @return integer - */ - public function key() - { - return $this->position; - } - - /** - * Iterator interface. - * - * @return void - */ - public function next() - { - $this->position++; - } - - /** - * Iterator interface. - * - * @return boolean - */ - public function valid() - { - return isset($this->sessions[$this->position]); - } - - /** - * ArrayAccess interface. - * - * @return boolean - */ - public function offsetExists($offset) - { - return isset($this->sessions[$offset]); - } - - /** - * ArrayAccess interface. - * - * @return SessionInformation - */ - public function offsetGet($offset) - { - return $this->sessions[$offset]; - } - - /** - * ArrayAccess interface. - * - * @param integer $offset - * @param SessionInformation $sessionInformation - * @return void - */ - public function offsetSet($offset, $sessionInformation) - { - if ($sessionInformation instanceof SessionInformation) { - $this->sessions[$offset] = $sessionInformation; - } - } - - /** - * ArrayAccess interface. - * - * @param integer $offset - * @return void - */ - public function offsetUnset($offset) - { - if ($this->offsetExists($offset)) { - unset($this->sessions[$offset]); - } - } - - /** - * Adds a SessionInformation object to the iterator. - * - * @param SessionInformation $sessionInformation - * @return void - */ - public function add(SessionInformation $sessionInformation) - { - $this->sessions[] = $sessionInformation; - } - - /** - * Sorts the session informations by the last request date. - * - * @return void - */ - function sort() - { - $sessionsSorted = array(); - foreach ($this->sessions as $session) { - $sessionsSorted[$session->getLastRequest()->getTimestamp()] = $session; - } - krsort($sessionsSorted); - - $this->sessions = array_values($sessionsSorted); - } - - /** - * Returns the number of session information objects. - * - * @return integer - */ - function count() - { - return count($this->sessions); - } -} diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index 10f9fbaa9d8c1..c1c25afda65d1 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -24,13 +24,11 @@ class SessionRegistry { protected $sessionRegistryStorage; protected $sessionInformationClass; - protected $sessionInformationIteratorClass; - public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage, $sessionInformationClass, $sessionInformationIteratorClass) + public function __construct(SessionRegistryStorageInterface $sessionRegistryStorage, $sessionInformationClass) { $this->sessionRegistryStorage = $sessionRegistryStorage; $this->sessionInformationClass = $sessionInformationClass; - $this->sessionInformationIteratorClass = $sessionInformationIteratorClass; } /** @@ -48,7 +46,7 @@ public function getAllUsers() * * @param UserInterface $user the specified user. * @param boolean $includeExpiredSessions. - * @return SessionInformationIterator $sessions the known sessions. + * @return array An array of SessionInformation objects. */ public function getAllSessions(UserInterface $user, $includeExpiredSessions = false) { diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php index cbcfea2b795d3..35cd277a06677 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -38,8 +38,9 @@ function getSessionInformation($sessionId); /** * Obtains the maintained information for one user. * - * @param string $sessionId the session identifier key. - * @return SessionInformation a SessionInformation object. + * @param string $username The user identifier. + * @param boolean $includeExpiredSessions. + * @return array An array of SessionInformation objects. */ function getSessionInformations($username, $includeExpiredSessions); From 3cb882c12de130f630e460accde2c902add108fe Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 2 Jun 2011 22:26:35 +0200 Subject: [PATCH 21/30] forgot query condition --- .../Doctrine/DoctrineSessionRegistryStorage.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php index 4439f4358d7c2..54f3dae4aae5e 100644 --- a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php +++ b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php @@ -48,8 +48,10 @@ public function getSessionInformation($sessionId) public function getSessionInformations($username, $includeExpiredSessions = false) { $query = $this->em->createQuery( - 'SELECT si FROM Security:DoctrineSessionInformation si'.($includeExpiredSessions ? '' : ' WHERE si.expired IS NULL').' ORDER BY si.lastRequest DESC' + 'SELECT si FROM Security:DoctrineSessionInformation si WHERE si.username = ?1'.($includeExpiredSessions ? '' : ' AND si.expired IS NULL').' ORDER BY si.lastRequest DESC' ); + $query->setParameter(1, $username); + return $query->getResult(); } From 0aa935350e4b664d4277816039fb9217dff21907 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 22 Dec 2011 12:08:14 +0100 Subject: [PATCH 22/30] added DBAL default implementation of Symfony\Component\Security\Http\Session\SessionRegistryStorageInterface --- .../Command/InitConcurrentSessionsCommand.php | 31 +++-- .../DependencyInjection/MainConfiguration.php | 20 +++ .../DependencyInjection/SecurityExtension.php | 29 +++++ .../Doctrine/DoctrineSessionInformation.php | 55 --------- .../DoctrineSessionRegistryStorage.php | 84 ------------- .../Resources/config/security.xml | 19 --- .../config/security_session_registry.xml | 25 ++++ .../config/security_session_registry_dbal.xml | 25 ++++ .../Session/Dbal/DbalSessionInformation.php | 37 ++++++ .../Security/Http/Session/Dbal/Schema.php | 52 ++++++++ .../Session/Dbal/SessionRegistryStorage.php | 115 ++++++++++++++++++ .../Http/Session/SessionInformation.php | 34 +++++- 12 files changed, 356 insertions(+), 170 deletions(-) delete mode 100644 src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php delete mode 100644 src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.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/Http/Session/Dbal/DbalSessionInformation.php create mode 100644 src/Symfony/Component/Security/Http/Session/Dbal/Schema.php create mode 100644 src/Symfony/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php diff --git a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php index 942b578f566cf..9106130e9dbde 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/InitConcurrentSessionsCommand.php @@ -11,18 +11,17 @@ namespace Symfony\Bundle\SecurityBundle\Command; +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Security\Http\Session\Dbal\Schema; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Output\Output; -use Doctrine\ORM\Tools\Console\Command\SchemaTool\CreateCommand; -use Symfony\Bundle\DoctrineBundle\Command\Proxy\DoctrineCommandHelper; /** * Installs the database schema required by the concurrent session Doctrine implementation * * @author Stefan Paschke */ -class InitConcurrentSessionsCommand extends CreateCommand +class InitConcurrentSessionsCommand extends ContainerAwareCommand { /** * @see Command @@ -33,7 +32,7 @@ protected function configure() $this ->setName('init:concurrent-session') - ->setDescription('Executes the SQL needed to generate the database schema reqired by the concurrent sessions feature.') + ->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: @@ -52,8 +51,26 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - DoctrineCommandHelper::setApplicationEntityManager($this->getApplication(), 'security'); + $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); + } - parent::execute($input, $output); + $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 f051e7f13e0cb..30eba54855172 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -79,10 +79,30 @@ 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() + ->end() + ->end() + ->end() + ; + } + private function addAclSection(ArrayNodeDefinition $rootNode) { $rootNode diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index bb9315398fe52..97df10d3ae276 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -62,6 +62,10 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('templating_twig.xml'); $loader->load('collectors.xml'); + if (isset($config['session_registry'])) { + $this->sessionRegistryLoad($config['session_registry'], $container); + } + // set some global scalars $container->setParameter('security.access.denied_url', $config['access_denied_url']); $container->setParameter('security.authentication.manager.erase_credentials', $config['erase_credentials']); @@ -158,6 +162,31 @@ 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 = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('security_session_registry.xml'); + + if (isset($config['session_registry_storage'])) { + // FIXME: define custom 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. * diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php deleted file mode 100644 index c9a999ef6b509..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionInformation.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\SecurityBundle\Doctrine; - -use Doctrine\ORM\Mapping\ClassMetadata; -use Symfony\Component\Security\Http\Session\SessionInformation; - -/** - * DoctrineSessionInformation. - * - * Allows to persist SessionInformation using Doctrine. - * - * @author Stefan Paschke - */ -class DoctrineSessionInformation extends SessionInformation -{ - public static function loadMetadata(ClassMetadata $metadata) - { - $metadata->setTableName('session_information'); - - $metadata->mapField(array( - 'id' => true, - 'fieldName' => 'sessionId', - 'columnName' => 'session_id', - 'type' => 'string' - )); - - $metadata->mapField(array( - 'fieldName' => 'username', - 'type' => 'string' - )); - - $metadata->mapField(array( - 'fieldName' => 'expired', - 'type' => 'datetime', - 'nullable' => true - )); - - $metadata->mapField(array( - 'fieldName' => 'lastRequest', - 'columnName' => 'last_request', - 'type' => 'datetime', - 'nullable' => true - )); - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php b/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php deleted file mode 100644 index 54f3dae4aae5e..0000000000000 --- a/src/Symfony/Bundle/SecurityBundle/Doctrine/DoctrineSessionRegistryStorage.php +++ /dev/null @@ -1,84 +0,0 @@ -em = $em; - - $driver = new StaticPHPDriver(__DIR__); - $this->em->getConfiguration()->setMetadataDriverImpl($driver); - } - - /** - * 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 DoctrineSessionInformation a SessionInformation object. - */ - public function getSessionInformation($sessionId) - { - return $this->em->find('Security:DoctrineSessionInformation', $sessionId); - } - - /** - * 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) - { - $query = $this->em->createQuery( - 'SELECT si FROM Security:DoctrineSessionInformation si WHERE si.username = ?1'.($includeExpiredSessions ? '' : ' AND si.expired IS NULL').' ORDER BY si.lastRequest DESC' - ); - $query->setParameter(1, $username); - - return $query->getResult(); - } - - /** - * Adds information for one session. - * - * @param string $sessionId the session identifier key. - * @param DoctrineSessionInformation a SessionInformation object. - * @return void - */ - public function setSessionInformation(SessionInformation $sessionInformation) - { - $this->em->persist($sessionInformation); - $this->em->flush(); - } - - /** - * Deletes the maintained information of one session. - * - * @param string $sessionId the session identifier key. - * @return void - */ - public function removeSessionInformation($sessionId) - { - if (null !== $sessionInformation = $this->getSessionInformation($sessionId)) { - $this->em->remove($sessionInformation); - $this->em->flush(); - } - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 74c47456cff00..0c723a02a9972 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -24,10 +24,6 @@ Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy - Symfony\Component\Security\Http\Session\ConcurrentSessionControlStrategy - Symfony\Component\Security\Http\Session\SessionRegistry - Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionRegistryStorage - Symfony\Bundle\SecurityBundle\Doctrine\DoctrineSessionInformation Symfony\Component\Security\Core\Authorization\AccessDecisionManager @@ -72,21 +68,6 @@ %security.authentication.session_strategy.strategy% - - - - - - - - - %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 new file mode 100644 index 0000000000000..3ca7f881fba0e --- /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_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..217b3b9cbd5c8 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry_dbal.xml @@ -0,0 +1,25 @@ + + + + + + Symfony\Component\Security\Http\Session\Dbal\SessionRegistryStorage + Symfony\Component\Security\Http\Session\Dbal\DbalSessionInformation + + + + + + + + + + %security.session_registry.dbal.session_information_table_name% + %security.authentication.session_information.class% + + + + + diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php b/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php new file mode 100644 index 0000000000000..bdbdac5495a0a --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php @@ -0,0 +1,37 @@ + + * + * 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\Dbal; + +use Symfony\Component\Security\Http\Session\SessionInformation; + +/** + * DbalSessionInformation. + * + * Allows to persist SessionInformation using Doctrine DBAL. + * + * @author Stefan Paschke + */ +class DbalSessionInformation extends SessionInformation +{ + 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); + } + } +} diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php new file mode 100644 index 0000000000000..268991ebba90c --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/Dbal/Schema.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\Session\Dbal; + +use Doctrine\DBAL\Schema\Schema as BaseSchema; + +/** + * The schema used for the ACL system. + * + * @author Stefan Paschke + */ +final class Schema extends BaseSchema +{ + protected $options; + + /** + * Constructor + * + * @param array $options the names for tables + */ + public function __construct(array $options) + { + parent::__construct(); + + $this->options = $options; + + $this->addSessionInformationTable(); + } + + /** + * Adds the session_information table to the schema + */ + protected function addSessionInformationTable() + { + $table = $this->createTable($this->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/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php b/src/Symfony/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php new file mode 100644 index 0000000000000..85a5201218050 --- /dev/null +++ b/src/Symfony/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php @@ -0,0 +1,115 @@ +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. + * @return void + */ + 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. + * @return void + */ + 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/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 33ca7375e3969..5833dc679f035 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -27,9 +27,8 @@ class SessionInformation public function __construct($sessionId, $username) { - $this->sessionId = $sessionId; - $this->username = $username; - $this->refreshLastRequest(); + $this->setSessionId($sessionId); + $this->setUsername($username); } /** @@ -39,7 +38,7 @@ public function __construct($sessionId, $username) */ public function expireNow() { - $this->expired = new \DateTime(); + $this->setExpired(new \DateTime()); } /** @@ -79,7 +78,7 @@ public function getSessionId() */ public function isExpired() { - return $this->expired && $this->expired->getTimestamp() < time(); + return $this->getExpired() && $this->getExpired()->getTimestamp() < time(); } /** @@ -91,4 +90,29 @@ 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; + } } From ff2122be9d2f8da2e7b247990cbcf6831d937101 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 22 Dec 2011 14:06:24 +0100 Subject: [PATCH 23/30] recreate SessionInformation if they were lost from the registry --- .../Security/Http/Firewall/ConcurrentSessionListener.php | 3 +++ .../Security/Http/Session/Dbal/DbalSessionInformation.php | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php index 5d0f2ff834cb9..1481b4fa29caa 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -84,6 +84,9 @@ public function handle(GetResponseEvent $event) $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/Dbal/DbalSessionInformation.php b/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php index bdbdac5495a0a..7f0bcb74d7b8f 100644 --- a/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php @@ -34,4 +34,9 @@ public function __construct($sessionId, $username, \DateTime $lastRequest = null $this->setExpired($expired); } } + + public function getExpired() + { + return parent::getExpired(); + } } From efc783b16e170efdbcb8f9137d576964cff74c1a Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Thu, 22 Dec 2011 20:08:57 +0100 Subject: [PATCH 24/30] minor fixes --- .../SecurityBundle/DependencyInjection/MainConfiguration.php | 1 + .../SecurityBundle/DependencyInjection/SecurityExtension.php | 2 +- .../Security/Http/Firewall/ConcurrentSessionListener.php | 1 - .../Component/Security/Http/Session/SessionInformation.php | 2 +- .../Component/Security/Http/Session/SessionRegistry.php | 4 ++-- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 30eba54855172..a6dfb28e467c4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -97,6 +97,7 @@ private function addSessionRegistrySection(ArrayNodeDefinition $rootNode) ->scalarNode('session_information')->defaultValue('cs_session_information')->end() ->end() ->end() + ->scalarNode('session_registry_storage')->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 97df10d3ae276..b0962f0f630e0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -168,7 +168,7 @@ private function sessionRegistryLoad($config, ContainerBuilder $container) $loader->load('security_session_registry.xml'); if (isset($config['session_registry_storage'])) { - // FIXME: define custom session registry storage + $container->setAlias('security.authentication.session_registry_storage', $config['session_registry_storage']); return; } diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php index 1481b4fa29caa..a172de15a944d 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -89,6 +89,5 @@ public function handle(GetResponseEvent $event) $this->sessionRegistry->registerNewSession($session->getId(), $token->getUser()); } } - } } diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 5833dc679f035..4a686770a06df 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -44,7 +44,7 @@ public function expireNow() /** * Obtain the last request date. * - * @return integer UNIX Timestamp of the last request date and time. + * @return DateTime the last request date and time. */ public function getLastRequest() { diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index c1c25afda65d1..281c88e03c99f 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -85,7 +85,7 @@ public function refreshLastRequest($sessionId) { if ($sessionInformation = $this->getSessionInformation($sessionId)) { $sessionInformation->refreshLastRequest(); - $this->sessionRegistryStorage->setSessionInformation($sessionInformation); + $this->setSessionInformation($sessionInformation); } } @@ -101,7 +101,7 @@ public function registerNewSession($sessionId, UserInterface $user) $sessionInformation = new $this->sessionInformationClass($sessionId, $user->getUsername()); $sessionInformation->refreshLastRequest(); - $this->sessionRegistryStorage->setSessionInformation($sessionInformation); + $this->setSessionInformation($sessionInformation); } /** From bed28d200accc152de296cd238f12ae6d9e2cf57 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Fri, 23 Dec 2011 12:11:47 +0100 Subject: [PATCH 25/30] moved DBAL implemention of SessionInformation to /Bridge --- .../Doctrine/Security/SessionRegistry}/Schema.php | 2 +- .../Security/SessionRegistry/SessionInformation.php} | 8 ++++---- .../Security/SessionRegistry}/SessionRegistryStorage.php | 2 +- .../Command/InitConcurrentSessionsCommand.php | 2 +- .../Resources/config/security_session_registry_dbal.xml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) rename src/Symfony/{Component/Security/Http/Session/Dbal => Bridge/Doctrine/Security/SessionRegistry}/Schema.php (95%) rename src/Symfony/{Component/Security/Http/Session/Dbal/DbalSessionInformation.php => Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php} (78%) rename src/Symfony/{Component/Security/Http/Session/Dbal => Bridge/Doctrine/Security/SessionRegistry}/SessionRegistryStorage.php (98%) diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php similarity index 95% rename from src/Symfony/Component/Security/Http/Session/Dbal/Schema.php rename to src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php index 268991ebba90c..9fde44e5cf29a 100644 --- a/src/Symfony/Component/Security/Http/Session/Dbal/Schema.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Session\Dbal; +namespace Symfony\Bridge\Doctrine\Security\SessionRegistry; use Doctrine\DBAL\Schema\Schema as BaseSchema; diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php similarity index 78% rename from src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php rename to src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php index 7f0bcb74d7b8f..f9eb8eb0ed957 100644 --- a/src/Symfony/Component/Security/Http/Session/Dbal/DbalSessionInformation.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionInformation.php @@ -9,18 +9,18 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Security\Http\Session\Dbal; +namespace Symfony\Bridge\Doctrine\Security\SessionRegistry; -use Symfony\Component\Security\Http\Session\SessionInformation; +use Symfony\Component\Security\Http\Session\SessionInformation as BaseSessionInformation; /** - * DbalSessionInformation. + * SessionInformation. * * Allows to persist SessionInformation using Doctrine DBAL. * * @author Stefan Paschke */ -class DbalSessionInformation extends SessionInformation +class SessionInformation extends BaseSessionInformation { public function __construct($sessionId, $username, \DateTime $lastRequest = null, \DateTime $expired = null) { diff --git a/src/Symfony/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php similarity index 98% rename from src/Symfony/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php rename to src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index 85a5201218050..037e218680b5e 100644 --- a/src/Symfony/Component/Security/Http/Session/Dbal/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -1,6 +1,6 @@ - Symfony\Component\Security\Http\Session\Dbal\SessionRegistryStorage - Symfony\Component\Security\Http\Session\Dbal\DbalSessionInformation + Symfony\Bridge\Doctrine\Security\SessionRegistry\SessionRegistryStorage + Symfony\Bridge\Doctrine\Security\SessionRegistry\SessionInformation From 7a0f46aa7177f7941283eb1e2ba8bddc53f5700a Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Wed, 4 Jan 2012 14:43:43 +0100 Subject: [PATCH 26/30] use HttpUtils in ConcurrentSessionListener --- .../DependencyInjection/SecurityExtension.php | 2 +- .../Resources/config/security_listeners.xml | 1 + .../Security/Http/Firewall/ConcurrentSessionListener.php | 9 ++++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index b0962f0f630e0..f4f4e20322437 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -596,7 +596,7 @@ private function createConcurrentSessionListener($container, $id, $config) { $concurrentSessionListenerId = 'security.authentication.concurrentsession_listener.'.$id; $listener = $container->setDefinition($concurrentSessionListenerId, new DefinitionDecorator('security.authentication.concurrentsession_listener')); - $listener->replaceArgument(2, $config['expiration_url']); + $listener->replaceArgument(3, $config['expiration_url']); $listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session'))); return $concurrentSessionListenerId; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 304cf623515a1..c563f8e7c8e02 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -180,6 +180,7 @@ + diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php index a172de15a944d..d7ffdaf820479 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -13,24 +13,27 @@ 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\HttpFoundation\RedirectResponse; 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, SessionRegistry $sessionRegistry, $targetUrl = '/', LogoutSuccessHandlerInterface $successHandler = null, LoggerInterface $logger = null) + 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; @@ -70,7 +73,7 @@ public function handle(GetResponseEvent $event) throw new \RuntimeException('Logout Success Handler did not return a Response.'); } } else { - $response = new RedirectResponse(0 !== strpos($this->targetUrl, 'http') ? $request->getUriForPath($this->targetUrl) : $this->targetUrl, 302); + $response = $this->httpUtils->createRedirectResponse($request, $this->targetUrl); } foreach ($this->handlers as $handler) { From ac7a1d62e4707c648c4d4a22c46de759e9194369 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Sun, 8 Apr 2012 15:56:49 +0200 Subject: [PATCH 27/30] removed @return void comments --- .../Security/SessionRegistry/SessionRegistryStorage.php | 2 -- .../Security/Http/Firewall/ConcurrentSessionListener.php | 1 - .../Http/Session/ConcurrentSessionControlStrategy.php | 6 ------ .../Component/Security/Http/Session/SessionInformation.php | 2 -- .../Component/Security/Http/Session/SessionRegistry.php | 4 ---- .../Http/Session/SessionRegistryStorageInterface.php | 2 -- 6 files changed, 17 deletions(-) diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php index 037e218680b5e..742713a1789fc 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/SessionRegistryStorage.php @@ -75,7 +75,6 @@ public function getSessionInformations($username, $includeExpiredSessions = fals * * @param string $sessionId the session identifier key. * @param SessionInformation a SessionInformation object. - * @return void */ public function setSessionInformation(SessionInformation $sessionInformation) { @@ -96,7 +95,6 @@ public function setSessionInformation(SessionInformation $sessionInformation) * Deletes the maintained information of one session. * * @param string $sessionId the session identifier key. - * @return void */ public function removeSessionInformation($sessionId) { diff --git a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php index d7ffdaf820479..539919524a6f5 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -45,7 +45,6 @@ public function __construct(SecurityContextInterface $securityContext, HttpUtils * Adds a logout handler * * @param LogoutHandlerInterface $handler - * @return void */ public function addHandler(LogoutHandlerInterface $handler) { diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php index 9087e289c2b96..e2f9e61e9f44d 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php @@ -44,7 +44,6 @@ public function __construct(SessionRegistry $registry, $maximumSessions, $sessio * * @param Request $request * @param TokenInterface $token - * @return void */ public function onAuthentication(Request $request, TokenInterface $token) { @@ -75,7 +74,6 @@ public function onAuthentication(Request $request, TokenInterface $token) * Sets a boolean flag that allows to bypass allowableSessionsExceeded(). * * param boolean $alwaysCreateSession - * @return void */ public function setAlwaysCreateSession($alwaysCreateSession) { @@ -86,7 +84,6 @@ public function setAlwaysCreateSession($alwaysCreateSession) * Sets a boolean flag that causes a RuntimeException to be thrown if the number of sessions is exceeded. * * @param boolean $exceptionIfMaximumExceeded - * @return void */ public function setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded) { @@ -97,7 +94,6 @@ public function setExceptionIfMaximumExceeded($exceptionIfMaximumExceeded) * Sets the maxSessions property. * * @param $maximumSessions - * @return void */ public function setMaximumSessions($maximumSessions) { @@ -110,7 +106,6 @@ public function setMaximumSessions($maximumSessions) * @param array $sessions * @param integer $allowableSessions * @param SessionRegistry $registry - * @return void */ protected function allowableSessionsExceeded($sessions, $allowableSessions, SessionRegistry $registry) { @@ -138,7 +133,6 @@ protected function getMaximumSessionsForThisUser(UserInterface $user) * @param string $originalSessionId * @param string $newSessionId * @param TokenInterface $token - * @return void */ protected function onSessionChange($originalSessionId, $newSessionId) { diff --git a/src/Symfony/Component/Security/Http/Session/SessionInformation.php b/src/Symfony/Component/Security/Http/Session/SessionInformation.php index 4a686770a06df..81b47f3974e8d 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionInformation.php +++ b/src/Symfony/Component/Security/Http/Session/SessionInformation.php @@ -34,7 +34,6 @@ public function __construct($sessionId, $username) /** * Sets the session informations expired date to the current date and time. * - * @return void */ public function expireNow() { @@ -84,7 +83,6 @@ public function isExpired() /** * Set the last request date to the current date and time. * - * @return void */ public function refreshLastRequest() { diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index 281c88e03c99f..d73acb5b4dc57 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -68,7 +68,6 @@ public function getSessionInformation($sessionId) * Sets a SessionInformation object. * * @param SessionInformation $sessionInformation - * @return void */ public function setSessionInformation(SessionInformation $sessionInformation) { @@ -79,7 +78,6 @@ public function setSessionInformation(SessionInformation $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. - * @return void */ public function refreshLastRequest($sessionId) { @@ -94,7 +92,6 @@ public function refreshLastRequest($sessionId) * * @param string $sessionId the session identifier key. * @param UserInterface $user the specified user. - * @return void */ public function registerNewSession($sessionId, UserInterface $user) { @@ -109,7 +106,6 @@ public function registerNewSession($sessionId, UserInterface $user) * * @param string $sessionId the session identifier key. * @param UserInterface $user the specified user. - * @return void */ 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 35cd277a06677..9a2159ec183b8 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -49,7 +49,6 @@ function getSessionInformations($username, $includeExpiredSessions); * * @param string $sessionId the session identifier key. * @param SessionInformation a SessionInformation object. - * @return void */ function setSessionInformation(SessionInformation $sessionInformation); @@ -57,7 +56,6 @@ function setSessionInformation(SessionInformation $sessionInformation); * Deletes the maintained information of one session. * * @param string $sessionId the session identifier key. - * @return void */ function removeSessionInformation($sessionId); } From f7dfb3fbb439572e08ebf497fc387962fc104506 Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Mon, 9 Apr 2012 11:51:56 +0200 Subject: [PATCH 28/30] reacting to comments on PR 786 --- .../Security/SessionRegistry/Schema.php | 10 +++----- .../MaxSessionsExceededException.php | 23 +++++++++++++++++++ .../Firewall/ConcurrentSessionListener.php | 4 +++- .../ConcurrentSessionControlStrategy.php | 7 ++---- .../Security/Http/Session/SessionRegistry.php | 14 +++-------- .../SessionRegistryStorageInterface.php | 9 ++++---- 6 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 src/Symfony/Component/Security/Core/Exception/MaxSessionsExceededException.php diff --git a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php index 9fde44e5cf29a..355e662b29fba 100644 --- a/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php +++ b/src/Symfony/Bridge/Doctrine/Security/SessionRegistry/Schema.php @@ -20,8 +20,6 @@ */ final class Schema extends BaseSchema { - protected $options; - /** * Constructor * @@ -31,17 +29,15 @@ public function __construct(array $options) { parent::__construct(); - $this->options = $options; - - $this->addSessionInformationTable(); + $this->addSessionInformationTable($options); } /** * Adds the session_information table to the schema */ - protected function addSessionInformationTable() + protected function addSessionInformationTable(array $options) { - $table = $this->createTable($this->options['session_information_table_name']); + $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)); 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 index 539919524a6f5..95dbbf795eb15 100644 --- a/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php +++ b/src/Symfony/Component/Security/Http/Firewall/ConcurrentSessionListener.php @@ -62,7 +62,9 @@ public function handle(GetResponseEvent $event) $session = $request->hasSession() ? $request->getSession() : null; - if (null !== $session && null !== $token = $this->securityContext->getToken()) { + if (null === $session || null === $token = $this->securityContext->getToken()) { + return; + } else { if ($sessionInformation = $this->sessionRegistry->getSessionInformation($session->getId())) { if ($sessionInformation->isExpired()) { if (null !== $this->successHandler) { diff --git a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php index e2f9e61e9f44d..36909330248f3 100644 --- a/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php +++ b/src/Symfony/Component/Security/Http/Session/ConcurrentSessionControlStrategy.php @@ -40,10 +40,7 @@ public function __construct(SessionRegistry $registry, $maximumSessions, $sessio } /** - * Called when a user is newly authenticated. - * - * @param Request $request - * @param TokenInterface $token + * {@inheritDoc} */ public function onAuthentication(Request $request, TokenInterface $token) { @@ -61,7 +58,7 @@ public function onAuthentication(Request $request, TokenInterface $token) if (count($sessions) >= $maxSessions && $this->alwaysCreateSession !== true) { if ($this->exceptionIfMaximumExceeded) { - throw new \RuntimeException(sprintf('Maximum of sessions (%s) exceeded', $maxSessions)); + throw new MaxSessionsExceededException(sprintf('Maximum of sessions (%s) exceeded', $maxSessions)); } $this->allowableSessionsExceeded($sessions, $maxSessions, $this->registry); diff --git a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php index d73acb5b4dc57..ab34177c31a93 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistry.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistry.php @@ -54,10 +54,7 @@ public function getAllSessions(UserInterface $user, $includeExpiredSessions = fa } /** - * Obtains the session information for the specified sessionId. - * - * @param string $sessionId the session identifier key. - * @return SessionInformation $sessionInformation + * {@inheritDoc} */ public function getSessionInformation($sessionId) { @@ -65,9 +62,7 @@ public function getSessionInformation($sessionId) } /** - * Sets a SessionInformation object. - * - * @param SessionInformation $sessionInformation + * {@inheritDoc} */ public function setSessionInformation(SessionInformation $sessionInformation) { @@ -102,10 +97,7 @@ public function registerNewSession($sessionId, UserInterface $user) } /** - * Deletes all the session information being maintained for the specified sessionId. - * - * @param string $sessionId the session identifier key. - * @param UserInterface $user the specified user. + * {@inheritDoc} */ 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 9a2159ec183b8..6c43c7d864a9c 100644 --- a/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php +++ b/src/Symfony/Component/Security/Http/Session/SessionRegistryStorageInterface.php @@ -28,10 +28,10 @@ interface SessionRegistryStorageInterface function getUsers(); /** - * Obtains the maintained information for one session. + * Obtains the session information for the specified sessionId. * * @param string $sessionId the session identifier key. - * @return SessionInformation a SessionInformation object. + * @return SessionInformation $sessionInformation */ function getSessionInformation($sessionId); @@ -45,10 +45,9 @@ function getSessionInformation($sessionId); function getSessionInformations($username, $includeExpiredSessions); /** - * Adds information for one session. + * Sets a SessionInformation object. * - * @param string $sessionId the session identifier key. - * @param SessionInformation a SessionInformation object. + * @param SessionInformation $sessionInformation */ function setSessionInformation(SessionInformation $sessionInformation); From 3cef9bb322a105653bbd3b7c760e3bb379fe48ce Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Mon, 9 Apr 2012 13:00:24 +0200 Subject: [PATCH 29/30] reuse the instance level loader n src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension::sessionRegistryLoad --- .../SecurityBundle/DependencyInjection/SecurityExtension.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 560ad563d3cdc..881e99fd10ad9 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -63,7 +63,7 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('collectors.xml'); if (isset($config['session_registry'])) { - $this->sessionRegistryLoad($config['session_registry'], $container); + $this->sessionRegistryLoad($config['session_registry'], $container, $loader); } // set some global scalars @@ -172,9 +172,8 @@ 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) + private function sessionRegistryLoad($config, ContainerBuilder $container, $loader) { - $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('security_session_registry.xml'); if (isset($config['session_registry_storage'])) { From d5679801e7e29f1df8c1edbb2ddba5fedc91d46d Mon Sep 17 00:00:00 2001 From: Stefan Paschke Date: Mon, 9 Apr 2012 19:38:17 +0200 Subject: [PATCH 30/30] passing sessionStrategy into authenticationListenerFactories individually --- .../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 | 25 ++++++++----------- .../config/security_session_registry.xml | 4 +-- 9 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index b0cfadd5468f0..4cdf5a2db4026 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -38,13 +38,13 @@ abstract class AbstractFactory implements SecurityFactoryInterface 'failure_forward' => false, ); - 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)) { @@ -144,10 +144,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, array_intersect_key($config, $this->options)); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index ce06bba7d281a..ba18399ccb515 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); if (isset($config['csrf_provider'])) { $container 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 08c4a33d283ef..c5dd83b683b4b 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 5ef4c00fff0b3..8a3bd7c78b3af 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 { - function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint); + function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint, $sessionStrategy); 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 d24942b6722ac..cefe700d8ca51 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) { $provider = '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 881e99fd10ad9..677b48efae34b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -79,19 +79,6 @@ public function load(array $configs, ContainerBuilder $container) $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']); - if (isset($config['firewalls'])) { - foreach ($config['firewalls'] as $firewallconfig) { - if (!empty($firewallconfig['session_concurrency']['max_sessions'])) { - $container->getDefinition('security.authentication.concurrent_session_strategy') - ->replaceArgument(1, $firewallconfig['session_concurrency']['max_sessions']) - ->replaceArgument(2, $config['session_fixation_strategy']); - $container->setDefinition('security.authentication.session_strategy', $container->getDefinition('security.authentication.concurrent_session_strategy')); - - break; - } - } - } - $this->createFirewalls($config, $container); $this->createAuthorization($config, $container); $this->createRoleHierarchy($config, $container); @@ -426,6 +413,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()); @@ -433,7 +430,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; diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml index 3ca7f881fba0e..93eaa73eec193 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_session_registry.xml @@ -11,10 +11,10 @@ - + - + %security.authentication.session_strategy.strategy%