@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
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement SSHD glue and Conduit SSH endpoint

Summary:
- Build "sshd-auth" (for authentication) and "sshd-exec" (for command execution) binaries. These are callable by "sshd-vcs", located [[https://github.com/epriestley/sshd-vcs | in my account on GitHub]]. They are based on precursors [[https://github.com/epriestley/sshd-vcs-glue | here on GitHub]] which I deployed for TenXer about a year ago, so I have some confidence they at least basically work.
- The problem this solves is that normally every user would need an account on a machine to connect to it, and/or their public keys would all need to be listed in `~/.authorized_keys`. This is a big pain in most installs. Software like Gitosis/Gitolite solve this problem by giving you an easy way to add public keys to `~/.authorized_keys`, but this is pretty gross.
- Roughly, instead of looking in `~/.authorized_keys` when a user connects, the patched sshd instead runs `echo <public key> | sshd-auth`. The `sshd-auth` script looks up the public key and authorizes the matching user, if they exist. It also forces sshd to run `sshd-exec` instead of a normal shell.
- `sshd-exec` receives the authenticated user and any command which was passed to ssh (like `git receive-pack`) and can route them appropriately.
- Overall, this permits a single account to be set up on a server which all Phabricator users can connect to without any extra work, and which can safely execute commands and apply appropriate permissions, and disable users when they are disabled in Phabricator and all that stuff.
- Build out "sshd-exec" to do more thorough checks and setup, and delegate command execution to Workflows (they now exist, and did not when I originally built this stuff).
- Convert @btrahan's conduit API script into a workflow and slightly simplify it (ConduitCall did not exist at the time it was written).

The next steps here on the Repository side are to implement Workflows for Git, SVN and HG wire protocols. These will mostly just proxy the protocols, but also need to enforce permissions. So the approach will basically be:

- Implement workflows for stuff like `git receive-pack`.
- These workflows will implement enough of the underlying protocol to determine what resource the user is trying to access, and whether they want to read or write it.
- They'll then do a permissons check, and kick the user out if they don't have permission to do whatever they are trying to do.
- If the user does have permission, we just proxy the rest of the transaction.

Next steps on the Conduit side are more simple:

- Make ConduitClient understand "ssh://" URLs.

Test Plan: Ran `sshd-exec --phabricator-ssh-user epriestley conduit differential.query`, etc. This will get a more comprehensive test once I set up sshd-vcs.

Reviewers: btrahan, vrana

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T603, T550

Differential Revision: https://secure.phabricator.com/D4229

+269 -79
+1
bin/ssh-auth
··· 1 + ../scripts/ssh/ssh-auth.php
+1
bin/ssh-exec
··· 1 + ../scripts/ssh/ssh-exec.php
-79
scripts/conduit/api.php
··· 1 - #!/usr/bin/env php 2 - <?php 3 - 4 - $root = dirname(dirname(dirname(__FILE__))); 5 - require_once $root.'/scripts/__init_script__.php'; 6 - 7 - $time_start = microtime(true); 8 - 9 - if ($argc !== 3) { 10 - echo "usage: api.php <user_phid> <method>\n"; 11 - exit(1); 12 - } 13 - 14 - $user = null; 15 - $user_str = $argv[1]; 16 - try { 17 - $user = id(new PhabricatorUser()) 18 - ->loadOneWhere('phid = %s', $user_str); 19 - } catch (Exception $e) { 20 - // no op; we'll error in a line or two 21 - } 22 - if (empty($user)) { 23 - echo "usage: api.php <user_phid> <method>\n" . 24 - "user {$user_str} does not exist or failed to load\n"; 25 - exit(1); 26 - } 27 - 28 - $method = $argv[2]; 29 - $method_class_str = ConduitAPIMethod::getClassNameFromAPIMethodName($method); 30 - try { 31 - $method_class = newv($method_class_str, array()); 32 - } catch (Exception $e) { 33 - echo "usage: api.php <user_phid> <method>\n" . 34 - "method {$method_class_str} does not exist\n"; 35 - exit(1); 36 - } 37 - $log = new PhabricatorConduitMethodCallLog(); 38 - $log->setMethod($method); 39 - 40 - $params = @file_get_contents('php://stdin'); 41 - $params = json_decode($params, true); 42 - if (!is_array($params)) { 43 - echo "provide method parameters on stdin as a JSON blob"; 44 - exit(1); 45 - } 46 - 47 - // build a quick ConduitAPIRequest from stdin PLUS the authenticated user 48 - $conduit_request = new ConduitAPIRequest($params); 49 - $conduit_request->setUser($user); 50 - 51 - try { 52 - $result = $method_class->executeMethod($conduit_request); 53 - $error_code = null; 54 - $error_info = null; 55 - } catch (ConduitException $ex) { 56 - $result = null; 57 - $error_code = $ex->getMessage(); 58 - if ($ex->getErrorDescription()) { 59 - $error_info = $ex->getErrorDescription(); 60 - } else { 61 - $error_info = $method_handler->getErrorDescription($error_code); 62 - } 63 - } 64 - $time_end = microtime(true); 65 - 66 - $response = id(new ConduitAPIResponse()) 67 - ->setResult($result) 68 - ->setErrorCode($error_code) 69 - ->setErrorInfo($error_info); 70 - echo json_encode($response->toDictionary()), "\n"; 71 - 72 - // TODO -- how get $connection_id from SSH? 73 - $connection_id = null; 74 - $log->setConnectionID($connection_id); 75 - $log->setError((string)$error_code); 76 - $log->setDuration(1000000 * ($time_end - $time_start)); 77 - $log->save(); 78 - 79 - exit();
+54
scripts/ssh/ssh-auth.php
··· 1 + #!/usr/bin/env php 2 + <?php 3 + 4 + $root = dirname(dirname(dirname(__FILE__))); 5 + require_once $root.'/scripts/__init_script__.php'; 6 + 7 + $cert = file_get_contents('php://stdin'); 8 + 9 + $user = null; 10 + if ($cert) { 11 + $user_dao = new PhabricatorUser(); 12 + $ssh_dao = new PhabricatorUserSSHKey(); 13 + $conn = $user_dao->establishConnection('r'); 14 + 15 + list($type, $body) = array_merge( 16 + explode(' ', $cert), 17 + array('', '')); 18 + 19 + $row = queryfx_one( 20 + $conn, 21 + 'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID 22 + WHERE ssh.keyBody = %s AND ssh.keyType = %s', 23 + $user_dao->getTableName(), 24 + $ssh_dao->getTableName(), 25 + $body, 26 + $type); 27 + if ($row) { 28 + $user = idx($row, 'userName'); 29 + } 30 + } 31 + 32 + if (!$user) { 33 + exit(1); 34 + } 35 + 36 + if (!PhabricatorUser::validateUsername($user)) { 37 + exit(1); 38 + } 39 + 40 + $bin = $root.'/bin/ssh-exec'; 41 + $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); 42 + // This is additional escaping for the SSH 'command="..."' string. 43 + $cmd = str_replace('"', '\\"', $cmd); 44 + 45 + $options = array( 46 + 'command="'.$cmd.'"', 47 + 'no-port-forwarding', 48 + 'no-X11-forwarding', 49 + 'no-agent-forwarding', 50 + 'no-pty', 51 + ); 52 + 53 + echo implode(',', $options); 54 + exit(0);
+87
scripts/ssh/ssh-exec.php
··· 1 + #!/usr/bin/env php 2 + <?php 3 + 4 + $root = dirname(dirname(dirname(__FILE__))); 5 + require_once $root.'/scripts/__init_script__.php'; 6 + 7 + $args = new PhutilArgumentParser($argv); 8 + $args->setTagline('receive SSH requests'); 9 + $args->setSynopsis(<<<EOSYNOPSIS 10 + **ssh-exec** --phabricator-ssh-user __user__ __commmand__ [__options__] 11 + Receive SSH requests. 12 + 13 + EOSYNOPSIS 14 + ); 15 + 16 + // NOTE: Do NOT parse standard arguments. Arguments are coming from a remote 17 + // client over SSH, and they should not be able to execute "--xprofile", 18 + // "--recon", etc. 19 + 20 + $args->parsePartial( 21 + array( 22 + array( 23 + 'name' => 'phabricator-ssh-user', 24 + 'param' => 'username', 25 + ), 26 + )); 27 + 28 + try { 29 + $user_name = $args->getArg('phabricator-ssh-user'); 30 + if (!strlen($user_name)) { 31 + throw new Exception("No username."); 32 + } 33 + 34 + $user = id(new PhabricatorUser())->loadOneWhere( 35 + 'userName = %s', 36 + $user_name); 37 + if (!$user) { 38 + throw new Exception("Invalid username."); 39 + } 40 + 41 + if ($user->getIsDisabled()) { 42 + throw new Exception("You have been exiled."); 43 + } 44 + 45 + $workflows = array( 46 + new ConduitSSHWorkflow(), 47 + ); 48 + 49 + // This duplicates logic in parseWorkflows(), but allows us to raise more 50 + // concise/relevant exceptions when the client is a remote SSH. 51 + $remain = $args->getUnconsumedArgumentVector(); 52 + if (empty($remain)) { 53 + throw new Exception("No command."); 54 + } else { 55 + $command = head($remain); 56 + $workflow_names = mpull($workflows, 'getName', 'getName'); 57 + if (empty($workflow_names[$command])) { 58 + throw new Exception("Invalid command."); 59 + } 60 + } 61 + 62 + $workflow = $args->parseWorkflows($workflows); 63 + $workflow->setUser($user); 64 + 65 + $sock_stdin = fopen('php://stdin', 'r'); 66 + if (!$sock_stdin) { 67 + throw new Exception("Unable to open stdin."); 68 + } 69 + 70 + $sock_stdout = fopen('php://stdout', 'w'); 71 + if (!$sock_stdout) { 72 + throw new Exception("Unable to open stdout."); 73 + } 74 + 75 + $socket_channel = new PhutilSocketChannel( 76 + $sock_stdin, 77 + $sock_stdout); 78 + $metrics_channel = new PhutilMetricsChannel($socket_channel); 79 + $workflow->setIOChannel($metrics_channel); 80 + 81 + $err = $workflow->execute($args); 82 + 83 + $metrics_channel->flush(); 84 + } catch (Exception $ex) { 85 + echo "phabricator-ssh-exec: ".$ex->getMessage()."\n"; 86 + exit(1); 87 + }
+4
src/__phutil_library_map__.php
··· 193 193 'ConduitCall' => 'applications/conduit/call/ConduitCall.php', 194 194 'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php', 195 195 'ConduitException' => 'applications/conduit/protocol/ConduitException.php', 196 + 'ConduitSSHWorkflow' => 'applications/conduit/ssh/ConduitSSHWorkflow.php', 196 197 'DarkConsoleConfigPlugin' => 'aphront/console/plugin/DarkConsoleConfigPlugin.php', 197 198 'DarkConsoleController' => 'aphront/console/DarkConsoleController.php', 198 199 'DarkConsoleCore' => 'aphront/console/DarkConsoleCore.php', ··· 1074 1075 'PhabricatorRequestOverseer' => 'infrastructure/PhabricatorRequestOverseer.php', 1075 1076 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 1076 1077 'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php', 1078 + 'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php', 1077 1079 'PhabricatorScopedEnv' => 'infrastructure/PhabricatorScopedEnv.php', 1078 1080 'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php', 1079 1081 'PhabricatorSearchAttachController' => 'applications/search/controller/PhabricatorSearchAttachController.php', ··· 1521 1523 'ConduitAPI_user_whoami_Method' => 'ConduitAPI_user_Method', 1522 1524 'ConduitCallTestCase' => 'PhabricatorTestCase', 1523 1525 'ConduitException' => 'Exception', 1526 + 'ConduitSSHWorkflow' => 'PhabricatorSSHWorkflow', 1524 1527 'DarkConsoleConfigPlugin' => 'DarkConsolePlugin', 1525 1528 'DarkConsoleController' => 'PhabricatorController', 1526 1529 'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin', ··· 2337 2340 'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO', 2338 2341 'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase', 2339 2342 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', 2343 + 'PhabricatorSSHWorkflow' => 'PhutilArgumentWorkflow', 2340 2344 'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController', 2341 2345 'PhabricatorSearchBaseController' => 'PhabricatorController', 2342 2346 'PhabricatorSearchCommitIndexer' => 'PhabricatorSearchDocumentIndexer',
+81
src/applications/conduit/ssh/ConduitSSHWorkflow.php
··· 1 + <?php 2 + 3 + final class ConduitSSHWorkflow extends PhabricatorSSHWorkflow { 4 + 5 + public function didConstruct() { 6 + $this->setName('conduit'); 7 + $this->setArguments( 8 + array( 9 + array( 10 + 'name' => 'method', 11 + 'wildcard' => true, 12 + ), 13 + )); 14 + } 15 + 16 + public function execute(PhutilArgumentParser $args) { 17 + $time_start = microtime(true); 18 + 19 + $methodv = $args->getArg('method'); 20 + if (!$methodv) { 21 + throw new Exception("No Conduit method provided."); 22 + } else if (count($methodv) > 1) { 23 + throw new Exception("Too many Conduit methods provided."); 24 + } 25 + 26 + $method = head($methodv); 27 + 28 + $json = $this->readAllInput(); 29 + $raw_params = json_decode($json, true); 30 + if (!is_array($raw_params)) { 31 + throw new Exception("Invalid JSON input."); 32 + } 33 + 34 + $params = $raw_params; 35 + unset($params['__conduit__']); 36 + $metadata = idx($raw_params, '__conduit__', array()); 37 + 38 + $call = null; 39 + $error_code = null; 40 + $error_info = null; 41 + 42 + try { 43 + $call = new ConduitCall($method, $params); 44 + $call->setUser($this->getUser()); 45 + 46 + $result = $call->execute(); 47 + } catch (ConduitException $ex) { 48 + $result = null; 49 + $error_code = $ex->getMessage(); 50 + if ($ex->getErrorDescription()) { 51 + $error_info = $ex->getErrorDescription(); 52 + } else if ($call) { 53 + $error_info = $call->getErrorDescription($error_code); 54 + } 55 + } 56 + 57 + $response = id(new ConduitAPIResponse()) 58 + ->setResult($result) 59 + ->setErrorCode($error_code) 60 + ->setErrorInfo($error_info); 61 + 62 + $json_out = json_encode($response->toDictionary()); 63 + $json_out = $json_out."\n"; 64 + 65 + $this->getIOChannel()->write($json_out); 66 + 67 + // NOTE: Flush here so we can get an accurate result for the duration, 68 + // if the response is large and the receiver is slow to read it. 69 + $this->getIOChannel()->flush(); 70 + 71 + $time_end = microtime(true); 72 + 73 + $connection_id = idx($metadata, 'connectionID'); 74 + $log = new PhabricatorConduitMethodCallLog(); 75 + $log->setConnectionID($connection_id); 76 + $log->setMethod($method); 77 + $log->setError((string)$error_code); 78 + $log->setDuration(1000000 * ($time_end - $time_start)); 79 + $log->save(); 80 + } 81 + }
+41
src/infrastructure/ssh/PhabricatorSSHWorkflow.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorSSHWorkflow extends PhutilArgumentWorkflow { 4 + 5 + private $user; 6 + private $iochannel; 7 + 8 + public function setUser(PhabricatorUser $user) { 9 + $this->user = $user; 10 + return $this; 11 + } 12 + 13 + public function getUser() { 14 + return $this->user; 15 + } 16 + 17 + final public function isExecutable() { 18 + return false; 19 + } 20 + 21 + public function setIOChannel(PhutilChannel $channel) { 22 + $this->iochannel = $channel; 23 + return $this; 24 + } 25 + 26 + public function getIOChannel() { 27 + return $this->iochannel; 28 + } 29 + 30 + public function readAllInput() { 31 + $channel = $this->getIOChannel(); 32 + while ($channel->update()) { 33 + PhutilChannel::waitForAny(array($channel)); 34 + if (!$channel->isOpenForReading()) { 35 + break; 36 + } 37 + } 38 + return $channel->read(); 39 + } 40 + 41 + }