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);
 }