1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-14 02:42:40 +01:00

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
This commit is contained in:
epriestley 2015-01-28 14:41:24 -08:00
parent fe0ca0abf2
commit 8798083ad9
9 changed files with 289 additions and 48 deletions

View file

@ -8,15 +8,6 @@ $keys = id(new PhabricatorAuthSSHKeyQuery())
->setViewer(PhabricatorUser::getOmnipotentUser()) ->setViewer(PhabricatorUser::getOmnipotentUser())
->execute(); ->execute();
foreach ($keys as $key => $ssh_key) {
// For now, filter out any keys which don't belong to users. Eventually we
// may allow devices to use this channel.
if (!($ssh_key->getObject() instanceof PhabricatorUser)) {
unset($keys[$key]);
continue;
}
}
if (!$keys) { if (!$keys) {
echo pht('No keys found.')."\n"; echo pht('No keys found.')."\n";
exit(1); exit(1);
@ -24,11 +15,26 @@ if (!$keys) {
$bin = $root.'/bin/ssh-exec'; $bin = $root.'/bin/ssh-exec';
foreach ($keys as $ssh_key) { foreach ($keys as $ssh_key) {
$user = $ssh_key->getObject()->getUsername();
$key_argv = array(); $key_argv = array();
$key_argv[] = '--phabricator-ssh-user'; $object = $ssh_key->getObject();
$key_argv[] = $user; if ($object instanceof PhabricatorUser) {
$key_argv[] = '--phabricator-ssh-user';
$key_argv[] = $object->getUsername();
} else if ($object instanceof AlmanacDevice) {
if (!$ssh_key->getIsTrusted()) {
// If this key is not a trusted device key, don't allow SSH
// authentication.
continue;
}
$key_argv[] = '--phabricator-ssh-device';
$key_argv[] = $object->getName();
} else {
// We don't know what sort of key this is; don't permit SSH auth.
continue;
}
$key_argv[] = '--phabricator-ssh-key';
$key_argv[] = $ssh_key->getID();
$cmd = csprintf('%s %Ls', $bin, $key_argv); $cmd = csprintf('%s %Ls', $bin, $key_argv);

View file

@ -8,12 +8,14 @@ require_once $root.'/scripts/__init_script__.php';
$ssh_log = PhabricatorSSHLog::getLog(); $ssh_log = PhabricatorSSHLog::getLog();
// First, figure out the authenticated user.
$args = new PhutilArgumentParser($argv); $args = new PhutilArgumentParser($argv);
$args->setTagline('receive SSH requests'); $args->setTagline('execute SSH requests');
$args->setSynopsis(<<<EOSYNOPSIS $args->setSynopsis(<<<EOSYNOPSIS
**ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__] **ssh-exec** --phabricator-ssh-user __user__ [--ssh-command __commmand__]
Receive SSH requests. **ssh-exec** --phabricator-ssh-device __device__ [--ssh-command __commmand__]
Execute authenticated SSH requests. This script is normally invoked
via SSHD, but can be invoked manually for testing.
EOSYNOPSIS EOSYNOPSIS
); );
@ -22,24 +24,150 @@ $args->parse(
array( array(
'name' => 'phabricator-ssh-user', 'name' => 'phabricator-ssh-user',
'param' => 'username', 'param' => 'username',
'help' => pht(
'If the request authenticated with a user key, the name of the '.
'user.'),
),
array(
'name' => 'phabricator-ssh-device',
'param' => 'name',
'help' => pht(
'If the request authenticated with a device key, the name of the '.
'device.'),
),
array(
'name' => 'phabricator-ssh-key',
'param' => 'id',
'help' => pht(
'The ID of the SSH key which authenticated this request. This is '.
'used to allow logs to report when specific keys were used, to make '.
'it easier to manage credentials.'),
), ),
array( array(
'name' => 'ssh-command', 'name' => 'ssh-command',
'param' => 'command', 'param' => 'command',
'help' => pht(
'Provide a command to execute. This makes testing this script '.
'easier. When running normally, the command is read from the '.
'environment (SSH_ORIGINAL_COMMAND), which is populated by sshd.'),
), ),
)); ));
try { try {
$user_name = $args->getArg('phabricator-ssh-user'); $remote_address = null;
if (!strlen($user_name)) { $ssh_client = getenv('SSH_CLIENT');
throw new Exception('No username.'); if ($ssh_client) {
// This has the format "<ip> <remote-port> <local-port>". Grab the IP.
$remote_address = head(explode(' ', $ssh_client));
$ssh_log->setData(
array(
'r' => $remote_address,
));
} }
$user = id(new PhabricatorUser())->loadOneWhere( $key_id = $args->getArg('phabricator-ssh-key');
'userName = %s', if ($key_id) {
$user_name); $ssh_log->setData(
if (!$user) { array(
throw new Exception('Invalid username.'); 'k' => $key_id,
));
}
$user_name = $args->getArg('phabricator-ssh-user');
$device_name = $args->getArg('phabricator-ssh-device');
$user = null;
$device = null;
$is_cluster_request = false;
if ($user_name && $device_name) {
throw new Exception(
pht(
'The --phabricator-ssh-user and --phabricator-ssh-device flags are '.
'mutually exclusive. You can not authenticate as both a user ("%s") '.
'and a device ("%s"). Specify one or the other, but not both.',
$user_name,
$device_name));
} else if (strlen($user_name)) {
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($user_name))
->executeOne();
if (!$user) {
throw new Exception(
pht(
'Invalid username ("%s"). There is no user with this username.',
$user_name));
}
} else if (strlen($device_name)) {
if (!$remote_address) {
throw new Exception(
pht(
'Unable to identify remote address from the SSH_CLIENT environment '.
'variable. Device authentication is accepted only from trusted '.
'sources.'));
}
if (!PhabricatorEnv::isClusterAddress($remote_address)) {
throw new Exception(
pht(
'This request originates from outside of the Phabricator cluster '.
'address range. Requests signed with a trusted device key must '.
'originate from trusted hosts.'));
}
$device = id(new AlmanacDeviceQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withNames(array($device_name))
->executeOne();
if (!$device) {
throw new Exception(
pht(
'Invalid device name ("%s"). There is no device with this name.',
$device->getName()));
}
// We're authenticated as a device, but we're going to read the user out of
// the command below.
$is_cluster_request = true;
} else {
throw new Exception(
pht(
'This script must be invoked with either the --phabricator-ssh-user '.
'or --phabricator-ssh-device flag.'));
}
if ($args->getArg('ssh-command')) {
$original_command = $args->getArg('ssh-command');
} else {
$original_command = getenv('SSH_ORIGINAL_COMMAND');
}
$original_argv = id(new PhutilShellLexer())
->splitArguments($original_command);
if ($device) {
$act_as_name = array_shift($original_argv);
if (!preg_match('/^@/', $act_as_name)) {
throw new Exception(
pht(
'Commands executed by devices must identify an acting user in the '.
'first command argument. This request was not constructed '.
'properly.'));
}
$act_as_name = substr($act_as_name, 1);
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withUsernames(array($act_as_name))
->executeOne();
if (!$user) {
throw new Exception(
pht(
'Device request identifies an acting user with an invalid '.
'username ("%s"). There is no user with this username.',
$act_as_name));
}
} }
$ssh_log->setData( $ssh_log->setData(
@ -49,13 +177,11 @@ try {
)); ));
if (!$user->isUserActivated()) { if (!$user->isUserActivated()) {
throw new Exception(pht('Your account is not activated.')); throw new Exception(
} pht(
'Your account ("%s") is not activated. Visit the web interface '.
if ($args->getArg('ssh-command')) { 'for more information.',
$original_command = $args->getArg('ssh-command'); $user->getUsername()));
} else {
$original_command = getenv('SSH_ORIGINAL_COMMAND');
} }
$workflows = id(new PhutilSymbolLoader()) $workflows = id(new PhutilSymbolLoader())
@ -64,9 +190,6 @@ try {
$workflow_names = mpull($workflows, 'getName', 'getName'); $workflow_names = mpull($workflows, 'getName', 'getName');
// Now, rebuild the original command.
$original_argv = id(new PhutilShellLexer())
->splitArguments($original_command);
if (!$original_argv) { if (!$original_argv) {
throw new Exception( throw new Exception(
pht( pht(
@ -82,7 +205,7 @@ try {
implode(', ', $workflow_names))); implode(', ', $workflow_names)));
} }
$log_argv = implode(' ', array_slice($original_argv, 1)); $log_argv = implode(' ', $original_argv);
$log_argv = id(new PhutilUTF8StringTruncator()) $log_argv = id(new PhutilUTF8StringTruncator())
->setMaximumCodepoints(128) ->setMaximumCodepoints(128)
->truncateString($log_argv); ->truncateString($log_argv);
@ -94,16 +217,20 @@ try {
)); ));
$command = head($original_argv); $command = head($original_argv);
array_unshift($original_argv, 'phabricator-ssh-exec');
$original_args = new PhutilArgumentParser($original_argv); $parseable_argv = $original_argv;
array_unshift($parseable_argv, 'phabricator-ssh-exec');
$parsed_args = new PhutilArgumentParser($parseable_argv);
if (empty($workflow_names[$command])) { if (empty($workflow_names[$command])) {
throw new Exception('Invalid command.'); throw new Exception('Invalid command.');
} }
$workflow = $original_args->parseWorkflows($workflows); $workflow = $parsed_args->parseWorkflows($workflows);
$workflow->setUser($user); $workflow->setUser($user);
$workflow->setOriginalArguments($original_argv);
$workflow->setIsClusterRequest($is_cluster_request);
$sock_stdin = fopen('php://stdin', 'r'); $sock_stdin = fopen('php://stdin', 'r');
if (!$sock_stdin) { if (!$sock_stdin) {
@ -130,7 +257,7 @@ try {
$rethrow = null; $rethrow = null;
try { try {
$err = $workflow->execute($original_args); $err = $workflow->execute($parsed_args);
$metrics_channel->flush(); $metrics_channel->flush();
$error_channel->flush(); $error_channel->flush();

View file

@ -37,6 +37,7 @@ final class PhabricatorAccessLogConfigOptions
$ssh_map = $common_map + array( $ssh_map = $common_map + array(
's' => pht('The system user.'), 's' => pht('The system user.'),
'S' => pht('The system sudo user.'), 'S' => pht('The system sudo user.'),
'k' => pht('ID of the SSH key used to authenticate the request.'),
); );
$http_desc = pht( $http_desc = pht(

View file

@ -19,7 +19,11 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow {
// This is a write, and must have write access. // This is a write, and must have write access.
$this->requireWriteAccess(); $this->requireWriteAccess();
$command = csprintf('git-receive-pack %s', $repository->getLocalPath()); if ($this->shouldProxy()) {
$command = $this->getProxyCommand();
} else {
$command = csprintf('git-receive-pack %s', $repository->getLocalPath());
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = id(new ExecFuture('%C', $command)) $future = id(new ExecFuture('%C', $command))

View file

@ -16,7 +16,11 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow {
protected function executeRepositoryOperations() { protected function executeRepositoryOperations() {
$repository = $this->getRepository(); $repository = $this->getRepository();
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath()); if ($this->shouldProxy()) {
$command = $this->getProxyCommand();
} else {
$command = csprintf('git-upload-pack -- %s', $repository->getLocalPath());
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = id(new ExecFuture('%C', $command)) $future = id(new ExecFuture('%C', $command))

View file

@ -42,7 +42,13 @@ final class DiffusionMercurialServeSSHWorkflow
throw new Exception('Expected `hg ... serve`!'); throw new Exception('Expected `hg ... serve`!');
} }
$command = csprintf('hg -R %s serve --stdio', $repository->getLocalPath()); if ($this->shouldProxy()) {
$command = $this->getProxyCommand();
} else {
$command = csprintf(
'hg -R %s serve --stdio',
$repository->getLocalPath());
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = id(new ExecFuture('%C', $command)) $future = id(new ExecFuture('%C', $command))

View file

@ -5,6 +5,7 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
private $args; private $args;
private $repository; private $repository;
private $hasWriteAccess; private $hasWriteAccess;
private $proxyURI;
public function getRepository() { public function getRepository() {
if (!$this->repository) { if (!$this->repository) {
@ -49,14 +50,82 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow {
return $this; return $this;
} }
protected function shouldProxy() {
return (bool)$this->proxyURI;
}
protected function getProxyCommand() {
$uri = new PhutilURI($this->proxyURI);
$username = PhabricatorEnv::getEnvConfig('cluster.instance');
if (!strlen($username)) {
$username = PhabricatorEnv::getEnvConfig('diffusion.ssh-user');
if (!strlen($username)) {
throw new Exception(
pht(
'Unable to determine the username to connect with when trying '.
'to proxy an SSH request within the Phabricator cluster.'));
}
}
$port = $uri->getPort();
$host = $uri->getDomain();
$key_path = AlmanacKeys::getKeyPath('device.key');
if (!Filesystem::pathExists($key_path)) {
throw new Exception(
pht(
'Unable to proxy this SSH request within the cluster: this device '.
'is not registered and has a missing device key (expected to '.
'find key at "%s").',
$key_path));
}
$options = array();
$options[] = '-o';
$options[] = 'StrictHostKeyChecking=no';
$options[] = '-o';
$options[] = 'UserKnownHostsFile=/dev/null';
// This is suppressing "added <address> to the list of known hosts"
// messages, which are confusing and irrelevant when they arise from
// proxied requests. It might also be suppressing lots of useful errors,
// of course. Ideally, we would enforce host keys eventually.
$options[] = '-o';
$options[] = 'LogLevel=quiet';
// NOTE: We prefix the command with "@username", which the far end of the
// connection will parse in order to act as the specified user. This
// behavior is only available to cluster requests signed by a trusted
// device key.
return csprintf(
'ssh %Ls -l %s -i %s -p %s %s -- %s %Ls',
$options,
$username,
$key_path,
$port,
$host,
'@'.$this->getUser()->getUsername(),
$this->getOriginalArguments());
}
final public function execute(PhutilArgumentParser $args) { final public function execute(PhutilArgumentParser $args) {
$this->args = $args; $this->args = $args;
$repository = $this->identifyRepository(); $repository = $this->identifyRepository();
$this->setRepository($repository); $this->setRepository($repository);
// TODO: Here, we would make a proxying decision, had I implemented $is_cluster_request = $this->getIsClusterRequest();
// proxying yet. $uri = $repository->getAlmanacServiceURI(
$this->getUser(),
$is_cluster_request,
array(
'ssh',
));
if ($uri) {
$this->proxyURI = $uri;
}
try { try {
return $this->executeRepositoryOperations(); return $this->executeRepositoryOperations();

View file

@ -144,11 +144,15 @@ final class DiffusionSubversionServeSSHWorkflow
throw new Exception('Expected `svnserve -t`!'); throw new Exception('Expected `svnserve -t`!');
} }
$command = csprintf( if ($this->shouldProxy()) {
'svnserve -t --tunnel-user=%s', $command = $this->getProxyCommand();
$this->getUser()->getUsername()); } else {
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $command = csprintf(
'svnserve -t --tunnel-user=%s',
$this->getUser()->getUsername());
}
$command = PhabricatorDaemon::sudoCommandAsDaemonUser($command);
$future = new ExecFuture('%C', $command); $future = new ExecFuture('%C', $command);
$this->inProtocol = new DiffusionSubversionWireProtocol(); $this->inProtocol = new DiffusionSubversionWireProtocol();

View file

@ -5,6 +5,8 @@ abstract class PhabricatorSSHWorkflow extends PhabricatorManagementWorkflow {
private $user; private $user;
private $iochannel; private $iochannel;
private $errorChannel; private $errorChannel;
private $isClusterRequest;
private $originalArguments;
public function isExecutable() { public function isExecutable() {
return false; return false;
@ -63,4 +65,22 @@ abstract class PhabricatorSSHWorkflow extends PhabricatorManagementWorkflow {
->setErrorChannel($this->getErrorChannel()); ->setErrorChannel($this->getErrorChannel());
} }
public function setIsClusterRequest($is_cluster_request) {
$this->isClusterRequest = $is_cluster_request;
return $this;
}
public function getIsClusterRequest() {
return $this->isClusterRequest;
}
public function setOriginalArguments(array $original_arguments) {
$this->originalArguments = $original_arguments;
return $this;
}
public function getOriginalArguments() {
return $this->originalArguments;
}
} }