protected function getProxyCommand() { $uri = new PhutilURI($this->proxyURI); $username = AlmanacKeys::getClusterSSHUser(); if ($username === null) { 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()); }
/** * Build a new Conduit client in order to make a service call to this * repository. * * If the repository is hosted locally, this method may return `null`. The * caller should use `ConduitCall` or other local logic to complete the * request. * * By default, we will return a @{class:ConduitClient} for any repository with * a service, even if that service is on the current device. * * We do this because this configuration does not make very much sense in a * production context, but is very common in a test/development context * (where the developer's machine is both the web host and the repository * service). By proxying in development, we get more consistent behavior * between development and production, and don't have a major untested * codepath. * * The `$never_proxy` parameter can be used to prevent this local proxying. * If the flag is passed: * * - The method will return `null` (implying a local service call) * if the repository service is hosted on the current device. * - The method will throw if it would need to return a client. * * This is used to prevent loops in Conduit: the first request will proxy, * even in development, but the second request will be identified as a * cluster request and forced not to proxy. * * For lower-level service resolution, see @{method:getAlmanacServiceURI}. * * @param PhabricatorUser Viewing user. * @param bool `true` to throw if a client would be returned. * @return ConduitClient|null Client, or `null` for local repositories. */ public function newConduitClient(PhabricatorUser $viewer, $never_proxy = false) { $uri = $this->getAlmanacServiceURI($viewer, $never_proxy, array('http', 'https')); if ($uri === null) { return null; } $domain = id(new PhutilURI(PhabricatorEnv::getURI('/')))->getDomain(); $client = id(new ConduitClient($uri))->setHost($domain); if ($viewer->isOmnipotent()) { // If the caller is the omnipotent user (normally, a daemon), we will // sign the request with this host's asymmetric keypair. $public_path = AlmanacKeys::getKeyPath('device.pub'); try { $public_key = Filesystem::readFile($public_path); } catch (Exception $ex) { throw new PhutilAggregateException(pht('Unable to read device public key while attempting to make ' . 'authenticated method call within the Phabricator cluster. ' . 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $private_path = AlmanacKeys::getKeyPath('device.key'); try { $private_key = Filesystem::readFile($private_path); $private_key = new PhutilOpaqueEnvelope($private_key); } catch (Exception $ex) { throw new PhutilAggregateException(pht('Unable to read device private key while attempting to make ' . 'authenticated method call within the Phabricator cluster. ' . 'Use `%s` to register keys for this device. Exception: %s', 'bin/almanac register', $ex->getMessage()), array($ex)); } $client->setSigningKeys($public_key, $private_key); } else { // If the caller is a normal user, we generate or retrieve a cluster // API token. $token = PhabricatorConduitToken::loadClusterTokenForUser($viewer); if ($token) { $client->setConduitToken($token->getToken()); } } return $client; }
throw new Exception(pht('Attempting to proxy an SSH connection that authenticates with ' . 'both the current device and a specific credential. These options ' . 'are mutually exclusive.')); } } if ($credential_phid) { $viewer = PhabricatorUser::getOmnipotentUser(); $key = PassphraseSSHKey::loadFromPHID($credential_phid, $viewer); $pattern[] = '-l %P'; $arguments[] = $key->getUsernameEnvelope(); $pattern[] = '-i %P'; $arguments[] = $key->getKeyfileEnvelope(); } if ($as_device) { $pattern[] = '-l %R'; $arguments[] = AlmanacKeys::getClusterSSHUser(); $pattern[] = '-i %R'; $arguments[] = AlmanacKeys::getKeyPath('device.key'); } // Subversion passes us a host in the form "domain.com:port", which is not // valid for normal SSH but which we can parse into a valid "-p" flag. $passthru_args = $unconsumed_argv; $host = array_shift($passthru_args); $parts = explode(':', $host, 2); $host = $parts[0]; $port = $args->getArg('port'); if (!$port) { if (count($parts) == 2) { $port = $parts[1]; } } if ($port) { $pattern[] = '-p %d';
public function execute(PhutilArgumentParser $args) { $viewer = $this->getViewer(); $device_name = $args->getArg('device'); if (!strlen($device_name)) { throw new PhutilArgumentUsageException(pht('Specify a device with --device.')); } $device = id(new AlmanacDeviceQuery())->setViewer($viewer)->withNames(array($device_name))->executeOne(); if (!$device) { throw new PhutilArgumentUsageException(pht('No such device "%s" exists!', $device_name)); } $identify_as = $args->getArg('identify-as'); $raw_device = $device_name; if (strlen($identify_as)) { $raw_device = $identify_as; } $identity_device = id(new AlmanacDeviceQuery())->setViewer($viewer)->withNames(array($raw_device))->executeOne(); if (!$identity_device) { throw new PhutilArgumentUsageException(pht('No such device "%s" exists!', $raw_device)); } $private_key_path = $args->getArg('private-key'); if (!strlen($private_key_path)) { throw new PhutilArgumentUsageException(pht('Specify a private key with --private-key.')); } if (!Filesystem::pathExists($private_key_path)) { throw new PhutilArgumentUsageException(pht('No private key exists at path "%s"!', $private_key_path)); } $raw_private_key = Filesystem::readFile($private_key_path); $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); if (!$phd_user) { throw new PhutilArgumentUsageException(pht('Config option "phd.user" is not set. You must set this option ' . 'so the private key can be stored with the correct permissions.')); } $tmp = new TempFile(); list($err) = exec_manual('chown %s %s', $phd_user, $tmp); if ($err) { throw new PhutilArgumentUsageException(pht('Unable to change ownership of an identity file to daemon user ' . '"%s". Run this command as %s or root.', $phd_user, $phd_user)); } $stored_public_path = AlmanacKeys::getKeyPath('device.pub'); $stored_private_path = AlmanacKeys::getKeyPath('device.key'); $stored_device_path = AlmanacKeys::getKeyPath('device.id'); if (!$args->getArg('force')) { if (Filesystem::pathExists($stored_public_path)) { throw new PhutilArgumentUsageException(pht('This host already has a registered public key ("%s"). ' . 'Remove this key before registering the host, or use ' . '--force to overwrite it.', Filesystem::readablePath($stored_public_path))); } if (Filesystem::pathExists($stored_private_path)) { throw new PhutilArgumentUsageException(pht('This host already has a registered private key ("%s"). ' . 'Remove this key before registering the host, or use ' . '--force to overwrite it.', Filesystem::readablePath($stored_private_path))); } } // NOTE: We're writing the private key here so we can change permissions // on it without causing weird side effects to the file specified with // the `--private-key` flag. The file needs to have restrictive permissions // before `ssh-keygen` will willingly operate on it. $tmp_private = new TempFile(); Filesystem::changePermissions($tmp_private, 0600); execx('chown %s %s', $phd_user, $tmp_private); Filesystem::writeFile($tmp_private, $raw_private_key); list($raw_public_key) = execx('ssh-keygen -y -f %s', $tmp_private); $key_object = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_public_key); $public_key = id(new PhabricatorAuthSSHKeyQuery())->setViewer($this->getViewer())->withKeys(array($key_object))->withIsActive(true)->executeOne(); if (!$public_key) { throw new PhutilArgumentUsageException(pht('The public key corresponding to the given private key is not ' . 'yet known to Phabricator. Associate the public key with an ' . 'Almanac device in the web interface before registering hosts ' . 'with it.')); } if ($public_key->getObjectPHID() !== $device->getPHID()) { $public_phid = $public_key->getObjectPHID(); $public_handles = $viewer->loadHandles(array($public_phid)); $public_handle = $public_handles[$public_phid]; throw new PhutilArgumentUsageException(pht('The public key corresponding to the given private key is already ' . 'associated with an object ("%s") other than the specified ' . 'device ("%s"). You can not use a single private key to identify ' . 'multiple devices or users.', $public_handle->getFullName(), $device->getName())); } if (!$public_key->getIsTrusted()) { throw new PhutilArgumentUsageException(pht('The public key corresponding to the given private key is ' . 'properly associated with the device, but is not yet trusted. ' . 'Trust this key before registering devices with it.')); } echo tsprintf("%s\n", pht('Installing public key...')); $tmp_public = new TempFile(); Filesystem::changePermissions($tmp_public, 0600); execx('chown %s %s', $phd_user, $tmp_public); Filesystem::writeFile($tmp_public, $raw_public_key); execx('mv -f %s %s', $tmp_public, $stored_public_path); echo tsprintf("%s\n", pht('Installing private key...')); execx('mv -f %s %s', $tmp_private, $stored_private_path); echo tsprintf("%s\n", pht('Installing device %s...', $raw_device)); // The permissions on this file are more open because the webserver also // needs to read it. $tmp_device = new TempFile(); Filesystem::changePermissions($tmp_device, 0644); execx('chown %s %s', $phd_user, $tmp_device); Filesystem::writeFile($tmp_device, $raw_device); execx('mv -f %s %s', $tmp_device, $stored_device_path); echo tsprintf("**<bg:green> %s </bg>** %s\n", pht('HOST REGISTERED'), pht('This host has been registered as "%s" and a trusted keypair ' . 'has been installed.', $raw_device)); }
public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $device_name = $args->getArg('device'); if (!strlen($device_name)) { throw new PhutilArgumentUsageException(pht('Specify a device with --device.')); } $device = id(new AlmanacDeviceQuery())->setViewer($this->getViewer())->withNames(array($device_name))->executeOne(); if (!$device) { throw new PhutilArgumentUsageException(pht('No such device "%s" exists!', $device_name)); } $private_key_path = $args->getArg('private-key'); if (!strlen($private_key_path)) { throw new PhutilArgumentUsageException(pht('Specify a private key with --private-key.')); } if (!Filesystem::pathExists($private_key_path)) { throw new PhutilArgumentUsageException(pht('Private key "%s" does not exist!', $private_key_path)); } $raw_private_key = Filesystem::readFile($private_key_path); $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); if (!$phd_user) { throw new PhutilArgumentUsageException(pht('Config option "phd.user" is not set. You must set this option ' . 'so the private key can be stored with the correct permissions.')); } $tmp = new TempFile(); list($err) = exec_manual('chown %s %s', $phd_user, $tmp); if ($err) { throw new PhutilArgumentUsageException(pht('Unable to change ownership of a file to daemon user "%s". Run ' . 'this command as %s or root.', $phd_user, $phd_user)); } $stored_public_path = AlmanacKeys::getKeyPath('device.pub'); $stored_private_path = AlmanacKeys::getKeyPath('device.key'); $stored_device_path = AlmanacKeys::getKeyPath('device.id'); if (!$args->getArg('force')) { if (Filesystem::pathExists($stored_public_path)) { throw new PhutilArgumentUsageException(pht('This host already has a registered public key ("%s"). ' . 'Remove this key before registering the host, or use ' . '--force to overwrite it.', Filesystem::readablePath($stored_public_path))); } if (Filesystem::pathExists($stored_private_path)) { throw new PhutilArgumentUsageException(pht('This host already has a registered private key ("%s"). ' . 'Remove this key before registering the host, or use ' . '--force to overwrite it.', Filesystem::readablePath($stored_private_path))); } } // NOTE: We're writing the private key here so we can change permissions // on it without causing weird side effects to the file specified with // the `--private-key` flag. The file needs to have restrictive permissions // before `ssh-keygen` will willingly operate on it. $tmp_private = new TempFile(); Filesystem::changePermissions($tmp_private, 0600); execx('chown %s %s', $phd_user, $tmp_private); Filesystem::writeFile($tmp_private, $raw_private_key); list($raw_public_key) = execx('ssh-keygen -y -f %s', $tmp_private); $key_object = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_public_key); $public_key = id(new PhabricatorAuthSSHKeyQuery())->setViewer($this->getViewer())->withKeys(array($key_object))->executeOne(); if ($public_key) { if ($public_key->getObjectPHID() !== $device->getPHID()) { throw new PhutilArgumentUsageException(pht('The public key corresponding to the given private key is ' . 'already associated with an object other than the specified ' . 'device. You can not use a single private key to identify ' . 'multiple devices or users.')); } else { if (!$public_key->getIsTrusted()) { throw new PhutilArgumentUsageException(pht('The public key corresponding to the given private key is ' . 'already associated with the device, but is not trusted. ' . 'Registering this key would trust the other entities which ' . 'hold it. Use a unique key, or explicitly enable trust for the ' . 'current key.')); } else { if (!$args->getArg('allow-key-reuse')) { throw new PhutilArgumentUsageException(pht('The public key corresponding to the given private key is ' . 'already associated with the device. If you do not want to ' . 'use a unique key, use --allow-key-reuse to permit ' . 'reassociation.')); } } } } else { $public_key = id(new PhabricatorAuthSSHKey())->setObjectPHID($device->getPHID())->attachObject($device)->setName($device->getSSHKeyDefaultName())->setKeyType($key_object->getType())->setKeyBody($key_object->getBody())->setKeyComment(pht('Registered'))->setIsTrusted(1); } $console->writeOut("%s\n", pht('Installing public key...')); $tmp_public = new TempFile(); Filesystem::changePermissions($tmp_public, 0600); execx('chown %s %s', $phd_user, $tmp_public); Filesystem::writeFile($tmp_public, $raw_public_key); execx('mv -f %s %s', $tmp_public, $stored_public_path); $console->writeOut("%s\n", pht('Installing private key...')); execx('mv -f %s %s', $tmp_private, $stored_private_path); $raw_device = $device_name; $identify_as = $args->getArg('identify-as'); if (strlen($identify_as)) { $raw_device = $identify_as; } $console->writeOut("%s\n", pht('Installing device %s...', $raw_device)); // The permissions on this file are more open because the webserver also // needs to read it. $tmp_device = new TempFile(); Filesystem::changePermissions($tmp_device, 0644); execx('chown %s %s', $phd_user, $tmp_device); Filesystem::writeFile($tmp_device, $raw_device); execx('mv -f %s %s', $tmp_device, $stored_device_path); if (!$public_key->getID()) { $console->writeOut("%s\n", pht('Registering device key...')); $public_key->save(); } $console->writeOut("**<bg:green> %s </bg>** %s\n", pht('HOST REGISTERED'), pht('This host has been registered as "%s" and a trusted keypair ' . 'has been installed.', $raw_device)); }