@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 a query/policy layer on top of SSH keys for Almanac

Summary:
Ref T5833. Currently, SSH keys are associated only with users, and are a bit un-modern. I want to let Almanac Devices have SSH keys so devices in a cluster can identify to one another.

For example, with hosted installs, initialization will go something like this:

- A request comes in for `company.phacility.com`.
- A SiteSource (from D10787) makes a Conduit call to Almanac on the master install to check if `company` is a valid install and pull config if it is.
- This call can be signed with an SSH key which identifies a trusted Almanac Device.

In the cluster case, a web host can make an authenticated call to a repository host with similar key signing.

To move toward this, put a proper Query class on top of SSH key access (this diff). In following diffs, I'll:

- Rename `userPHID` to `objectPHID`.
- Move this to the `auth` database.
- Provide UI for device/key association.

An alternative approach would be to build some kind of special token layer in Conduit, but I think that would be a lot harder to manage in the hosting case. This gives us a more direct attack on trusting requests from machines and recognizing machines as first (well, sort of second-class) actors without needing things like fake user accounts.

Test Plan:
- Added and removed SSH keys.
- Added and removed SSH keys from a bot account.
- Tried to edit an unonwned SSH key (denied).
- Ran `bin/ssh-auth`, got sensible output.
- Ran `bin/ssh-auth-key`, got sensible output.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5833

Differential Revision: https://secure.phabricator.com/D10790

+291 -109
+12 -32
scripts/ssh/ssh-auth-key.php
··· 4 4 $root = dirname(dirname(dirname(__FILE__))); 5 5 require_once $root.'/scripts/__init_script__.php'; 6 6 7 - $cert = file_get_contents('php://stdin'); 8 - 9 - if (!$cert) { 10 - exit(1); 11 - } 12 - 13 - $parts = preg_split('/\s+/', $cert); 14 - if (count($parts) < 2) { 15 - exit(1); 16 - } 17 - 18 - list($type, $body) = $parts; 19 - 20 - $user_dao = new PhabricatorUser(); 21 - $ssh_dao = new PhabricatorUserSSHKey(); 22 - $conn_r = $user_dao->establishConnection('r'); 23 - 24 - $row = queryfx_one( 25 - $conn_r, 26 - 'SELECT userName FROM %T u JOIN %T ssh ON u.phid = ssh.userPHID 27 - WHERE ssh.keyType = %s AND ssh.keyBody = %s', 28 - $user_dao->getTableName(), 29 - $ssh_dao->getTableName(), 30 - $type, 31 - $body); 32 - 33 - if (!$row) { 7 + try { 8 + $cert = file_get_contents('php://stdin'); 9 + $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($cert); 10 + } catch (Exception $ex) { 34 11 exit(1); 35 12 } 36 13 37 - $user = idx($row, 'userName'); 38 - 39 - if (!$user) { 14 + $key = id(new PhabricatorAuthSSHKeyQuery()) 15 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 16 + ->withKeys(array($public_key)) 17 + ->executeOne(); 18 + if (!$key) { 40 19 exit(1); 41 20 } 42 21 43 - if (!PhabricatorUser::validateUsername($user)) { 22 + $object = $key->getObject(); 23 + if (!($object instanceof PhabricatorUser)) { 44 24 exit(1); 45 25 } 46 26 47 27 $bin = $root.'/bin/ssh-exec'; 48 - $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); 28 + $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $object->getUsername()); 49 29 // This is additional escaping for the SSH 'command="..."' string. 50 30 $cmd = addcslashes($cmd, '"\\'); 51 31
+16 -15
scripts/ssh/ssh-auth.php
··· 4 4 $root = dirname(dirname(dirname(__FILE__))); 5 5 require_once $root.'/scripts/__init_script__.php'; 6 6 7 - $user_dao = new PhabricatorUser(); 8 - $ssh_dao = new PhabricatorUserSSHKey(); 9 - $conn_r = $user_dao->establishConnection('r'); 7 + $keys = id(new PhabricatorAuthSSHKeyQuery()) 8 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 9 + ->execute(); 10 10 11 - $rows = queryfx_all( 12 - $conn_r, 13 - 'SELECT userName, keyBody, keyType FROM %T u JOIN %T ssh 14 - ON u.phid = ssh.userPHID', 15 - $user_dao->getTableName(), 16 - $ssh_dao->getTableName()); 11 + foreach ($keys as $key => $ssh_key) { 12 + // For now, filter out any keys which don't belong to users. Eventually we 13 + // may allow devices to use this channel. 14 + if (!($ssh_key->getObject() instanceof PhabricatorUser)) { 15 + unset($keys[$key]); 16 + continue; 17 + } 18 + } 17 19 18 - if (!$rows) { 20 + if (!$keys) { 19 21 echo pht('No keys found.')."\n"; 20 22 exit(1); 21 23 } 22 24 23 25 $bin = $root.'/bin/ssh-exec'; 24 - foreach ($rows as $row) { 25 - $user = $row['userName']; 26 + foreach ($keys as $ssh_key) { 27 + $user = $ssh_key->getObject()->getUsername(); 26 28 27 29 $cmd = csprintf('%s --phabricator-ssh-user %s', $bin, $user); 28 30 // This is additional escaping for the SSH 'command="..."' string. ··· 30 32 31 33 // Strip out newlines and other nonsense from the key type and key body. 32 34 33 - $type = $row['keyType']; 35 + $type = $ssh_key->getKeyType(); 34 36 $type = preg_replace('@[\x00-\x20]+@', '', $type); 35 37 36 - $key = $row['keyBody']; 38 + $key = $ssh_key->getKeyBody(); 37 39 $key = preg_replace('@[\x00-\x20]+@', '', $key); 38 - 39 40 40 41 $options = array( 41 42 'command="'.$cmd.'"',
+8 -1
src/__phutil_library_map__.php
··· 1317 1317 'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php', 1318 1318 'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php', 1319 1319 'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php', 1320 + 'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php', 1321 + 'PhabricatorAuthSSHPublicKey' => 'applications/auth/storage/PhabricatorAuthSSHPublicKey.php', 1320 1322 'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php', 1321 1323 'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php', 1322 1324 'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php', ··· 4381 4383 'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 4382 4384 'PhabricatorAuthRegisterController' => 'PhabricatorAuthController', 4383 4385 'PhabricatorAuthRevokeTokenController' => 'PhabricatorAuthController', 4386 + 'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4387 + 'PhabricatorAuthSSHPublicKey' => 'Phobject', 4384 4388 'PhabricatorAuthSession' => array( 4385 4389 'PhabricatorAuthDAO', 4386 4390 'PhabricatorPolicyInterface', ··· 5627 5631 'PhabricatorUserProfileEditor' => 'PhabricatorApplicationTransactionEditor', 5628 5632 'PhabricatorUserRealNameField' => 'PhabricatorUserCustomField', 5629 5633 'PhabricatorUserRolesField' => 'PhabricatorUserCustomField', 5630 - 'PhabricatorUserSSHKey' => 'PhabricatorUserDAO', 5634 + 'PhabricatorUserSSHKey' => array( 5635 + 'PhabricatorUserDAO', 5636 + 'PhabricatorPolicyInterface', 5637 + ), 5631 5638 'PhabricatorUserSchemaSpec' => 'PhabricatorConfigSchemaSpec', 5632 5639 'PhabricatorUserSearchIndexer' => 'PhabricatorSearchDocumentIndexer', 5633 5640 'PhabricatorUserSinceField' => 'PhabricatorUserCustomField',
+104
src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthSSHKeyQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $objectPHIDs; 8 + private $keys; 9 + 10 + public function withIDs(array $ids) { 11 + $this->ids = $ids; 12 + return $this; 13 + } 14 + 15 + public function withObjectPHIDs(array $object_phids) { 16 + $this->objectPHIDs = $object_phids; 17 + return $this; 18 + } 19 + 20 + public function withKeys(array $keys) { 21 + assert_instances_of($keys, 'PhabricatorAuthSSHPublicKey'); 22 + $this->keys = $keys; 23 + return $this; 24 + } 25 + 26 + protected function loadPage() { 27 + $table = new PhabricatorUserSSHKey(); 28 + $conn_r = $table->establishConnection('r'); 29 + 30 + $data = queryfx_all( 31 + $conn_r, 32 + 'SELECT * FROM %T %Q %Q %Q', 33 + $table->getTableName(), 34 + $this->buildWhereClause($conn_r), 35 + $this->buildOrderClause($conn_r), 36 + $this->buildLimitClause($conn_r)); 37 + 38 + return $table->loadAllFromArray($data); 39 + } 40 + 41 + protected function willFilterPage(array $keys) { 42 + $object_phids = mpull($keys, 'getObjectPHID'); 43 + 44 + $objects = id(new PhabricatorObjectQuery()) 45 + ->setViewer($this->getViewer()) 46 + ->setParentQuery($this) 47 + ->withPHIDs($object_phids) 48 + ->execute(); 49 + $objects = mpull($objects, null, 'getPHID'); 50 + 51 + foreach ($keys as $key => $ssh_key) { 52 + $object = idx($objects, $ssh_key->getObjectPHID()); 53 + if (!$object) { 54 + unset($keys[$key]); 55 + continue; 56 + } 57 + $ssh_key->attachObject($object); 58 + } 59 + 60 + return $keys; 61 + } 62 + 63 + protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { 64 + $where = array(); 65 + 66 + if ($this->ids !== null) { 67 + $where[] = qsprintf( 68 + $conn_r, 69 + 'id IN (%Ld)', 70 + $this->ids); 71 + } 72 + 73 + if ($this->objectPHIDs !== null) { 74 + $where[] = qsprintf( 75 + $conn_r, 76 + 'userPHID IN (%Ls)', 77 + $this->objectPHIDs); 78 + } 79 + 80 + if ($this->keys !== null) { 81 + // TODO: This could take advantage of a better key, and the hashing 82 + // scheme for this table is a bit nonstandard and questionable. 83 + 84 + $sql = array(); 85 + foreach ($this->keys as $key) { 86 + $sql[] = qsprintf( 87 + $conn_r, 88 + '(keyType = %s AND keyBody = %s)', 89 + $key->getType(), 90 + $key->getBody()); 91 + } 92 + $where[] = implode(' OR ', $sql); 93 + } 94 + 95 + $where[] = $this->buildPagingClause($conn_r); 96 + 97 + return $this->formatWhereClause($where); 98 + } 99 + 100 + public function getQueryApplicationClass() { 101 + return 'PhabricatorAuthApplication'; 102 + } 103 + 104 + }
+86
src/applications/auth/storage/PhabricatorAuthSSHPublicKey.php
··· 1 + <?php 2 + 3 + /** 4 + * Data structure representing a raw public key. 5 + */ 6 + final class PhabricatorAuthSSHPublicKey extends Phobject { 7 + 8 + private $type; 9 + private $body; 10 + private $comment; 11 + 12 + private function __construct() { 13 + // <internal> 14 + } 15 + 16 + public static function newFromRawKey($entire_key) { 17 + $entire_key = trim($entire_key); 18 + if (!strlen($entire_key)) { 19 + throw new Exception(pht('No public key was provided.')); 20 + } 21 + 22 + $parts = str_replace("\n", '', $entire_key); 23 + 24 + // The third field (the comment) can have spaces in it, so split this 25 + // into a maximum of three parts. 26 + $parts = preg_split('/\s+/', $parts, 3); 27 + 28 + if (preg_match('/private\s*key/i', $entire_key)) { 29 + // Try to give the user a better error message if it looks like 30 + // they uploaded a private key. 31 + throw new Exception(pht('Provide a public key, not a private key!')); 32 + } 33 + 34 + switch (count($parts)) { 35 + case 1: 36 + throw new Exception( 37 + pht('Provided public key is not properly formatted.')); 38 + case 2: 39 + // Add an empty comment part. 40 + $parts[] = ''; 41 + break; 42 + case 3: 43 + // This is the expected case. 44 + break; 45 + } 46 + 47 + list($type, $body, $comment) = $parts; 48 + 49 + $recognized_keys = array( 50 + 'ssh-dsa', 51 + 'ssh-dss', 52 + 'ssh-rsa', 53 + 'ecdsa-sha2-nistp256', 54 + 'ecdsa-sha2-nistp384', 55 + 'ecdsa-sha2-nistp521', 56 + ); 57 + 58 + if (!in_array($type, $recognized_keys)) { 59 + $type_list = implode(', ', $recognized_keys); 60 + throw new Exception( 61 + pht( 62 + 'Public key type should be one of: %s', 63 + $type_list)); 64 + } 65 + 66 + $public_key = new PhabricatorAuthSSHPublicKey(); 67 + $public_key->type = $type; 68 + $public_key->body = $body; 69 + $public_key->comment = $comment; 70 + 71 + return $public_key; 72 + } 73 + 74 + public function getType() { 75 + return $this->type; 76 + } 77 + 78 + public function getBody() { 79 + return $this->body; 80 + } 81 + 82 + public function getComment() { 83 + return $this->comment; 84 + } 85 + 86 + }
+23 -60
src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
··· 45 45 46 46 $id = nonempty($edit, $delete); 47 47 48 - if ($id && is_numeric($id)) { 49 - // NOTE: This prevents editing/deleting of keys not owned by the user. 50 - $key = id(new PhabricatorUserSSHKey())->loadOneWhere( 51 - 'userPHID = %s AND id = %d', 52 - $user->getPHID(), 53 - (int)$id); 48 + if ($id) { 49 + $key = id(new PhabricatorAuthSSHKeyQuery()) 50 + ->setViewer($viewer) 51 + ->withIDs(array($id)) 52 + ->requireCapabilities( 53 + array( 54 + PhabricatorPolicyCapability::CAN_VIEW, 55 + PhabricatorPolicyCapability::CAN_EDIT, 56 + )) 57 + ->executeOne(); 54 58 if (!$key) { 55 59 return new Aphront404Response(); 56 60 } ··· 77 81 } else { 78 82 79 83 try { 80 - list($type, $body, $comment) = self::parsePublicKey($entire_key); 84 + $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($entire_key); 85 + 86 + $type = $public_key->getType(); 87 + $body = $public_key->getBody(); 88 + $comment = $public_key->getComment(); 81 89 82 90 $key->setKeyType($type); 83 91 $key->setKeyBody($body); ··· 153 161 $user = $this->getUser(); 154 162 $viewer = $request->getUser(); 155 163 156 - $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 157 - 'userPHID = %s', 158 - $user->getPHID()); 164 + $keys = id(new PhabricatorAuthSSHKeyQuery()) 165 + ->setViewer($viewer) 166 + ->withObjectPHIDs(array($user->getPHID())) 167 + ->execute(); 159 168 160 169 $rows = array(); 161 170 foreach ($keys as $key) { ··· 294 303 'viewPolicy' => $viewer->getPHID(), 295 304 )); 296 305 297 - list($type, $body, $comment) = self::parsePublicKey($public_key); 306 + $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($public_key); 307 + 308 + $type = $public_key->getType(); 309 + $body = $public_key->getBody(); 298 310 299 311 $key = id(new PhabricatorUserSSHKey()) 300 312 ->setUserPHID($user->getPHID()) ··· 375 387 376 388 return id(new AphrontDialogResponse()) 377 389 ->setDialog($dialog); 378 - } 379 - 380 - private static function parsePublicKey($entire_key) { 381 - $parts = str_replace("\n", '', trim($entire_key)); 382 - 383 - // The third field (the comment) can have spaces in it, so split this 384 - // into a maximum of three parts. 385 - $parts = preg_split('/\s+/', $parts, 3); 386 - 387 - if (preg_match('/private\s*key/i', $entire_key)) { 388 - // Try to give the user a better error message if it looks like 389 - // they uploaded a private key. 390 - throw new Exception( 391 - pht('Provide your public key, not your private key!')); 392 - } 393 - 394 - switch (count($parts)) { 395 - case 1: 396 - throw new Exception( 397 - pht('Provided public key is not properly formatted.')); 398 - case 2: 399 - // Add an empty comment part. 400 - $parts[] = ''; 401 - break; 402 - case 3: 403 - // This is the expected case. 404 - break; 405 - } 406 - 407 - list($type, $body, $comment) = $parts; 408 - 409 - $recognized_keys = array( 410 - 'ssh-dsa', 411 - 'ssh-dss', 412 - 'ssh-rsa', 413 - 'ecdsa-sha2-nistp256', 414 - 'ecdsa-sha2-nistp384', 415 - 'ecdsa-sha2-nistp521', 416 - ); 417 - 418 - if (!in_array($type, $recognized_keys)) { 419 - $type_list = implode(', ', $recognized_keys); 420 - throw new Exception( 421 - pht( 422 - 'Public key type should be one of: %s', 423 - $type_list)); 424 - } 425 - 426 - return array($type, $body, $comment); 427 390 } 428 391 429 392 }
+42 -1
src/applications/settings/storage/PhabricatorUserSSHKey.php
··· 1 1 <?php 2 2 3 - final class PhabricatorUserSSHKey extends PhabricatorUserDAO { 3 + final class PhabricatorUserSSHKey 4 + extends PhabricatorUserDAO 5 + implements PhabricatorPolicyInterface { 4 6 5 7 protected $userPHID; 6 8 protected $name; ··· 8 10 protected $keyBody; 9 11 protected $keyHash; 10 12 protected $keyComment; 13 + 14 + private $object = self::ATTACHABLE; 15 + 16 + public function getObjectPHID() { 17 + return $this->getUserPHID(); 18 + } 11 19 12 20 public function getConfiguration() { 13 21 return array( ··· 40 48 $this->getKeyComment(), 41 49 ); 42 50 return trim(implode(' ', $parts)); 51 + } 52 + 53 + public function getObject() { 54 + return $this->assertAttached($this->object); 55 + } 56 + 57 + public function attachObject($object) { 58 + $this->object = $object; 59 + return $this; 60 + } 61 + 62 + 63 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 64 + 65 + 66 + public function getCapabilities() { 67 + return array( 68 + PhabricatorPolicyCapability::CAN_VIEW, 69 + PhabricatorPolicyCapability::CAN_EDIT, 70 + ); 71 + } 72 + 73 + public function getPolicy($capability) { 74 + return $this->getObject()->getPolicy($capability); 75 + } 76 + 77 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 78 + return $this->getObject()->hasAutomaticCapability($capability, $viewer); 79 + } 80 + 81 + public function describeAutomaticCapability($capability) { 82 + return pht( 83 + 'SSH keys inherit the policies of the user or object they authenticate.'); 43 84 } 44 85 45 86 }