|
8 | 8 |
|
9 | 9 | $ssh_log = PhabricatorSSHLog::getLog();
|
10 | 10 |
|
11 |
| -// First, figure out the authenticated user. |
12 | 11 | $args = new PhutilArgumentParser($argv);
|
13 |
| -$args->setTagline('receive SSH requests'); |
| 12 | +$args->setTagline('execute SSH requests'); |
14 | 13 | $args->setSynopsis(<<<EOSYNOPSIS
|
15 | 14 | **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 | +
|
17 | 19 | EOSYNOPSIS
|
18 | 20 | );
|
19 | 21 |
|
|
22 | 24 | array(
|
23 | 25 | 'name' => 'phabricator-ssh-user',
|
24 | 26 | '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.'), |
25 | 45 | ),
|
26 | 46 | array(
|
27 | 47 | 'name' => 'ssh-command',
|
28 | 48 | '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.'), |
29 | 53 | ),
|
30 | 54 | ));
|
31 | 55 |
|
32 | 56 | 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 | + |
33 | 76 | $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'); |
36 | 144 | }
|
37 | 145 |
|
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 | + } |
43 | 171 | }
|
44 | 172 |
|
45 | 173 | $ssh_log->setData(
|
|
49 | 177 | ));
|
50 | 178 |
|
51 | 179 | 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())); |
59 | 185 | }
|
60 | 186 |
|
61 | 187 | $workflows = id(new PhutilSymbolLoader())
|
|
64 | 190 |
|
65 | 191 | $workflow_names = mpull($workflows, 'getName', 'getName');
|
66 | 192 |
|
67 |
| - // Now, rebuild the original command. |
68 |
| - $original_argv = id(new PhutilShellLexer()) |
69 |
| - ->splitArguments($original_command); |
70 | 193 | if (!$original_argv) {
|
71 | 194 | throw new Exception(
|
72 | 195 | pht(
|
|
82 | 205 | implode(', ', $workflow_names)));
|
83 | 206 | }
|
84 | 207 |
|
85 |
| - $log_argv = implode(' ', array_slice($original_argv, 1)); |
| 208 | + $log_argv = implode(' ', $original_argv); |
86 | 209 | $log_argv = id(new PhutilUTF8StringTruncator())
|
87 | 210 | ->setMaximumCodepoints(128)
|
88 | 211 | ->truncateString($log_argv);
|
|
94 | 217 | ));
|
95 | 218 |
|
96 | 219 | $command = head($original_argv);
|
97 |
| - array_unshift($original_argv, 'phabricator-ssh-exec'); |
98 | 220 |
|
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); |
100 | 225 |
|
101 | 226 | if (empty($workflow_names[$command])) {
|
102 | 227 | throw new Exception('Invalid command.');
|
103 | 228 | }
|
104 | 229 |
|
105 |
| - $workflow = $original_args->parseWorkflows($workflows); |
| 230 | + $workflow = $parsed_args->parseWorkflows($workflows); |
106 | 231 | $workflow->setUser($user);
|
| 232 | + $workflow->setOriginalArguments($original_argv); |
| 233 | + $workflow->setIsClusterRequest($is_cluster_request); |
107 | 234 |
|
108 | 235 | $sock_stdin = fopen('php://stdin', 'r');
|
109 | 236 | if (!$sock_stdin) {
|
|
130 | 257 |
|
131 | 258 | $rethrow = null;
|
132 | 259 | try {
|
133 |
| - $err = $workflow->execute($original_args); |
| 260 | + $err = $workflow->execute($parsed_args); |
134 | 261 |
|
135 | 262 | $metrics_channel->flush();
|
136 | 263 | $error_channel->flush();
|
|
0 commit comments