1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 08:52:39 +01:00

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
This commit is contained in:
epriestley 2014-12-15 11:14:41 -08:00
parent 39f2bbaeea
commit 0507626f01
7 changed files with 186 additions and 33 deletions

View file

@ -1430,6 +1430,7 @@ phutil_register_library_map(array(
'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php', 'PhabricatorConduitMethodQuery' => 'applications/conduit/query/PhabricatorConduitMethodQuery.php',
'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php', 'PhabricatorConduitSearchEngine' => 'applications/conduit/query/PhabricatorConduitSearchEngine.php',
'PhabricatorConduitSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitSettingsPanel.php', 'PhabricatorConduitSettingsPanel' => 'applications/conduit/settings/PhabricatorConduitSettingsPanel.php',
'PhabricatorConduitTestCase' => '__tests__/PhabricatorConduitTestCase.php',
'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php', 'PhabricatorConduitToken' => 'applications/conduit/storage/PhabricatorConduitToken.php',
'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php', 'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php',
'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php', 'PhabricatorConduitTokenEditController' => 'applications/conduit/controller/PhabricatorConduitTokenEditController.php',
@ -4549,6 +4550,7 @@ phutil_register_library_map(array(
'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitMethodQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorConduitSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorConduitSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorConduitSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorConduitTestCase' => 'PhabricatorTestCase',
'PhabricatorConduitToken' => array( 'PhabricatorConduitToken' => array(
'PhabricatorConduitDAO', 'PhabricatorConduitDAO',
'PhabricatorPolicyInterface', 'PhabricatorPolicyInterface',

View file

@ -0,0 +1,19 @@
<?php
final class PhabricatorConduitTestCase extends PhabricatorTestCase {
public function testConduitMethods() {
$methods = id(new PhutilSymbolLoader())
->setAncestorClass('ConduitAPIMethod')
->loadObjects();
// We're just looking for a side effect of ConduitCall construction
// here: it will throw if any methods define reserved parameter names.
foreach ($methods as $method) {
new ConduitCall($method->getAPIMethodName(), array());
}
$this->assertTrue(true);
}
}

View file

@ -18,13 +18,26 @@ final class ConduitCall {
$this->method = $method; $this->method = $method;
$this->handler = $this->buildMethodHandler($method); $this->handler = $this->buildMethodHandler($method);
$invalid_params = array_diff_key( $param_types = $this->handler->defineParamTypes();
$params,
$this->handler->defineParamTypes()); foreach ($param_types as $key => $spec) {
if (ConduitAPIMethod::getParameterMetadataKey($key) !== null) {
throw new ConduitException(
pht(
'API Method "%s" defines a disallowed parameter, "%s". This '.
'parameter name is reserved.',
$method,
$key));
}
}
$invalid_params = array_diff_key($params, $param_types);
if ($invalid_params) { if ($invalid_params) {
throw new ConduitException( throw new ConduitException(
"Method '{$method}' doesn't define these parameters: '". pht(
implode("', '", array_keys($invalid_params))."'."); 'API Method "%s" does not define these parameters: %s.',
$method,
"'".implode("', '", array_keys($invalid_params))."'"));
} }
$this->request = new ConduitAPIRequest($params); $this->request = new ConduitAPIRequest($params);

View file

@ -28,12 +28,9 @@ final class PhabricatorConduitAPIController
try { try {
$params = $this->decodeConduitParams($request, $method); list($metadata, $params) = $this->decodeConduitParams($request, $method);
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
$call = new ConduitCall( $call = new ConduitCall($method, $params);
$method, $params, idx($metadata, 'isProxied', false));
$result = null; $result = null;
@ -296,9 +293,78 @@ final class PhabricatorConduitAPIController
); );
} }
$token_string = idx($metadata, 'token');
if (strlen($token_string)) {
if (strlen($token_string) != 32) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" has the wrong length. API tokens should be '.
'32 characters long.'),
);
}
$type = head(explode('-', $token_string));
switch ($type) {
case 'api':
case 'tmp':
break;
default:
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" has the wrong format. API tokens should begin '.
'with "api-" or "tmp-" and be 32 characters long.',
$token_string),
);
}
$token = id(new PhabricatorConduitTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokens(array($token_string))
->withExpired(false)
->executeOne();
if (!$token) {
$token = id(new PhabricatorConduitTokenQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTokens(array($token_string))
->withExpired(true)
->executeOne();
if ($token) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" was previously valid, but has expired.',
$token_string),
);
} else {
return array(
'ERR-INVALID-AUTH',
pht(
'API token "%s" is not valid.',
$token_string),
);
}
}
$user = $token->getObject();
if (!($user instanceof PhabricatorUser)) {
return array(
'ERR-INVALID-AUTH',
pht(
'API token is not associated with a valid user.'),
);
}
return $this->validateAuthenticatedUser(
$api_request,
$user);
}
// handle oauth // handle oauth
$access_token = $request->getStr('access_token'); $access_token = idx($metadata, 'access_token');
$method_scope = $metadata['scope']; $method_scope = idx($metadata, 'scope');
if ($access_token && if ($access_token &&
$method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) { $method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) {
$token = id(new PhabricatorOAuthServerAccessToken()) $token = id(new PhabricatorOAuthServerAccessToken())
@ -337,7 +403,10 @@ final class PhabricatorConduitAPIController
$user); $user);
} }
// Handle sessionless auth. TOOD: This is super messy. // Handle sessionless auth.
// TODO: This is super messy.
// TODO: Remove this in favor of token-based auth.
if (isset($metadata['authUser'])) { if (isset($metadata['authUser'])) {
$user = id(new PhabricatorUser())->loadOneWhere( $user = id(new PhabricatorUser())->loadOneWhere(
'userName = %s', 'userName = %s',
@ -362,6 +431,9 @@ final class PhabricatorConduitAPIController
$user); $user);
} }
// Handle session-based auth.
// TODO: Remove this in favor of token-based auth.
$session_key = idx($metadata, 'sessionKey'); $session_key = idx($metadata, 'sessionKey');
if (!$session_key) { if (!$session_key) {
return array( return array(
@ -525,28 +597,16 @@ final class PhabricatorConduitAPIController
$params[$key] = $decoded_value; $params[$key] = $decoded_value;
} }
return $params; $metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
return array($metadata, $params);
} }
// Otherwise, look for a single parameter called 'params' which has the // Otherwise, look for a single parameter called 'params' which has the
// entire param dictionary JSON encoded. This is the usual case for remote // entire param dictionary JSON encoded.
// requests.
$params_json = $request->getStr('params'); $params_json = $request->getStr('params');
if (!strlen($params_json)) { if (strlen($params_json)) {
if ($request->getBool('allowEmptyParams')) {
// TODO: This is a bit messy, but otherwise you can't call
// "conduit.ping" from the web console.
$params = array();
} else {
throw new Exception(
"Request has no 'params' key. This may mean that an extension like ".
"Suhosin has dropped data from the request. Check the PHP ".
"configuration on your server. If you are developing a Conduit ".
"client, you MUST provide a 'params' parameter when making a ".
"Conduit request, even if the value is empty (e.g., provide '{}').");
}
} else {
$params = json_decode($params_json, true); $params = json_decode($params_json, true);
if (!is_array($params)) { if (!is_array($params)) {
throw new Exception( throw new Exception(
@ -554,9 +614,27 @@ final class PhabricatorConduitAPIController
"'{$method}', could not decode JSON serialization. Data: ". "'{$method}', could not decode JSON serialization. Data: ".
$params_json); $params_json);
} }
$metadata = idx($params, '__conduit__', array());
unset($params['__conduit__']);
return array($metadata, $params);
} }
return $params; // If we do not have `params`, assume this is a simple HTTP request with
// HTTP key-value pairs.
$params = array();
$metadata = array();
foreach ($request->getPassthroughRequestData() as $key => $value) {
$meta_key = ConduitAPIMethod::getParameterMetadataKey($key);
if ($meta_key !== null) {
$metadata[$meta_key] = $value;
} else {
$params[$key] = $value;
}
}
return array($metadata, $params);
} }
} }

View file

@ -67,7 +67,6 @@ final class PhabricatorConduitConsoleController
$form $form
->setUser($request->getUser()) ->setUser($request->getUser())
->setAction('/api/'.$this->method) ->setAction('/api/'.$this->method)
->addHiddenInput('allowEmptyParams', 1)
->appendChild( ->appendChild(
id(new AphrontFormStaticControl()) id(new AphrontFormStaticControl())
->setLabel('Description') ->setLabel('Description')

View file

@ -149,6 +149,35 @@ abstract class ConduitAPIMethod
return 'string-constant<'.$constants.'>'; return 'string-constant<'.$constants.'>';
} }
public static function getParameterMetadataKey($key) {
if (strncmp($key, 'api.', 4) === 0) {
// All keys passed beginning with "api." are always metadata keys.
return substr($key, 4);
} else {
switch ($key) {
// These are real keys which always belong to request metadata.
case 'access_token':
case 'scope':
case 'output':
// This is not a real metadata key; it is included here only to
// prevent Conduit methods from defining it.
case '__conduit__':
// This is prevented globally as a blanket defense against OAuth
// redirection attacks. It is included here to stop Conduit methods
// from defining it.
case 'code':
// This is not a real metadata key, but the presence of this
// parameter triggers an alternate request decoding pathway.
case 'params':
return $key;
}
}
return null;
}
/* -( Paging Results )----------------------------------------------------- */ /* -( Paging Results )----------------------------------------------------- */

View file

@ -6,6 +6,7 @@ final class PhabricatorConduitTokenQuery
private $ids; private $ids;
private $objectPHIDs; private $objectPHIDs;
private $expired; private $expired;
private $tokens;
public function withExpired($expired) { public function withExpired($expired) {
$this->expired = $expired; $this->expired = $expired;
@ -22,6 +23,11 @@ final class PhabricatorConduitTokenQuery
return $this; return $this;
} }
public function withTokens(array $tokens) {
$this->tokens = $tokens;
return $this;
}
public function loadPage() { public function loadPage() {
$table = new PhabricatorConduitToken(); $table = new PhabricatorConduitToken();
$conn_r = $table->establishConnection('r'); $conn_r = $table->establishConnection('r');
@ -54,6 +60,13 @@ final class PhabricatorConduitTokenQuery
$this->objectPHIDs); $this->objectPHIDs);
} }
if ($this->tokens !== null) {
$where[] = qsprintf(
$conn_r,
'token IN (%Ls)',
$this->tokens);
}
if ($this->expired !== null) { if ($this->expired !== null) {
if ($this->expired) { if ($this->expired) {
$where[] = qsprintf( $where[] = qsprintf(