mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-27 07:50:57 +01:00
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
This commit is contained in:
parent
49eb6403a4
commit
0308d580d7
18 changed files with 82 additions and 12 deletions
2
resources/sql/autopatches/20160518.ssh.01.activecol.sql
Normal file
2
resources/sql/autopatches/20160518.ssh.01.activecol.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_auth.auth_sshkey
|
||||
ADD isActive BOOL;
|
2
resources/sql/autopatches/20160518.ssh.02.activeval.sql
Normal file
2
resources/sql/autopatches/20160518.ssh.02.activeval.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
UPDATE {$NAMESPACE}_auth.auth_sshkey
|
||||
SET isActive = 1;
|
2
resources/sql/autopatches/20160518.ssh.03.activekey.sql
Normal file
2
resources/sql/autopatches/20160518.ssh.03.activekey.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_auth.auth_sshkey
|
||||
ADD UNIQUE KEY `key_activeunique` (keyIndex, isActive);
|
|
@ -14,6 +14,7 @@ try {
|
|||
$key = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer(PhabricatorUser::getOmnipotentUser())
|
||||
->withKeys(array($public_key))
|
||||
->withIsActive(true)
|
||||
->executeOne();
|
||||
if (!$key) {
|
||||
exit(1);
|
||||
|
|
|
@ -6,6 +6,7 @@ require_once $root.'/scripts/__init_script__.php';
|
|||
|
||||
$keys = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer(PhabricatorUser::getOmnipotentUser())
|
||||
->withIsActive(true)
|
||||
->execute();
|
||||
|
||||
if (!$keys) {
|
||||
|
|
|
@ -146,6 +146,7 @@ final class AlmanacDeviceViewController
|
|||
$keys = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer($viewer)
|
||||
->withObjectPHIDs(array($device_phid))
|
||||
->withIsActive(true)
|
||||
->execute();
|
||||
|
||||
$table = id(new PhabricatorAuthSSHKeyTableView())
|
||||
|
|
|
@ -141,6 +141,7 @@ final class AlmanacManagementRegisterWorkflow
|
|||
$public_key = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer($this->getViewer())
|
||||
->withKeys(array($key_object))
|
||||
->withIsActive(true)
|
||||
->executeOne();
|
||||
|
||||
if (!$public_key) {
|
||||
|
|
|
@ -35,6 +35,11 @@ final class AlmanacManagementTrustKeyWorkflow
|
|||
pht('No public key exists with ID "%s".', $id));
|
||||
}
|
||||
|
||||
if (!$key->getIsActive()) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Public key "%s" is not an active key.', $id));
|
||||
}
|
||||
|
||||
if ($key->getIsTrusted()) {
|
||||
throw new PhutilArgumentUsageException(
|
||||
pht('Public key with ID %s is already trusted.', $id));
|
||||
|
|
|
@ -28,7 +28,8 @@ final class PhabricatorAuthQueryPublicKeysConduitAPIMethod
|
|||
$viewer = $request->getUser();
|
||||
|
||||
$query = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer($viewer);
|
||||
->setViewer($viewer)
|
||||
->withIsActive(true);
|
||||
|
||||
$ids = $request->getValue('ids');
|
||||
if ($ids !== null) {
|
||||
|
|
|
@ -25,9 +25,7 @@ abstract class PhabricatorAuthSSHKeyController
|
|||
return null;
|
||||
}
|
||||
|
||||
return id(new PhabricatorAuthSSHKey())
|
||||
->setObjectPHID($object_phid)
|
||||
->attachObject($object);
|
||||
return PhabricatorAuthSSHKey::initializeNewSSHKey($viewer, $object);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ final class PhabricatorAuthSSHKeyDeleteController
|
|||
$key = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer($viewer)
|
||||
->withIDs(array($request->getURIData('id')))
|
||||
->withIsActive(true)
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
|
@ -27,8 +28,11 @@ final class PhabricatorAuthSSHKeyDeleteController
|
|||
$cancel_uri);
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
// TODO: It would be nice to write an edge transaction here or something.
|
||||
$key->delete();
|
||||
|
||||
// TODO: Convert to transactions.
|
||||
$key->setIsActive(null);
|
||||
$key->save();
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ final class PhabricatorAuthSSHKeyEditController
|
|||
$key = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer($viewer)
|
||||
->withIDs(array($id))
|
||||
->withIsActive(true)
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
|
|
|
@ -32,6 +32,10 @@ final class PhabricatorAuthSSHKeyPHIDType
|
|||
foreach ($handles as $phid => $handle) {
|
||||
$key = $objects[$phid];
|
||||
$handle->setName(pht('SSH Key %d', $key->getID()));
|
||||
|
||||
if (!$key->getIsActive()) {
|
||||
$handle->setClosed(pht('Inactive'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ final class PhabricatorAuthSSHKeyQuery
|
|||
private $phids;
|
||||
private $objectPHIDs;
|
||||
private $keys;
|
||||
private $isActive;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
$this->ids = $ids;
|
||||
|
@ -29,6 +30,11 @@ final class PhabricatorAuthSSHKeyQuery
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function withIsActive($active) {
|
||||
$this->isActive = $active;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function newResultObject() {
|
||||
return new PhabricatorAuthSSHKey();
|
||||
}
|
||||
|
@ -100,6 +106,19 @@ final class PhabricatorAuthSSHKeyQuery
|
|||
$where[] = implode(' OR ', $sql);
|
||||
}
|
||||
|
||||
if ($this->isActive !== null) {
|
||||
if ($this->isActive) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'isActive = %d',
|
||||
1);
|
||||
} else {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'isActive IS NULL');
|
||||
}
|
||||
}
|
||||
|
||||
return $where;
|
||||
|
||||
}
|
||||
|
|
|
@ -13,9 +13,28 @@ final class PhabricatorAuthSSHKey
|
|||
protected $keyBody;
|
||||
protected $keyComment = '';
|
||||
protected $isTrusted = 0;
|
||||
protected $isActive;
|
||||
|
||||
private $object = self::ATTACHABLE;
|
||||
|
||||
public static function initializeNewSSHKey(
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorSSHPublicKeyInterface $object) {
|
||||
|
||||
// You must be able to edit an object to create a new key on it.
|
||||
PhabricatorPolicyFilter::requireCapability(
|
||||
$viewer,
|
||||
$object,
|
||||
PhabricatorPolicyCapability::CAN_EDIT);
|
||||
|
||||
$object_phid = $object->getPHID();
|
||||
|
||||
return id(new self())
|
||||
->setIsActive(1)
|
||||
->setObjectPHID($object_phid)
|
||||
->attachObject($object);
|
||||
}
|
||||
|
||||
protected function getConfiguration() {
|
||||
return array(
|
||||
self::CONFIG_AUX_PHID => true,
|
||||
|
@ -26,13 +45,19 @@ final class PhabricatorAuthSSHKey
|
|||
'keyBody' => 'text',
|
||||
'keyComment' => 'text255',
|
||||
'isTrusted' => 'bool',
|
||||
'isActive' => 'bool?',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_object' => array(
|
||||
'columns' => array('objectPHID'),
|
||||
),
|
||||
'key_unique' => array(
|
||||
'columns' => array('keyIndex'),
|
||||
'key_active' => array(
|
||||
'columns' => array('isActive', 'objectPHID'),
|
||||
),
|
||||
// NOTE: This unique key includes a nullable column, effectively
|
||||
// constraining uniqueness on active keys only.
|
||||
'key_activeunique' => array(
|
||||
'columns' => array('keyIndex', 'isActive'),
|
||||
'unique' => true,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -204,6 +204,7 @@ final class PhabricatorConduitAPIController
|
|||
$stored_key = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer(PhabricatorUser::getOmnipotentUser())
|
||||
->withKeys(array($public_key))
|
||||
->withIsActive(true)
|
||||
->executeOne();
|
||||
if (!$stored_key) {
|
||||
return array(
|
||||
|
|
|
@ -1291,11 +1291,12 @@ final class PhabricatorUser
|
|||
$profile->delete();
|
||||
}
|
||||
|
||||
$keys = id(new PhabricatorAuthSSHKey())->loadAllWhere(
|
||||
'objectPHID = %s',
|
||||
$this->getPHID());
|
||||
$keys = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer($engine->getViewer())
|
||||
->withObjectPHIDs(array($this->getPHID()))
|
||||
->execute();
|
||||
foreach ($keys as $key) {
|
||||
$key->delete();
|
||||
$engine->destroyObject($key);
|
||||
}
|
||||
|
||||
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
|
||||
|
|
|
@ -33,6 +33,7 @@ final class PhabricatorSSHKeysSettingsPanel extends PhabricatorSettingsPanel {
|
|||
$keys = id(new PhabricatorAuthSSHKeyQuery())
|
||||
->setViewer($viewer)
|
||||
->withObjectPHIDs(array($user->getPHID()))
|
||||
->withIsActive(true)
|
||||
->execute();
|
||||
|
||||
$table = id(new PhabricatorAuthSSHKeyTableView())
|
||||
|
|
Loading…
Reference in a new issue