3 abstract class DiffusionSSHWorkflow
extends PhabricatorSSHWorkflow
{
7 private $hasWriteAccess;
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;
23 public function getArgs() {
27 public function getEnvironment() {
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;
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);
63 protected function getCurrentDeviceName() {
64 $device = AlmanacKeys
::getLiveDevice();
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(
85 'neverProxy' => $is_cluster_request,
89 'writable' => $for_write,
95 'Failed to generate an intracluster proxy URI even though this '.
96 'request was routed as a proxy request.'));
102 final protected function getProxyCommand($for_write) {
103 $refs = $this->getAlmanacServiceRefs($for_write);
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) {
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)) {
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").',
137 $options[] = 'StrictHostKeyChecking=no';
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.
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
154 'ssh %Ls -l %s -i %s -p %s %s -- %s %Ls',
160 '@'.$this->getSSHUser()->getUsername(),
161 $this->getOriginalArguments());
164 final public function execute(PhutilArgumentParser
$args) {
167 $viewer = $this->getSSHUser();
168 $have_diffusion = PhabricatorApplication
::isClassInstalledForViewer(
169 'PhabricatorDiffusionApplication',
171 if (!$have_diffusion) {
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
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(
194 'neverProxy' => $is_cluster_request,
195 'protocols' => array(
199 $this->shouldProxy
= (bool)$uri;
202 return $this->executeRepositoryOperations();
203 } catch (Exception
$ex) {
204 $this->writeError(get_class($ex).': '.$ex->getMessage());
209 protected function loadRepositoryWithPath($path, $vcs) {
210 $viewer = $this->getSSHUser();
212 $info = PhabricatorRepository
::parseRepositoryServicePath($path, $vcs);
213 if ($info === null) {
216 'Unrecognized repository path "%s". Expected a path like "%s", '.
221 '/source/thaumaturgy.git'));
224 $identifier = $info['identifier'];
225 $base = $info['base'];
227 $this->baseRequestPath
= $base;
229 $repository = id(new PhabricatorRepositoryQuery())
231 ->withIdentifiers(array($identifier))
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)) {
245 'This repository ("%s") is not available over SSH.',
246 $repository->getDisplayName()));
249 if ($repository->getVersionControlSystem() != $vcs) {
250 $this->raiseWrongVCSException($repository);
256 protected function requireWriteAccess($protocol_command = null) {
257 if ($this->hasWriteAccess
=== true) {
261 $repository = $this->getRepository();
262 $viewer = $this->getSSHUser();
264 if ($viewer->isOmnipotent()) {
267 'This request is authenticated as a cluster device, but is '.
268 'performing a write. Writes must be performed with a real '.
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(
281 DiffusionPushCapability
::CAPABILITY
);
284 pht('You do not have permission to push to this repository.'));
287 if ($protocol_command !== null) {
290 'This repository is read-only over SSH (tried to execute '.
291 'protocol command "%s").',
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()) {
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());