1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-10 23:01:04 +01:00

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
This commit is contained in:
epriestley 2016-04-03 08:25:02 -07:00
parent 694a8543d8
commit 60133b6fa5
11 changed files with 138 additions and 81 deletions

View file

@ -65,10 +65,6 @@ final class ConduitCall extends Phobject {
return $this->handler->shouldAllowUnguardedWrites(); return $this->handler->shouldAllowUnguardedWrites();
} }
public function getRequiredScope() {
return $this->handler->getRequiredScope();
}
public function getErrorDescription($code) { public function getErrorDescription($code) {
return $this->handler->getErrorDescription($code); return $this->handler->getErrorDescription($code);
} }

View file

@ -45,7 +45,6 @@ final class PhabricatorConduitAPIController
$auth_error = null; $auth_error = null;
$conduit_username = '-'; $conduit_username = '-';
if ($call->shouldRequireAuthentication()) { if ($call->shouldRequireAuthentication()) {
$metadata['scope'] = $call->getRequiredScope();
$auth_error = $this->authenticateUser($api_request, $metadata, $method); $auth_error = $this->authenticateUser($api_request, $metadata, $method);
// If we've explicitly authenticated the user here and either done // If we've explicitly authenticated the user here and either done
// CSRF validation or are using a non-web authentication mechanism. // CSRF validation or are using a non-web authentication mechanism.
@ -185,11 +184,6 @@ final class PhabricatorConduitAPIController
// First, verify the signature. // First, verify the signature.
try { try {
$protocol_data = $metadata; $protocol_data = $metadata;
// TODO: We should stop writing this into the protocol data when
// processing a request.
unset($protocol_data['scope']);
ConduitClient::verifySignature( ConduitClient::verifySignature(
$method, $method,
$api_request->getAllParameters(), $api_request->getAllParameters(),
@ -362,11 +356,8 @@ final class PhabricatorConduitAPIController
$user); $user);
} }
// handle oauth
$access_token = idx($metadata, 'access_token'); $access_token = idx($metadata, 'access_token');
$method_scope = idx($metadata, 'scope'); if ($access_token) {
if ($access_token &&
$method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) {
$token = id(new PhabricatorOAuthServerAccessToken()) $token = id(new PhabricatorOAuthServerAccessToken())
->loadOneWhere('token = %s', $access_token); ->loadOneWhere('token = %s', $access_token);
if (!$token) { if (!$token) {
@ -377,25 +368,33 @@ final class PhabricatorConduitAPIController
} }
$oauth_server = new PhabricatorOAuthServer(); $oauth_server = new PhabricatorOAuthServer();
$valid = $oauth_server->validateAccessToken($token, $authorization = $oauth_server->authorizeToken($token);
$method_scope); if (!$authorization) {
if (!$valid) {
return array( return array(
'ERR-INVALID-AUTH', 'ERR-INVALID-AUTH',
pht('Access token is invalid.'), pht('Access token is invalid or expired.'),
); );
} }
// valid token, so let's log in the user! $user = id(new PhabricatorPeopleQuery())
$user_phid = $token->getUserPHID(); ->setViewer(PhabricatorUser::getOmnipotentUser())
$user = id(new PhabricatorUser()) ->withPHIDs(array($token->getUserPHID()))
->loadOneWhere('phid = %s', $user_phid); ->executeOne();
if (!$user) { if (!$user) {
return array( return array(
'ERR-INVALID-AUTH', 'ERR-INVALID-AUTH',
pht('Access token is for invalid user.'), pht('Access token is for invalid user.'),
); );
} }
$ok = $this->authorizeOAuthMethodAccess($authorization, $method);
if (!$ok) {
return array(
'ERR-OAUTH-ACCESS',
pht('You do not have authorization to call this method.'),
);
}
return $this->validateAuthenticatedUser( return $this->validateAuthenticatedUser(
$api_request, $api_request,
$user); $user);
@ -642,4 +641,30 @@ final class PhabricatorConduitAPIController
return array($metadata, $params); return array($metadata, $params);
} }
private function authorizeOAuthMethodAccess(
PhabricatorOAuthClientAuthorization $authorization,
$method_name) {
$method = ConduitAPIMethod::getConduitMethod($method_name);
if (!$method) {
return false;
}
$required_scope = $method->getRequiredScope();
switch ($required_scope) {
case ConduitAPIMethod::SCOPE_ALWAYS:
return true;
case ConduitAPIMethod::SCOPE_NEVER:
return false;
}
$authorization_scope = $authorization->getScope();
if (!empty($authorization_scope[$required_scope])) {
return true;
}
return false;
}
} }

View file

@ -142,6 +142,36 @@ final class PhabricatorConduitConsoleController
pht('Errors'), pht('Errors'),
$error_description); $error_description);
$scope = $method->getRequiredScope();
switch ($scope) {
case ConduitAPIMethod::SCOPE_ALWAYS:
$oauth_icon = 'fa-globe green';
$oauth_description = pht(
'OAuth clients may always call this method.');
break;
case ConduitAPIMethod::SCOPE_NEVER:
$oauth_icon = 'fa-ban red';
$oauth_description = pht(
'OAuth clients may never call this method.');
break;
default:
$oauth_icon = 'fa-unlock-alt blue';
$oauth_description = pht(
'OAuth clients may call this method after requesting access to '.
'the "%s" scope.',
$scope);
break;
}
$view->addProperty(
pht('OAuth Scope'),
array(
id(new PHUIIconView())->setIcon($oauth_icon),
' ',
$oauth_description,
));
$view->addSectionHeader( $view->addSectionHeader(
pht('Description'), PHUIPropertyListView::ICON_SUMMARY); pht('Description'), PHUIPropertyListView::ICON_SUMMARY);
$view->addTextContent( $view->addTextContent(

View file

@ -15,6 +15,8 @@ abstract class ConduitAPIMethod
const METHOD_STATUS_UNSTABLE = 'unstable'; const METHOD_STATUS_UNSTABLE = 'unstable';
const METHOD_STATUS_DEPRECATED = 'deprecated'; const METHOD_STATUS_DEPRECATED = 'deprecated';
const SCOPE_NEVER = 'scope.never';
const SCOPE_ALWAYS = 'scope.always';
/** /**
* Get a short, human-readable text summary of the method. * Get a short, human-readable text summary of the method.
@ -108,8 +110,7 @@ abstract class ConduitAPIMethod
} }
public function getRequiredScope() { public function getRequiredScope() {
// by default, conduit methods are not accessible via OAuth return self::SCOPE_NEVER;
return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE;
} }
public function executeMethod(ConduitAPIRequest $request) { public function executeMethod(ConduitAPIRequest $request) {

View file

@ -24,6 +24,10 @@ final class ConduitGetCapabilitiesConduitAPIMethod extends ConduitAPIMethod {
return 'dict<string, any>'; return 'dict<string, any>';
} }
public function getRequiredScope() {
return self::SCOPE_ALWAYS;
}
protected function execute(ConduitAPIRequest $request) { protected function execute(ConduitAPIRequest $request) {
$authentication = array( $authentication = array(
'token', 'token',

View file

@ -18,6 +18,10 @@ final class ConduitQueryConduitAPIMethod extends ConduitAPIMethod {
return 'dict<dict>'; return 'dict<dict>';
} }
public function getRequiredScope() {
return self::SCOPE_ALWAYS;
}
protected function execute(ConduitAPIRequest $request) { protected function execute(ConduitAPIRequest $request) {
$methods = id(new PhabricatorConduitMethodQuery()) $methods = id(new PhabricatorConduitMethodQuery())
->setViewer($request->getUser()) ->setViewer($request->getUser())

View file

@ -29,7 +29,6 @@
final class PhabricatorOAuthServer extends Phobject { final class PhabricatorOAuthServer extends Phobject {
const AUTHORIZATION_CODE_TIMEOUT = 300; const AUTHORIZATION_CODE_TIMEOUT = 300;
const ACCESS_TOKEN_TIMEOUT = 3600;
private $user; private $user;
private $client; private $client;
@ -158,39 +157,35 @@ final class PhabricatorOAuthServer extends Phobject {
/** /**
* @task token * @task token
*/ */
public function validateAccessToken( public function authorizeToken(
PhabricatorOAuthServerAccessToken $token, PhabricatorOAuthServerAccessToken $token) {
$required_scope) {
$created_time = $token->getDateCreated(); $user_phid = $token->getUserPHID();
$must_be_used_by = $created_time + self::ACCESS_TOKEN_TIMEOUT; $client_phid = $token->getClientPHID();
$expired = time() > $must_be_used_by;
$authorization = id(new PhabricatorOAuthClientAuthorization())
->loadOneWhere(
'userPHID = %s AND clientPHID = %s',
$token->getUserPHID(),
$token->getClientPHID());
$authorization = id(new PhabricatorOAuthClientAuthorizationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUserPHIDs(array($user_phid))
->withClientPHIDs(array($client_phid))
->executeOne();
if (!$authorization) { if (!$authorization) {
return false; return null;
}
$token_scope = $authorization->getScope();
if (!isset($token_scope[$required_scope])) {
return false;
} }
$valid = true; // TODO: This should probably be reworked; expiration should be an
if ($expired) { // exclusive property of the token. For now, this logic reads: tokens for
$valid = false; // authorizations with "offline_access" never expire.
// check if the scope includes "offline_access", which makes the
// token valid despite being expired $is_expired = $token->isExpired();
if (isset( if ($is_expired) {
$token_scope[PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS])) { $offline_access = PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS;
$valid = true; $authorization_scope = $authorization->getScope();
if (empty($authorization_scope[$offline_access])) {
return null;
} }
} }
return $valid; return $authorization;
} }
/** /**

View file

@ -4,13 +4,7 @@ final class PhabricatorOAuthServerScope extends Phobject {
const SCOPE_OFFLINE_ACCESS = 'offline_access'; const SCOPE_OFFLINE_ACCESS = 'offline_access';
const SCOPE_WHOAMI = 'whoami'; const SCOPE_WHOAMI = 'whoami';
const SCOPE_NOT_ACCESSIBLE = 'not_accessible';
/*
* Note this does not contain SCOPE_NOT_ACCESSIBLE which is magic
* used to simplify code for data that is not currently accessible
* via OAuth.
*/
public static function getScopesDict() { public static function getScopesDict() {
return array( return array(
self::SCOPE_OFFLINE_ACCESS => 1, self::SCOPE_OFFLINE_ACCESS => 1,

View file

@ -144,7 +144,7 @@ final class PhabricatorOAuthServerTokenController
$result = array( $result = array(
'access_token' => $access_token->getToken(), 'access_token' => $access_token->getToken(),
'token_type' => 'Bearer', 'token_type' => 'Bearer',
'expires_in' => PhabricatorOAuthServer::ACCESS_TOKEN_TIMEOUT, 'expires_in' => $access_token->getExpiresDuration(),
); );
return $response->setContent($result); return $response->setContent($result);
} catch (Exception $e) { } catch (Exception $e) {

View file

@ -7,7 +7,7 @@ final class PhabricatorOAuthClientAuthorizationQuery
private $userPHIDs; private $userPHIDs;
private $clientPHIDs; private $clientPHIDs;
public function witHPHIDs(array $phids) { public function withPHIDs(array $phids) {
$this->phids = $phids; $this->phids = $phids;
return $this; return $this;
} }
@ -22,19 +22,12 @@ final class PhabricatorOAuthClientAuthorizationQuery
return $this; return $this;
} }
public function newResultObject() {
return new PhabricatorOAuthClientAuthorization();
}
protected function loadPage() { protected function loadPage() {
$table = new PhabricatorOAuthClientAuthorization(); return $this->loadStandardPage($this->newResultObject());
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T auth %Q %Q %Q',
$table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
} }
protected function willFilterPage(array $authorizations) { protected function willFilterPage(array $authorizations) {
@ -49,43 +42,44 @@ final class PhabricatorOAuthClientAuthorizationQuery
foreach ($authorizations as $key => $authorization) { foreach ($authorizations as $key => $authorization) {
$client = idx($clients, $authorization->getClientPHID()); $client = idx($clients, $authorization->getClientPHID());
if (!$client) { if (!$client) {
$this->didRejectResult($authorization);
unset($authorizations[$key]); unset($authorizations[$key]);
continue; continue;
} }
$authorization->attachClient($client); $authorization->attachClient($client);
} }
return $authorizations; return $authorizations;
} }
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = array(); $where = parent::buildWhereClauseParts($conn);
if ($this->phids) { if ($this->phids !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn,
'phid IN (%Ls)', 'phid IN (%Ls)',
$this->phids); $this->phids);
} }
if ($this->userPHIDs) { if ($this->userPHIDs !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn,
'userPHID IN (%Ls)', 'userPHID IN (%Ls)',
$this->userPHIDs); $this->userPHIDs);
} }
if ($this->clientPHIDs) { if ($this->clientPHIDs !== null) {
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn,
'clientPHID IN (%Ls)', 'clientPHID IN (%Ls)',
$this->clientPHIDs); $this->clientPHIDs);
} }
$where[] = $this->buildPagingClause($conn_r); return $where;
return $this->formatWhereClause($where);
} }
public function getQueryApplicationClass() { public function getQueryApplicationClass() {

View file

@ -22,4 +22,18 @@ final class PhabricatorOAuthServerAccessToken
) + parent::getConfiguration(); ) + parent::getConfiguration();
} }
public function isExpired() {
$now = PhabricatorTime::getNow();
$expires_epoch = $this->getExpiresEpoch();
return ($now > $expires_epoch);
}
public function getExpiresEpoch() {
return $this->getDateCreated() + 3600;
}
public function getExpiresDuration() {
return PhabricatorTime::getNow() - $this->getExpiresEpoch();
}
} }