diff --git a/CHANGELOG b/CHANGELOG index ab5ebe8b01..ea8314e2c0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,11 @@ This is not a complete list of changes, just of API or workflow changes that may break existing installs. Newer changes are listed at the top. If you pull new changes and things stop working, check here first! +May 11 2011 - New session code + There's some new code which allows you to establish multiple web sessions. + When you update to it, all users will be logged out. This is expected, just + log in again and everything should work properly. + May 10 2011 - PhabricatorMailImplementationAdapter The signatures of setFrom() and addReplyTo() have changed, and they now accept a second "$name = ''" parameter. This represents a human-readable diff --git a/conf/default.conf.php b/conf/default.conf.php index 2e80ab0e21..a99149a64e 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -183,6 +183,15 @@ return array( // OAuth providers instead. 'auth.password-auth-enabled' => true, + // Maximum number of simultaneous web sessions each user is permitted to have. + // Setting this to "1" will prevent a user from logging in on more than one + // browser at the same time. + 'auth.sessions.web' => 5, + + // Maximum number of simultaneous Conduit sessions each user is permitted + // to have. + 'auth.sessions.conduit' => 3, + // -- Accounts -------------------------------------------------------------- // diff --git a/src/applications/base/controller/base/PhabricatorController.php b/src/applications/base/controller/base/PhabricatorController.php index da73919a0a..69b2a0d4f7 100644 --- a/src/applications/base/controller/base/PhabricatorController.php +++ b/src/applications/base/controller/base/PhabricatorController.php @@ -43,10 +43,10 @@ abstract class PhabricatorController extends AphrontController { $info = queryfx_one( $user->establishConnection('r'), 'SELECT u.* FROM %T u JOIN %T s ON u.phid = s.userPHID - AND s.type = %s AND s.sessionKey = %s', + AND s.type LIKE %> AND s.sessionKey = %s', $user->getTableName(), 'phabricator_session', - 'web', + 'web-', $phsid); if ($info) { $user->loadFromArray($info); diff --git a/src/applications/conduit/method/conduit/connect/ConduitAPI_conduit_connect_Method.php b/src/applications/conduit/method/conduit/connect/ConduitAPI_conduit_connect_Method.php index cfadd87066..486ade37c2 100644 --- a/src/applications/conduit/method/conduit/connect/ConduitAPI_conduit_connect_Method.php +++ b/src/applications/conduit/method/conduit/connect/ConduitAPI_conduit_connect_Method.php @@ -125,31 +125,7 @@ class ConduitAPI_conduit_connect_Method extends ConduitAPIMethod { if ($valid != $signature) { throw new ConduitException('ERR-INVALID-CERTIFICATE'); } - - $sessions = queryfx_all( - $user->establishConnection('r'), - 'SELECT * FROM %T WHERE userPHID = %s AND type LIKE %>', - PhabricatorUser::SESSION_TABLE, - $user->getPHID(), - 'conduit-'); - - $session_type = null; - - $sessions = ipull($sessions, null, 'type'); - for ($ii = 1; $ii <= 3; $ii++) { - if (empty($sessions['conduit-'.$ii])) { - $session_type = 'conduit-'.$ii; - break; - } - } - - if (!$session_type) { - $sessions = isort($sessions, 'sessionStart'); - $oldest = reset($sessions); - $session_type = $oldest['type']; - } - - $session_key = $user->establishSession($session_type); + $session_key = $user->establishSession('conduit'); } else { throw new ConduitException('ERR-NO-CERTIFICATE'); } diff --git a/src/applications/conduit/method/conduit/connect/__init__.php b/src/applications/conduit/method/conduit/connect/__init__.php index 94a6716784..9b5ee7f26c 100644 --- a/src/applications/conduit/method/conduit/connect/__init__.php +++ b/src/applications/conduit/method/conduit/connect/__init__.php @@ -11,7 +11,6 @@ phutil_require_module('phabricator', 'applications/conduit/protocol/exception'); phutil_require_module('phabricator', 'applications/conduit/storage/connectionlog'); phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'infrastructure/env'); -phutil_require_module('phabricator', 'storage/queryfx'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/people/storage/user/PhabricatorUser.php b/src/applications/people/storage/user/PhabricatorUser.php index eef526b83d..8fcb289b5c 100644 --- a/src/applications/people/storage/user/PhabricatorUser.php +++ b/src/applications/people/storage/user/PhabricatorUser.php @@ -134,12 +134,85 @@ class PhabricatorUser extends PhabricatorUserDAO { return substr(sha1($vec), 0, $len); } + /** + * Issue a new session key to this user. Phabricator supports different + * types of sessions (like "web" and "conduit") and each session type may + * have multiple concurrent sessions (this allows a user to be logged in on + * multiple browsers at the same time, for instance). + * + * Note that this method is transport-agnostic and does not set cookies or + * issue other types of tokens, it ONLY generates a new session key. + * + * You can configure the maximum number of concurrent sessions for various + * session types in the Phabricator configuration. + * + * @param string Session type, like "web". + * @return string Newly generated session key. + */ public function establishSession($session_type) { $conn_w = $this->establishConnection('w'); - $entropy = Filesystem::readRandomBytes(20); + if (strpos($session_type, '-') !== false) { + throw new Exception("Session type must not contain hyphen ('-')!"); + } + // We allow multiple sessions of the same type, so when a caller requests + // a new session of type "web", we give them the first available session in + // "web-1", "web-2", ..., "web-N", up to some configurable limit. If none + // of these sessions is available, we overwrite the oldest session and + // reissue a new one in its place. + + $session_limit = 1; + switch ($session_type) { + case 'web': + $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web'); + break; + case 'conduit': + $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit'); + break; + default: + throw new Exception("Unknown session type '{$session_type}'!"); + } + + $session_limit = (int)$session_limit; + if ($session_limit <= 0) { + throw new Exception( + "Session limit for '{$session_type}' must be at least 1!"); + } + + // Load all the currently active sessions. + $sessions = queryfx_all( + $conn_w, + 'SELECT type, sessionStart FROM %T WHERE userPHID = %s AND type LIKE %>', + PhabricatorUser::SESSION_TABLE, + $this->getPHID(), + $session_type.'-'); + + // Choose which 'type' we'll actually establish, i.e. what number we're + // going to append to the basic session type. To do this, just check all + // the numbers sequentially until we find an available session. + $establish_type = null; + $sessions = ipull($sessions, null, 'type'); + for ($ii = 1; $ii <= $session_limit; $ii++) { + if (empty($sessions[$session_type.'-'.$ii])) { + $establish_type = $session_type.'-'.$ii; + break; + } + } + + // If we didn't find an available session, choose the oldest session and + // overwrite it. + if (!$establish_type) { + $sessions = isort($sessions, 'sessionStart'); + $oldest = reset($sessions); + $establish_type = $oldest['type']; + } + + // Consume entropy to generate a new session key, forestalling the eventual + // heat death of the universe. + $entropy = Filesystem::readRandomBytes(20); $session_key = sha1($entropy); + queryfx( $conn_w, 'INSERT INTO %T '. @@ -151,11 +224,9 @@ class PhabricatorUser extends PhabricatorUserDAO { 'sessionStart = VALUES(sessionStart)', self::SESSION_TABLE, $this->getPHID(), - $session_type, + $establish_type, $session_key); - $this->sessionKey = $session_key; - return $session_key; }