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