@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 329 lines 9.6 kB view raw
1<?php 2 3abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { 4 5 private $args; 6 private $repository; 7 private $hasWriteAccess; 8 private $shouldProxy; 9 private $baseRequestPath; 10 11 public function getRepository() { 12 if (!$this->repository) { 13 throw new Exception(pht('Repository is not available yet!')); 14 } 15 return $this->repository; 16 } 17 18 private function setRepository(PhabricatorRepository $repository) { 19 $this->repository = $repository; 20 return $this; 21 } 22 23 public function getArgs() { 24 return $this->args; 25 } 26 27 public function getEnvironment() { 28 $env = array( 29 DiffusionCommitHookEngine::ENV_USER => $this->getSSHUser()->getUsername(), 30 DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL => 'ssh', 31 ); 32 33 $identifier = $this->getRequestIdentifier(); 34 if ($identifier !== null) { 35 $env[DiffusionCommitHookEngine::ENV_REQUEST] = $identifier; 36 } 37 38 $remote_address = $this->getSSHRemoteAddress(); 39 if ($remote_address !== null) { 40 $env[DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS] = $remote_address; 41 } 42 43 return $env; 44 } 45 46 /** 47 * Identify and load the affected repository. 48 */ 49 abstract protected function identifyRepository(); 50 abstract protected function executeRepositoryOperations(); 51 abstract protected function raiseWrongVCSException( 52 PhabricatorRepository $repository); 53 54 protected function getBaseRequestPath() { 55 return $this->baseRequestPath; 56 } 57 58 protected function writeError($message) { 59 $this->getErrorChannel()->write($message); 60 return $this; 61 } 62 63 protected function getCurrentDeviceName() { 64 $device = AlmanacKeys::getLiveDevice(); 65 if ($device) { 66 return $device->getName(); 67 } 68 69 return php_uname('n'); 70 } 71 72 protected function shouldProxy() { 73 return $this->shouldProxy; 74 } 75 76 final protected function getAlmanacServiceRefs($for_write) { 77 $viewer = $this->getSSHUser(); 78 $repository = $this->getRepository(); 79 80 $is_cluster_request = $this->getIsClusterRequest(); 81 82 $refs = $repository->getAlmanacServiceRefs( 83 $viewer, 84 array( 85 'neverProxy' => $is_cluster_request, 86 'protocols' => array( 87 'ssh', 88 ), 89 'writable' => $for_write, 90 )); 91 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.')); 97 } 98 99 return $refs; 100 } 101 102 final protected function getProxyCommand($for_write) { 103 $refs = $this->getAlmanacServiceRefs($for_write); 104 105 $ref = head($refs); 106 107 return $this->getProxyCommandForServiceRef($ref); 108 } 109 110 final protected function getProxyCommandForServiceRef( 111 DiffusionServiceRef $ref) { 112 113 $uri = new PhutilURI($ref->getURI()); 114 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 cluster.')); 121 } 122 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)); 133 } 134 135 $options = array(); 136 $options[] = '-o'; 137 $options[] = 'StrictHostKeyChecking=no'; 138 $options[] = '-o'; 139 $options[] = 'UserKnownHostsFile=/dev/null'; 140 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'; 147 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. 152 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()); 162 } 163 164 final public function execute(PhutilArgumentParser $args) { 165 $this->args = $args; 166 167 $viewer = $this->getSSHUser(); 168 $have_diffusion = PhabricatorApplication::isClassInstalledForViewer( 169 PhabricatorDiffusionApplication::class, 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.')); 176 } 177 178 $repository = $this->identifyRepository(); 179 $this->setRepository($repository); 180 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. 184 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). 189 190 $is_cluster_request = $this->getIsClusterRequest(); 191 $uri = $repository->getAlmanacServiceURI( 192 $viewer, 193 array( 194 'neverProxy' => $is_cluster_request, 195 'protocols' => array( 196 'ssh', 197 ), 198 )); 199 $this->shouldProxy = (bool)$uri; 200 201 try { 202 return $this->executeRepositoryOperations(); 203 } catch (Exception $ex) { 204 $this->writeError(get_class($ex).': '.$ex->getMessage()); 205 return 1; 206 } 207 } 208 209 protected function loadRepositoryWithPath($path, $vcs) { 210 $viewer = $this->getSSHUser(); 211 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')); 222 } 223 224 $identifier = $info['identifier']; 225 $base = $info['base']; 226 227 $this->baseRequestPath = $base; 228 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)); 237 } 238 239 $is_cluster = $this->getIsClusterRequest(); 240 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())); 247 } 248 249 if ($repository->getVersionControlSystem() != $vcs) { 250 $this->raiseWrongVCSException($repository); 251 } 252 253 return $repository; 254 } 255 256 protected function requireWriteAccess($protocol_command = null) { 257 if ($this->hasWriteAccess === true) { 258 return; 259 } 260 261 $repository = $this->getRepository(); 262 $viewer = $this->getSSHUser(); 263 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.')); 270 } 271 272 if ($repository->isReadOnly()) { 273 throw new Exception($repository->getReadOnlyMessageForDisplay()); 274 } 275 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.')); 285 } 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.')); 296 } 297 } 298 299 $this->hasWriteAccess = true; 300 return $this->hasWriteAccess; 301 } 302 303 protected function shouldSkipReadSynchronization() { 304 $viewer = $this->getSSHUser(); 305 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; 311 } 312 313 return false; 314 } 315 316 protected function newPullEvent() { 317 $viewer = $this->getSSHUser(); 318 $repository = $this->getRepository(); 319 $remote_address = $this->getSSHRemoteAddress(); 320 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()); 327 } 328 329}