protected function loadPage() { $table = new PhabricatorConduitToken(); $conn_r = $table->establishConnection('r'); $data = queryfx_all($conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); }
protected function collectGarbage() { $table = new PhabricatorConduitToken(); $conn_w = $table->establishConnection('w'); queryfx($conn_w, 'DELETE FROM %T WHERE expires <= %d ORDER BY dateCreated ASC LIMIT 100', $table->getTableName(), PhabricatorTime::getNow()); return $conn_w->getAffectedRows() == 100; }
public static function initializeNewToken($object_phid, $token_type) { $token = new PhabricatorConduitToken(); $token->objectPHID = $object_phid; $token->tokenType = $token_type; $token->expires = $token->getTokenExpires($token_type); $secret = $token_type . '-' . Filesystem::readRandomCharacters(32); $secret = substr($secret, 0, 32); $token->token = $secret; return $token; }
public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession($viewer, $request, '/'); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $token = PhabricatorConduitToken::initializeNewToken($viewer->getPHID(), PhabricatorConduitToken::TYPE_COMMANDLINE); $token->save(); unset($unguarded); $form = id(new AphrontFormView())->setUser($viewer)->appendRemarkupInstructions(pht('Copy-paste the API Token below to grant access to your account.'))->appendChild(id(new AphrontFormTextControl())->setLabel(pht('API Token'))->setValue($token->getToken()))->appendRemarkupInstructions(pht('This will authorize the requesting script to act on your behalf ' . 'permanently, like giving the script your account password.'))->appendRemarkupInstructions(pht('If you change your mind, you can revoke this token later in ' . '{nav icon=wrench,name=Settings > Conduit API Tokens}.')); return $this->newDialog()->setTitle(pht('Grant Account Access'))->setWidth(AphrontDialogView::WIDTH_FULL)->appendForm($form)->addCancelButton('/'); }
public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); $id = $request->getURIData('id'); if ($id) { $token = id(new PhabricatorConduitTokenQuery())->setViewer($viewer)->withIDs(array($id))->withExpired(false)->requireCapabilities(array(PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT))->executeOne(); if (!$token) { return new Aphront404Response(); } $object = $token->getObject(); $is_new = false; $title = pht('View API Token'); } else { $object = id(new PhabricatorObjectQuery())->setViewer($viewer)->withPHIDs(array($request->getStr('objectPHID')))->requireCapabilities(array(PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT))->executeOne(); if (!$object) { return new Aphront404Response(); } $token = PhabricatorConduitToken::initializeNewToken($object->getPHID(), PhabricatorConduitToken::TYPE_STANDARD); $is_new = true; $title = pht('Generate API Token'); $submit_button = pht('Generate Token'); } if ($viewer->getPHID() == $object->getPHID()) { $panel_uri = '/settings/panel/apitokens/'; } else { $panel_uri = '/settings/' . $object->getID() . '/panel/apitokens/'; } id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession($viewer, $request, $panel_uri); if ($request->isFormPost()) { $token->save(); if ($is_new) { $token_uri = '/conduit/token/edit/' . $token->getID() . '/'; } else { $token_uri = $panel_uri; } return id(new AphrontRedirectResponse())->setURI($token_uri); } $dialog = $this->newDialog()->setTitle($title)->addHiddenInput('objectPHID', $object->getPHID()); if ($is_new) { $dialog->appendParagraph(pht('Generate a new API token?'))->addSubmitButton($submit_button)->addCancelButton($panel_uri); } else { $form = id(new AphrontFormView())->setUser($viewer); if ($token->getTokenType() === PhabricatorConduitToken::TYPE_CLUSTER) { $dialog->appendChild(pht('This token is automatically generated by Phabricator, and used ' . 'to make requests between nodes in a Phabricator cluster. You ' . 'can not use this token in external applications.')); } else { $form->appendChild(id(new AphrontFormTextControl())->setLabel(pht('Token'))->setValue($token->getToken())); } $dialog->appendForm($form)->addCancelButton($panel_uri, pht('Done')); } return $dialog; }
public function processRequest(AphrontRequest $request) { $viewer = $this->getViewer(); $user = $this->getUser(); $tokens = id(new PhabricatorConduitTokenQuery())->setViewer($viewer)->withObjectPHIDs(array($user->getPHID()))->withExpired(false)->requireCapabilities(array(PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT))->execute(); $rows = array(); foreach ($tokens as $token) { $rows[] = array(javelin_tag('a', array('href' => '/conduit/token/edit/' . $token->getID() . '/', 'sigil' => 'workflow'), $token->getPublicTokenName()), PhabricatorConduitToken::getTokenTypeName($token->getTokenType()), phabricator_datetime($token->getDateCreated(), $viewer), $token->getExpires() ? phabricator_datetime($token->getExpires(), $viewer) : pht('Never'), javelin_tag('a', array('class' => 'button small grey', 'href' => '/conduit/token/terminate/' . $token->getID() . '/', 'sigil' => 'workflow'), pht('Terminate'))); } $table = new AphrontTableView($rows); $table->setNoDataString(pht("You don't have any active API tokens.")); $table->setHeaders(array(pht('Token'), pht('Type'), pht('Created'), pht('Expires'), null)); $table->setColumnClasses(array('wide pri', '', 'right', 'right', 'action')); $generate_button = id(new PHUIButtonView())->setText(pht('Generate API Token'))->setHref('/conduit/token/edit/?objectPHID=' . $user->getPHID())->setTag('a')->setWorkflow(true)->setIcon('fa-plus'); $terminate_button = id(new PHUIButtonView())->setText(pht('Terminate All Tokens'))->setHref('/conduit/token/terminate/?objectPHID=' . $user->getPHID())->setTag('a')->setWorkflow(true)->setIcon('fa-exclamation-triangle'); $header = id(new PHUIHeaderView())->setHeader(pht('Active API Tokens'))->addActionLink($generate_button)->addActionLink($terminate_button); $panel = id(new PHUIObjectBoxView())->setHeader($header)->setTable($table); return $panel; }
/** * Build a new Conduit client in order to make a service call to this * repository. * * If the repository is hosted locally, this method may return `null`. The * caller should use `ConduitCall` or other local logic to complete the * request. * * By default, we will return a @{class:ConduitClient} for any repository with * a service, even if that service is on the current device. * * We do this because this configuration does not make very much sense in a * production context, but is very common in a test/development context * (where the developer's machine is both the web host and the repository * service). By proxying in development, we get more consistent behavior * between development and production, and don't have a major untested * codepath. * * The `$never_proxy` parameter can be used to prevent this local proxying. * If the flag is passed: * * - The method will return `null` (implying a local service call) * if the repository service is hosted on the current device. * - The method will throw if it would need to return a client. * * This is used to prevent loops in Conduit: the first request will proxy, * even in development, but the second request will be identified as a * cluster request and forced not to proxy. * * For lower-level service resolution, see @{method:getAlmanacServiceURI}. * * @param PhabricatorUser Viewing user. * @param bool `true` to throw if a client would be returned. * @return ConduitClient|null Client, or `null` for local repositories. */ public function newConduitClient(PhabricatorUser $viewer, $never_proxy = false) { $uri = $this->getAlmanacServiceURI($viewer, $never_proxy, array('http', 'https')); if ($uri === null) { return null; } $domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain(); $client = id(new ConduitClient($uri))->setHost($domain); if ($viewer->isOmnipotent()) { // If the caller is the omnipotent user (normally, a daemon), we will // sign the request with this host's asymmetric keypair. $public_path = AlmanacKeys::getKeyPath('device.pub'); try { $public_key = Filesystem::readFile($public_path); } catch (Exception $ex) { throw new PhutilAggregateException(pht('Unable to read device public key while attempting to make ' . 'authenticated method call within the Phabricator cluster. ' . 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $private_path = AlmanacKeys::getKeyPath('device.key'); try { $private_key = Filesystem::readFile($private_path); $private_key = new PhutilOpaqueEnvelope($private_key); } catch (Exception $ex) { throw new PhutilAggregateException(pht('Unable to read device private key while attempting to make ' . 'authenticated method call within the Phabricator cluster. ' . 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $client->setSigningKeys($public_key, $private_key); } else { // If the caller is a normal user, we generate or retrieve a cluster // API token. $token = PhabricatorConduitToken::loadClusterTokenForUser($viewer); if ($token) { $client->setConduitToken($token->getToken()); } } return $client; }
/** * 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) { $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($this->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) { $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) { return array('ERR-INVALID-AUTH', pht('Access token is invalid.')); } // valid token, so let's log in the user! $user_phid = $token->getUserPHID(); $user = id(new PhabricatorUser())->loadOneWhere('phid = %s', $user_phid); if (!$user) { return array('ERR-INVALID-AUTH', pht('Access token is for invalid user.')); } 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); }