mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-22 05:20:56 +01:00
Add bin/almanac register
to associate a host with an Almanac device and trust it
Summary: Ref T2783. This is basically a more refined version of D10400, which churned a bit on things like SSH key storage, the actual way the signing protocol shook out, etc. - When Phabricator tries to make an intra-cluster service call as the omnipotent user, sign it with the host's device key. - Add `bin/almanac register` to say "this host is X device, identified by private key Y". This stores the keypair locally, adds the public key to Almanac, and trusts it. Net effect is that once a host has been registered, the daemons can make calls to other nodes as the omnipotent user. This is primarily necessary so they can access repository API methods on remote hosts. Test Plan: - Ran `bin/almanac register` with various valid and invalid inputs. - Verified keys get generated/added/stored properly. - Made a device-signed cluster Conduit call. - Made a normal old user-signed cluster Conduit call. Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T2783 Differential Revision: https://secure.phabricator.com/D11158
This commit is contained in:
parent
8dee37a132
commit
c84b9d408c
6 changed files with 260 additions and 5 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -13,8 +13,8 @@
|
||||||
/conf/local/local.json
|
/conf/local/local.json
|
||||||
/conf/local/ENVIRONMENT
|
/conf/local/ENVIRONMENT
|
||||||
/conf/local/VERSION
|
/conf/local/VERSION
|
||||||
/conf/local/HOSTKEY
|
/conf/keys/device.pub
|
||||||
/conf/local/HOSTID
|
/conf/keys/device.key
|
||||||
|
|
||||||
# Impact Font
|
# Impact Font
|
||||||
/resources/font/impact.ttf
|
/resources/font/impact.ttf
|
||||||
|
|
0
conf/keys/.keep
Normal file
0
conf/keys/.keep
Normal file
|
@ -49,7 +49,9 @@ phutil_register_library_map(array(
|
||||||
'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
|
'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
|
||||||
'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
|
'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
|
||||||
'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
|
'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
|
||||||
|
'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php',
|
||||||
'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php',
|
'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php',
|
||||||
|
'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php',
|
||||||
'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
|
'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
|
||||||
'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php',
|
'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php',
|
||||||
'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
|
'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
|
||||||
|
@ -3091,7 +3093,9 @@ phutil_register_library_map(array(
|
||||||
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
|
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
|
||||||
'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
'AlmanacInterfaceTableView' => 'AphrontView',
|
'AlmanacInterfaceTableView' => 'AphrontView',
|
||||||
|
'AlmanacKeys' => 'Phobject',
|
||||||
'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow',
|
'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
|
'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow',
|
'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AlmanacManagementRegisterWorkflow
|
||||||
|
extends AlmanacManagementWorkflow {
|
||||||
|
|
||||||
|
public function didConstruct() {
|
||||||
|
$this
|
||||||
|
->setName('register')
|
||||||
|
->setSynopsis(pht('Register this host as an Almanac device.'))
|
||||||
|
->setArguments(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'name' => 'device',
|
||||||
|
'param' => 'name',
|
||||||
|
'help' => pht('Almanac device name to register.'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'private-key',
|
||||||
|
'param' => 'key',
|
||||||
|
'help' => pht('Path to a private key for the host.'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'allow-key-reuse',
|
||||||
|
'help' => pht(
|
||||||
|
'Register even if another host is already registered with this '.
|
||||||
|
'keypair.'),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'name' => 'force',
|
||||||
|
'help' => pht(
|
||||||
|
'Register this host even if keys already exist.'),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list($raw_public_key) = execx('ssh-keygen -y -f %s', $private_key_path);
|
||||||
|
|
||||||
|
$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...'));
|
||||||
|
|
||||||
|
$tmp_private = new TempFile();
|
||||||
|
Filesystem::changePermissions($tmp_private, 0600);
|
||||||
|
execx('chown %s %s', $phd_user, $tmp_private);
|
||||||
|
Filesystem::writeFile($tmp_private, $raw_private_key);
|
||||||
|
execx('mv -f %s %s', $tmp_private, $stored_private_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.',
|
||||||
|
$device_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
src/applications/almanac/util/AlmanacKeys.php
Normal file
12
src/applications/almanac/util/AlmanacKeys.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class AlmanacKeys extends Phobject {
|
||||||
|
|
||||||
|
public static function getKeyPath($key_name) {
|
||||||
|
$root = dirname(phutil_get_library_root('phabricator'));
|
||||||
|
$keys = $root.'/conf/keys/';
|
||||||
|
|
||||||
|
return $keys.ltrim($key_name, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -60,6 +60,16 @@ abstract class DiffusionQuery extends PhabricatorQuery {
|
||||||
$core_params['branch'] = $drequest->getBranch();
|
$core_params['branch'] = $drequest->getBranch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the method we're calling doesn't actually take some of the implicit
|
||||||
|
// parameters we derive from the DiffusionRequest, omit them.
|
||||||
|
$method_object = ConduitAPIMethod::getConduitMethod($method);
|
||||||
|
$method_params = $method_object->defineParamTypes();
|
||||||
|
foreach ($core_params as $key => $value) {
|
||||||
|
if (empty($method_params[$key])) {
|
||||||
|
unset($core_params[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$params = $params + $core_params;
|
$params = $params + $core_params;
|
||||||
|
|
||||||
$service_phid = $repository->getAlmanacServicePHID();
|
$service_phid = $repository->getAlmanacServicePHID();
|
||||||
|
@ -123,9 +133,48 @@ abstract class DiffusionQuery extends PhabricatorQuery {
|
||||||
$client = id(new ConduitClient($uri))
|
$client = id(new ConduitClient($uri))
|
||||||
->setHost($domain);
|
->setHost($domain);
|
||||||
|
|
||||||
$token = PhabricatorConduitToken::loadClusterTokenForUser($user);
|
if ($user->isOmnipotent()) {
|
||||||
if ($token) {
|
// If the caller is the omnipotent user (normally, a daemon), we will
|
||||||
$client->setConduitToken($token->getToken());
|
// 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 `bin/almanac register` to register keys for this device. '.
|
||||||
|
'Exception: %s',
|
||||||
|
$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 `bin/almanac register` to register keys for this device. '.
|
||||||
|
'Exception: %s',
|
||||||
|
$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($user);
|
||||||
|
if ($token) {
|
||||||
|
$client->setConduitToken($token->getToken());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $client->callMethodSynchronous($method, $params);
|
return $client->callMethodSynchronous($method, $params);
|
||||||
|
|
Loading…
Reference in a new issue