diff --git a/src/applications/conduit/call/ConduitCall.php b/src/applications/conduit/call/ConduitCall.php index 89bddad542..017d96ae8b 100644 --- a/src/applications/conduit/call/ConduitCall.php +++ b/src/applications/conduit/call/ConduitCall.php @@ -1,159 +1,155 @@ 'value')); * $call->setUser($user); * $result = $call->execute(); * */ final class ConduitCall extends Phobject { private $method; private $handler; private $request; private $user; public function __construct($method, array $params) { $this->method = $method; $this->handler = $this->buildMethodHandler($method); $param_types = $this->handler->getParamTypes(); 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) { throw new ConduitException( pht( 'API Method "%s" does not define these parameters: %s.', $method, "'".implode("', '", array_keys($invalid_params))."'")); } $this->request = new ConduitAPIRequest($params); } public function getAPIRequest() { return $this->request; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function getUser() { return $this->user; } public function shouldRequireAuthentication() { return $this->handler->shouldRequireAuthentication(); } public function shouldAllowUnguardedWrites() { return $this->handler->shouldAllowUnguardedWrites(); } - public function getRequiredScope() { - return $this->handler->getRequiredScope(); - } - public function getErrorDescription($code) { return $this->handler->getErrorDescription($code); } public function execute() { $profiler = PhutilServiceProfiler::getInstance(); $call_id = $profiler->beginServiceCall( array( 'type' => 'conduit', 'method' => $this->method, )); try { $result = $this->executeMethod(); } catch (Exception $ex) { $profiler->endServiceCall($call_id, array()); throw $ex; } $profiler->endServiceCall($call_id, array()); return $result; } private function executeMethod() { $user = $this->getUser(); if (!$user) { $user = new PhabricatorUser(); } $this->request->setUser($user); if (!$this->shouldRequireAuthentication()) { // No auth requirement here. } else { $allow_public = $this->handler->shouldAllowPublic() && PhabricatorEnv::getEnvConfig('policy.allow-public'); if (!$allow_public) { if (!$user->isLoggedIn() && !$user->isOmnipotent()) { // TODO: As per below, this should get centralized and cleaned up. throw new ConduitException('ERR-INVALID-AUTH'); } } // TODO: This would be slightly cleaner by just using a Query, but the // Conduit auth workflow requires the Call and User be built separately. // Just do it this way for the moment. $application = $this->handler->getApplication(); if ($application) { $can_view = PhabricatorPolicyFilter::hasCapability( $user, $application, PhabricatorPolicyCapability::CAN_VIEW); if (!$can_view) { throw new ConduitException( pht( 'You do not have access to the application which provides this '. 'API method.')); } } } return $this->handler->executeMethod($this->request); } protected function buildMethodHandler($method_name) { $method = ConduitAPIMethod::getConduitMethod($method_name); if (!$method) { throw new ConduitMethodDoesNotExistException($method_name); } $application = $method->getApplication(); if ($application && !$application->isInstalled()) { $app_name = $application->getName(); throw new ConduitApplicationNotInstalledException($method, $app_name); } return $method; } public function getMethodImplementation() { return $this->handler; } } diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index ce215468d9..ac0d115a40 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -1,645 +1,670 @@ getURIData('method'); $time_start = microtime(true); $api_request = null; $method_implementation = null; $log = new PhabricatorConduitMethodCallLog(); $log->setMethod($method); $metadata = array(); $multimeter = MultimeterControl::getInstance(); if ($multimeter) { $multimeter->setEventContext('api.'.$method); } try { list($metadata, $params) = $this->decodeConduitParams($request, $method); $call = new ConduitCall($method, $params); $method_implementation = $call->getMethodImplementation(); $result = null; // TODO: The relationship between ConduitAPIRequest and ConduitCall is a // little odd here and could probably be improved. Specifically, the // APIRequest is a sub-object of the Call, which does not parallel the // role of AphrontRequest (which is an indepenent object). // In particular, the setUser() and getUser() existing independently on // the Call and APIRequest is very awkward. $api_request = $call->getAPIRequest(); $allow_unguarded_writes = false; $auth_error = null; $conduit_username = '-'; if ($call->shouldRequireAuthentication()) { - $metadata['scope'] = $call->getRequiredScope(); $auth_error = $this->authenticateUser($api_request, $metadata, $method); // If we've explicitly authenticated the user here and either done // CSRF validation or are using a non-web authentication mechanism. $allow_unguarded_writes = true; if ($auth_error === null) { $conduit_user = $api_request->getUser(); if ($conduit_user && $conduit_user->getPHID()) { $conduit_username = $conduit_user->getUsername(); } $call->setUser($api_request->getUser()); } } $access_log = PhabricatorAccessLog::getLog(); if ($access_log) { $access_log->setData( array( 'u' => $conduit_username, 'm' => $method, )); } if ($call->shouldAllowUnguardedWrites()) { $allow_unguarded_writes = true; } if ($auth_error === null) { if ($allow_unguarded_writes) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); } try { $result = $call->execute(); $error_code = null; $error_info = null; } catch (ConduitException $ex) { $result = null; $error_code = $ex->getMessage(); if ($ex->getErrorDescription()) { $error_info = $ex->getErrorDescription(); } else { $error_info = $call->getErrorDescription($error_code); } } if ($allow_unguarded_writes) { unset($unguarded); } } else { list($error_code, $error_info) = $auth_error; } } catch (Exception $ex) { if (!($ex instanceof ConduitMethodNotFoundException)) { phlog($ex); } $result = null; $error_code = ($ex instanceof ConduitException ? 'ERR-CONDUIT-CALL' : 'ERR-CONDUIT-CORE'); $error_info = $ex->getMessage(); } $time_end = microtime(true); $log ->setCallerPHID( isset($conduit_user) ? $conduit_user->getPHID() : null) ->setError((string)$error_code) ->setDuration(1000000 * ($time_end - $time_start)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $log->save(); unset($unguarded); $response = id(new ConduitAPIResponse()) ->setResult($result) ->setErrorCode($error_code) ->setErrorInfo($error_info); switch ($request->getStr('output')) { case 'human': return $this->buildHumanReadableResponse( $method, $api_request, $response->toDictionary(), $method_implementation); case 'json': default: return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($response->toDictionary()); } } /** * Authenticate the client making the request to a Phabricator user account. * * @param ConduitAPIRequest Request being executed. * @param dict Request metadata. * @return null|pair Null to indicate successful authentication, or * an error code and error message pair. */ private function authenticateUser( ConduitAPIRequest $api_request, array $metadata, $method) { $request = $this->getRequest(); if ($request->getUser()->getPHID()) { $request->validateCSRF(); return $this->validateAuthenticatedUser( $api_request, $request->getUser()); } $auth_type = idx($metadata, 'auth.type'); if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) { $host = idx($metadata, 'auth.host'); if (!$host) { return array( 'ERR-INVALID-AUTH', pht( 'Request is missing required "%s" parameter.', 'auth.host'), ); } // TODO: Validate that we are the host! $raw_key = idx($metadata, 'auth.key'); $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key); $ssl_public_key = $public_key->toPKCS8(); // First, verify the signature. try { $protocol_data = $metadata; - - // TODO: We should stop writing this into the protocol data when - // processing a request. - unset($protocol_data['scope']); - ConduitClient::verifySignature( $method, $api_request->getAllParameters(), $protocol_data, $ssl_public_key); } catch (Exception $ex) { return array( 'ERR-INVALID-AUTH', pht( 'Signature verification failure. %s', $ex->getMessage()), ); } // If the signature is valid, find the user or device which is // associated with this public key. $stored_key = id(new PhabricatorAuthSSHKeyQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withKeys(array($public_key)) ->executeOne(); if (!$stored_key) { return array( 'ERR-INVALID-AUTH', pht('No user or device is associated with that public key.'), ); } $object = $stored_key->getObject(); if ($object instanceof PhabricatorUser) { $user = $object; } else { if (!$stored_key->getIsTrusted()) { return array( 'ERR-INVALID-AUTH', pht( 'The key which signed this request is not trusted. Only '. 'trusted keys can be used to sign API calls.'), ); } if (!PhabricatorEnv::isClusterRemoteAddress()) { return array( 'ERR-INVALID-AUTH', pht( 'This request originates from outside of the Phabricator '. 'cluster address range. Requests signed with trusted '. 'device keys must originate from within the cluster.'), ); } $user = PhabricatorUser::getOmnipotentUser(); // Flag this as an intracluster request. $api_request->setIsClusterRequest(true); } return $this->validateAuthenticatedUser( $api_request, $user); } else if ($auth_type === null) { // No specified authentication type, continue with other authentication // methods below. } else { return array( 'ERR-INVALID-AUTH', pht( 'Provided "%s" ("%s") is not recognized.', 'auth.type', $auth_type), ); } $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.', $token_string), ); } $type = head(explode('-', $token_string)); $valid_types = PhabricatorConduitToken::getAllTokenTypes(); $valid_types = array_fuse($valid_types); if (empty($valid_types[$type])) { return array( 'ERR-INVALID-AUTH', pht( 'API token "%s" has the wrong format. API tokens should be '. '32 characters long and begin with one of these prefixes: %s.', $token_string, implode(', ', $valid_types)), ); } $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), ); } } // If this is a "cli-" token, it expires shortly after it is generated // by default. Once it is actually used, we extend its lifetime and make // it permanent. This allows stray tokens to get cleaned up automatically // if they aren't being used. if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) { if ($token->getExpires()) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $token->setExpires(null); $token->save(); unset($unguarded); } } // If this is a "clr-" token, Phabricator must be configured in cluster // mode and the remote address must be a cluster node. if ($token->getTokenType() == PhabricatorConduitToken::TYPE_CLUSTER) { if (!PhabricatorEnv::isClusterRemoteAddress()) { return array( 'ERR-INVALID-AUTH', pht( 'This request originates from outside of the Phabricator '. 'cluster address range. Requests signed with cluster API '. 'tokens must originate from within the cluster.'), ); } // Flag this as an intracluster request. $api_request->setIsClusterRequest(true); } $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 $access_token = idx($metadata, 'access_token'); - $method_scope = idx($metadata, 'scope'); - if ($access_token && - $method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) { + if ($access_token) { $token = id(new PhabricatorOAuthServerAccessToken()) ->loadOneWhere('token = %s', $access_token); if (!$token) { return array( 'ERR-INVALID-AUTH', pht('Access token does not exist.'), ); } $oauth_server = new PhabricatorOAuthServer(); - $valid = $oauth_server->validateAccessToken($token, - $method_scope); - if (!$valid) { + $authorization = $oauth_server->authorizeToken($token); + if (!$authorization) { return array( '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_phid = $token->getUserPHID(); - $user = id(new PhabricatorUser()) - ->loadOneWhere('phid = %s', $user_phid); + $user = id(new PhabricatorPeopleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($token->getUserPHID())) + ->executeOne(); if (!$user) { return array( 'ERR-INVALID-AUTH', 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( $api_request, $user); } // Handle sessionless auth. // TODO: This is super messy. // TODO: Remove this in favor of token-based auth. if (isset($metadata['authUser'])) { $user = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $metadata['authUser']); if (!$user) { return array( 'ERR-INVALID-AUTH', pht('Authentication is invalid.'), ); } $token = idx($metadata, 'authToken'); $signature = idx($metadata, 'authSignature'); $certificate = $user->getConduitCertificate(); $hash = sha1($token.$certificate); if (!phutil_hashes_are_identical($hash, $signature)) { return array( 'ERR-INVALID-AUTH', pht('Authentication is invalid.'), ); } return $this->validateAuthenticatedUser( $api_request, $user); } // Handle session-based auth. // TODO: Remove this in favor of token-based auth. $session_key = idx($metadata, 'sessionKey'); if (!$session_key) { return array( 'ERR-INVALID-SESSION', pht('Session key is not present.'), ); } $user = id(new PhabricatorAuthSessionEngine()) ->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key); if (!$user) { return array( 'ERR-INVALID-SESSION', pht('Session key is invalid.'), ); } return $this->validateAuthenticatedUser( $api_request, $user); } private function validateAuthenticatedUser( ConduitAPIRequest $request, PhabricatorUser $user) { if (!$user->canEstablishAPISessions()) { return array( 'ERR-INVALID-AUTH', pht('User account is not permitted to use the API.'), ); } $request->setUser($user); return null; } private function buildHumanReadableResponse( $method, ConduitAPIRequest $request = null, $result = null, ConduitAPIMethod $method_implementation = null) { $param_rows = array(); $param_rows[] = array('Method', $this->renderAPIValue($method)); if ($request) { foreach ($request->getAllParameters() as $key => $value) { $param_rows[] = array( $key, $this->renderAPIValue($value), ); } } $param_table = new AphrontTableView($param_rows); $param_table->setColumnClasses( array( 'header', 'wide', )); $result_rows = array(); foreach ($result as $key => $value) { $result_rows[] = array( $key, $this->renderAPIValue($value), ); } $result_table = new AphrontTableView($result_rows); $result_table->setColumnClasses( array( 'header', 'wide', )); $param_panel = new PHUIObjectBoxView(); $param_panel->setHeaderText(pht('Method Parameters')); $param_panel->setTable($param_table); $result_panel = new PHUIObjectBoxView(); $result_panel->setHeaderText(pht('Method Result')); $result_panel->setTable($result_table); $method_uri = $this->getApplicationURI('method/'.$method.'/'); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($method, $method_uri) ->addTextCrumb(pht('Call')); $example_panel = null; if ($request && $method_implementation) { $params = $request->getAllParameters(); $example_panel = $this->renderExampleBox( $method_implementation, $params); } return $this->buildApplicationPage( array( $crumbs, $param_panel, $result_panel, $example_panel, ), array( 'title' => pht('Method Call Result'), )); } private function renderAPIValue($value) { $json = new PhutilJSON(); if (is_array($value)) { $value = $json->encodeFormatted($value); } $value = phutil_tag( 'pre', array('style' => 'white-space: pre-wrap;'), $value); return $value; } private function decodeConduitParams( AphrontRequest $request, $method) { // Look for parameters from the Conduit API Console, which are encoded // as HTTP POST parameters in an array, e.g.: // // params[name]=value¶ms[name2]=value2 // // The fields are individually JSON encoded, since we require users to // enter JSON so that we avoid type ambiguity. $params = $request->getArr('params', null); if ($params !== null) { foreach ($params as $key => $value) { if ($value == '') { // Interpret empty string null (e.g., the user didn't type anything // into the box). $value = 'null'; } $decoded_value = json_decode($value, true); if ($decoded_value === null && strtolower($value) != 'null') { // When json_decode() fails, it returns null. This almost certainly // indicates that a user was using the web UI and didn't put quotes // around a string value. We can either do what we think they meant // (treat it as a string) or fail. For now, err on the side of // caution and fail. In the future, if we make the Conduit API // actually do type checking, it might be reasonable to treat it as // a string if the parameter type is string. throw new Exception( pht( "The value for parameter '%s' is not valid JSON. All ". "parameters must be encoded as JSON values, including strings ". "(which means you need to surround them in double quotes). ". "Check your syntax. Value was: %s.", $key, $value)); } $params[$key] = $decoded_value; } $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']); return array($metadata, $params); } // Otherwise, look for a single parameter called 'params' which has the // entire param dictionary JSON encoded. $params_json = $request->getStr('params'); if (strlen($params_json)) { $params = null; try { $params = phutil_json_decode($params_json); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht( "Invalid parameter information was passed to method '%s'.", $method), $ex); } $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']); return array($metadata, $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); } + 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; + } + + } diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php index a0481126c6..f11415795e 100644 --- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php +++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php @@ -1,154 +1,184 @@ getViewer(); $method_name = $request->getURIData('method'); $method = id(new PhabricatorConduitMethodQuery()) ->setViewer($viewer) ->withMethods(array($method_name)) ->executeOne(); if (!$method) { return new Aphront404Response(); } $method->setViewer($viewer); $call_uri = '/api/'.$method->getAPIMethodName(); $status = $method->getMethodStatus(); $reason = $method->getMethodStatusDescription(); $errors = array(); switch ($status) { case ConduitAPIMethod::METHOD_STATUS_DEPRECATED: $reason = nonempty($reason, pht('This method is deprecated.')); $errors[] = pht('Deprecated Method: %s', $reason); break; case ConduitAPIMethod::METHOD_STATUS_UNSTABLE: $reason = nonempty( $reason, pht( 'This method is new and unstable. Its interface is subject '. 'to change.')); $errors[] = pht('Unstable Method: %s', $reason); break; } $form = id(new AphrontFormView()) ->setAction($call_uri) ->setUser($request->getUser()) ->appendRemarkupInstructions( pht( 'Enter parameters using **JSON**. For instance, to enter a '. 'list, type: `%s`', '["apple", "banana", "cherry"]')); $params = $method->getParamTypes(); foreach ($params as $param => $desc) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel($param) ->setName("params[{$param}]") ->setCaption($desc)); } $must_login = !$viewer->isLoggedIn() && $method->shouldRequireAuthentication(); if ($must_login) { $errors[] = pht( 'Login Required: This method requires authentication. You must '. 'log in before you can make calls to it.'); } else { $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Output Format')) ->setName('output') ->setOptions( array( 'human' => pht('Human Readable'), 'json' => pht('JSON'), ))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Call Method'))); } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($method->getAPIMethodName()); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Call Method')) ->setForm($form); $content = array(); $properties = $this->buildMethodProperties($method); $info_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('API Method: %s', $method->getAPIMethodName())) ->setFormErrors($errors) ->appendChild($properties); $content[] = $info_box; $content[] = $method->getMethodDocumentation(); $content[] = $form_box; $content[] = $this->renderExampleBox($method, null); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($method->getAPIMethodName()); return $this->buildApplicationPage( array( $crumbs, $content, ), array( 'title' => $method->getAPIMethodName(), )); } private function buildMethodProperties(ConduitAPIMethod $method) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()); $view->addProperty( pht('Returns'), $method->getReturnType()); $error_types = $method->getErrorTypes(); $error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.'); $error_description = array(); foreach ($error_types as $error => $meaning) { $error_description[] = hsprintf( '
  • %s: %s
  • ', $error, $meaning); } $error_description = phutil_tag('ul', array(), $error_description); $view->addProperty( pht('Errors'), $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( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $view->addTextContent( new PHUIRemarkupView($viewer, $method->getMethodDescription())); return $view; } } diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index 4fd7c416bb..5b6c16bb93 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -1,406 +1,407 @@ getMethodDescription(); } /** * Get a detailed description of the method. * * This method should return remarkup. * * @return string Detailed description of the method. * @task info */ abstract public function getMethodDescription(); public function getMethodDocumentation() { return null; } abstract protected function defineParamTypes(); abstract protected function defineReturnType(); protected function defineErrorTypes() { return array(); } abstract protected function execute(ConduitAPIRequest $request); public function getParamTypes() { $types = $this->defineParamTypes(); $query = $this->newQueryObject(); if ($query) { $types['order'] = 'optional order'; $types += $this->getPagerParamTypes(); } return $types; } public function getReturnType() { return $this->defineReturnType(); } public function getErrorTypes() { return $this->defineErrorTypes(); } /** * This is mostly for compatibility with * @{class:PhabricatorCursorPagedPolicyAwareQuery}. */ public function getID() { return $this->getAPIMethodName(); } /** * Get the status for this method (e.g., stable, unstable or deprecated). * Should return a METHOD_STATUS_* constant. By default, methods are * "stable". * * @return const METHOD_STATUS_* constant. * @task status */ public function getMethodStatus() { return self::METHOD_STATUS_STABLE; } /** * Optional description to supplement the method status. In particular, if * a method is deprecated, you can return a string here describing the reason * for deprecation and stable alternatives. * * @return string|null Description of the method status, if available. * @task status */ public function getMethodStatusDescription() { return null; } public function getErrorDescription($error_code) { return idx($this->getErrorTypes(), $error_code, pht('Unknown Error')); } public function getRequiredScope() { - // by default, conduit methods are not accessible via OAuth - return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE; + return self::SCOPE_NEVER; } public function executeMethod(ConduitAPIRequest $request) { $this->setViewer($request->getUser()); return $this->execute($request); } abstract public function getAPIMethodName(); /** * Return a key which sorts methods by application name, then method status, * then method name. */ public function getSortOrder() { $name = $this->getAPIMethodName(); $map = array( self::METHOD_STATUS_STABLE => 0, self::METHOD_STATUS_UNSTABLE => 1, self::METHOD_STATUS_DEPRECATED => 2, ); $ord = idx($map, $this->getMethodStatus(), 0); list($head, $tail) = explode('.', $name, 2); return "{$head}.{$ord}.{$tail}"; } public static function getMethodStatusMap() { $map = array( self::METHOD_STATUS_STABLE => pht('Stable'), self::METHOD_STATUS_UNSTABLE => pht('Unstable'), self::METHOD_STATUS_DEPRECATED => pht('Deprecated'), ); return $map; } public function getApplicationName() { return head(explode('.', $this->getAPIMethodName(), 2)); } public static function loadAllConduitMethods() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getAPIMethodName') ->execute(); } public static function getConduitMethod($method_name) { $method_map = self::loadAllConduitMethods(); return idx($method_map, $method_name); } public function shouldRequireAuthentication() { return true; } public function shouldAllowPublic() { return false; } public function shouldAllowUnguardedWrites() { return false; } /** * Optionally, return a @{class:PhabricatorApplication} which this call is * part of. The call will be disabled when the application is uninstalled. * * @return PhabricatorApplication|null Related application. */ public function getApplication() { return null; } protected function formatStringConstants($constants) { foreach ($constants as $key => $value) { $constants[$key] = '"'.$value.'"'; } $constants = implode(', ', $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; } final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } final public function getViewer() { return $this->viewer; } /* -( Paging Results )----------------------------------------------------- */ /** * @task pager */ protected function getPagerParamTypes() { return array( 'before' => 'optional string', 'after' => 'optional string', 'limit' => 'optional int (default = 100)', ); } /** * @task pager */ protected function newPager(ConduitAPIRequest $request) { $limit = $request->getValue('limit', 100); $limit = min(1000, $limit); $limit = max(1, $limit); $pager = id(new AphrontCursorPagerView()) ->setPageSize($limit); $before_id = $request->getValue('before'); if ($before_id !== null) { $pager->setBeforeID($before_id); } $after_id = $request->getValue('after'); if ($after_id !== null) { $pager->setAfterID($after_id); } return $pager; } /** * @task pager */ protected function addPagerResults( array $results, AphrontCursorPagerView $pager) { $results['cursor'] = array( 'limit' => $pager->getPageSize(), 'after' => $pager->getNextPageID(), 'before' => $pager->getPrevPageID(), ); return $results; } /* -( Implementing Query Methods )----------------------------------------- */ public function newQueryObject() { return null; } protected function newQueryForRequest(ConduitAPIRequest $request) { $query = $this->newQueryObject(); if (!$query) { throw new Exception( pht( 'You can not call newQueryFromRequest() in this method ("%s") '. 'because it does not implement newQueryObject().', get_class($this))); } if (!($query instanceof PhabricatorCursorPagedPolicyAwareQuery)) { throw new Exception( pht( 'Call to method newQueryObject() did not return an object of class '. '"%s".', 'PhabricatorCursorPagedPolicyAwareQuery')); } $query->setViewer($request->getUser()); $order = $request->getValue('order'); if ($order !== null) { if (is_scalar($order)) { $query->setOrder($order); } else { $query->setOrderVector($order); } } return $query; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getPHID() { return null; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // Application methods get application visibility; other methods get open // visibility. $application = $this->getApplication(); if ($application) { return $application->getPolicy($capability); } return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if (!$this->shouldRequireAuthentication()) { // Make unauthenticated methods universally visible. return true; } return false; } public function describeAutomaticCapability($capability) { return null; } protected function hasApplicationCapability( $capability, PhabricatorUser $viewer) { $application = $this->getApplication(); if (!$application) { return false; } return PhabricatorPolicyFilter::hasCapability( $viewer, $application, $capability); } protected function requireApplicationCapability( $capability, PhabricatorUser $viewer) { $application = $this->getApplication(); if (!$application) { return; } PhabricatorPolicyFilter::requireCapability( $viewer, $this->getApplication(), $capability); } } diff --git a/src/applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php b/src/applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php index 44acf3e0d3..2cb84b1f83 100644 --- a/src/applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitGetCapabilitiesConduitAPIMethod.php @@ -1,56 +1,60 @@ '; } + public function getRequiredScope() { + return self::SCOPE_ALWAYS; + } + protected function execute(ConduitAPIRequest $request) { $authentication = array( 'token', 'asymmetric', 'session', 'sessionless', ); $oauth_app = 'PhabricatorOAuthServerApplication'; if (PhabricatorApplication::isClassInstalled($oauth_app)) { $authentication[] = 'oauth'; } return array( 'authentication' => $authentication, 'signatures' => array( 'consign', ), 'input' => array( 'json', 'urlencoded', ), 'output' => array( 'json', 'human', ), ); } } diff --git a/src/applications/conduit/method/ConduitQueryConduitAPIMethod.php b/src/applications/conduit/method/ConduitQueryConduitAPIMethod.php index 04e8b6d05b..a63d004e5e 100644 --- a/src/applications/conduit/method/ConduitQueryConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitQueryConduitAPIMethod.php @@ -1,38 +1,42 @@ '; } + public function getRequiredScope() { + return self::SCOPE_ALWAYS; + } + protected function execute(ConduitAPIRequest $request) { $methods = id(new PhabricatorConduitMethodQuery()) ->setViewer($request->getUser()) ->execute(); $map = array(); foreach ($methods as $method) { $map[$method->getAPIMethodName()] = array( 'description' => $method->getMethodDescription(), 'params' => $method->getParamTypes(), 'return' => $method->getReturnType(), ); } return $map; } } diff --git a/src/applications/oauthserver/PhabricatorOAuthServer.php b/src/applications/oauthserver/PhabricatorOAuthServer.php index 38b2e34623..dc4a9ab2e9 100644 --- a/src/applications/oauthserver/PhabricatorOAuthServer.php +++ b/src/applications/oauthserver/PhabricatorOAuthServer.php @@ -1,272 +1,267 @@ user) { throw new PhutilInvalidStateException('setUser'); } return $this->user; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } private function getClient() { if (!$this->client) { throw new PhutilInvalidStateException('setClient'); } return $this->client; } public function setClient(PhabricatorOAuthServerClient $client) { $this->client = $client; return $this; } /** * @task auth * @return tuple */ public function userHasAuthorizedClient(array $scope) { $authorization = id(new PhabricatorOAuthClientAuthorization()) ->loadOneWhere( 'userPHID = %s AND clientPHID = %s', $this->getUser()->getPHID(), $this->getClient()->getPHID()); if (empty($authorization)) { return array(false, null); } if ($scope) { $missing_scope = array_diff_key($scope, $authorization->getScope()); } else { $missing_scope = false; } if ($missing_scope) { return array(false, $authorization); } return array(true, $authorization); } /** * @task auth */ public function authorizeClient(array $scope) { $authorization = new PhabricatorOAuthClientAuthorization(); $authorization->setUserPHID($this->getUser()->getPHID()); $authorization->setClientPHID($this->getClient()->getPHID()); $authorization->setScope($scope); $authorization->save(); return $authorization; } /** * @task auth */ public function generateAuthorizationCode(PhutilURI $redirect_uri) { $code = Filesystem::readRandomCharacters(32); $client = $this->getClient(); $authorization_code = new PhabricatorOAuthServerAuthorizationCode(); $authorization_code->setCode($code); $authorization_code->setClientPHID($client->getPHID()); $authorization_code->setClientSecret($client->getSecret()); $authorization_code->setUserPHID($this->getUser()->getPHID()); $authorization_code->setRedirectURI((string)$redirect_uri); $authorization_code->save(); return $authorization_code; } /** * @task token */ public function generateAccessToken() { $token = Filesystem::readRandomCharacters(32); $access_token = new PhabricatorOAuthServerAccessToken(); $access_token->setToken($token); $access_token->setUserPHID($this->getUser()->getPHID()); $access_token->setClientPHID($this->getClient()->getPHID()); $access_token->save(); return $access_token; } /** * @task token */ public function validateAuthorizationCode( PhabricatorOAuthServerAuthorizationCode $test_code, PhabricatorOAuthServerAuthorizationCode $valid_code) { // check that all the meta data matches if ($test_code->getClientPHID() != $valid_code->getClientPHID()) { return false; } if ($test_code->getClientSecret() != $valid_code->getClientSecret()) { return false; } // check that the authorization code hasn't timed out $created_time = $test_code->getDateCreated(); $must_be_used_by = $created_time + self::AUTHORIZATION_CODE_TIMEOUT; return (time() < $must_be_used_by); } /** * @task token */ - public function validateAccessToken( - PhabricatorOAuthServerAccessToken $token, - $required_scope) { - - $created_time = $token->getDateCreated(); - $must_be_used_by = $created_time + self::ACCESS_TOKEN_TIMEOUT; - $expired = time() > $must_be_used_by; - $authorization = id(new PhabricatorOAuthClientAuthorization()) - ->loadOneWhere( - 'userPHID = %s AND clientPHID = %s', - $token->getUserPHID(), - $token->getClientPHID()); + public function authorizeToken( + PhabricatorOAuthServerAccessToken $token) { + $user_phid = $token->getUserPHID(); + $client_phid = $token->getClientPHID(); + + $authorization = id(new PhabricatorOAuthClientAuthorizationQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withUserPHIDs(array($user_phid)) + ->withClientPHIDs(array($client_phid)) + ->executeOne(); if (!$authorization) { - return false; - } - $token_scope = $authorization->getScope(); - if (!isset($token_scope[$required_scope])) { - return false; + return null; } - $valid = true; - if ($expired) { - $valid = false; - // check if the scope includes "offline_access", which makes the - // token valid despite being expired - if (isset( - $token_scope[PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS])) { - $valid = true; + // TODO: This should probably be reworked; expiration should be an + // exclusive property of the token. For now, this logic reads: tokens for + // authorizations with "offline_access" never expire. + + $is_expired = $token->isExpired(); + if ($is_expired) { + $offline_access = PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS; + $authorization_scope = $authorization->getScope(); + if (empty($authorization_scope[$offline_access])) { + return null; } } - return $valid; + return $authorization; } /** * See http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2 * for details on what makes a given redirect URI "valid". */ public function validateRedirectURI(PhutilURI $uri) { if (!PhabricatorEnv::isValidRemoteURIForLink($uri)) { return false; } if ($uri->getFragment()) { return false; } if (!$uri->getDomain()) { return false; } return true; } /** * If there's a URI specified in an OAuth request, it must be validated in * its own right. Further, it must have the same domain, the same path, the * same port, and (at least) the same query parameters as the primary URI. */ public function validateSecondaryRedirectURI( PhutilURI $secondary_uri, PhutilURI $primary_uri) { // The secondary URI must be valid. if (!$this->validateRedirectURI($secondary_uri)) { return false; } // Both URIs must point at the same domain. if ($secondary_uri->getDomain() != $primary_uri->getDomain()) { return false; } // Both URIs must have the same path if ($secondary_uri->getPath() != $primary_uri->getPath()) { return false; } // Both URIs must have the same port if ($secondary_uri->getPort() != $primary_uri->getPort()) { return false; } // Any query parameters present in the first URI must be exactly present // in the second URI. $need_params = $primary_uri->getQueryParams(); $have_params = $secondary_uri->getQueryParams(); foreach ($need_params as $key => $value) { if (!array_key_exists($key, $have_params)) { return false; } if ((string)$have_params[$key] != (string)$value) { return false; } } // If the first URI is HTTPS, the second URI must also be HTTPS. This // defuses an attack where a third party with control over the network // tricks you into using HTTP to authenticate over a link which is supposed // to be HTTPS only and sniffs all your token cookies. if (strtolower($primary_uri->getProtocol()) == 'https') { if (strtolower($secondary_uri->getProtocol()) != 'https') { return false; } } return true; } } diff --git a/src/applications/oauthserver/PhabricatorOAuthServerScope.php b/src/applications/oauthserver/PhabricatorOAuthServerScope.php index 4ab6c455d8..105c9cd33d 100644 --- a/src/applications/oauthserver/PhabricatorOAuthServerScope.php +++ b/src/applications/oauthserver/PhabricatorOAuthServerScope.php @@ -1,127 +1,121 @@ 1, self::SCOPE_WHOAMI => 1, ); } public static function getDefaultScope() { return self::SCOPE_WHOAMI; } public static function getCheckboxControl( array $current_scopes) { $have_options = false; $scopes = self::getScopesDict(); $scope_keys = array_keys($scopes); sort($scope_keys); $default_scope = self::getDefaultScope(); $checkboxes = new AphrontFormCheckboxControl(); foreach ($scope_keys as $scope) { if ($scope == $default_scope) { continue; } if (!isset($current_scopes[$scope])) { continue; } $checkboxes->addCheckbox( $name = $scope, $value = 1, $label = self::getCheckboxLabel($scope), $checked = isset($current_scopes[$scope])); $have_options = true; } if ($have_options) { $checkboxes->setLabel(pht('Scope')); return $checkboxes; } return null; } private static function getCheckboxLabel($scope) { $label = null; switch ($scope) { case self::SCOPE_OFFLINE_ACCESS: $label = pht('Make access tokens granted to this client never expire.'); break; case self::SCOPE_WHOAMI: $label = pht('Read access to Conduit method %s.', 'user.whoami'); break; } return $label; } public static function getScopesFromRequest(AphrontRequest $request) { $scopes = self::getScopesDict(); $requested_scopes = array(); foreach ($scopes as $scope => $bit) { if ($request->getBool($scope)) { $requested_scopes[$scope] = 1; } } $requested_scopes[self::getDefaultScope()] = 1; return $requested_scopes; } /** * A scopes list is considered valid if each scope is a known scope * and each scope is seen only once. Otherwise, the list is invalid. */ public static function validateScopesList($scope_list) { $scopes = explode(' ', $scope_list); $known_scopes = self::getScopesDict(); $seen_scopes = array(); foreach ($scopes as $scope) { if (!isset($known_scopes[$scope])) { return false; } if (isset($seen_scopes[$scope])) { return false; } $seen_scopes[$scope] = 1; } return true; } /** * A scopes dictionary is considered valid if each key is a known scope. * Otherwise, the dictionary is invalid. */ public static function validateScopesDict($scope_dict) { $known_scopes = self::getScopesDict(); $unknown_scopes = array_diff_key($scope_dict, $known_scopes); return empty($unknown_scopes); } /** * Transforms a space-delimited scopes list into a scopes dict. The list * should be validated by @{method:validateScopesList} before * transformation. */ public static function scopesListToDict($scope_list) { $scopes = explode(' ', $scope_list); return array_fill_keys($scopes, 1); } } diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php index 7e35574f61..7197ef368d 100644 --- a/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php +++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php @@ -1,160 +1,160 @@ getStr('grant_type'); $code = $request->getStr('code'); $redirect_uri = $request->getStr('redirect_uri'); $client_phid = $request->getStr('client_id'); $client_secret = $request->getStr('client_secret'); $response = new PhabricatorOAuthResponse(); $server = new PhabricatorOAuthServer(); if ($grant_type != 'authorization_code') { $response->setError('unsupported_grant_type'); $response->setErrorDescription( pht( 'Only %s %s is supported.', 'grant_type', 'authorization_code')); return $response; } if (!$code) { $response->setError('invalid_request'); $response->setErrorDescription(pht('Required parameter code missing.')); return $response; } if (!$client_phid) { $response->setError('invalid_request'); $response->setErrorDescription( pht( 'Required parameter %s missing.', 'client_id')); return $response; } if (!$client_secret) { $response->setError('invalid_request'); $response->setErrorDescription( pht( 'Required parameter %s missing.', 'client_secret')); return $response; } // one giant try / catch around all the exciting database stuff so we // can return a 'server_error' response if something goes wrong! try { $auth_code = id(new PhabricatorOAuthServerAuthorizationCode()) ->loadOneWhere('code = %s', $code); if (!$auth_code) { $response->setError('invalid_grant'); $response->setErrorDescription( pht( 'Authorization code %d not found.', $code)); return $response; } // if we have an auth code redirect URI, there must be a redirect_uri // in the request and it must match the auth code redirect uri *exactly* $auth_code_redirect_uri = $auth_code->getRedirectURI(); if ($auth_code_redirect_uri) { $auth_code_redirect_uri = new PhutilURI($auth_code_redirect_uri); $redirect_uri = new PhutilURI($redirect_uri); if (!$redirect_uri->getDomain() || $redirect_uri != $auth_code_redirect_uri) { $response->setError('invalid_grant'); $response->setErrorDescription( pht( 'Redirect URI in request must exactly match redirect URI '. 'from authorization code.')); return $response; } } else if ($redirect_uri) { $response->setError('invalid_grant'); $response->setErrorDescription( pht( 'Redirect URI in request and no redirect URI in authorization '. 'code. The two must exactly match.')); return $response; } $client = id(new PhabricatorOAuthServerClient()) ->loadOneWhere('phid = %s', $client_phid); if (!$client) { $response->setError('invalid_client'); $response->setErrorDescription( pht( 'Client with %s %d not found.', 'client_id', $client_phid)); return $response; } $server->setClient($client); $user_phid = $auth_code->getUserPHID(); $user = id(new PhabricatorUser()) ->loadOneWhere('phid = %s', $user_phid); if (!$user) { $response->setError('invalid_grant'); $response->setErrorDescription( pht( 'User with PHID %d not found.', $user_phid)); return $response; } $server->setUser($user); $test_code = new PhabricatorOAuthServerAuthorizationCode(); $test_code->setClientSecret($client_secret); $test_code->setClientPHID($client_phid); $is_good_code = $server->validateAuthorizationCode( $auth_code, $test_code); if (!$is_good_code) { $response->setError('invalid_grant'); $response->setErrorDescription( pht( 'Invalid authorization code %d.', $code)); return $response; } $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $access_token = $server->generateAccessToken(); $auth_code->delete(); unset($unguarded); $result = array( 'access_token' => $access_token->getToken(), 'token_type' => 'Bearer', - 'expires_in' => PhabricatorOAuthServer::ACCESS_TOKEN_TIMEOUT, + 'expires_in' => $access_token->getExpiresDuration(), ); return $response->setContent($result); } catch (Exception $e) { $response->setError('server_error'); $response->setErrorDescription( pht( 'The authorization server encountered an unexpected condition '. 'which prevented it from fulfilling the request.')); return $response; } } } diff --git a/src/applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php b/src/applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php index f19be14434..a746008f55 100644 --- a/src/applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php +++ b/src/applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php @@ -1,95 +1,89 @@ phids = $phids; return $this; } public function withUserPHIDs(array $phids) { $this->userPHIDs = $phids; return $this; } public function withClientPHIDs(array $phids) { $this->clientPHIDs = $phids; return $this; } + public function newResultObject() { + return new PhabricatorOAuthClientAuthorization(); + } + protected function loadPage() { - $table = new PhabricatorOAuthClientAuthorization(); - $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); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $authorizations) { $client_phids = mpull($authorizations, 'getClientPHID'); $clients = id(new PhabricatorOAuthServerClientQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($client_phids) ->execute(); $clients = mpull($clients, null, 'getPHID'); foreach ($authorizations as $key => $authorization) { $client = idx($clients, $authorization->getClientPHID()); + if (!$client) { + $this->didRejectResult($authorization); unset($authorizations[$key]); continue; } + $authorization->attachClient($client); } return $authorizations; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } - if ($this->userPHIDs) { + if ($this->userPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'userPHID IN (%Ls)', $this->userPHIDs); } - if ($this->clientPHIDs) { + if ($this->clientPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'clientPHID IN (%Ls)', $this->clientPHIDs); } - $where[] = $this->buildPagingClause($conn_r); - - return $this->formatWhereClause($where); + return $where; } public function getQueryApplicationClass() { return 'PhabricatorOAuthServerApplication'; } } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php index 68b21d23cb..c50cac93ea 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php @@ -1,25 +1,39 @@ array( 'token' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'token' => array( 'columns' => array('token'), 'unique' => true, ), ), ) + 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(); + } + }