diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php b/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php index 17a6de0f7..ff06173f5 100644 --- a/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php +++ b/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php @@ -1,100 +1,109 @@ <?php final class PhabricatorConduitTokenEditController extends PhabricatorConduitController { 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) - ->appendChild( + ->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; } } diff --git a/src/applications/conduit/query/PhabricatorConduitTokenQuery.php b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php index e0d088627..38a258c4e 100644 --- a/src/applications/conduit/query/PhabricatorConduitTokenQuery.php +++ b/src/applications/conduit/query/PhabricatorConduitTokenQuery.php @@ -1,115 +1,128 @@ <?php final class PhabricatorConduitTokenQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $ids; private $objectPHIDs; private $expired; private $tokens; + private $tokenTypes; public function withExpired($expired) { $this->expired = $expired; return $this; } public function withIDs(array $ids) { $this->ids = $ids; return $this; } public function withObjectPHIDs(array $phids) { $this->objectPHIDs = $phids; return $this; } public function withTokens(array $tokens) { $this->tokens = $tokens; return $this; } + public function withTokenTypes(array $types) { + $this->tokenTypes = $types; + return $this; + } + public 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);; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids !== null) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( $conn_r, 'objectPHID IN (%Ls)', $this->objectPHIDs); } if ($this->tokens !== null) { $where[] = qsprintf( $conn_r, 'token IN (%Ls)', $this->tokens); } + if ($this->tokenTypes !== null) { + $where[] = qsprintf( + $conn_r, + 'tokenType IN (%Ls)', + $this->tokenTypes); + } + if ($this->expired !== null) { if ($this->expired) { $where[] = qsprintf( $conn_r, 'expires <= %d', PhabricatorTime::getNow()); } else { $where[] = qsprintf( $conn_r, 'expires IS NULL OR expires > %d', PhabricatorTime::getNow()); } } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } protected function willFilterPage(array $tokens) { $object_phids = mpull($tokens, 'getObjectPHID'); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); foreach ($tokens as $key => $token) { $object = idx($objects, $token->getObjectPHID(), null); if (!$object) { $this->didRejectResult($token); unset($tokens[$key]); continue; } $token->attachObject($object); } return $tokens; } public function getQueryApplicationClass() { return 'PhabricatorConduitApplication'; } } diff --git a/src/applications/conduit/settings/PhabricatorConduitSettingsPanel.php b/src/applications/conduit/settings/PhabricatorConduitSettingsPanel.php index 40c982477..1e2d1d9d5 100644 --- a/src/applications/conduit/settings/PhabricatorConduitSettingsPanel.php +++ b/src/applications/conduit/settings/PhabricatorConduitSettingsPanel.php @@ -1,116 +1,116 @@ <?php final class PhabricatorConduitSettingsPanel extends PhabricatorSettingsPanel { public function isEditableByAdministrators() { return true; } public function getPanelKey() { return 'apitokens'; } public function getPanelName() { return pht('Conduit API Tokens'); } public function getPanelGroup() { return pht('Sessions and Logs'); } public function isEnabled() { return true; } 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', ), - substr($token->getToken(), 0, 8).'...'), + $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_icon = id(new PHUIIconView()) ->setIconFont('fa-plus'); $generate_button = id(new PHUIButtonView()) ->setText(pht('Generate API Token')) ->setHref('/conduit/token/edit/?objectPHID='.$user->getPHID()) ->setTag('a') ->setWorkflow(true) ->setIcon($generate_icon); $terminate_icon = id(new PHUIIconView()) ->setIconFont('fa-exclamation-triangle'); $terminate_button = id(new PHUIButtonView()) ->setText(pht('Terminate All Tokens')) ->setHref('/conduit/token/terminate/?objectPHID='.$user->getPHID()) ->setTag('a') ->setWorkflow(true) ->setIcon($terminate_icon); $header = id(new PHUIHeaderView()) ->setHeader(pht('Active API Tokens')) ->addActionLink($generate_button) ->addActionLink($terminate_button); $panel = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($table); return $panel; } } diff --git a/src/applications/conduit/storage/PhabricatorConduitToken.php b/src/applications/conduit/storage/PhabricatorConduitToken.php index 8ebcecb16..5c39173c0 100644 --- a/src/applications/conduit/storage/PhabricatorConduitToken.php +++ b/src/applications/conduit/storage/PhabricatorConduitToken.php @@ -1,118 +1,165 @@ <?php final class PhabricatorConduitToken extends PhabricatorConduitDAO implements PhabricatorPolicyInterface { protected $objectPHID; protected $tokenType; protected $token; protected $expires; private $object = self::ATTACHABLE; const TYPE_STANDARD = 'api'; - const TYPE_TEMPORARY = 'tmp'; const TYPE_COMMANDLINE = 'cli'; + const TYPE_CLUSTER = 'clr'; public function getConfiguration() { return array( self::CONFIG_COLUMN_SCHEMA => array( 'tokenType' => 'text32', 'token' => 'text32', 'expires' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID', 'tokenType'), ), 'key_token' => array( 'columns' => array('token'), 'unique' => true, ), 'key_expires' => array( 'columns' => array('expires'), ), ), ) + parent::getConfiguration(); } + public static function loadClusterTokenForUser(PhabricatorUser $user) { + if (!$user->isLoggedIn()) { + return null; + } + + $tokens = id(new PhabricatorConduitTokenQuery()) + ->setViewer($user) + ->withObjectPHIDs(array($user->getPHID())) + ->withTokenTypes(array(self::TYPE_CLUSTER)) + ->withExpired(false) + ->execute(); + + // Only return a token if it has at least 5 minutes left before + // expiration. Cluster tokens cycle regularly, so we don't want to use + // one that's going to expire momentarily. + $now = PhabricatorTime::getNow(); + $must_expire_after = $now + phutil_units('5 minutes in seconds'); + + foreach ($tokens as $token) { + if ($token->getExpires() > $must_expire_after) { + return $token; + } + } + + // We didn't find any existing tokens (or the existing tokens are all about + // to expire) so generate a new token. + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $token = PhabricatorConduitToken::initializeNewToken( + $user->getPHID(), + self::TYPE_CLUSTER); + $token->save(); + unset($unguarded); + + return $token; + } + 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 static function getTokenTypeName($type) { $map = array( self::TYPE_STANDARD => pht('Standard API Token'), - self::TYPE_TEMPORARY => pht('Temporary API Token'), self::TYPE_COMMANDLINE => pht('Command Line API Token'), + self::TYPE_CLUSTER => pht('Cluster API Token'), ); return idx($map, $type, $type); } public static function getAllTokenTypes() { return array( self::TYPE_STANDARD, - self::TYPE_TEMPORARY, self::TYPE_COMMANDLINE, + self::TYPE_CLUSTER, ); } private function getTokenExpires($token_type) { + $now = PhabricatorTime::getNow(); switch ($token_type) { case self::TYPE_STANDARD: return null; - case self::TYPE_TEMPORARY: - return PhabricatorTime::getNow() + phutil_units('24 hours in seconds'); case self::TYPE_COMMANDLINE: - return PhabricatorTime::getNow() + phutil_units('1 hour in seconds'); + return $now + phutil_units('1 hour in seconds'); + case self::TYPE_CLUSTER: + return $now + phutil_units('30 minutes in seconds'); default: throw new Exception( pht('Unknown Conduit token type "%s"!', $token_type)); } } + public function getPublicTokenName() { + switch ($this->getTokenType()) { + case self::TYPE_CLUSTER: + return pht('Cluster API Token'); + default: + return substr($this->getToken(), 0, 8).'...'; + } + } + public function getObject() { return $this->assertAttached($this->object); } public function attachObject(PhabricatorUser $object) { $this->object = $object; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getObject()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getObject()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'Conduit tokens inherit the policies of the user they authenticate.'); } } diff --git a/src/applications/diffusion/query/DiffusionQuery.php b/src/applications/diffusion/query/DiffusionQuery.php index 87714249f..642927c06 100644 --- a/src/applications/diffusion/query/DiffusionQuery.php +++ b/src/applications/diffusion/query/DiffusionQuery.php @@ -1,252 +1,256 @@ <?php abstract class DiffusionQuery extends PhabricatorQuery { private $request; final protected function __construct() { // <protected> } protected static function newQueryObject( $base_class, DiffusionRequest $request) { $repository = $request->getRepository(); $obj = self::initQueryObject($base_class, $repository); $obj->request = $request; return $obj; } final protected static function initQueryObject( $base_class, PhabricatorRepository $repository) { $map = array( PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'Git', PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL => 'Mercurial', PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'Svn', ); $name = idx($map, $repository->getVersionControlSystem()); if (!$name) { throw new Exception('Unsupported VCS!'); } $class = str_replace('Diffusion', 'Diffusion'.$name, $base_class); $obj = new $class(); return $obj; } final protected function getRequest() { return $this->request; } final public static function callConduitWithDiffusionRequest( PhabricatorUser $user, DiffusionRequest $drequest, $method, array $params = array()) { $repository = $drequest->getRepository(); $core_params = array( 'callsign' => $repository->getCallsign(), ); if ($drequest->getBranch() !== null) { $core_params['branch'] = $drequest->getBranch(); } $params = $params + $core_params; $service_phid = $repository->getAlmanacServicePHID(); if ($service_phid === null) { return id(new ConduitCall($method, $params)) ->setUser($user) ->execute(); } $service = id(new AlmanacServiceQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($service_phid)) ->needBindings(true) ->executeOne(); if (!$service) { throw new Exception( pht( 'The Alamnac service for this repository is invalid or could not '. 'be loaded.')); } $bindings = $service->getBindings(); if (!$bindings) { throw new Exception( pht( 'The Alamanc service for this repository is not bound to any '. 'interfaces.')); } $uris = array(); foreach ($bindings as $binding) { $iface = $binding->getInterface(); $protocol = $binding->getAlmanacPropertyValue('protocol'); if ($protocol === 'http') { $uris[] = 'http://'.$iface->renderDisplayAddress().'/'; } else if ($protocol === 'https' || $protocol === null) { $uris[] = 'https://'.$iface->renderDisplayAddress().'/'; } else { throw new Exception( pht( 'The Almanac service for this repository has a binding to an '. 'invalid interface with an unknown protocol ("%s").', $protocol)); } } shuffle($uris); $uri = head($uris); $domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain(); - // TODO: This call needs authentication, which is blocked by T5955. + $client = id(new ConduitClient($uri)) + ->setHost($domain); - return id(new ConduitClient($uri)) - ->setHost($domain) - ->callMethodSynchronous($method, $params); + $token = PhabricatorConduitToken::loadClusterTokenForUser($user); + if ($token) { + $client->setConduitToken($token->getToken()); + } + + return $client->callMethodSynchronous($method, $params); } public function execute() { return $this->executeQuery(); } abstract protected function executeQuery(); /* -( Query Utilities )---------------------------------------------------- */ final public static function loadCommitsByIdentifiers( array $identifiers, DiffusionRequest $drequest) { if (!$identifiers) { return array(); } $commits = array(); $commit_data = array(); $repository = $drequest->getRepository(); $commits = id(new PhabricatorRepositoryCommit())->loadAllWhere( 'repositoryID = %d AND commitIdentifier IN (%Ls)', $repository->getID(), $identifiers); $commits = mpull($commits, null, 'getCommitIdentifier'); // Build empty commit objects for every commit, so we can show unparsed // commits in history views (as "Importing") instead of not showing them. // This makes the process of importing and parsing commits clearer to the // user. $commit_list = array(); foreach ($identifiers as $identifier) { $commit_obj = idx($commits, $identifier); if (!$commit_obj) { $commit_obj = new PhabricatorRepositoryCommit(); $commit_obj->setRepositoryID($repository->getID()); $commit_obj->setCommitIdentifier($identifier); $commit_obj->makeEphemeral(); } $commit_list[$identifier] = $commit_obj; } $commits = $commit_list; $commit_ids = array_filter(mpull($commits, 'getID')); if ($commit_ids) { $commit_data = id(new PhabricatorRepositoryCommitData())->loadAllWhere( 'commitID in (%Ld)', $commit_ids); $commit_data = mpull($commit_data, null, 'getCommitID'); } foreach ($commits as $commit) { if (!$commit->getID()) { continue; } if (idx($commit_data, $commit->getID())) { $commit->attachCommitData($commit_data[$commit->getID()]); } } return $commits; } final public static function loadHistoryForCommitIdentifiers( array $identifiers, DiffusionRequest $drequest) { if (!$identifiers) { return array(); } $repository = $drequest->getRepository(); $commits = self::loadCommitsByIdentifiers($identifiers, $drequest); if (!$commits) { return array(); } $path = $drequest->getPath(); $conn_r = $repository->establishConnection('r'); $path_normal = DiffusionPathIDQuery::normalizePath($path); $paths = queryfx_all( $conn_r, 'SELECT id, path FROM %T WHERE pathHash IN (%Ls)', PhabricatorRepository::TABLE_PATH, array(md5($path_normal))); $paths = ipull($paths, 'id', 'path'); $path_id = idx($paths, $path_normal); $commit_ids = array_filter(mpull($commits, 'getID')); $path_changes = array(); if ($path_id && $commit_ids) { $path_changes = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE commitID IN (%Ld) AND pathID = %d', PhabricatorRepository::TABLE_PATHCHANGE, $commit_ids, $path_id); $path_changes = ipull($path_changes, null, 'commitID'); } $history = array(); foreach ($identifiers as $identifier) { $item = new DiffusionPathChange(); $item->setCommitIdentifier($identifier); $commit = idx($commits, $identifier); if ($commit) { $item->setCommit($commit); try { $item->setCommitData($commit->getCommitData()); } catch (Exception $ex) { // Ignore, commit just doesn't have data. } $change = idx($path_changes, $commit->getID()); if ($change) { $item->setChangeType($change['changeType']); $item->setFileType($change['fileType']); } } $history[] = $item; } return $history; } }