@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.

Begin cleaning up OAuth scope handling

Summary:
Ref T7303. OAuth scope handling never got fully modernized and is a bit of a mess.

Also introduce implicit "ALWAYS" and "NEVER" scopes.

Always give tokens access to meta-methods like `conduit.getcapabilities` and `conduit.query`. These do not expose user information.

Test Plan:
- Used a token to call `user.whoami`.
- Used a token to call `conduit.query`.
- Used a token to try to call `user.query`, got rebuffed.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T7303

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

+138 -81
-4
src/applications/conduit/call/ConduitCall.php
··· 65 65 return $this->handler->shouldAllowUnguardedWrites(); 66 66 } 67 67 68 - public function getRequiredScope() { 69 - return $this->handler->getRequiredScope(); 70 - } 71 - 72 68 public function getErrorDescription($code) { 73 69 return $this->handler->getErrorDescription($code); 74 70 }
+43 -18
src/applications/conduit/controller/PhabricatorConduitAPIController.php
··· 45 45 $auth_error = null; 46 46 $conduit_username = '-'; 47 47 if ($call->shouldRequireAuthentication()) { 48 - $metadata['scope'] = $call->getRequiredScope(); 49 48 $auth_error = $this->authenticateUser($api_request, $metadata, $method); 50 49 // If we've explicitly authenticated the user here and either done 51 50 // CSRF validation or are using a non-web authentication mechanism. ··· 185 184 // First, verify the signature. 186 185 try { 187 186 $protocol_data = $metadata; 188 - 189 - // TODO: We should stop writing this into the protocol data when 190 - // processing a request. 191 - unset($protocol_data['scope']); 192 - 193 187 ConduitClient::verifySignature( 194 188 $method, 195 189 $api_request->getAllParameters(), ··· 362 356 $user); 363 357 } 364 358 365 - // handle oauth 366 359 $access_token = idx($metadata, 'access_token'); 367 - $method_scope = idx($metadata, 'scope'); 368 - if ($access_token && 369 - $method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) { 360 + if ($access_token) { 370 361 $token = id(new PhabricatorOAuthServerAccessToken()) 371 362 ->loadOneWhere('token = %s', $access_token); 372 363 if (!$token) { ··· 377 368 } 378 369 379 370 $oauth_server = new PhabricatorOAuthServer(); 380 - $valid = $oauth_server->validateAccessToken($token, 381 - $method_scope); 382 - if (!$valid) { 371 + $authorization = $oauth_server->authorizeToken($token); 372 + if (!$authorization) { 383 373 return array( 384 374 'ERR-INVALID-AUTH', 385 - pht('Access token is invalid.'), 375 + pht('Access token is invalid or expired.'), 386 376 ); 387 377 } 388 378 389 - // valid token, so let's log in the user! 390 - $user_phid = $token->getUserPHID(); 391 - $user = id(new PhabricatorUser()) 392 - ->loadOneWhere('phid = %s', $user_phid); 379 + $user = id(new PhabricatorPeopleQuery()) 380 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 381 + ->withPHIDs(array($token->getUserPHID())) 382 + ->executeOne(); 393 383 if (!$user) { 394 384 return array( 395 385 'ERR-INVALID-AUTH', 396 386 pht('Access token is for invalid user.'), 397 387 ); 398 388 } 389 + 390 + $ok = $this->authorizeOAuthMethodAccess($authorization, $method); 391 + if (!$ok) { 392 + return array( 393 + 'ERR-OAUTH-ACCESS', 394 + pht('You do not have authorization to call this method.'), 395 + ); 396 + } 397 + 399 398 return $this->validateAuthenticatedUser( 400 399 $api_request, 401 400 $user); ··· 641 640 642 641 return array($metadata, $params); 643 642 } 643 + 644 + private function authorizeOAuthMethodAccess( 645 + PhabricatorOAuthClientAuthorization $authorization, 646 + $method_name) { 647 + 648 + $method = ConduitAPIMethod::getConduitMethod($method_name); 649 + if (!$method) { 650 + return false; 651 + } 652 + 653 + $required_scope = $method->getRequiredScope(); 654 + switch ($required_scope) { 655 + case ConduitAPIMethod::SCOPE_ALWAYS: 656 + return true; 657 + case ConduitAPIMethod::SCOPE_NEVER: 658 + return false; 659 + } 660 + 661 + $authorization_scope = $authorization->getScope(); 662 + if (!empty($authorization_scope[$required_scope])) { 663 + return true; 664 + } 665 + 666 + return false; 667 + } 668 + 644 669 645 670 }
+30
src/applications/conduit/controller/PhabricatorConduitConsoleController.php
··· 142 142 pht('Errors'), 143 143 $error_description); 144 144 145 + 146 + $scope = $method->getRequiredScope(); 147 + switch ($scope) { 148 + case ConduitAPIMethod::SCOPE_ALWAYS: 149 + $oauth_icon = 'fa-globe green'; 150 + $oauth_description = pht( 151 + 'OAuth clients may always call this method.'); 152 + break; 153 + case ConduitAPIMethod::SCOPE_NEVER: 154 + $oauth_icon = 'fa-ban red'; 155 + $oauth_description = pht( 156 + 'OAuth clients may never call this method.'); 157 + break; 158 + default: 159 + $oauth_icon = 'fa-unlock-alt blue'; 160 + $oauth_description = pht( 161 + 'OAuth clients may call this method after requesting access to '. 162 + 'the "%s" scope.', 163 + $scope); 164 + break; 165 + } 166 + 167 + $view->addProperty( 168 + pht('OAuth Scope'), 169 + array( 170 + id(new PHUIIconView())->setIcon($oauth_icon), 171 + ' ', 172 + $oauth_description, 173 + )); 174 + 145 175 $view->addSectionHeader( 146 176 pht('Description'), PHUIPropertyListView::ICON_SUMMARY); 147 177 $view->addTextContent(
+3 -2
src/applications/conduit/method/ConduitAPIMethod.php
··· 15 15 const METHOD_STATUS_UNSTABLE = 'unstable'; 16 16 const METHOD_STATUS_DEPRECATED = 'deprecated'; 17 17 18 + const SCOPE_NEVER = 'scope.never'; 19 + const SCOPE_ALWAYS = 'scope.always'; 18 20 19 21 /** 20 22 * Get a short, human-readable text summary of the method. ··· 108 110 } 109 111 110 112 public function getRequiredScope() { 111 - // by default, conduit methods are not accessible via OAuth 112 - return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE; 113 + return self::SCOPE_NEVER; 113 114 } 114 115 115 116 public function executeMethod(ConduitAPIRequest $request) {
+4
src/applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php
··· 24 24 return 'dict<string, any>'; 25 25 } 26 26 27 + public function getRequiredScope() { 28 + return self::SCOPE_ALWAYS; 29 + } 30 + 27 31 protected function execute(ConduitAPIRequest $request) { 28 32 $authentication = array( 29 33 'token',
+4
src/applications/conduit/method/ConduitQueryConduitAPIMethod.php
··· 18 18 return 'dict<dict>'; 19 19 } 20 20 21 + public function getRequiredScope() { 22 + return self::SCOPE_ALWAYS; 23 + } 24 + 21 25 protected function execute(ConduitAPIRequest $request) { 22 26 $methods = id(new PhabricatorConduitMethodQuery()) 23 27 ->setViewer($request->getUser())
+21 -26
src/applications/oauthserver/PhabricatorOAuthServer.php
··· 29 29 final class PhabricatorOAuthServer extends Phobject { 30 30 31 31 const AUTHORIZATION_CODE_TIMEOUT = 300; 32 - const ACCESS_TOKEN_TIMEOUT = 3600; 33 32 34 33 private $user; 35 34 private $client; ··· 158 157 /** 159 158 * @task token 160 159 */ 161 - public function validateAccessToken( 162 - PhabricatorOAuthServerAccessToken $token, 163 - $required_scope) { 160 + public function authorizeToken( 161 + PhabricatorOAuthServerAccessToken $token) { 164 162 165 - $created_time = $token->getDateCreated(); 166 - $must_be_used_by = $created_time + self::ACCESS_TOKEN_TIMEOUT; 167 - $expired = time() > $must_be_used_by; 168 - $authorization = id(new PhabricatorOAuthClientAuthorization()) 169 - ->loadOneWhere( 170 - 'userPHID = %s AND clientPHID = %s', 171 - $token->getUserPHID(), 172 - $token->getClientPHID()); 163 + $user_phid = $token->getUserPHID(); 164 + $client_phid = $token->getClientPHID(); 173 165 166 + $authorization = id(new PhabricatorOAuthClientAuthorizationQuery()) 167 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 168 + ->withUserPHIDs(array($user_phid)) 169 + ->withClientPHIDs(array($client_phid)) 170 + ->executeOne(); 174 171 if (!$authorization) { 175 - return false; 176 - } 177 - $token_scope = $authorization->getScope(); 178 - if (!isset($token_scope[$required_scope])) { 179 - return false; 172 + return null; 180 173 } 181 174 182 - $valid = true; 183 - if ($expired) { 184 - $valid = false; 185 - // check if the scope includes "offline_access", which makes the 186 - // token valid despite being expired 187 - if (isset( 188 - $token_scope[PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS])) { 189 - $valid = true; 175 + // TODO: This should probably be reworked; expiration should be an 176 + // exclusive property of the token. For now, this logic reads: tokens for 177 + // authorizations with "offline_access" never expire. 178 + 179 + $is_expired = $token->isExpired(); 180 + if ($is_expired) { 181 + $offline_access = PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS; 182 + $authorization_scope = $authorization->getScope(); 183 + if (empty($authorization_scope[$offline_access])) { 184 + return null; 190 185 } 191 186 } 192 187 193 - return $valid; 188 + return $authorization; 194 189 } 195 190 196 191 /**
-6
src/applications/oauthserver/PhabricatorOAuthServerScope.php
··· 4 4 5 5 const SCOPE_OFFLINE_ACCESS = 'offline_access'; 6 6 const SCOPE_WHOAMI = 'whoami'; 7 - const SCOPE_NOT_ACCESSIBLE = 'not_accessible'; 8 7 9 - /* 10 - * Note this does not contain SCOPE_NOT_ACCESSIBLE which is magic 11 - * used to simplify code for data that is not currently accessible 12 - * via OAuth. 13 - */ 14 8 public static function getScopesDict() { 15 9 return array( 16 10 self::SCOPE_OFFLINE_ACCESS => 1,
+1 -1
src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php
··· 144 144 $result = array( 145 145 'access_token' => $access_token->getToken(), 146 146 'token_type' => 'Bearer', 147 - 'expires_in' => PhabricatorOAuthServer::ACCESS_TOKEN_TIMEOUT, 147 + 'expires_in' => $access_token->getExpiresDuration(), 148 148 ); 149 149 return $response->setContent($result); 150 150 } catch (Exception $e) {
+18 -24
src/applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php
··· 7 7 private $userPHIDs; 8 8 private $clientPHIDs; 9 9 10 - public function witHPHIDs(array $phids) { 10 + public function withPHIDs(array $phids) { 11 11 $this->phids = $phids; 12 12 return $this; 13 13 } ··· 22 22 return $this; 23 23 } 24 24 25 + public function newResultObject() { 26 + return new PhabricatorOAuthClientAuthorization(); 27 + } 28 + 25 29 protected function loadPage() { 26 - $table = new PhabricatorOAuthClientAuthorization(); 27 - $conn_r = $table->establishConnection('r'); 28 - 29 - $data = queryfx_all( 30 - $conn_r, 31 - 'SELECT * FROM %T auth %Q %Q %Q', 32 - $table->getTableName(), 33 - $this->buildWhereClause($conn_r), 34 - $this->buildOrderClause($conn_r), 35 - $this->buildLimitClause($conn_r)); 36 - 37 - return $table->loadAllFromArray($data); 30 + return $this->loadStandardPage($this->newResultObject()); 38 31 } 39 32 40 33 protected function willFilterPage(array $authorizations) { ··· 49 42 50 43 foreach ($authorizations as $key => $authorization) { 51 44 $client = idx($clients, $authorization->getClientPHID()); 45 + 52 46 if (!$client) { 47 + $this->didRejectResult($authorization); 53 48 unset($authorizations[$key]); 54 49 continue; 55 50 } 51 + 56 52 $authorization->attachClient($client); 57 53 } 58 54 59 55 return $authorizations; 60 56 } 61 57 62 - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { 63 - $where = array(); 58 + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { 59 + $where = parent::buildWhereClauseParts($conn); 64 60 65 - if ($this->phids) { 61 + if ($this->phids !== null) { 66 62 $where[] = qsprintf( 67 - $conn_r, 63 + $conn, 68 64 'phid IN (%Ls)', 69 65 $this->phids); 70 66 } 71 67 72 - if ($this->userPHIDs) { 68 + if ($this->userPHIDs !== null) { 73 69 $where[] = qsprintf( 74 - $conn_r, 70 + $conn, 75 71 'userPHID IN (%Ls)', 76 72 $this->userPHIDs); 77 73 } 78 74 79 - if ($this->clientPHIDs) { 75 + if ($this->clientPHIDs !== null) { 80 76 $where[] = qsprintf( 81 - $conn_r, 77 + $conn, 82 78 'clientPHID IN (%Ls)', 83 79 $this->clientPHIDs); 84 80 } 85 81 86 - $where[] = $this->buildPagingClause($conn_r); 87 - 88 - return $this->formatWhereClause($where); 82 + return $where; 89 83 } 90 84 91 85 public function getQueryApplicationClass() {
+14
src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php
··· 22 22 ) + parent::getConfiguration(); 23 23 } 24 24 25 + public function isExpired() { 26 + $now = PhabricatorTime::getNow(); 27 + $expires_epoch = $this->getExpiresEpoch(); 28 + return ($now > $expires_epoch); 29 + } 30 + 31 + public function getExpiresEpoch() { 32 + return $this->getDateCreated() + 3600; 33 + } 34 + 35 + public function getExpiresDuration() { 36 + return PhabricatorTime::getNow() - $this->getExpiresEpoch(); 37 + } 38 + 25 39 }