Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php
12242 views
1
<?php
2
3
abstract 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',
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
}
330
331