Skip to content

Commit 2ec45d4

Browse files
author
epriestley
committedJan 16, 2014
Remove session limits and sequencing
Summary: Ref T4310. Fixes T3720. This change: - Removes concurrent session limits. Instead, unused sessions are GC'd after a while. - Collapses all existing "web-1", "web-2", etc., sessions into "web" sessions. - Dramatically simplifies the code for establishing a session (like omg). Test Plan: Ran migration, checked Sessions panel and database for sanity. Used existing session. Logged out, logged in. Ran Conduit commands. Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T4310, T3720 Differential Revision: https://secure.phabricator.com/D7978
1 parent d740374 commit 2ec45d4

File tree

6 files changed

+60
-168
lines changed

6 files changed

+60
-168
lines changed
 

‎conf/default.conf.php

-9
Original file line numberDiff line numberDiff line change
@@ -543,15 +543,6 @@
543543

544544
// -- Auth ------------------------------------------------------------------ //
545545

546-
// Maximum number of simultaneous web sessions each user is permitted to have.
547-
// Setting this to "1" will prevent a user from logging in on more than one
548-
// browser at the same time.
549-
'auth.sessions.web' => 5,
550-
551-
// Maximum number of simultaneous Conduit sessions each user is permitted
552-
// to have.
553-
'auth.sessions.conduit' => 5,
554-
555546
// If true, email addresses must be verified (by clicking a link in an
556547
// email) before a user can login. By default, verification is optional
557548
// unless 'auth.email-domains' is nonempty (see below).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
// Prior to this patch, we issued sessions "web-1", "web-2", etc., up to some
4+
// limit. This collapses all the "web-X" sessions into "web" sessions.
5+
6+
$session_table = new PhabricatorAuthSession();
7+
$conn_w = $session_table->establishConnection('w');
8+
9+
foreach (new LiskMigrationIterator($session_table) as $session) {
10+
$id = $session->getID();
11+
12+
echo "Migrating session {$id}...\n";
13+
$old_type = $session->getType();
14+
$new_type = preg_replace('/-.*$/', '', $old_type);
15+
16+
if ($old_type !== $new_type) {
17+
queryfx(
18+
$conn_w,
19+
'UPDATE %T SET type = %s WHERE id = %d',
20+
$session_table->getTableName(),
21+
$new_type,
22+
$id);
23+
}
24+
}
25+
26+
echo "Done.\n";

‎src/applications/auth/engine/PhabricatorAuthSessionEngine.php

+24-131
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ public function loadUserForSession($session_type, $session_key) {
1414
$conn_r,
1515
'SELECT s.sessionExpires AS _sessionExpires, s.id AS _sessionID, u.*
1616
FROM %T u JOIN %T s ON u.phid = s.userPHID
17-
AND s.type LIKE %> AND s.sessionKey = %s',
17+
AND s.type = %s AND s.sessionKey = %s',
1818
$user_table->getTableName(),
1919
$session_table->getTableName(),
20-
$session_type.'-',
20+
$session_type,
2121
PhabricatorHash::digest($session_key));
2222

2323
if (!$info) {
@@ -72,142 +72,35 @@ public function establishSession($session_type, $identity_phid) {
7272
$session_table = new PhabricatorAuthSession();
7373
$conn_w = $session_table->establishConnection('w');
7474

75-
if (strpos($session_type, '-') !== false) {
76-
throw new Exception("Session type must not contain hyphen ('-')!");
77-
}
78-
79-
// We allow multiple sessions of the same type, so when a caller requests
80-
// a new session of type "web", we give them the first available session in
81-
// "web-1", "web-2", ..., "web-N", up to some configurable limit. If none
82-
// of these sessions is available, we overwrite the oldest session and
83-
// reissue a new one in its place.
84-
85-
$session_limit = 1;
86-
switch ($session_type) {
87-
case PhabricatorAuthSession::TYPE_WEB:
88-
$session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web');
89-
break;
90-
case PhabricatorAuthSession::TYPE_CONDUIT:
91-
$session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit');
92-
break;
93-
default:
94-
throw new Exception("Unknown session type '{$session_type}'!");
95-
}
96-
97-
$session_limit = (int)$session_limit;
98-
if ($session_limit <= 0) {
99-
throw new Exception(
100-
"Session limit for '{$session_type}' must be at least 1!");
101-
}
102-
103-
// NOTE: Session establishment is sensitive to race conditions, as when
104-
// piping `arc` to `arc`:
105-
//
106-
// arc export ... | arc paste ...
107-
//
108-
// To avoid this, we overwrite an old session only if it hasn't been
109-
// re-established since we read it.
110-
11175
// Consume entropy to generate a new session key, forestalling the eventual
11276
// heat death of the universe.
11377
$session_key = Filesystem::readRandomCharacters(40);
114-
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
115-
116-
// Load all the currently active sessions.
117-
$sessions = queryfx_all(
118-
$conn_w,
119-
'SELECT type, sessionKey, sessionStart FROM %T
120-
WHERE userPHID = %s AND type LIKE %>',
121-
$session_table->getTableName(),
122-
$identity_phid,
123-
$session_type.'-');
124-
$sessions = ipull($sessions, null, 'type');
125-
$sessions = isort($sessions, 'sessionStart');
12678

127-
$existing_sessions = array_keys($sessions);
79+
// This has a side effect of validating the session type.
80+
$session_ttl = PhabricatorAuthSession::getSessionTypeTTL($session_type);
12881

129-
// UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
82+
// Logging-in users don't have CSRF stuff yet, so we have to unguard this
83+
// write.
13084
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
131-
132-
$retries = 0;
133-
while (true) {
134-
135-
// Choose which 'type' we'll actually establish, i.e. what number we're
136-
// going to append to the basic session type. To do this, just check all
137-
// the numbers sequentially until we find an available session.
138-
$establish_type = null;
139-
for ($ii = 1; $ii <= $session_limit; $ii++) {
140-
$try_type = $session_type.'-'.$ii;
141-
if (!in_array($try_type, $existing_sessions)) {
142-
$establish_type = $try_type;
143-
$expect_key = PhabricatorHash::digest($session_key);
144-
$existing_sessions[] = $try_type;
145-
146-
// Ensure the row exists so we can issue an update below. We don't
147-
// care if we race here or not.
148-
queryfx(
149-
$conn_w,
150-
'INSERT IGNORE INTO %T
151-
(userPHID, type, sessionKey, sessionStart, sessionExpires)
152-
VALUES (%s, %s, %s, 0, UNIX_TIMESTAMP() + %d)',
153-
$session_table->getTableName(),
154-
$identity_phid,
155-
$establish_type,
156-
PhabricatorHash::digest($session_key),
157-
$session_ttl);
158-
break;
159-
}
160-
}
161-
162-
// If we didn't find an available session, choose the oldest session and
163-
// overwrite it.
164-
if (!$establish_type) {
165-
$oldest = reset($sessions);
166-
$establish_type = $oldest['type'];
167-
$expect_key = $oldest['sessionKey'];
168-
}
169-
170-
// This is so that we'll only overwrite the session if it hasn't been
171-
// refreshed since we read it. If it has, the session key will be
172-
// different and we know we're racing other processes. Whichever one
173-
// won gets the session, we go back and try again.
174-
175-
queryfx(
176-
$conn_w,
177-
'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP(),
178-
sessionExpires = UNIX_TIMESTAMP() + %d
179-
WHERE userPHID = %s AND type = %s AND sessionKey = %s',
180-
$session_table->getTableName(),
181-
PhabricatorHash::digest($session_key),
182-
$session_ttl,
85+
id(new PhabricatorAuthSession())
86+
->setUserPHID($identity_phid)
87+
->setType($session_type)
88+
->setSessionKey(PhabricatorHash::digest($session_key))
89+
->setSessionStart(time())
90+
->setSessionExpires(time() + $session_ttl)
91+
->save();
92+
93+
$log = PhabricatorUserLog::initializeNewLog(
94+
null,
18395
$identity_phid,
184-
$establish_type,
185-
$expect_key);
186-
187-
if ($conn_w->getAffectedRows()) {
188-
// The update worked, so the session is valid.
189-
break;
190-
} else {
191-
// We know this just got grabbed, so don't try it again.
192-
unset($sessions[$establish_type]);
193-
}
194-
195-
if (++$retries > $session_limit) {
196-
throw new Exception("Failed to establish a session!");
197-
}
198-
}
199-
200-
$log = PhabricatorUserLog::initializeNewLog(
201-
null,
202-
$identity_phid,
203-
PhabricatorUserLog::ACTION_LOGIN);
204-
$log->setDetails(
205-
array(
206-
'session_type' => $session_type,
207-
'session_issued' => $establish_type,
208-
));
209-
$log->setSession($session_key);
210-
$log->save();
96+
PhabricatorUserLog::ACTION_LOGIN);
97+
$log->setDetails(
98+
array(
99+
'session_type' => $session_type,
100+
));
101+
$log->setSession($session_key);
102+
$log->save();
103+
unset($unguarded);
211104

212105
return $session_key;
213106
}

‎src/applications/auth/query/PhabricatorAuthSessionQuery.php

+4-12
Original file line numberDiff line numberDiff line change
@@ -81,25 +81,17 @@ protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
8181
}
8282

8383
if ($this->sessionTypes) {
84-
$clauses = array();
85-
foreach ($this->sessionTypes as $session_type) {
86-
$clauses[] = qsprintf(
87-
$conn_r,
88-
'type LIKE %>',
89-
$session_type.'-');
90-
}
91-
$where[] = '('.implode(') OR (', $clauses).')';
84+
$where[] = qsprintf(
85+
$conn_r,
86+
'type IN (%Ls)',
87+
$this->sessionTypes);
9288
}
9389

9490
$where[] = $this->buildPagingClause($conn_r);
9591

9692
return $this->formatWhereClause($where);
9793
}
9894

99-
public function getPagingColumn() {
100-
return 'sessionKey';
101-
}
102-
10395
public function getQueryApplicationClass() {
10496
return 'PhabricatorApplicationAuth';
10597
}

‎src/applications/config/check/PhabricatorSetupCheckExtraConfig.php

+6
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ public static function getAncientConfig() {
146146
'PhabricatorRemarkupCustomInlineRule or '.
147147
'PhabricatorRemarkupCustomBlockRule.');
148148

149+
$session_reason = pht(
150+
'Sessions now expire and are garbage collected rather than having an '.
151+
'arbitrary concurrency limit.');
152+
149153
$ancient_config += array(
150154
'phid.external-loaders' =>
151155
pht(
@@ -172,6 +176,8 @@ public static function getAncientConfig() {
172176
'multiple maps. See T4222.'),
173177
'metamta.send-immediately' => pht(
174178
'Mail is now always delivered by the daemons.'),
179+
'auth.sessions.conduit' => $session_reason,
180+
'auth.sessions.web' => $session_reason,
175181
);
176182

177183
return $ancient_config;

‎src/applications/config/option/PhabricatorAuthenticationConfigOptions.php

-16
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,6 @@ public function getDescription() {
1313

1414
public function getOptions() {
1515
return array(
16-
$this->newOption('auth.sessions.web', 'int', 5)
17-
->setSummary(
18-
pht("Number of web sessions a user can have simultaneously."))
19-
->setDescription(
20-
pht(
21-
"Maximum number of simultaneous web sessions each user is ".
22-
"permitted to have. Setting this to '1' will prevent a user from ".
23-
"logging in on more than one browser at the same time.")),
24-
$this->newOption('auth.sessions.conduit', 'int', 5)
25-
->setSummary(
26-
pht(
27-
"Number of simultaneous Conduit sessions each user is permitted."))
28-
->setDescription(
29-
pht(
30-
"Maximum number of simultaneous Conduit sessions each user is ".
31-
"permitted to have.")),
3216
$this->newOption('auth.require-email-verification', 'bool', false)
3317
->setBoolOptions(
3418
array(

0 commit comments

Comments
 (0)
Failed to load comments.