Skip to content

Commit 8798083

Browse files
author
epriestley
committedJan 28, 2015
Proxy VCS SSH requests
Summary: Fixes T7034. Like HTTP, proxy requests to the correct host if a repository has an Almanac service host. Test Plan: Ran VCS requests through the proxy. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T7034 Differential Revision: https://secure.phabricator.com/D11543
1 parent fe0ca0a commit 8798083

9 files changed

+288
-47
lines changed
 

‎scripts/ssh/ssh-auth.php

+19-13
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,33 @@
88
->setViewer(PhabricatorUser::getOmnipotentUser())
99
->execute();
1010

11-
foreach ($keys as $key => $ssh_key) {
12-
// For now, filter out any keys which don't belong to users. Eventually we
13-
// may allow devices to use this channel.
14-
if (!($ssh_key->getObject() instanceof PhabricatorUser)) {
15-
unset($keys[$key]);
16-
continue;
17-
}
18-
}
19-
2011
if (!$keys) {
2112
echo pht('No keys found.')."\n";
2213
exit(1);
2314
}
2415

2516
$bin = $root.'/bin/ssh-exec';
2617
foreach ($keys as $ssh_key) {
27-
$user = $ssh_key->getObject()->getUsername();
28-
2918
$key_argv = array();
30-
$key_argv[] = '--phabricator-ssh-user';
31-
$key_argv[] = $user;
19+
$object = $ssh_key->getObject();
20+
if ($object instanceof PhabricatorUser) {
21+
$key_argv[] = '--phabricator-ssh-user';
22+
$key_argv[] = $object->getUsername();
23+
} else if ($object instanceof AlmanacDevice) {
24+
if (!$ssh_key->getIsTrusted()) {
25+
// If this key is not a trusted device key, don't allow SSH
26+
// authentication.
27+
continue;
28+
}
29+
$key_argv[] = '--phabricator-ssh-device';
30+
$key_argv[] = $object->getName();
31+
} else {
32+
// We don't know what sort of key this is; don't permit SSH auth.
33+
continue;
34+
}
35+
36+
$key_argv[] = '--phabricator-ssh-key';
37+
$key_argv[] = $ssh_key->getID();
3238

3339
$cmd = csprintf('%s %Ls', $bin, $key_argv);
3440

‎scripts/ssh/ssh-exec.php

+152-25
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88

99
$ssh_log = PhabricatorSSHLog::getLog();
1010

11-
// First, figure out the authenticated user.
1211
$args = new PhutilArgumentParser($argv);
13-
$args->setTagline('receive SSH requests');
12+
$args->setTagline('execute SSH requests');
1413
$args->setSynopsis(<<<EOSYNOPSIS
1514
**ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__]
16-
Receive SSH requests.
15+
**ssh-exec** --phabricator-ssh-device __device__ [--ssh-command __commmand__]
16+
Execute authenticated SSH requests. This script is normally invoked
17+
via SSHD, but can be invoked manually for testing.
18+
1719
EOSYNOPSIS
1820
);
1921

@@ -22,24 +24,150 @@
2224
array(
2325
'name' => 'phabricator-ssh-user',
2426
'param' => 'username',
27+
'help' => pht(
28+
'If the request authenticated with a user key, the name of the '.
29+
'user.'),
30+
),
31+
array(
32+
'name' => 'phabricator-ssh-device',
33+
'param' => 'name',
34+
'help' => pht(
35+
'If the request authenticated with a device key, the name of the '.
36+
'device.'),
37+
),
38+
array(
39+
'name' => 'phabricator-ssh-key',
40+
'param' => 'id',
41+
'help' => pht(
42+
'The ID of the SSH key which authenticated this request. This is '.
43+
'used to allow logs to report when specific keys were used, to make '.
44+
'it easier to manage credentials.'),
2545
),
2646
array(
2747
'name' => 'ssh-command',
2848
'param' => 'command',
49+
'help' => pht(
50+
'Provide a command to execute. This makes testing this script '.
51+
'easier. When running normally, the command is read from the '.
52+
'environment (SSH_ORIGINAL_COMMAND), which is populated by sshd.'),
2953
),
3054
));
3155

3256
try {
57+
$remote_address = null;
58+
$ssh_client = getenv('SSH_CLIENT');
59+
if ($ssh_client) {
60+
// This has the format "<ip> <remote-port> <local-port>". Grab the IP.
61+
$remote_address = head(explode(' ', $ssh_client));
62+
$ssh_log->setData(
63+
array(
64+
'r' => $remote_address,
65+
));
66+
}
67+
68+
$key_id = $args->getArg('phabricator-ssh-key');
69+
if ($key_id) {
70+
$ssh_log->setData(
71+
array(
72+
'k' => $key_id,
73+
));
74+
}
75+
3376
$user_name = $args->getArg('phabricator-ssh-user');
34-
if (!strlen($user_name)) {
35-
throw new Exception('No username.');
77+
$device_name = $args->getArg('phabricator-ssh-device');
78+
79+
$user = null;
80+
$device = null;
81+
$is_cluster_request = false;
82+
83+
if ($user_name && $device_name) {
84+
throw new Exception(
85+
pht(
86+
'The --phabricator-ssh-user and --phabricator-ssh-device flags are '.
87+
'mutually exclusive. You can not authenticate as both a user ("%s") '.
88+
'and a device ("%s"). Specify one or the other, but not both.',
89+
$user_name,
90+
$device_name));
91+
} else if (strlen($user_name)) {
92+
$user = id(new PhabricatorPeopleQuery())
93+
->setViewer(PhabricatorUser::getOmnipotentUser())
94+
->withUsernames(array($user_name))
95+
->executeOne();
96+
if (!$user) {
97+
throw new Exception(
98+
pht(
99+
'Invalid username ("%s"). There is no user with this username.',
100+
$user_name));
101+
}
102+
} else if (strlen($device_name)) {
103+
if (!$remote_address) {
104+
throw new Exception(
105+
pht(
106+
'Unable to identify remote address from the SSH_CLIENT environment '.
107+
'variable. Device authentication is accepted only from trusted '.
108+
'sources.'));
109+
}
110+
111+
if (!PhabricatorEnv::isClusterAddress($remote_address)) {
112+
throw new Exception(
113+
pht(
114+
'This request originates from outside of the Phabricator cluster '.
115+
'address range. Requests signed with a trusted device key must '.
116+
'originate from trusted hosts.'));
117+
}
118+
119+
$device = id(new AlmanacDeviceQuery())
120+
->setViewer(PhabricatorUser::getOmnipotentUser())
121+
->withNames(array($device_name))
122+
->executeOne();
123+
if (!$device) {
124+
throw new Exception(
125+
pht(
126+
'Invalid device name ("%s"). There is no device with this name.',
127+
$device->getName()));
128+
}
129+
130+
// We're authenticated as a device, but we're going to read the user out of
131+
// the command below.
132+
$is_cluster_request = true;
133+
} else {
134+
throw new Exception(
135+
pht(
136+
'This script must be invoked with either the --phabricator-ssh-user '.
137+
'or --phabricator-ssh-device flag.'));
138+
}
139+
140+
if ($args->getArg('ssh-command')) {
141+
$original_command = $args->getArg('ssh-command');
142+
} else {
143+
$original_command = getenv('SSH_ORIGINAL_COMMAND');
36144
}
37145

38-
$user = id(new PhabricatorUser())->loadOneWhere(
39-
'userName = %s',
40-
$user_name);
41-
if (!$user) {
42-
throw new Exception('Invalid username.');
146+
$original_argv = id(new PhutilShellLexer())
147+
->splitArguments($original_command);
148+
149+
if ($device) {
150+
$act_as_name = array_shift($original_argv);
151+
if (!preg_match('/^@/', $act_as_name)) {
152+
throw new Exception(
153+
pht(
154+
'Commands executed by devices must identify an acting user in the '.
155+
'first command argument. This request was not constructed '.
156+
'properly.'));
157+
}
158+
159+
$act_as_name = substr($act_as_name, 1);
160+
$user = id(new PhabricatorPeopleQuery())
161+
->setViewer(PhabricatorUser::getOmnipotentUser())
162+
->withUsernames(array($act_as_name))
163+
->executeOne();
164+
if (!$user) {
165+
throw new Exception(
166+
pht(
167+
'Device request identifies an acting user with an invalid '.
168+
'username ("%s"). There is no user with this username.',
169+
$act_as_name));
170+
}
43171
}
44172

45173
$ssh_log->setData(
@@ -49,13 +177,11 @@
49177
));
50178

51179
if (!$user->isUserActivated()) {
52-
throw new Exception(pht('Your account is not activated.'));
53-
}
54-
55-
if ($args->getArg('ssh-command')) {
56-
$original_command = $args->getArg('ssh-command');
57-
} else {
58-
$original_command = getenv('SSH_ORIGINAL_COMMAND');
180+
throw new Exception(
181+
pht(
182+
'Your account ("%s") is not activated. Visit the web interface '.
183+
'for more information.',
184+
$user->getUsername()));
59185
}
60186

61187
$workflows = id(new PhutilSymbolLoader())
@@ -64,9 +190,6 @@
64190

65191
$workflow_names = mpull($workflows, 'getName', 'getName');
66192

67-
// Now, rebuild the original command.
68-
$original_argv = id(new PhutilShellLexer())
69-
->splitArguments($original_command);
70193
if (!$original_argv) {
71194
throw new Exception(
72195
pht(
@@ -82,7 +205,7 @@
82205
implode(', ', $workflow_names)));
83206
}
84207

85-
$log_argv = implode(' ', array_slice($original_argv, 1));
208+
$log_argv = implode(' ', $original_argv);
86209
$log_argv = id(new PhutilUTF8StringTruncator())
87210
->setMaximumCodepoints(128)
88211
->truncateString($log_argv);
@@ -94,16 +217,20 @@
94217
));
95218

96219
$command = head($original_argv);
97-
array_unshift($original_argv, 'phabricator-ssh-exec');
98220

99-
$original_args = new PhutilArgumentParser($original_argv);
221+
$parseable_argv = $original_argv;
222+
array_unshift($parseable_argv, 'phabricator-ssh-exec');
223+
224+
$parsed_args = new PhutilArgumentParser($parseable_argv);
100225

101226
if (empty($workflow_names[$command])) {
102227
throw new Exception('Invalid command.');
103228
}
104229

105-
$workflow = $original_args->parseWorkflows($workflows);
230+
$workflow = $parsed_args->parseWorkflows($workflows);
106231
$workflow->setUser($user);
232+
$workflow->setOriginalArguments($original_argv);
233+
$workflow->setIsClusterRequest($is_cluster_request);
107234

108235
$sock_stdin = fopen('php://stdin', 'r');
109236
if (!$sock_stdin) {
@@ -130,7 +257,7 @@
130257

131258
$rethrow = null;
132259
try {
133-
$err = $workflow->execute($original_args);
260+
$err = $workflow->execute($parsed_args);
134261

135262
$metrics_channel->flush();
136263
$error_channel->flush();

‎src/applications/config/option/PhabricatorAccessLogConfigOptions.php

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public function getOptions() {
3737
$ssh_map = $common_map + array(
3838
's' => pht('The system user.'),
3939
'S' => pht('The system sudo user.'),
40+
'k' => pht('ID of the SSH key used to authenticate the request.'),
4041
);
4142

4243
$http_desc = pht(

‎src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ protected function executeRepositoryOperations() {
1919
// This is a write, and must have write access.
2020
$this->requireWriteAccess();
2121

22-
$command = csprintf('git-receive-pack %s', $repository->getLocalPath());
22+
if ($this->shouldProxy()) {
23+
$command = $this->getProxyCommand();
24+
} else {
25+
$command = csprintf('git-receive-pack %s', $repository->getLocalPath());
26+
}
2327
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
2428

2529
$future = id(new ExecFuture('%C', $command))

‎src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ protected function didConstruct() {
1616
protected function executeRepositoryOperations() {
1717
$repository = $this->getRepository();
1818

19-
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath());
19+
if ($this->shouldProxy()) {
20+
$command = $this->getProxyCommand();
21+
} else {
22+
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath());
23+
}
2024
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
2125

2226
$future = id(new ExecFuture('%C', $command))

‎src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ protected function executeRepositoryOperations() {
4242
throw new Exception('Expected `hg ... serve`!');
4343
}
4444

45-
$command = csprintf('hg -R %s serve --stdio', $repository->getLocalPath());
45+
if ($this->shouldProxy()) {
46+
$command = $this->getProxyCommand();
47+
} else {
48+
$command = csprintf(
49+
'hg -R %s serve --stdio',
50+
$repository->getLocalPath());
51+
}
4652
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
4753

4854
$future = id(new ExecFuture('%C', $command))

0 commit comments

Comments
 (0)
Failed to load comments.