Skip to content

Commit c84b9d4

Browse files
author
epriestley
committedJan 2, 2015
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
1 parent 8dee37a commit c84b9d4

File tree

6 files changed

+260
-5
lines changed

6 files changed

+260
-5
lines changed
 

‎.gitignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
/conf/local/local.json
1414
/conf/local/ENVIRONMENT
1515
/conf/local/VERSION
16-
/conf/local/HOSTKEY
17-
/conf/local/HOSTID
16+
/conf/keys/device.pub
17+
/conf/keys/device.key
1818

1919
# Impact Font
2020
/resources/font/impact.ttf

‎conf/keys/.keep

Whitespace-only changes.

‎src/__phutil_library_map__.php

+4
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
'AlmanacInterfacePHIDType' => 'applications/almanac/phid/AlmanacInterfacePHIDType.php',
5050
'AlmanacInterfaceQuery' => 'applications/almanac/query/AlmanacInterfaceQuery.php',
5151
'AlmanacInterfaceTableView' => 'applications/almanac/view/AlmanacInterfaceTableView.php',
52+
'AlmanacKeys' => 'applications/almanac/util/AlmanacKeys.php',
5253
'AlmanacManagementLockWorkflow' => 'applications/almanac/management/AlmanacManagementLockWorkflow.php',
54+
'AlmanacManagementRegisterWorkflow' => 'applications/almanac/management/AlmanacManagementRegisterWorkflow.php',
5355
'AlmanacManagementTrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php',
5456
'AlmanacManagementUnlockWorkflow' => 'applications/almanac/management/AlmanacManagementUnlockWorkflow.php',
5557
'AlmanacManagementUntrustKeyWorkflow' => 'applications/almanac/management/AlmanacManagementUntrustKeyWorkflow.php',
@@ -3091,7 +3093,9 @@
30913093
'AlmanacInterfacePHIDType' => 'PhabricatorPHIDType',
30923094
'AlmanacInterfaceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
30933095
'AlmanacInterfaceTableView' => 'AphrontView',
3096+
'AlmanacKeys' => 'Phobject',
30943097
'AlmanacManagementLockWorkflow' => 'AlmanacManagementWorkflow',
3098+
'AlmanacManagementRegisterWorkflow' => 'AlmanacManagementWorkflow',
30953099
'AlmanacManagementTrustKeyWorkflow' => 'AlmanacManagementWorkflow',
30963100
'AlmanacManagementUnlockWorkflow' => 'AlmanacManagementWorkflow',
30973101
'AlmanacManagementUntrustKeyWorkflow' => 'AlmanacManagementWorkflow',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
}

‎src/applications/diffusion/query/DiffusionQuery.php

+52-3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ final public static function callConduitWithDiffusionRequest(
6060
$core_params['branch'] = $drequest->getBranch();
6161
}
6262

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+
6373
$params = $params + $core_params;
6474

6575
$service_phid = $repository->getAlmanacServicePHID();
@@ -123,9 +133,48 @@ final public static function callConduitWithDiffusionRequest(
123133
$client = id(new ConduitClient($uri))
124134
->setHost($domain);
125135

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+
}
129178
}
130179

131180
return $client->callMethodSynchronous($method, $params);

0 commit comments

Comments
 (0)
Failed to load comments.