Skip to content

Commit 0308d58

Browse files
author
epriestley
committedMay 18, 2016
Deactivate SSH keys instead of destroying them completely
Summary: Ref T10917. Currently, when you delete an SSH key, we really truly delete it forever. This isn't very consistent with other applications, but we built this stuff a long time ago before we were as rigorous about retaining data and making it auditable. In partiular, destroying data isn't good for auditing after security issues, since it means we can't show you logs of any changes an attacker might have made to your keys. To prepare to improve this, stop destoying data. This will allow later changes to become transaction-oriented and show normal transaction logs. The tricky part here is that we have a `UNIQUE KEY` on the public key part of the key. Instead, I changed this to `UNIQUE (key, isActive)`, where `isActive` is a nullable boolean column. This works because MySQL does not enforce "unique" if part of the key is `NULL`. So you can't have two rows with `("A", 1)`, but you can have as many rows as you want with `("A", null)`. This lets us keep the "each key may only be active for one user/object" rule without requiring us to delete any data. Test Plan: - Ran schema changes. - Viewed public keys. - Tried to add a duplicate key, got rejected (already associated with another object). - Deleted SSH key. - Verified that the key was no longer actually deleted from the database, just marked inactive (in future changes, I'll update the UI to be more clear about this). - Uploaded a new copy of the same public key, worked fine (no duplicate key rejection). - Tried to upload yet another copy, got rejected. - Generated a new keypair. - Tried to upload a duplicate to an Almanac device, got rejected. - Generated a new pair for a device. - Trusted a device key. - Untrusted a device key. - "Deleted" a device key. - Tried to trust a deleted device key, got "inactive" message. - Ran `bin/ssh-auth`, got good output with unique keys. - Ran `cat ~/.ssh/id_rsa.pub | ./bin/ssh-auth-key`, got good output with one key. - Used `auth.querypublickeys` Conduit method to query keys, got good active keys. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10917 Differential Revision: https://secure.phabricator.com/D15943
1 parent 49eb640 commit 0308d58

18 files changed

+82
-12
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE {$NAMESPACE}_auth.auth_sshkey
2+
ADD isActive BOOL;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
UPDATE {$NAMESPACE}_auth.auth_sshkey
2+
SET isActive = 1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE {$NAMESPACE}_auth.auth_sshkey
2+
ADD UNIQUE KEY `key_activeunique` (keyIndex, isActive);

‎scripts/ssh/ssh-auth-key.php

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
$key = id(new PhabricatorAuthSSHKeyQuery())
1515
->setViewer(PhabricatorUser::getOmnipotentUser())
1616
->withKeys(array($public_key))
17+
->withIsActive(true)
1718
->executeOne();
1819
if (!$key) {
1920
exit(1);

‎scripts/ssh/ssh-auth.php

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
$keys = id(new PhabricatorAuthSSHKeyQuery())
88
->setViewer(PhabricatorUser::getOmnipotentUser())
9+
->withIsActive(true)
910
->execute();
1011

1112
if (!$keys) {

‎src/applications/almanac/controller/AlmanacDeviceViewController.php

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ private function buildSSHKeysTable(AlmanacDevice $device) {
146146
$keys = id(new PhabricatorAuthSSHKeyQuery())
147147
->setViewer($viewer)
148148
->withObjectPHIDs(array($device_phid))
149+
->withIsActive(true)
149150
->execute();
150151

151152
$table = id(new PhabricatorAuthSSHKeyTableView())

‎src/applications/almanac/management/AlmanacManagementRegisterWorkflow.php

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ public function execute(PhutilArgumentParser $args) {
141141
$public_key = id(new PhabricatorAuthSSHKeyQuery())
142142
->setViewer($this->getViewer())
143143
->withKeys(array($key_object))
144+
->withIsActive(true)
144145
->executeOne();
145146

146147
if (!$public_key) {

‎src/applications/almanac/management/AlmanacManagementTrustKeyWorkflow.php

+5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ public function execute(PhutilArgumentParser $args) {
3535
pht('No public key exists with ID "%s".', $id));
3636
}
3737

38+
if (!$key->getIsActive()) {
39+
throw new PhutilArgumentUsageException(
40+
pht('Public key "%s" is not an active key.', $id));
41+
}
42+
3843
if ($key->getIsTrusted()) {
3944
throw new PhutilArgumentUsageException(
4045
pht('Public key with ID %s is already trusted.', $id));

‎src/applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ protected function execute(ConduitAPIRequest $request) {
2828
$viewer = $request->getUser();
2929

3030
$query = id(new PhabricatorAuthSSHKeyQuery())
31-
->setViewer($viewer);
31+
->setViewer($viewer)
32+
->withIsActive(true);
3233

3334
$ids = $request->getValue('ids');
3435
if ($ids !== null) {

‎src/applications/auth/controller/PhabricatorAuthSSHKeyController.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ protected function newKeyForObjectPHID($object_phid) {
2525
return null;
2626
}
2727

28-
return id(new PhabricatorAuthSSHKey())
29-
->setObjectPHID($object_phid)
30-
->attachObject($object);
28+
return PhabricatorAuthSSHKey::initializeNewSSHKey($viewer, $object);
3129
}
3230

3331
}

‎src/applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php

+6-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public function handleRequest(AphrontRequest $request) {
99
$key = id(new PhabricatorAuthSSHKeyQuery())
1010
->setViewer($viewer)
1111
->withIDs(array($request->getURIData('id')))
12+
->withIsActive(true)
1213
->requireCapabilities(
1314
array(
1415
PhabricatorPolicyCapability::CAN_VIEW,
@@ -27,8 +28,11 @@ public function handleRequest(AphrontRequest $request) {
2728
$cancel_uri);
2829

2930
if ($request->isFormPost()) {
30-
// TODO: It would be nice to write an edge transaction here or something.
31-
$key->delete();
31+
32+
// TODO: Convert to transactions.
33+
$key->setIsActive(null);
34+
$key->save();
35+
3236
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
3337
}
3438

‎src/applications/auth/controller/PhabricatorAuthSSHKeyEditController.php

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public function handleRequest(AphrontRequest $request) {
1111
$key = id(new PhabricatorAuthSSHKeyQuery())
1212
->setViewer($viewer)
1313
->withIDs(array($id))
14+
->withIsActive(true)
1415
->requireCapabilities(
1516
array(
1617
PhabricatorPolicyCapability::CAN_VIEW,

‎src/applications/auth/phid/PhabricatorAuthSSHKeyPHIDType.php

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ public function loadHandles(
3232
foreach ($handles as $phid => $handle) {
3333
$key = $objects[$phid];
3434
$handle->setName(pht('SSH Key %d', $key->getID()));
35+
36+
if (!$key->getIsActive()) {
37+
$handle->setClosed(pht('Inactive'));
38+
}
3539
}
3640
}
3741

‎src/applications/auth/query/PhabricatorAuthSSHKeyQuery.php

+19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ final class PhabricatorAuthSSHKeyQuery
77
private $phids;
88
private $objectPHIDs;
99
private $keys;
10+
private $isActive;
1011

1112
public function withIDs(array $ids) {
1213
$this->ids = $ids;
@@ -29,6 +30,11 @@ public function withKeys(array $keys) {
2930
return $this;
3031
}
3132

33+
public function withIsActive($active) {
34+
$this->isActive = $active;
35+
return $this;
36+
}
37+
3238
public function newResultObject() {
3339
return new PhabricatorAuthSSHKey();
3440
}
@@ -100,6 +106,19 @@ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
100106
$where[] = implode(' OR ', $sql);
101107
}
102108

109+
if ($this->isActive !== null) {
110+
if ($this->isActive) {
111+
$where[] = qsprintf(
112+
$conn,
113+
'isActive = %d',
114+
1);
115+
} else {
116+
$where[] = qsprintf(
117+
$conn,
118+
'isActive IS NULL');
119+
}
120+
}
121+
103122
return $where;
104123

105124
}

‎src/applications/auth/storage/PhabricatorAuthSSHKey.php

+27-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,28 @@ final class PhabricatorAuthSSHKey
1313
protected $keyBody;
1414
protected $keyComment = '';
1515
protected $isTrusted = 0;
16+
protected $isActive;
1617

1718
private $object = self::ATTACHABLE;
1819

20+
public static function initializeNewSSHKey(
21+
PhabricatorUser $viewer,
22+
PhabricatorSSHPublicKeyInterface $object) {
23+
24+
// You must be able to edit an object to create a new key on it.
25+
PhabricatorPolicyFilter::requireCapability(
26+
$viewer,
27+
$object,
28+
PhabricatorPolicyCapability::CAN_EDIT);
29+
30+
$object_phid = $object->getPHID();
31+
32+
return id(new self())
33+
->setIsActive(1)
34+
->setObjectPHID($object_phid)
35+
->attachObject($object);
36+
}
37+
1938
protected function getConfiguration() {
2039
return array(
2140
self::CONFIG_AUX_PHID => true,
@@ -26,13 +45,19 @@ protected function getConfiguration() {
2645
'keyBody' => 'text',
2746
'keyComment' => 'text255',
2847
'isTrusted' => 'bool',
48+
'isActive' => 'bool?',
2949
),
3050
self::CONFIG_KEY_SCHEMA => array(
3151
'key_object' => array(
3252
'columns' => array('objectPHID'),
3353
),
34-
'key_unique' => array(
35-
'columns' => array('keyIndex'),
54+
'key_active' => array(
55+
'columns' => array('isActive', 'objectPHID'),
56+
),
57+
// NOTE: This unique key includes a nullable column, effectively
58+
// constraining uniqueness on active keys only.
59+
'key_activeunique' => array(
60+
'columns' => array('keyIndex', 'isActive'),
3661
'unique' => true,
3762
),
3863
),

‎src/applications/conduit/controller/PhabricatorConduitAPIController.php

+1
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ private function authenticateUser(
204204
$stored_key = id(new PhabricatorAuthSSHKeyQuery())
205205
->setViewer(PhabricatorUser::getOmnipotentUser())
206206
->withKeys(array($public_key))
207+
->withIsActive(true)
207208
->executeOne();
208209
if (!$stored_key) {
209210
return array(

‎src/applications/people/storage/PhabricatorUser.php

+5-4
Original file line numberDiff line numberDiff line change
@@ -1291,11 +1291,12 @@ public function destroyObjectPermanently(
12911291
$profile->delete();
12921292
}
12931293

1294-
$keys = id(new PhabricatorAuthSSHKey())->loadAllWhere(
1295-
'objectPHID = %s',
1296-
$this->getPHID());
1294+
$keys = id(new PhabricatorAuthSSHKeyQuery())
1295+
->setViewer($engine->getViewer())
1296+
->withObjectPHIDs(array($this->getPHID()))
1297+
->execute();
12971298
foreach ($keys as $key) {
1298-
$key->delete();
1299+
$engine->destroyObject($key);
12991300
}
13001301

13011302
$emails = id(new PhabricatorUserEmail())->loadAllWhere(

‎src/applications/settings/panel/PhabricatorSSHKeysSettingsPanel.php

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public function processRequest(AphrontRequest $request) {
3333
$keys = id(new PhabricatorAuthSSHKeyQuery())
3434
->setViewer($viewer)
3535
->withObjectPHIDs(array($user->getPHID()))
36+
->withIsActive(true)
3637
->execute();
3738

3839
$table = id(new PhabricatorAuthSSHKeyTableView())

0 commit comments

Comments
 (0)
Failed to load comments.