@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Separate session management from PhabricatorUser

Summary: Ref T4310. Ref T3720. Session operations are currently part of PhabricatorUser. This is more tightly coupled than needbe, and makes it difficult to establish login sessions for non-users. Move all the session management code to a `SessionEngine`.

Test Plan:
- Viewed sessions.
- Regenerated Conduit certificate.
- Verified Conduit sessions were destroyed.
- Logged out.
- Logged in.
- Ran conduit commands.
- Viewed sessions again.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T4310, T3720

Differential Revision: https://secure.phabricator.com/D7962

+255 -205
+2
src/__phutil_library_map__.php
··· 1203 1203 'PhabricatorAuthProviderPersona' => 'applications/auth/provider/PhabricatorAuthProviderPersona.php', 1204 1204 'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php', 1205 1205 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', 1206 + 'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php', 1206 1207 'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php', 1207 1208 'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php', 1208 1209 'PhabricatorAuthUnlinkController' => 'applications/auth/controller/PhabricatorAuthUnlinkController.php', ··· 3768 3769 0 => 'PhabricatorAuthDAO', 3769 3770 1 => 'PhabricatorPolicyInterface', 3770 3771 ), 3772 + 'PhabricatorAuthSessionEngine' => 'Phobject', 3771 3773 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3772 3774 'PhabricatorAuthStartController' => 'PhabricatorAuthController', 3773 3775 'PhabricatorAuthUnlinkController' => 'PhabricatorAuthController',
+2 -1
src/applications/auth/controller/PhabricatorAuthController.php
··· 81 81 82 82 $should_login = $event->getValue('shouldLogin'); 83 83 if ($should_login) { 84 - $session_key = $user->establishSession($session_type); 84 + $session_key = id(new PhabricatorAuthSessionEngine()) 85 + ->establishSession($session_type, $user->getPHID()); 85 86 86 87 // NOTE: We allow disabled users to login and roadblock them later, so 87 88 // there's no check for users being disabled here.
+7 -1
src/applications/auth/controller/PhabricatorLogoutController.php
··· 34 34 // try to login again and tell them to clear any junk. 35 35 $phsid = $request->getCookie('phsid'); 36 36 if ($phsid) { 37 - $user->destroySession($phsid); 37 + $session = id(new PhabricatorAuthSessionQuery()) 38 + ->setViewer($user) 39 + ->withSessionKeys(array($phsid)) 40 + ->executeOne(); 41 + if ($session) { 42 + $session->delete(); 43 + } 38 44 } 39 45 $request->clearCookie('phsid'); 40 46
+182
src/applications/auth/engine/PhabricatorAuthSessionEngine.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthSessionEngine extends Phobject { 4 + 5 + public function loadUserForSession($session_type, $session_key) { 6 + $session_table = new PhabricatorAuthSession(); 7 + $user_table = new PhabricatorUser(); 8 + $conn_r = $session_table->establishConnection('w'); 9 + 10 + $info = queryfx_one( 11 + $conn_r, 12 + 'SELECT u.* FROM %T u JOIN %T s ON u.phid = s.userPHID 13 + AND s.type LIKE %> AND s.sessionKey = %s', 14 + $user_table->getTableName(), 15 + $session_table->getTableName(), 16 + $session_type.'-', 17 + PhabricatorHash::digest($session_key)); 18 + 19 + if (!$info) { 20 + return null; 21 + } 22 + 23 + return $user_table->loadFromArray($info); 24 + } 25 + 26 + 27 + /** 28 + * Issue a new session key for a given identity. Phabricator supports 29 + * different types of sessions (like "web" and "conduit") and each session 30 + * type may have multiple concurrent sessions (this allows a user to be 31 + * logged in on multiple browsers at the same time, for instance). 32 + * 33 + * Note that this method is transport-agnostic and does not set cookies or 34 + * issue other types of tokens, it ONLY generates a new session key. 35 + * 36 + * You can configure the maximum number of concurrent sessions for various 37 + * session types in the Phabricator configuration. 38 + * 39 + * @param string Session type, like "web". 40 + * @param phid Identity to establish a session for, usually a user PHID. 41 + * @return string Newly generated session key. 42 + */ 43 + public function establishSession($session_type, $identity_phid) { 44 + $session_table = new PhabricatorAuthSession(); 45 + $conn_w = $session_table->establishConnection('w'); 46 + 47 + if (strpos($session_type, '-') !== false) { 48 + throw new Exception("Session type must not contain hyphen ('-')!"); 49 + } 50 + 51 + // We allow multiple sessions of the same type, so when a caller requests 52 + // a new session of type "web", we give them the first available session in 53 + // "web-1", "web-2", ..., "web-N", up to some configurable limit. If none 54 + // of these sessions is available, we overwrite the oldest session and 55 + // reissue a new one in its place. 56 + 57 + $session_limit = 1; 58 + switch ($session_type) { 59 + case 'web': 60 + $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web'); 61 + break; 62 + case 'conduit': 63 + $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit'); 64 + break; 65 + default: 66 + throw new Exception("Unknown session type '{$session_type}'!"); 67 + } 68 + 69 + $session_limit = (int)$session_limit; 70 + if ($session_limit <= 0) { 71 + throw new Exception( 72 + "Session limit for '{$session_type}' must be at least 1!"); 73 + } 74 + 75 + // NOTE: Session establishment is sensitive to race conditions, as when 76 + // piping `arc` to `arc`: 77 + // 78 + // arc export ... | arc paste ... 79 + // 80 + // To avoid this, we overwrite an old session only if it hasn't been 81 + // re-established since we read it. 82 + 83 + // Consume entropy to generate a new session key, forestalling the eventual 84 + // heat death of the universe. 85 + $session_key = Filesystem::readRandomCharacters(40); 86 + 87 + // Load all the currently active sessions. 88 + $sessions = queryfx_all( 89 + $conn_w, 90 + 'SELECT type, sessionKey, sessionStart FROM %T 91 + WHERE userPHID = %s AND type LIKE %>', 92 + $session_table->getTableName(), 93 + $identity_phid, 94 + $session_type.'-'); 95 + $sessions = ipull($sessions, null, 'type'); 96 + $sessions = isort($sessions, 'sessionStart'); 97 + 98 + $existing_sessions = array_keys($sessions); 99 + 100 + // UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet. 101 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 102 + 103 + $retries = 0; 104 + while (true) { 105 + 106 + // Choose which 'type' we'll actually establish, i.e. what number we're 107 + // going to append to the basic session type. To do this, just check all 108 + // the numbers sequentially until we find an available session. 109 + $establish_type = null; 110 + for ($ii = 1; $ii <= $session_limit; $ii++) { 111 + $try_type = $session_type.'-'.$ii; 112 + if (!in_array($try_type, $existing_sessions)) { 113 + $establish_type = $try_type; 114 + $expect_key = PhabricatorHash::digest($session_key); 115 + $existing_sessions[] = $try_type; 116 + 117 + // Ensure the row exists so we can issue an update below. We don't 118 + // care if we race here or not. 119 + queryfx( 120 + $conn_w, 121 + 'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart) 122 + VALUES (%s, %s, %s, 0)', 123 + $session_table->getTableName(), 124 + $identity_phid, 125 + $establish_type, 126 + PhabricatorHash::digest($session_key)); 127 + break; 128 + } 129 + } 130 + 131 + // If we didn't find an available session, choose the oldest session and 132 + // overwrite it. 133 + if (!$establish_type) { 134 + $oldest = reset($sessions); 135 + $establish_type = $oldest['type']; 136 + $expect_key = $oldest['sessionKey']; 137 + } 138 + 139 + // This is so that we'll only overwrite the session if it hasn't been 140 + // refreshed since we read it. If it has, the session key will be 141 + // different and we know we're racing other processes. Whichever one 142 + // won gets the session, we go back and try again. 143 + 144 + queryfx( 145 + $conn_w, 146 + 'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP() 147 + WHERE userPHID = %s AND type = %s AND sessionKey = %s', 148 + $session_table->getTableName(), 149 + PhabricatorHash::digest($session_key), 150 + $identity_phid, 151 + $establish_type, 152 + $expect_key); 153 + 154 + if ($conn_w->getAffectedRows()) { 155 + // The update worked, so the session is valid. 156 + break; 157 + } else { 158 + // We know this just got grabbed, so don't try it again. 159 + unset($sessions[$establish_type]); 160 + } 161 + 162 + if (++$retries > $session_limit) { 163 + throw new Exception("Failed to establish a session!"); 164 + } 165 + } 166 + 167 + $log = PhabricatorUserLog::initializeNewLog( 168 + null, 169 + $identity_phid, 170 + PhabricatorUserLog::ACTION_LOGIN); 171 + $log->setDetails( 172 + array( 173 + 'session_type' => $session_type, 174 + 'session_issued' => $establish_type, 175 + )); 176 + $log->setSession($session_key); 177 + $log->save(); 178 + 179 + return $session_key; 180 + } 181 + 182 + }
+34
src/applications/auth/query/PhabricatorAuthSessionQuery.php
··· 4 4 extends PhabricatorCursorPagedPolicyAwareQuery { 5 5 6 6 private $identityPHIDs; 7 + private $sessionKeys; 8 + private $sessionTypes; 7 9 8 10 public function withIdentityPHIDs(array $identity_phids) { 9 11 $this->identityPHIDs = $identity_phids; 12 + return $this; 13 + } 14 + 15 + public function withSessionKeys(array $keys) { 16 + $this->sessionKeys = $keys; 17 + return $this; 18 + } 19 + 20 + public function withSessionTypes(array $types) { 21 + $this->sessionTypes = $types; 10 22 return $this; 11 23 } 12 24 ··· 55 67 $conn_r, 56 68 'userPHID IN (%Ls)', 57 69 $this->identityPHIDs); 70 + } 71 + 72 + if ($this->sessionKeys) { 73 + $hashes = array(); 74 + foreach ($this->sessionKeys as $session_key) { 75 + $hashes[] = PhabricatorHash::digest($session_key); 76 + } 77 + $where[] = qsprintf( 78 + $conn_r, 79 + 'sessionKey IN (%Ls)', 80 + $hashes); 81 + } 82 + 83 + 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).')'; 58 92 } 59 93 60 94 $where[] = $this->buildPagingClause($conn_r);
+10
src/applications/auth/storage/PhabricatorAuthSession.php
··· 36 36 return $this->assertAttached($this->identityObject); 37 37 } 38 38 39 + public function delete() { 40 + // TODO: We don't have a proper `id` column yet, so make this work as 41 + // expected until we do. 42 + queryfx( 43 + $this->establishConnection('w'), 44 + 'DELETE FROM %T WHERE sessionKey = %s', 45 + $this->getTableName(), 46 + $this->getSessionKey()); 47 + return $this; 48 + } 39 49 40 50 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 41 51
+5 -13
src/applications/base/controller/PhabricatorController.php
··· 35 35 } else { 36 36 $user = new PhabricatorUser(); 37 37 38 - $phusr = $request->getCookie('phusr'); 39 38 $phsid = $request->getCookie('phsid'); 40 - 41 - if (strlen($phusr) && $phsid) { 42 - $info = queryfx_one( 43 - $user->establishConnection('r'), 44 - 'SELECT u.* FROM %T u JOIN %T s ON u.phid = s.userPHID 45 - AND s.type LIKE %> AND s.sessionKey = %s', 46 - $user->getTableName(), 47 - PhabricatorUser::SESSION_TABLE, 48 - 'web-', 49 - PhabricatorHash::digest($phsid)); 50 - if ($info) { 51 - $user->loadFromArray($info); 39 + if ($phsid) { 40 + $session_user = id(new PhabricatorAuthSessionEngine()) 41 + ->loadUserForSession('web', $phsid); 42 + if ($session_user) { 43 + $user = $session_user; 52 44 } 53 45 } 54 46
+3 -18
src/applications/conduit/controller/PhabricatorConduitAPIController.php
··· 279 279 ); 280 280 } 281 281 282 - $session = queryfx_one( 283 - id(new PhabricatorUser())->establishConnection('r'), 284 - 'SELECT * FROM %T WHERE sessionKey = %s', 285 - PhabricatorUser::SESSION_TABLE, 286 - PhabricatorHash::digest($session_key)); 287 - if (!$session) { 288 - return array( 289 - 'ERR-INVALID-SESSION', 290 - 'Session key is invalid.', 291 - ); 292 - } 293 - 294 - // TODO: Make sessions timeout. 295 - // TODO: When we pull a session, read connectionID from the session table. 282 + $user = id(new PhabricatorAuthSessionEngine()) 283 + ->loadUserForSession('conduit', $session_key); 296 284 297 - $user = id(new PhabricatorUser())->loadOneWhere( 298 - 'phid = %s', 299 - $session['userPHID']); 300 285 if (!$user) { 301 286 return array( 302 287 'ERR-INVALID-SESSION', 303 - 'Session is for nonexistent user.', 288 + 'Session key is invalid.', 304 289 ); 305 290 } 306 291
+2 -1
src/applications/conduit/method/ConduitAPI_conduit_connect_Method.php
··· 142 142 if ($valid != $signature) { 143 143 throw new ConduitException('ERR-INVALID-CERTIFICATE'); 144 144 } 145 - $session_key = $user->establishSession('conduit'); 145 + $session_key = id(new PhabricatorAuthSessionEngine()) 146 + ->establishSession('conduit', $user->getPHID()); 146 147 } else { 147 148 throw new ConduitException('ERR-NO-CERTIFICATE'); 148 149 }
-164
src/applications/people/storage/PhabricatorUser.php
··· 292 292 return substr(PhabricatorHash::digest($vec), 0, $len); 293 293 } 294 294 295 - /** 296 - * Issue a new session key to this user. Phabricator supports different 297 - * types of sessions (like "web" and "conduit") and each session type may 298 - * have multiple concurrent sessions (this allows a user to be logged in on 299 - * multiple browsers at the same time, for instance). 300 - * 301 - * Note that this method is transport-agnostic and does not set cookies or 302 - * issue other types of tokens, it ONLY generates a new session key. 303 - * 304 - * You can configure the maximum number of concurrent sessions for various 305 - * session types in the Phabricator configuration. 306 - * 307 - * @param string Session type, like "web". 308 - * @return string Newly generated session key. 309 - */ 310 - public function establishSession($session_type) { 311 - $conn_w = $this->establishConnection('w'); 312 - 313 - if (strpos($session_type, '-') !== false) { 314 - throw new Exception("Session type must not contain hyphen ('-')!"); 315 - } 316 - 317 - // We allow multiple sessions of the same type, so when a caller requests 318 - // a new session of type "web", we give them the first available session in 319 - // "web-1", "web-2", ..., "web-N", up to some configurable limit. If none 320 - // of these sessions is available, we overwrite the oldest session and 321 - // reissue a new one in its place. 322 - 323 - $session_limit = 1; 324 - switch ($session_type) { 325 - case 'web': 326 - $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web'); 327 - break; 328 - case 'conduit': 329 - $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit'); 330 - break; 331 - default: 332 - throw new Exception("Unknown session type '{$session_type}'!"); 333 - } 334 - 335 - $session_limit = (int)$session_limit; 336 - if ($session_limit <= 0) { 337 - throw new Exception( 338 - "Session limit for '{$session_type}' must be at least 1!"); 339 - } 340 - 341 - // NOTE: Session establishment is sensitive to race conditions, as when 342 - // piping `arc` to `arc`: 343 - // 344 - // arc export ... | arc paste ... 345 - // 346 - // To avoid this, we overwrite an old session only if it hasn't been 347 - // re-established since we read it. 348 - 349 - // Consume entropy to generate a new session key, forestalling the eventual 350 - // heat death of the universe. 351 - $session_key = Filesystem::readRandomCharacters(40); 352 - 353 - // Load all the currently active sessions. 354 - $sessions = queryfx_all( 355 - $conn_w, 356 - 'SELECT type, sessionKey, sessionStart FROM %T 357 - WHERE userPHID = %s AND type LIKE %>', 358 - PhabricatorUser::SESSION_TABLE, 359 - $this->getPHID(), 360 - $session_type.'-'); 361 - $sessions = ipull($sessions, null, 'type'); 362 - $sessions = isort($sessions, 'sessionStart'); 363 - 364 - $existing_sessions = array_keys($sessions); 365 - 366 - // UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet. 367 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 368 - 369 - $retries = 0; 370 - while (true) { 371 - 372 - 373 - // Choose which 'type' we'll actually establish, i.e. what number we're 374 - // going to append to the basic session type. To do this, just check all 375 - // the numbers sequentially until we find an available session. 376 - $establish_type = null; 377 - for ($ii = 1; $ii <= $session_limit; $ii++) { 378 - $try_type = $session_type.'-'.$ii; 379 - if (!in_array($try_type, $existing_sessions)) { 380 - $establish_type = $try_type; 381 - $expect_key = PhabricatorHash::digest($session_key); 382 - $existing_sessions[] = $try_type; 383 - 384 - // Ensure the row exists so we can issue an update below. We don't 385 - // care if we race here or not. 386 - queryfx( 387 - $conn_w, 388 - 'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart) 389 - VALUES (%s, %s, %s, 0)', 390 - self::SESSION_TABLE, 391 - $this->getPHID(), 392 - $establish_type, 393 - PhabricatorHash::digest($session_key)); 394 - break; 395 - } 396 - } 397 - 398 - // If we didn't find an available session, choose the oldest session and 399 - // overwrite it. 400 - if (!$establish_type) { 401 - $oldest = reset($sessions); 402 - $establish_type = $oldest['type']; 403 - $expect_key = $oldest['sessionKey']; 404 - } 405 - 406 - // This is so that we'll only overwrite the session if it hasn't been 407 - // refreshed since we read it. If it has, the session key will be 408 - // different and we know we're racing other processes. Whichever one 409 - // won gets the session, we go back and try again. 410 - 411 - queryfx( 412 - $conn_w, 413 - 'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP() 414 - WHERE userPHID = %s AND type = %s AND sessionKey = %s', 415 - self::SESSION_TABLE, 416 - PhabricatorHash::digest($session_key), 417 - $this->getPHID(), 418 - $establish_type, 419 - $expect_key); 420 - 421 - if ($conn_w->getAffectedRows()) { 422 - // The update worked, so the session is valid. 423 - break; 424 - } else { 425 - // We know this just got grabbed, so don't try it again. 426 - unset($sessions[$establish_type]); 427 - } 428 - 429 - if (++$retries > $session_limit) { 430 - throw new Exception("Failed to establish a session!"); 431 - } 432 - } 433 - 434 - $log = PhabricatorUserLog::initializeNewLog( 435 - $this, 436 - $this->getPHID(), 437 - PhabricatorUserLog::ACTION_LOGIN); 438 - $log->setDetails( 439 - array( 440 - 'session_type' => $session_type, 441 - 'session_issued' => $establish_type, 442 - )); 443 - $log->setSession($session_key); 444 - $log->save(); 445 - 446 - return $session_key; 447 - } 448 - 449 - public function destroySession($session_key) { 450 - $conn_w = $this->establishConnection('w'); 451 - queryfx( 452 - $conn_w, 453 - 'DELETE FROM %T WHERE userPHID = %s AND sessionKey = %s', 454 - self::SESSION_TABLE, 455 - $this->getPHID(), 456 - PhabricatorHash::digest($session_key)); 457 - } 458 - 459 295 private function generateEmailToken( 460 296 PhabricatorUserEmail $email, 461 297 $offset = 0) {
+8 -7
src/applications/settings/panel/PhabricatorSettingsPanelConduit.php
··· 34 34 ->setDialog($dialog); 35 35 } 36 36 37 - $conn = $user->establishConnection('w'); 38 - queryfx( 39 - $conn, 40 - 'DELETE FROM %T WHERE userPHID = %s AND type LIKE %>', 41 - PhabricatorUser::SESSION_TABLE, 42 - $user->getPHID(), 43 - 'conduit'); 37 + $sessions = id(new PhabricatorAuthSessionQuery()) 38 + ->setViewer($user) 39 + ->withIdentityPHIDs(array($user->getPHID())) 40 + ->withSessionTypes(array('conduit')) 41 + ->execute(); 42 + foreach ($sessions as $session) { 43 + $session->delete(); 44 + } 44 45 45 46 // This implicitly regenerates the certificate. 46 47 $user->setConduitCertificate(null);