1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 08:52:39 +01:00

Separate SSH key management from the settings panel

Summary:
Ref T5833. I want to add SSH keys to Almanac devices, but the edit workflows for them are currently bound tightly to users.

Instead, decouple key management from users and the settings panel.

Test Plan:
  - Uploaded, generated, edited and deleted SSH keys.
  - Hit missing name, missing key, bad key format, duplicate key errors.
  - Edited/generated/deleted/etc keys for a bot user as an administrator.
  - Got HiSec'd on everything.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5833

Differential Revision: https://secure.phabricator.com/D10824
This commit is contained in:
Evan Priestley 2014-11-11 08:18:26 -08:00
parent ce55bb1d96
commit 32cdc23efc
12 changed files with 358 additions and 280 deletions

View file

@ -1318,8 +1318,12 @@ phutil_register_library_map(array(
'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php',
'PhabricatorAuthRevokeTokenController' => 'applications/auth/controller/PhabricatorAuthRevokeTokenController.php',
'PhabricatorAuthSSHKey' => 'applications/auth/storage/PhabricatorAuthSSHKey.php',
'PhabricatorAuthSSHKeyController' => 'applications/auth/controller/PhabricatorAuthSSHKeyController.php',
'PhabricatorAuthSSHKeyDeleteController' => 'applications/auth/controller/PhabricatorAuthSSHKeyDeleteController.php',
'PhabricatorAuthSSHKeyEditController' => 'applications/auth/controller/PhabricatorAuthSSHKeyEditController.php',
'PhabricatorAuthSSHKeyGenerateController' => 'applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php',
'PhabricatorAuthSSHKeyQuery' => 'applications/auth/query/PhabricatorAuthSSHKeyQuery.php',
'PhabricatorAuthSSHPublicKey' => 'applications/auth/storage/PhabricatorAuthSSHPublicKey.php',
'PhabricatorAuthSSHPublicKey' => 'applications/auth/sshkey/PhabricatorAuthSSHPublicKey.php',
'PhabricatorAuthSession' => 'applications/auth/storage/PhabricatorAuthSession.php',
'PhabricatorAuthSessionEngine' => 'applications/auth/engine/PhabricatorAuthSessionEngine.php',
'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
@ -2249,6 +2253,7 @@ phutil_register_library_map(array(
'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php',
'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
'PhabricatorSSHPublicKeyInterface' => 'applications/auth/sshkey/PhabricatorSSHPublicKeyInterface.php',
'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php',
'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php',
@ -4386,6 +4391,10 @@ phutil_register_library_map(array(
'PhabricatorAuthDAO',
'PhabricatorPolicyInterface',
),
'PhabricatorAuthSSHKeyController' => 'PhabricatorAuthController',
'PhabricatorAuthSSHKeyDeleteController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyEditController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyGenerateController' => 'PhabricatorAuthSSHKeyController',
'PhabricatorAuthSSHKeyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSSHPublicKey' => 'Phobject',
'PhabricatorAuthSession' => array(
@ -5608,6 +5617,7 @@ phutil_register_library_map(array(
'PhabricatorPolicyInterface',
'PhabricatorCustomFieldInterface',
'PhabricatorDestructibleInterface',
'PhabricatorSSHPublicKeyInterface',
),
'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField',
'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions',

View file

@ -109,6 +109,12 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
=> 'PhabricatorAuthDowngradeSessionController',
'multifactor/'
=> 'PhabricatorAuthNeedsMultiFactorController',
'sshkey/' => array(
'generate/' => 'PhabricatorAuthSSHKeyGenerateController',
'upload/' => 'PhabricatorAuthSSHKeyEditController',
'edit/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyEditController',
'delete/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyDeleteController',
),
),
'/oauth/(?P<provider>\w+)/login/'

View file

@ -0,0 +1,33 @@
<?php
abstract class PhabricatorAuthSSHKeyController
extends PhabricatorAuthController {
protected function newKeyForObjectPHID($object_phid) {
$viewer = $this->getViewer();
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$object) {
return null;
}
// If this kind of object can't have SSH keys, don't let the viewer
// add them.
if (!($object instanceof PhabricatorSSHPublicKeyInterface)) {
return null;
}
return id(new PhabricatorAuthSSHKey())
->setObjectPHID($object_phid)
->attachObject($object);
}
}

View file

@ -0,0 +1,48 @@
<?php
final class PhabricatorAuthSSHKeyDeleteController
extends PhabricatorAuthSSHKeyController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($viewer)
->withIDs(array($request->getURIData('id')))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$key) {
return new Aphront404Response();
}
$cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer);
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$cancel_uri);
if ($request->isFormPost()) {
// TODO: It would be nice to write an edge transaction here or something.
$key->delete();
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
}
$name = phutil_tag('strong', array(), $key->getName());
return $this->newDialog()
->setTitle(pht('Really delete SSH Public Key?'))
->appendParagraph(
pht(
'The key "%s" will be permanently deleted, and you will not longer '.
'be able to use the corresponding private key to authenticate.',
$name))
->addSubmitButton(pht('Delete Public Key'))
->addCancelButton($cancel_uri);
}
}

View file

@ -0,0 +1,127 @@
<?php
final class PhabricatorAuthSSHKeyEditController
extends PhabricatorAuthSSHKeyController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$id = $request->getURIData('id');
if ($id) {
$key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$key) {
return new Aphront404Response();
}
$is_new = false;
} else {
$key = $this->newKeyForObjectPHID($request->getStr('objectPHID'));
if (!$key) {
return new Aphront404Response();
}
$is_new = true;
}
$cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer);
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$cancel_uri);
$v_name = $key->getName();
$e_name = strlen($v_name) ? null : true;
$v_key = $key->getEntireKey();
$e_key = strlen($v_key) ? null : true;
$errors = array();
if ($request->isFormPost()) {
$v_name = $request->getStr('name');
$v_key = $request->getStr('key');
if (!strlen($v_name)) {
$errors[] = pht('You must provide a name for this public key.');
$e_name = pht('Required');
} else {
$key->setName($v_name);
}
if (!strlen($v_key)) {
$errors[] = pht('You must provide a public key.');
$e_key = pht('Required');
} else {
try {
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($v_key);
$type = $public_key->getType();
$body = $public_key->getBody();
$comment = $public_key->getComment();
$key->setKeyType($type);
$key->setKeyBody($body);
$key->setKeyComment($comment);
$e_key = null;
} catch (Exception $ex) {
$e_key = pht('Invalid');
$errors[] = $ex->getMessage();
}
}
if (!$errors) {
try {
$key->save();
return id(new AphrontRedirectResponse())->setURI($cancel_uri);
} catch (Exception $ex) {
$e_key = pht('Duplicate');
$errors[] = pht(
'This public key is already associated with another user or '.
'device. Each key must unambiguously identify a single unique '.
'owner.');
}
}
}
$form = id(new AphrontFormView())
->setUser($viewer)
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setError($e_name)
->setValue($v_name))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Public Key'))
->setName('key')
->setValue($v_key)
->setError($e_key));
if ($is_new) {
$title = pht('Upload SSH Public Key');
$save_button = pht('Upload Public Key');
$form->addHiddenInput('objectPHID', $key->getObject()->getPHID());
} else {
$title = pht('Edit SSH Public Key');
$save_button = pht('Save Changes');
}
return $this->newDialog()
->setTitle($title)
->setWidth(AphrontDialogView::WIDTH_FORM)
->setErrors($errors)
->appendForm($form)
->addSubmitButton($save_button)
->addCancelButton($cancel_uri);
}
}

View file

@ -0,0 +1,90 @@
<?php
final class PhabricatorAuthSSHKeyGenerateController
extends PhabricatorAuthSSHKeyController {
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$key = $this->newKeyForObjectPHID($request->getStr('objectPHID'));
if (!$key) {
return new Aphront404Response();
}
$cancel_uri = $key->getObject()->getSSHPublicKeyManagementURI($viewer);
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$cancel_uri);
if ($request->isFormPost()) {
$keys = PhabricatorSSHKeyGenerator::generateKeypair();
list($public_key, $private_key) = $keys;
$file = PhabricatorFile::buildFromFileDataOrHash(
$private_key,
array(
'name' => 'id_rsa_phabricator.key',
'ttl' => time() + (60 * 10),
'viewPolicy' => $viewer->getPHID(),
));
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($public_key);
$type = $public_key->getType();
$body = $public_key->getBody();
$key
->setName('id_rsa_phabricator')
->setKeyType($type)
->setKeyBody($body)
->setKeyComment(pht('Generated'))
->save();
// NOTE: We're disabling workflow on submit so the download works. We're
// disabling workflow on cancel so the page reloads, showing the new
// key.
return $this->newDialog()
->setTitle(pht('Download Private Key'))
->setDisableWorkflowOnCancel(true)
->setDisableWorkflowOnSubmit(true)
->setSubmitURI($file->getDownloadURI())
->appendParagraph(
pht(
'A keypair has been generated, and the public key has been '.
'added as a recognized key. Use the button below to download '.
'the private key.'))
->appendParagraph(
pht(
'After you download the private key, it will be destroyed. '.
'You will not be able to retrieve it if you lose your copy.'))
->addSubmitButton(pht('Download Private Key'))
->addCancelButton($cancel_uri, pht('Done'));
}
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
return $this->newDialog()
->setTitle(pht('Generate New Keypair'))
->addHiddenInput('objectPHID', $key->getObject()->getPHID())
->appendParagraph(
pht(
'This workflow will generate a new SSH keypair, add the public '.
'key, and let you download the private key.'))
->appendParagraph(
pht(
'Phabricator will not retain a copy of the private key.'))
->addSubmitButton(pht('Generate New Keypair'))
->addCancelButton($cancel_uri);
} catch (Exception $ex) {
return $this->newDialog()
->setTitle(pht('Unable to Generate Keys'))
->appendParagraph($ex->getMessage())
->addCancelButton($cancel_uri);
}
}
}

View file

@ -50,10 +50,14 @@ final class PhabricatorAuthSSHKeyQuery
foreach ($keys as $key => $ssh_key) {
$object = idx($objects, $ssh_key->getObjectPHID());
if (!$object) {
// We must have an object, and that object must be a valid object for
// SSH keys.
if (!$object || !($object instanceof PhabricatorSSHPublicKeyInterface)) {
unset($keys[$key]);
continue;
}
$ssh_key->attachObject($object);
}

View file

@ -0,0 +1,14 @@
<?php
interface PhabricatorSSHPublicKeyInterface {
/**
* Provide a URI for SSH key workflows to return to after completing.
*
* When an actor adds, edits or deletes a public key, they'll be returned to
* this URI. For example, editing user keys returns the actor to the settings
* panel. Editing device keys returns the actor to the device page.
*/
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer);
}

View file

@ -56,7 +56,7 @@ final class PhabricatorAuthSSHKey
return $this->assertAttached($this->object);
}
public function attachObject($object) {
public function attachObject(PhabricatorSSHPublicKeyInterface $object) {
$this->object = $object;
return $this;
}

View file

@ -9,7 +9,8 @@ final class PhabricatorUser
PhutilPerson,
PhabricatorPolicyInterface,
PhabricatorCustomFieldInterface,
PhabricatorDestructibleInterface {
PhabricatorDestructibleInterface,
PhabricatorSSHPublicKeyInterface {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
@ -928,4 +929,18 @@ EOBODY;
}
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
if ($viewer->getPHID() == $this->getPHID()) {
// If the viewer is managing their own keys, take them to the normal
// panel.
return '/settings/panel/ssh/';
} else {
// Otherwise, take them to the administrative panel for this user.
return '/settings/'.$this->getID().'/panel/ssh/';
}
}
}

View file

@ -24,138 +24,6 @@ final class PhabricatorSettingsPanelSSHKeys
}
public function processRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$user = $this->getUser();
$generate = $request->getStr('generate');
if ($generate) {
return $this->processGenerate($request);
}
$edit = $request->getStr('edit');
$delete = $request->getStr('delete');
if (!$edit && !$delete) {
return $this->renderKeyListView($request);
}
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$id = nonempty($edit, $delete);
if ($id && (int)$id) {
$key = id(new PhabricatorAuthSSHKeyQuery())
->setViewer($viewer)
->withIDs(array($id))
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->executeOne();
if (!$key) {
return new Aphront404Response();
}
} else {
$key = id(new PhabricatorAuthSSHKey())
->setObjectPHID($user->getPHID());
}
if ($delete) {
return $this->processDelete($request, $key);
}
$e_name = true;
$e_key = true;
$errors = array();
$entire_key = $key->getEntireKey();
if ($request->isFormPost()) {
$key->setName($request->getStr('name'));
$entire_key = $request->getStr('key');
if (!strlen($entire_key)) {
$errors[] = pht('You must provide an SSH Public Key.');
$e_key = pht('Required');
} else {
try {
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($entire_key);
$type = $public_key->getType();
$body = $public_key->getBody();
$comment = $public_key->getComment();
$key->setKeyType($type);
$key->setKeyBody($body);
$key->setKeyComment($comment);
$e_key = null;
} catch (Exception $ex) {
$e_key = pht('Invalid');
$errors[] = $ex->getMessage();
}
}
if (!strlen($key->getName())) {
$errors[] = pht('You must name this public key.');
$e_name = pht('Required');
} else {
$e_name = null;
}
if (!$errors) {
try {
$key->save();
return id(new AphrontRedirectResponse())
->setURI($this->getPanelURI());
} catch (AphrontDuplicateKeyQueryException $ex) {
$e_key = pht('Duplicate');
$errors[] = pht('This public key is already associated with a user '.
'account.');
}
}
}
$is_new = !$key->getID();
if ($is_new) {
$header = pht('Add New SSH Public Key');
$save = pht('Add Key');
} else {
$header = pht('Edit SSH Public Key');
$save = pht('Save Changes');
}
$form = id(new AphrontFormView())
->setUser($viewer)
->addHiddenInput('edit', $is_new ? 'true' : $key->getID())
->appendChild(
id(new AphrontFormTextControl())
->setLabel(pht('Name'))
->setName('name')
->setValue($key->getName())
->setError($e_name))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Public Key'))
->setName('key')
->setValue($entire_key)
->setError($e_key))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getPanelURI())
->setValue($save));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($header)
->setFormErrors($errors)
->setForm($form);
return $form_box;
}
private function renderKeyListView(AphrontRequest $request) {
$user = $this->getUser();
$viewer = $request->getUser();
@ -167,10 +35,11 @@ final class PhabricatorSettingsPanelSSHKeys
$rows = array();
foreach ($keys as $key) {
$rows[] = array(
phutil_tag(
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?edit='.$key->getID()),
'href' => '/auth/sshkey/edit/'.$key->getID().'/',
'sigil' => 'workflow',
),
$key->getName()),
$key->getKeyComment(),
@ -180,7 +49,7 @@ final class PhabricatorSettingsPanelSSHKeys
javelin_tag(
'a',
array(
'href' => $this->getPanelURI('?delete='.$key->getID()),
'href' => '/auth/sshkey/delete/'.$key->getID().'/',
'class' => 'small grey button',
'sigil' => 'workflow',
),
@ -216,7 +85,8 @@ final class PhabricatorSettingsPanelSSHKeys
->setIconFont('fa-upload');
$upload_button = id(new PHUIButtonView())
->setText(pht('Upload Public Key'))
->setHref($this->getPanelURI('?edit=true'))
->setHref('/auth/sshkey/upload/?objectPHID='.$user->getPHID())
->setWorkflow(true)
->setTag('a')
->setIcon($upload_icon);
@ -231,7 +101,7 @@ final class PhabricatorSettingsPanelSSHKeys
->setIconFont('fa-lock');
$generate_button = id(new PHUIButtonView())
->setText(pht('Generate Keypair'))
->setHref($this->getPanelURI('?generate=true'))
->setHref('/auth/sshkey/generate/?objectPHID='.$user->getPHID())
->setTag('a')
->setWorkflow(true)
->setDisabled(!$can_generate)
@ -247,143 +117,4 @@ final class PhabricatorSettingsPanelSSHKeys
return $panel;
}
private function processDelete(
AphrontRequest $request,
PhabricatorAuthSSHKey $key) {
$viewer = $request->getUser();
$user = $this->getUser();
$name = phutil_tag('strong', array(), $key->getName());
if ($request->isDialogFormPost()) {
$key->delete();
return id(new AphrontReloadResponse())
->setURI($this->getPanelURI());
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addHiddenInput('delete', $key->getID())
->setTitle(pht('Really delete SSH Public Key?'))
->appendChild(phutil_tag('p', array(), pht(
'The key "%s" will be permanently deleted, and you will not longer be '.
'able to use the corresponding private key to authenticate.',
$name)))
->addSubmitButton(pht('Delete Public Key'))
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
private function processGenerate(AphrontRequest $request) {
$user = $this->getUser();
$viewer = $request->getUser();
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
$this->getPanelURI());
$is_self = ($user->getPHID() == $viewer->getPHID());
if ($request->isFormPost()) {
$keys = PhabricatorSSHKeyGenerator::generateKeypair();
list($public_key, $private_key) = $keys;
$file = PhabricatorFile::buildFromFileDataOrHash(
$private_key,
array(
'name' => 'id_rsa_phabricator.key',
'ttl' => time() + (60 * 10),
'viewPolicy' => $viewer->getPHID(),
));
$public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($public_key);
$type = $public_key->getType();
$body = $public_key->getBody();
$key = id(new PhabricatorAuthSSHKey())
->setObjectPHID($user->getPHID())
->setName('id_rsa_phabricator')
->setKeyType($type)
->setKeyBody($body)
->setKeyComment(pht('Generated'))
->save();
// NOTE: We're disabling workflow on submit so the download works. We're
// disabling workflow on cancel so the page reloads, showing the new
// key.
if ($is_self) {
$what_happened = pht(
'The public key has been associated with your Phabricator '.
'account. Use the button below to download the private key.');
} else {
$what_happened = pht(
'The public key has been associated with the %s account. '.
'Use the button below to download the private key.',
phutil_tag('strong', array(), $user->getUsername()));
}
$dialog = id(new AphrontDialogView())
->setTitle(pht('Download Private Key'))
->setUser($viewer)
->setDisableWorkflowOnCancel(true)
->setDisableWorkflowOnSubmit(true)
->setSubmitURI($file->getDownloadURI())
->appendParagraph(
pht(
'Successfully generated a new keypair.'))
->appendParagraph($what_happened)
->appendParagraph(
pht(
'After you download the private key, it will be destroyed. '.
'You will not be able to retrieve it if you lose your copy.'))
->addSubmitButton(pht('Download Private Key'))
->addCancelButton($this->getPanelURI(), pht('Done'));
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addCancelButton($this->getPanelURI());
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
if ($is_self) {
$explain = pht(
'This will generate an SSH keypair, associate the public key '.
'with your account, and let you download the private key.');
} else {
$explain = pht(
'This will generate an SSH keypair, associate the public key with '.
'the %s account, and let you download the private key.',
phutil_tag('strong', array(), $user->getUsername()));
}
$dialog
->addHiddenInput('generate', true)
->setTitle(pht('Generate New Keypair'))
->appendParagraph($explain)
->appendParagraph(
pht(
'Phabricator will not retain a copy of the private key.'))
->addSubmitButton(pht('Generate Keypair'));
} catch (Exception $ex) {
$dialog
->setTitle(pht('Unable to Generate Keys'))
->appendParagraph($ex->getMessage());
}
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
}