Path: blob/master/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
12242 views
<?php12abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {34private $args;5private $repository;6private $hasWriteAccess;7private $shouldProxy;8private $baseRequestPath;910public function getRepository() {11if (!$this->repository) {12throw new Exception(pht('Repository is not available yet!'));13}14return $this->repository;15}1617private function setRepository(PhabricatorRepository $repository) {18$this->repository = $repository;19return $this;20}2122public function getArgs() {23return $this->args;24}2526public function getEnvironment() {27$env = array(28DiffusionCommitHookEngine::ENV_USER => $this->getSSHUser()->getUsername(),29DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh',30);3132$identifier = $this->getRequestIdentifier();33if ($identifier !== null) {34$env[DiffusionCommitHookEngine::ENV_REQUEST] = $identifier;35}3637$remote_address = $this->getSSHRemoteAddress();38if ($remote_address !== null) {39$env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address;40}4142return $env;43}4445/**46* Identify and load the affected repository.47*/48abstract protected function identifyRepository();49abstract protected function executeRepositoryOperations();50abstract protected function raiseWrongVCSException(51PhabricatorRepository $repository);5253protected function getBaseRequestPath() {54return $this->baseRequestPath;55}5657protected function writeError($message) {58$this->getErrorChannel()->write($message);59return $this;60}6162protected function getCurrentDeviceName() {63$device = AlmanacKeys::getLiveDevice();64if ($device) {65return $device->getName();66}6768return php_uname('n');69}7071protected function shouldProxy() {72return $this->shouldProxy;73}7475final protected function getAlmanacServiceRefs($for_write) {76$viewer = $this->getSSHUser();77$repository = $this->getRepository();7879$is_cluster_request = $this->getIsClusterRequest();8081$refs = $repository->getAlmanacServiceRefs(82$viewer,83array(84'neverProxy' => $is_cluster_request,85'protocols' => array(86'ssh',87),88'writable' => $for_write,89));9091if (!$refs) {92throw new Exception(93pht(94'Failed to generate an intracluster proxy URI even though this '.95'request was routed as a proxy request.'));96}9798return $refs;99}100101final protected function getProxyCommand($for_write) {102$refs = $this->getAlmanacServiceRefs($for_write);103104$ref = head($refs);105106return $this->getProxyCommandForServiceRef($ref);107}108109final protected function getProxyCommandForServiceRef(110DiffusionServiceRef $ref) {111112$uri = new PhutilURI($ref->getURI());113114$username = AlmanacKeys::getClusterSSHUser();115if ($username === null) {116throw new Exception(117pht(118'Unable to determine the username to connect with when trying '.119'to proxy an SSH request within the cluster.'));120}121122$port = $uri->getPort();123$host = $uri->getDomain();124$key_path = AlmanacKeys::getKeyPath('device.key');125if (!Filesystem::pathExists($key_path)) {126throw new Exception(127pht(128'Unable to proxy this SSH request within the cluster: this device '.129'is not registered and has a missing device key (expected to '.130'find key at "%s").',131$key_path));132}133134$options = array();135$options[] = '-o';136$options[] = 'StrictHostKeyChecking=no';137$options[] = '-o';138$options[] = 'UserKnownHostsFile=/dev/null';139140// This is suppressing "added <address> to the list of known hosts"141// messages, which are confusing and irrelevant when they arise from142// proxied requests. It might also be suppressing lots of useful errors,143// of course. Ideally, we would enforce host keys eventually. See T13121.144$options[] = '-o';145$options[] = 'LogLevel=ERROR';146147// NOTE: We prefix the command with "@username", which the far end of the148// connection will parse in order to act as the specified user. This149// behavior is only available to cluster requests signed by a trusted150// device key.151152return csprintf(153'ssh %Ls -l %s -i %s -p %s %s -- %s %Ls',154$options,155$username,156$key_path,157$port,158$host,159'@'.$this->getSSHUser()->getUsername(),160$this->getOriginalArguments());161}162163final public function execute(PhutilArgumentParser $args) {164$this->args = $args;165166$viewer = $this->getSSHUser();167$have_diffusion = PhabricatorApplication::isClassInstalledForViewer(168'PhabricatorDiffusionApplication',169$viewer);170if (!$have_diffusion) {171throw new Exception(172pht(173'You do not have permission to access the Diffusion application, '.174'so you can not interact with repositories over SSH.'));175}176177$repository = $this->identifyRepository();178$this->setRepository($repository);179180// NOTE: Here, we're just figuring out if this is a proxyable request to181// a clusterized repository or not. We don't (and can't) use the URI we get182// back directly.183184// For example, we may get a read-only URI here but be handling a write185// request. We only care if we get back `null` (which means we should186// handle the request locally) or anything else (which means we should187// proxy it to an appropriate device).188189$is_cluster_request = $this->getIsClusterRequest();190$uri = $repository->getAlmanacServiceURI(191$viewer,192array(193'neverProxy' => $is_cluster_request,194'protocols' => array(195'ssh',196),197));198$this->shouldProxy = (bool)$uri;199200try {201return $this->executeRepositoryOperations();202} catch (Exception $ex) {203$this->writeError(get_class($ex).': '.$ex->getMessage());204return 1;205}206}207208protected function loadRepositoryWithPath($path, $vcs) {209$viewer = $this->getSSHUser();210211$info = PhabricatorRepository::parseRepositoryServicePath($path, $vcs);212if ($info === null) {213throw new Exception(214pht(215'Unrecognized repository path "%s". Expected a path like "%s", '.216'"%s", or "%s".',217$path,218'/diffusion/X/',219'/diffusion/123/',220'/source/thaumaturgy.git'));221}222223$identifier = $info['identifier'];224$base = $info['base'];225226$this->baseRequestPath = $base;227228$repository = id(new PhabricatorRepositoryQuery())229->setViewer($viewer)230->withIdentifiers(array($identifier))231->needURIs(true)232->executeOne();233if (!$repository) {234throw new Exception(235pht('No repository "%s" exists!', $identifier));236}237238$is_cluster = $this->getIsClusterRequest();239240$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;241if (!$repository->canServeProtocol($protocol, false, $is_cluster)) {242throw new Exception(243pht(244'This repository ("%s") is not available over SSH.',245$repository->getDisplayName()));246}247248if ($repository->getVersionControlSystem() != $vcs) {249$this->raiseWrongVCSException($repository);250}251252return $repository;253}254255protected function requireWriteAccess($protocol_command = null) {256if ($this->hasWriteAccess === true) {257return;258}259260$repository = $this->getRepository();261$viewer = $this->getSSHUser();262263if ($viewer->isOmnipotent()) {264throw new Exception(265pht(266'This request is authenticated as a cluster device, but is '.267'performing a write. Writes must be performed with a real '.268'user account.'));269}270271if ($repository->isReadOnly()) {272throw new Exception($repository->getReadOnlyMessageForDisplay());273}274275$protocol = PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH;276if ($repository->canServeProtocol($protocol, true)) {277$can_push = PhabricatorPolicyFilter::hasCapability(278$viewer,279$repository,280DiffusionPushCapability::CAPABILITY);281if (!$can_push) {282throw new Exception(283pht('You do not have permission to push to this repository.'));284}285} else {286if ($protocol_command !== null) {287throw new Exception(288pht(289'This repository is read-only over SSH (tried to execute '.290'protocol command "%s").',291$protocol_command));292} else {293throw new Exception(294pht('This repository is read-only over SSH.'));295}296}297298$this->hasWriteAccess = true;299return $this->hasWriteAccess;300}301302protected function shouldSkipReadSynchronization() {303$viewer = $this->getSSHUser();304305// Currently, the only case where devices interact over SSH without306// assuming user credentials is when synchronizing before a read. These307// synchronizing reads do not themselves need to be synchronized.308if ($viewer->isOmnipotent()) {309return true;310}311312return false;313}314315protected function newPullEvent() {316$viewer = $this->getSSHUser();317$repository = $this->getRepository();318$remote_address = $this->getSSHRemoteAddress();319320return id(new PhabricatorRepositoryPullEvent())321->setEpoch(PhabricatorTime::getNow())322->setRemoteAddress($remote_address)323->setRemoteProtocol(PhabricatorRepositoryPullEvent::PROTOCOL_SSH)324->setPullerPHID($viewer->getPHID())325->setRepositoryPHID($repository->getPHID());326}327328}329330331