Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
epriestley
committed
Nov 6, 2014
1 parent
3ea31c9
commit 6f0d3b0
Showing
7 changed files
with
293 additions
and
111 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
<?php | ||
|
||
final class PhabricatorAuthSSHKeyQuery | ||
extends PhabricatorCursorPagedPolicyAwareQuery { | ||
|
||
private $ids; | ||
private $objectPHIDs; | ||
private $keys; | ||
|
||
public function withIDs(array $ids) { | ||
$this->ids = $ids; | ||
return $this; | ||
} | ||
|
||
public function withObjectPHIDs(array $object_phids) { | ||
$this->objectPHIDs = $object_phids; | ||
return $this; | ||
} | ||
|
||
public function withKeys(array $keys) { | ||
assert_instances_of($keys, 'PhabricatorAuthSSHPublicKey'); | ||
$this->keys = $keys; | ||
return $this; | ||
} | ||
|
||
protected function loadPage() { | ||
$table = new PhabricatorUserSSHKey(); | ||
$conn_r = $table->establishConnection('r'); | ||
|
||
$data = queryfx_all( | ||
$conn_r, | ||
'SELECT * FROM %T %Q %Q %Q', | ||
$table->getTableName(), | ||
$this->buildWhereClause($conn_r), | ||
$this->buildOrderClause($conn_r), | ||
$this->buildLimitClause($conn_r)); | ||
|
||
return $table->loadAllFromArray($data); | ||
} | ||
|
||
protected function willFilterPage(array $keys) { | ||
$object_phids = mpull($keys, 'getObjectPHID'); | ||
|
||
$objects = id(new PhabricatorObjectQuery()) | ||
->setViewer($this->getViewer()) | ||
->setParentQuery($this) | ||
->withPHIDs($object_phids) | ||
->execute(); | ||
$objects = mpull($objects, null, 'getPHID'); | ||
|
||
foreach ($keys as $key => $ssh_key) { | ||
$object = idx($objects, $ssh_key->getObjectPHID()); | ||
if (!$object) { | ||
unset($keys[$key]); | ||
continue; | ||
} | ||
$ssh_key->attachObject($object); | ||
} | ||
|
||
return $keys; | ||
} | ||
|
||
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { | ||
$where = array(); | ||
|
||
if ($this->ids !== null) { | ||
$where[] = qsprintf( | ||
$conn_r, | ||
'id IN (%Ld)', | ||
$this->ids); | ||
} | ||
|
||
if ($this->objectPHIDs !== null) { | ||
$where[] = qsprintf( | ||
$conn_r, | ||
'userPHID IN (%Ls)', | ||
$this->objectPHIDs); | ||
} | ||
|
||
if ($this->keys !== null) { | ||
// TODO: This could take advantage of a better key, and the hashing | ||
// scheme for this table is a bit nonstandard and questionable. | ||
|
||
$sql = array(); | ||
foreach ($this->keys as $key) { | ||
$sql[] = qsprintf( | ||
$conn_r, | ||
'(keyType = %s AND keyBody = %s)', | ||
$key->getType(), | ||
$key->getBody()); | ||
} | ||
$where[] = implode(' OR ', $sql); | ||
} | ||
|
||
$where[] = $this->buildPagingClause($conn_r); | ||
|
||
return $this->formatWhereClause($where); | ||
} | ||
|
||
public function getQueryApplicationClass() { | ||
return 'PhabricatorAuthApplication'; | ||
} | ||
|
||
} |
86 changes: 86 additions & 0 deletions
86
src/applications/auth/storage/PhabricatorAuthSSHPublicKey.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
<?php | ||
|
||
/** | ||
* Data structure representing a raw public key. | ||
*/ | ||
final class PhabricatorAuthSSHPublicKey extends Phobject { | ||
|
||
private $type; | ||
private $body; | ||
private $comment; | ||
|
||
private function __construct() { | ||
// <internal> | ||
} | ||
|
||
public static function newFromRawKey($entire_key) { | ||
$entire_key = trim($entire_key); | ||
if (!strlen($entire_key)) { | ||
throw new Exception(pht('No public key was provided.')); | ||
} | ||
|
||
$parts = str_replace("\n", '', $entire_key); | ||
|
||
// The third field (the comment) can have spaces in it, so split this | ||
// into a maximum of three parts. | ||
$parts = preg_split('/\s+/', $parts, 3); | ||
|
||
if (preg_match('/private\s*key/i', $entire_key)) { | ||
// Try to give the user a better error message if it looks like | ||
// they uploaded a private key. | ||
throw new Exception(pht('Provide a public key, not a private key!')); | ||
} | ||
|
||
switch (count($parts)) { | ||
case 1: | ||
throw new Exception( | ||
pht('Provided public key is not properly formatted.')); | ||
case 2: | ||
// Add an empty comment part. | ||
$parts[] = ''; | ||
break; | ||
case 3: | ||
// This is the expected case. | ||
break; | ||
} | ||
|
||
list($type, $body, $comment) = $parts; | ||
|
||
$recognized_keys = array( | ||
'ssh-dsa', | ||
'ssh-dss', | ||
'ssh-rsa', | ||
'ecdsa-sha2-nistp256', | ||
'ecdsa-sha2-nistp384', | ||
'ecdsa-sha2-nistp521', | ||
); | ||
|
||
if (!in_array($type, $recognized_keys)) { | ||
$type_list = implode(', ', $recognized_keys); | ||
throw new Exception( | ||
pht( | ||
'Public key type should be one of: %s', | ||
$type_list)); | ||
} | ||
|
||
$public_key = new PhabricatorAuthSSHPublicKey(); | ||
$public_key->type = $type; | ||
$public_key->body = $body; | ||
$public_key->comment = $comment; | ||
|
||
return $public_key; | ||
} | ||
|
||
public function getType() { | ||
return $this->type; | ||
} | ||
|
||
public function getBody() { | ||
return $this->body; | ||
} | ||
|
||
public function getComment() { | ||
return $this->comment; | ||
} | ||
|
||
} |
Oops, something went wrong.