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

Accept Conduit tokens as an authentication mechanism

Summary:
- Ref T5955. Accept the tokens introduced in D10985 as an authentication token.
- Ref T3628. Permit simple `curl`-compatible decoding of parameters.

Test Plan:
- Ran some sensible `curl` API commands:

```
epriestley@orbital ~/dev/phabricator $ curl -g "http://local.phacility.com/api/user.whoami?api.token=api-f7dfpoyelk4mmz6vxcueb6hcbtbk" ; echo
{"result":{"phid":"PHID-USER-cvfydnwadpdj7vdon36z","userName":"admin","realName":"asdf","image":"http:\/\/local.phacility.com\/res\/1410737307T\/phabricator\/3eb28cd9\/rsrc\/image\/avatar.png","uri":"http:\/\/local.phacility.com\/p\/admin\/","roles":["admin","verified","approved","activated"]},"error_code":null,"error_info":null}
```

```
epriestley@orbital ~/dev/phabricator $ curl -g "http://local.phacility.com/api/differential.query?api.token=api-f7dfpoyelk4mmz6vxcueb6hcbtbk&ids[]=1" ; echo
{"result":[{"id":"1","phid":"PHID-DREV-v3a67ixww3ccg5lqbxee","title":"zxcb","uri":"http:\/\/local.phacility.com\/D1","dateCreated":"1418405590","dateModified":"1418405590","authorPHID":"PHID-USER-cvfydnwadpdj7vdon36z","status":"0","statusName":"Needs Review","branch":null,"summary":"","testPlan":"zxcb","lineCount":"6","activeDiffPHID":"PHID-DIFF-pzbtc5rw6pe5j2kxtlr2","diffs":["1"],"commits":[],"reviewers":[],"ccs":[],"hashes":[],"auxiliary":{"phabricator:projects":[],"phabricator:depends-on":[],"organization.sqlmigration":null},"arcanistProjectPHID":null,"repositoryPHID":null,"sourcePath":null}],"error_code":null,"error_info":null}
```

- Ran older-style commands like `arc list` against the local install.
- Ran commands via web console.
- Added and ran a unit test to make sure nothing is using forbidden parameter names.
- Terminated a token and verified it no longer works.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T3628, T5955

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

+186 -33
+2
src/__phutil_library_map__.php
··· 1430 1430 'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php', 1431 1431 'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php', 1432 1432 'PhabricatorConduitSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitSettingsPanel.php', 1433 + 'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php', 1433 1434 'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php', 1434 1435 'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php', 1435 1436 'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php', ··· 4549 4550 'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4550 4551 'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine', 4551 4552 'PhabricatorConduitSettingsPanel' => 'PhabricatorSettingsPanel', 4553 + 'PhabricatorConduitTestCase' => 'PhabricatorTestCase', 4552 4554 'PhabricatorConduitToken' => array( 4553 4555 'PhabricatorConduitDAO', 4554 4556 'PhabricatorPolicyInterface',
+19
src/__tests__/PhabricatorConduitTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorConduitTestCase extends PhabricatorTestCase { 4 + 5 + public function testConduitMethods() { 6 + $methods = id(new PhutilSymbolLoader()) 7 + ->setAncestorClass('ConduitAPIMethod') 8 + ->loadObjects(); 9 + 10 + // We're just looking for a side effect of ConduitCall construction 11 + // here: it will throw if any methods define reserved parameter names. 12 + 13 + foreach ($methods as $method) { 14 + new ConduitCall($method->getAPIMethodName(), array()); 15 + } 16 + 17 + $this->assertTrue(true); 18 + } 19 + }
+18 -5
src/applications/conduit/call/ConduitCall.php
··· 18 18 $this->method = $method; 19 19 $this->handler = $this->buildMethodHandler($method); 20 20 21 - $invalid_params = array_diff_key( 22 - $params, 23 - $this->handler->defineParamTypes()); 21 + $param_types = $this->handler->defineParamTypes(); 22 + 23 + foreach ($param_types as $key => $spec) { 24 + if (ConduitAPIMethod::getParameterMetadataKey($key) !== null) { 25 + throw new ConduitException( 26 + pht( 27 + 'API Method "%s" defines a disallowed parameter, "%s". This '. 28 + 'parameter name is reserved.', 29 + $method, 30 + $key)); 31 + } 32 + } 33 + 34 + $invalid_params = array_diff_key($params, $param_types); 24 35 if ($invalid_params) { 25 36 throw new ConduitException( 26 - "Method '{$method}' doesn't define these parameters: '". 27 - implode("', '", array_keys($invalid_params))."'."); 37 + pht( 38 + 'API Method "%s" does not define these parameters: %s.', 39 + $method, 40 + "'".implode("', '", array_keys($invalid_params))."'")); 28 41 } 29 42 30 43 $this->request = new ConduitAPIRequest($params);
+105 -27
src/applications/conduit/controller/PhabricatorConduitAPIController.php
··· 28 28 29 29 try { 30 30 31 - $params = $this->decodeConduitParams($request, $method); 32 - $metadata = idx($params, '__conduit__', array()); 33 - unset($params['__conduit__']); 31 + list($metadata, $params) = $this->decodeConduitParams($request, $method); 34 32 35 - $call = new ConduitCall( 36 - $method, $params, idx($metadata, 'isProxied', false)); 33 + $call = new ConduitCall($method, $params); 37 34 38 35 $result = null; 39 36 ··· 296 293 ); 297 294 } 298 295 296 + $token_string = idx($metadata, 'token'); 297 + if (strlen($token_string)) { 298 + 299 + if (strlen($token_string) != 32) { 300 + return array( 301 + 'ERR-INVALID-AUTH', 302 + pht( 303 + 'API token "%s" has the wrong length. API tokens should be '. 304 + '32 characters long.'), 305 + ); 306 + } 307 + 308 + $type = head(explode('-', $token_string)); 309 + switch ($type) { 310 + case 'api': 311 + case 'tmp': 312 + break; 313 + default: 314 + return array( 315 + 'ERR-INVALID-AUTH', 316 + pht( 317 + 'API token "%s" has the wrong format. API tokens should begin '. 318 + 'with "api-" or "tmp-" and be 32 characters long.', 319 + $token_string), 320 + ); 321 + } 322 + 323 + $token = id(new PhabricatorConduitTokenQuery()) 324 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 325 + ->withTokens(array($token_string)) 326 + ->withExpired(false) 327 + ->executeOne(); 328 + if (!$token) { 329 + $token = id(new PhabricatorConduitTokenQuery()) 330 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 331 + ->withTokens(array($token_string)) 332 + ->withExpired(true) 333 + ->executeOne(); 334 + if ($token) { 335 + return array( 336 + 'ERR-INVALID-AUTH', 337 + pht( 338 + 'API token "%s" was previously valid, but has expired.', 339 + $token_string), 340 + ); 341 + } else { 342 + return array( 343 + 'ERR-INVALID-AUTH', 344 + pht( 345 + 'API token "%s" is not valid.', 346 + $token_string), 347 + ); 348 + } 349 + } 350 + 351 + $user = $token->getObject(); 352 + if (!($user instanceof PhabricatorUser)) { 353 + return array( 354 + 'ERR-INVALID-AUTH', 355 + pht( 356 + 'API token is not associated with a valid user.'), 357 + ); 358 + } 359 + 360 + return $this->validateAuthenticatedUser( 361 + $api_request, 362 + $user); 363 + } 364 + 299 365 // handle oauth 300 - $access_token = $request->getStr('access_token'); 301 - $method_scope = $metadata['scope']; 366 + $access_token = idx($metadata, 'access_token'); 367 + $method_scope = idx($metadata, 'scope'); 302 368 if ($access_token && 303 369 $method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) { 304 370 $token = id(new PhabricatorOAuthServerAccessToken()) ··· 337 403 $user); 338 404 } 339 405 340 - // Handle sessionless auth. TOOD: This is super messy. 406 + // Handle sessionless auth. 407 + // TODO: This is super messy. 408 + // TODO: Remove this in favor of token-based auth. 409 + 341 410 if (isset($metadata['authUser'])) { 342 411 $user = id(new PhabricatorUser())->loadOneWhere( 343 412 'userName = %s', ··· 361 430 $api_request, 362 431 $user); 363 432 } 433 + 434 + // Handle session-based auth. 435 + // TODO: Remove this in favor of token-based auth. 364 436 365 437 $session_key = idx($metadata, 'sessionKey'); 366 438 if (!$session_key) { ··· 525 597 $params[$key] = $decoded_value; 526 598 } 527 599 528 - return $params; 600 + $metadata = idx($params, '__conduit__', array()); 601 + unset($params['__conduit__']); 602 + 603 + return array($metadata, $params); 529 604 } 530 605 531 606 // Otherwise, look for a single parameter called 'params' which has the 532 - // entire param dictionary JSON encoded. This is the usual case for remote 533 - // requests. 534 - 607 + // entire param dictionary JSON encoded. 535 608 $params_json = $request->getStr('params'); 536 - if (!strlen($params_json)) { 537 - if ($request->getBool('allowEmptyParams')) { 538 - // TODO: This is a bit messy, but otherwise you can't call 539 - // "conduit.ping" from the web console. 540 - $params = array(); 541 - } else { 542 - throw new Exception( 543 - "Request has no 'params' key. This may mean that an extension like ". 544 - "Suhosin has dropped data from the request. Check the PHP ". 545 - "configuration on your server. If you are developing a Conduit ". 546 - "client, you MUST provide a 'params' parameter when making a ". 547 - "Conduit request, even if the value is empty (e.g., provide '{}')."); 548 - } 549 - } else { 609 + if (strlen($params_json)) { 550 610 $params = json_decode($params_json, true); 551 611 if (!is_array($params)) { 552 612 throw new Exception( ··· 554 614 "'{$method}', could not decode JSON serialization. Data: ". 555 615 $params_json); 556 616 } 617 + 618 + $metadata = idx($params, '__conduit__', array()); 619 + unset($params['__conduit__']); 620 + 621 + return array($metadata, $params); 557 622 } 558 623 559 - return $params; 624 + // If we do not have `params`, assume this is a simple HTTP request with 625 + // HTTP key-value pairs. 626 + $params = array(); 627 + $metadata = array(); 628 + foreach ($request->getPassthroughRequestData() as $key => $value) { 629 + $meta_key = ConduitAPIMethod::getParameterMetadataKey($key); 630 + if ($meta_key !== null) { 631 + $metadata[$meta_key] = $value; 632 + } else { 633 + $params[$key] = $value; 634 + } 635 + } 636 + 637 + return array($metadata, $params); 560 638 } 561 639 562 640 }
-1
src/applications/conduit/controller/PhabricatorConduitConsoleController.php
··· 67 67 $form 68 68 ->setUser($request->getUser()) 69 69 ->setAction('/api/'.$this->method) 70 - ->addHiddenInput('allowEmptyParams', 1) 71 70 ->appendChild( 72 71 id(new AphrontFormStaticControl()) 73 72 ->setLabel('Description')
+29
src/applications/conduit/method/ConduitAPIMethod.php
··· 149 149 return 'string-constant<'.$constants.'>'; 150 150 } 151 151 152 + public static function getParameterMetadataKey($key) { 153 + if (strncmp($key, 'api.', 4) === 0) { 154 + // All keys passed beginning with "api." are always metadata keys. 155 + return substr($key, 4); 156 + } else { 157 + switch ($key) { 158 + // These are real keys which always belong to request metadata. 159 + case 'access_token': 160 + case 'scope': 161 + case 'output': 162 + 163 + // This is not a real metadata key; it is included here only to 164 + // prevent Conduit methods from defining it. 165 + case '__conduit__': 166 + 167 + // This is prevented globally as a blanket defense against OAuth 168 + // redirection attacks. It is included here to stop Conduit methods 169 + // from defining it. 170 + case 'code': 171 + 172 + // This is not a real metadata key, but the presence of this 173 + // parameter triggers an alternate request decoding pathway. 174 + case 'params': 175 + return $key; 176 + } 177 + } 178 + 179 + return null; 180 + } 152 181 153 182 /* -( Paging Results )----------------------------------------------------- */ 154 183
+13
src/applications/conduit/query/PhabricatorConduitTokenQuery.php
··· 6 6 private $ids; 7 7 private $objectPHIDs; 8 8 private $expired; 9 + private $tokens; 9 10 10 11 public function withExpired($expired) { 11 12 $this->expired = $expired; ··· 19 20 20 21 public function withObjectPHIDs(array $phids) { 21 22 $this->objectPHIDs = $phids; 23 + return $this; 24 + } 25 + 26 + public function withTokens(array $tokens) { 27 + $this->tokens = $tokens; 22 28 return $this; 23 29 } 24 30 ··· 52 58 $conn_r, 53 59 'objectPHID IN (%Ls)', 54 60 $this->objectPHIDs); 61 + } 62 + 63 + if ($this->tokens !== null) { 64 + $where[] = qsprintf( 65 + $conn_r, 66 + 'token IN (%Ls)', 67 + $this->tokens); 55 68 } 56 69 57 70 if ($this->expired !== null) {