@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.

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

+260 -5
+2 -2
.gitignore
··· 13 13 /conf/local/local.json 14 14 /conf/local/ENVIRONMENT 15 15 /conf/local/VERSION 16 - /conf/local/HOSTKEY 17 - /conf/local/HOSTID 16 + /conf/keys/device.pub 17 + /conf/keys/device.key 18 18 19 19 # Impact Font 20 20 /resources/font/impact.ttf
conf/keys/.keep

This is a binary file and will not be displayed.

+4
src/__phutil_library_map__.php
··· 49 49 'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php', 50 50 'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php', 51 51 'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php', 52 + 'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php', 52 53 'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php', 54 + 'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php', 53 55 'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php', 54 56 'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php', 55 57 'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php', ··· 3091 3093 'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType', 3092 3094 'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3093 3095 'AlmanacInterfaceTableView' => 'AphrontView', 3096 + 'AlmanacKeys' => 'Phobject', 3094 3097 'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow', 3098 + 'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow', 3095 3099 'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow', 3096 3100 'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow', 3097 3101 'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
+190
src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php
··· 1 + <?php 2 + 3 + final class AlmanacManagementRegisterWorkflow 4 + extends AlmanacManagementWorkflow { 5 + 6 + public function didConstruct() { 7 + $this 8 + ->setName('register') 9 + ->setSynopsis(pht('Register this host as an Almanac device.')) 10 + ->setArguments( 11 + array( 12 + array( 13 + 'name' => 'device', 14 + 'param' => 'name', 15 + 'help' => pht('Almanac device name to register.'), 16 + ), 17 + array( 18 + 'name' => 'private-key', 19 + 'param' => 'key', 20 + 'help' => pht('Path to a private key for the host.'), 21 + ), 22 + array( 23 + 'name' => 'allow-key-reuse', 24 + 'help' => pht( 25 + 'Register even if another host is already registered with this '. 26 + 'keypair.'), 27 + ), 28 + array( 29 + 'name' => 'force', 30 + 'help' => pht( 31 + 'Register this host even if keys already exist.'), 32 + ), 33 + )); 34 + } 35 + 36 + public function execute(PhutilArgumentParser $args) { 37 + $console = PhutilConsole::getConsole(); 38 + 39 + $device_name = $args->getArg('device'); 40 + if (!strlen($device_name)) { 41 + throw new PhutilArgumentUsageException( 42 + pht('Specify a device with --device.')); 43 + } 44 + 45 + $device = id(new AlmanacDeviceQuery()) 46 + ->setViewer($this->getViewer()) 47 + ->withNames(array($device_name)) 48 + ->executeOne(); 49 + if (!$device) { 50 + throw new PhutilArgumentUsageException( 51 + pht('No such device "%s" exists!', $device_name)); 52 + } 53 + 54 + $private_key_path = $args->getArg('private-key'); 55 + if (!strlen($private_key_path)) { 56 + throw new PhutilArgumentUsageException( 57 + pht('Specify a private key with --private-key.')); 58 + } 59 + 60 + if (!Filesystem::pathExists($private_key_path)) { 61 + throw new PhutilArgumentUsageException( 62 + pht('Private key "%s" does not exist!', $private_key_path)); 63 + } 64 + 65 + $raw_private_key = Filesystem::readFile($private_key_path); 66 + 67 + $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); 68 + if (!$phd_user) { 69 + throw new PhutilArgumentUsageException( 70 + pht( 71 + 'Config option "phd.user" is not set. You must set this option '. 72 + 'so the private key can be stored with the correct permissions.')); 73 + } 74 + 75 + $tmp = new TempFile(); 76 + list($err) = exec_manual('chown %s %s', $phd_user, $tmp); 77 + if ($err) { 78 + throw new PhutilArgumentUsageException( 79 + pht( 80 + 'Unable to change ownership of a file to daemon user "%s". Run '. 81 + 'this command as %s or root.', 82 + $phd_user, 83 + $phd_user)); 84 + } 85 + 86 + $stored_public_path = AlmanacKeys::getKeyPath('device.pub'); 87 + $stored_private_path = AlmanacKeys::getKeyPath('device.key'); 88 + 89 + if (!$args->getArg('force')) { 90 + if (Filesystem::pathExists($stored_public_path)) { 91 + throw new PhutilArgumentUsageException( 92 + pht( 93 + 'This host already has a registered public key ("%s"). '. 94 + 'Remove this key before registering the host, or use '. 95 + '--force to overwrite it.', 96 + Filesystem::readablePath($stored_public_path))); 97 + } 98 + 99 + if (Filesystem::pathExists($stored_private_path)) { 100 + throw new PhutilArgumentUsageException( 101 + pht( 102 + 'This host already has a registered private key ("%s"). '. 103 + 'Remove this key before registering the host, or use '. 104 + '--force to overwrite it.', 105 + Filesystem::readablePath($stored_private_path))); 106 + } 107 + } 108 + 109 + list($raw_public_key) = execx('ssh-keygen -y -f %s', $private_key_path); 110 + 111 + $key_object = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_public_key); 112 + 113 + $public_key = id(new PhabricatorAuthSSHKeyQuery()) 114 + ->setViewer($this->getViewer()) 115 + ->withKeys(array($key_object)) 116 + ->executeOne(); 117 + 118 + if ($public_key) { 119 + if ($public_key->getObjectPHID() !== $device->getPHID()) { 120 + throw new PhutilArgumentUsageException( 121 + pht( 122 + 'The public key corresponding to the given private key is '. 123 + 'already associated with an object other than the specified '. 124 + 'device. You can not use a single private key to identify '. 125 + 'multiple devices or users.')); 126 + } else if (!$public_key->getIsTrusted()) { 127 + throw new PhutilArgumentUsageException( 128 + pht( 129 + 'The public key corresponding to the given private key is '. 130 + 'already associated with the device, but is not trusted. '. 131 + 'Registering this key would trust the other entities which '. 132 + 'hold it. Use a unique key, or explicitly enable trust for the '. 133 + 'current key.')); 134 + } else if (!$args->getArg('allow-key-reuse')) { 135 + throw new PhutilArgumentUsageException( 136 + pht( 137 + 'The public key corresponding to the given private key is '. 138 + 'already associated with the device. If you do not want to '. 139 + 'use a unique key, use --allow-key-reuse to permit '. 140 + 'reassociation.')); 141 + } 142 + } else { 143 + $public_key = id(new PhabricatorAuthSSHKey()) 144 + ->setObjectPHID($device->getPHID()) 145 + ->attachObject($device) 146 + ->setName($device->getSSHKeyDefaultName()) 147 + ->setKeyType($key_object->getType()) 148 + ->setKeyBody($key_object->getBody()) 149 + ->setKeyComment(pht('Registered')) 150 + ->setIsTrusted(1); 151 + } 152 + 153 + 154 + $console->writeOut( 155 + "%s\n", 156 + pht('Installing public key...')); 157 + 158 + $tmp_public = new TempFile(); 159 + Filesystem::changePermissions($tmp_public, 0600); 160 + execx('chown %s %s', $phd_user, $tmp_public); 161 + Filesystem::writeFile($tmp_public, $raw_public_key); 162 + execx('mv -f %s %s', $tmp_public, $stored_public_path); 163 + 164 + $console->writeOut( 165 + "%s\n", 166 + pht('Installing private key...')); 167 + 168 + $tmp_private = new TempFile(); 169 + Filesystem::changePermissions($tmp_private, 0600); 170 + execx('chown %s %s', $phd_user, $tmp_private); 171 + Filesystem::writeFile($tmp_private, $raw_private_key); 172 + execx('mv -f %s %s', $tmp_private, $stored_private_path); 173 + 174 + if (!$public_key->getID()) { 175 + $console->writeOut( 176 + "%s\n", 177 + pht('Registering device key...')); 178 + $public_key->save(); 179 + } 180 + 181 + $console->writeOut( 182 + "**<bg:green> %s </bg>** %s\n", 183 + pht('HOST REGISTERED'), 184 + pht( 185 + 'This host has been registered as "%s" and a trusted keypair '. 186 + 'has been installed.', 187 + $device_name)); 188 + } 189 + 190 + }
+12
src/applications/almanac/util/AlmanacKeys.php
··· 1 + <?php 2 + 3 + final class AlmanacKeys extends Phobject { 4 + 5 + public static function getKeyPath($key_name) { 6 + $root = dirname(phutil_get_library_root('phabricator')); 7 + $keys = $root.'/conf/keys/'; 8 + 9 + return $keys.ltrim($key_name, '/'); 10 + } 11 + 12 + }
+52 -3
src/applications/diffusion/query/DiffusionQuery.php
··· 60 60 $core_params['branch'] = $drequest->getBranch(); 61 61 } 62 62 63 + // If the method we're calling doesn't actually take some of the implicit 64 + // parameters we derive from the DiffusionRequest, omit them. 65 + $method_object = ConduitAPIMethod::getConduitMethod($method); 66 + $method_params = $method_object->defineParamTypes(); 67 + foreach ($core_params as $key => $value) { 68 + if (empty($method_params[$key])) { 69 + unset($core_params[$key]); 70 + } 71 + } 72 + 63 73 $params = $params + $core_params; 64 74 65 75 $service_phid = $repository->getAlmanacServicePHID(); ··· 123 133 $client = id(new ConduitClient($uri)) 124 134 ->setHost($domain); 125 135 126 - $token = PhabricatorConduitToken::loadClusterTokenForUser($user); 127 - if ($token) { 128 - $client->setConduitToken($token->getToken()); 136 + if ($user->isOmnipotent()) { 137 + // If the caller is the omnipotent user (normally, a daemon), we will 138 + // sign the request with this host's asymmetric keypair. 139 + 140 + $public_path = AlmanacKeys::getKeyPath('device.pub'); 141 + try { 142 + $public_key = Filesystem::readFile($public_path); 143 + } catch (Exception $ex) { 144 + throw new PhutilAggregateException( 145 + pht( 146 + 'Unable to read device public key while attempting to make '. 147 + 'authenticated method call within the Phabricator cluster. '. 148 + 'Use `bin/almanac register` to register keys for this device. '. 149 + 'Exception: %s', 150 + $ex->getMessage()), 151 + array($ex)); 152 + } 153 + 154 + $private_path = AlmanacKeys::getKeyPath('device.key'); 155 + try { 156 + $private_key = Filesystem::readFile($private_path); 157 + $private_key = new PhutilOpaqueEnvelope($private_key); 158 + } catch (Exception $ex) { 159 + throw new PhutilAggregateException( 160 + pht( 161 + 'Unable to read device private key while attempting to make '. 162 + 'authenticated method call within the Phabricator cluster. '. 163 + 'Use `bin/almanac register` to register keys for this device. '. 164 + 'Exception: %s', 165 + $ex->getMessage()), 166 + array($ex)); 167 + } 168 + 169 + $client->setSigningKeys($public_key, $private_key); 170 + } else { 171 + // If the caller is a normal user, we generate or retrieve a cluster 172 + // API token. 173 + 174 + $token = PhabricatorConduitToken::loadClusterTokenForUser($user); 175 + if ($token) { 176 + $client->setConduitToken($token->getToken()); 177 + } 129 178 } 130 179 131 180 return $client->callMethodSynchronous($method, $params);