1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 16:52:41 +01:00

Add a "Generate Keypair" option on the SSH Keys panel

Summary: Ref T4587. Add an option to automatically generate a keypair, associate the public key, and save the private key.

Test Plan: Generated some keypairs. Hit error conditions, etc.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: aran, epriestley

Maniphest Tasks: T4587

Differential Revision: https://secure.phabricator.com/D8513
This commit is contained in:
epriestley 2014-03-12 18:17:11 -07:00
parent d27cd5fb99
commit 44fc671b3f
6 changed files with 205 additions and 30 deletions

View file

@ -14,7 +14,7 @@ return array(
'differential.pkg.js' => '11a5b750',
'diffusion.pkg.css' => '3783278d',
'diffusion.pkg.js' => '5b4010f4',
'javelin.pkg.js' => '5b0f988e',
'javelin.pkg.js' => '65fa3049',
'maniphest.pkg.css' => 'f1887d71',
'maniphest.pkg.js' => '2fe8af22',
'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
@ -208,7 +208,7 @@ return array(
'rsrc/externals/javelin/lib/Resource.js' => '356de121',
'rsrc/externals/javelin/lib/URI.js' => 'd9a9b862',
'rsrc/externals/javelin/lib/Vector.js' => '403a3dce',
'rsrc/externals/javelin/lib/Workflow.js' => 'd16edeae',
'rsrc/externals/javelin/lib/Workflow.js' => 'f28bf201',
'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8',
'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
'rsrc/externals/javelin/lib/__tests__/JSON.js' => '2295d074',
@ -663,7 +663,7 @@ return array(
'javelin-view-interpreter' => '0c33c1a0',
'javelin-view-renderer' => '6c2b09a2',
'javelin-view-visitor' => 'efe49472',
'javelin-workflow' => 'd16edeae',
'javelin-workflow' => 'f28bf201',
'legalpad-document-css' => 'cd275275',
'lightbox-attachment-css' => '7acac05d',
'maniphest-batch-editor' => '8f380ebc',
@ -1742,17 +1742,6 @@ return array(
4 => 'javelin-fx',
5 => 'javelin-util',
),
'd16edeae' =>
array(
0 => 'javelin-stratcom',
1 => 'javelin-request',
2 => 'javelin-dom',
3 => 'javelin-vector',
4 => 'javelin-install',
5 => 'javelin-util',
6 => 'javelin-mask',
7 => 'javelin-uri',
),
'd254d646' =>
array(
0 => 'javelin-util',
@ -1880,6 +1869,17 @@ return array(
4 => 'javelin-request',
5 => 'javelin-workflow',
),
'f28bf201' =>
array(
0 => 'javelin-stratcom',
1 => 'javelin-request',
2 => 'javelin-dom',
3 => 'javelin-vector',
4 => 'javelin-install',
5 => 'javelin-util',
6 => 'javelin-mask',
7 => 'javelin-uri',
),
'f42bb8c6' =>
array(
0 => 'javelin-stratcom',

View file

@ -1958,6 +1958,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php',
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php',
'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
@ -4748,6 +4749,7 @@ phutil_register_library_map(array(
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO',
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
'PhabricatorSSHKeyGenerator' => 'Phobject',
'PhabricatorSSHLog' => 'Phobject',
'PhabricatorSSHPassthruCommand' => 'Phobject',
'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow',

View file

@ -23,6 +23,11 @@ final class PhabricatorSettingsPanelSSHKeys
$user = $request->getUser();
$generate = $request->getStr('generate');
if ($generate) {
return $this->processGenerate($request);
}
$edit = $request->getStr('edit');
$delete = $request->getStr('delete');
if (!$edit && !$delete) {
@ -220,18 +225,36 @@ final class PhabricatorSettingsPanelSSHKeys
$panel = new PHUIObjectBoxView();
$header = new PHUIHeaderView();
$icon = id(new PHUIIconView())
$upload_icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
->setSpriteIcon('new');
->setSpriteIcon('upload');
$upload_button = id(new PHUIButtonView())
->setText(pht('Upload Public Key'))
->setHref($this->getPanelURI('?edit=true'))
->setTag('a')
->setIcon($upload_icon);
$button = new PHUIButtonView();
$button->setText(pht('Add New Public Key'));
$button->setHref($this->getPanelURI('?edit=true'));
$button->setTag('a');
$button->setIcon($icon);
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
$can_generate = true;
} catch (Exception $ex) {
$can_generate = false;
}
$generate_icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
->setSpriteIcon('lock');
$generate_button = id(new PHUIButtonView())
->setText(pht('Generate Keypair'))
->setHref($this->getPanelURI('?generate=true'))
->setTag('a')
->setWorkflow(true)
->setDisabled(!$can_generate)
->setIcon($generate_icon);
$header->setHeader(pht('SSH Public Keys'));
$header->addActionLink($button);
$header->addActionLink($generate_button);
$header->addActionLink($upload_button);
$panel->setHeader($header);
$panel->appendChild($table);
@ -268,4 +291,84 @@ final class PhabricatorSettingsPanelSSHKeys
->setDialog($dialog);
}
private function processGenerate(
AphrontRequest $request) {
$viewer = $request->getUser();
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' => PhabricatorPolicies::POLICY_NOONE,
));
$key = id(new PhabricatorUserSSHKey())
->setUserPHID($viewer->getPHID())
->setName('id_rsa_phabricator')
->setKeyType('rsa')
->setKeyBody($public_key)
->setKeyHash(md5($public_key))
->setKeyComment(pht('Generated Key'))
->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.
$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(
pht(
'The public key has been associated with your Phabricator '.
'account. 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($this->getPanelURI(), pht('Done'));
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer)
->addCancelButton($this->getPanelURI());
try {
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
$dialog
->addHiddenInput('generate', true)
->setTitle(pht('Generate New Keypair'))
->appendParagraph(
pht(
"This will generate an SSH keypair, associate the public key ".
"with your account, and let you download the private key."))
->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);
}
}

View file

@ -0,0 +1,32 @@
<?php
final class PhabricatorSSHKeyGenerator extends Phobject {
public static function assertCanGenerateKeypair() {
$binary = 'ssh-keygen';
if (!Filesystem::resolveBinary($binary)) {
throw new Exception(
pht(
'Can not generate keys: unable to find "%s" in PATH!',
$binary));
}
}
public static function generateKeypair() {
self::assertCanGenerateKeypair();
$tempfile = new TempFile();
$keyfile = dirname($tempfile).DIRECTORY_SEPARATOR.'keytext';
execx(
'ssh-keygen -t rsa -N %s -f %s',
'',
$keyfile);
$public_key = Filesystem::readFile($keyfile.'.pub');
$private_key = Filesystem::readFile($keyfile);
return array($public_key, $private_key);
}
}

View file

@ -15,7 +15,13 @@ final class AphrontDialogView extends AphrontView {
private $footers = array();
private $isStandalone;
private $method = 'POST';
private $disableWorkflowOnSubmit;
private $disableWorkflowOnCancel;
private $width = 'default';
const WIDTH_DEFAULT = 'default';
const WIDTH_FORM = 'form';
const WIDTH_FULL = 'full';
public function setMethod($method) {
$this->method = $method;
@ -31,11 +37,6 @@ final class AphrontDialogView extends AphrontView {
return $this->isStandalone;
}
private $width = 'default';
const WIDTH_DEFAULT = 'default';
const WIDTH_FORM = 'form';
const WIDTH_FULL = 'full';
public function setSubmitURI($uri) {
$this->submitURI = $uri;
return $this;
@ -121,22 +122,51 @@ final class AphrontDialogView extends AphrontView {
$paragraph));
}
public function setDisableWorkflowOnSubmit($disable_workflow_on_submit) {
$this->disableWorkflowOnSubmit = $disable_workflow_on_submit;
return $this;
}
public function getDisableWorkflowOnSubmit() {
return $this->disableWorkflowOnSubmit;
}
public function setDisableWorkflowOnCancel($disable_workflow_on_cancel) {
$this->disableWorkflowOnCancel = $disable_workflow_on_cancel;
return $this;
}
public function getDisableWorkflowOnCancel() {
return $this->disableWorkflowOnCancel;
}
final public function render() {
require_celerity_resource('aphront-dialog-view-css');
$buttons = array();
if ($this->submitButton) {
$meta = array();
if ($this->disableWorkflowOnSubmit) {
$meta['disableWorkflow'] = true;
}
$buttons[] = javelin_tag(
'button',
array(
'name' => '__submit__',
'sigil' => '__default__',
'type' => 'submit',
'meta' => $meta,
),
$this->submitButton);
}
if ($this->cancelURI) {
$meta = array();
if ($this->disableWorkflowOnCancel) {
$meta['disableWorkflow'] = true;
}
$buttons[] = javelin_tag(
'a',
array(
@ -144,6 +174,7 @@ final class AphrontDialogView extends AphrontView {
'class' => 'button grey',
'name' => '__cancel__',
'sigil' => 'jx-workflow-button',
'meta' => $meta,
),
$this->cancelText);
}

View file

@ -88,14 +88,21 @@ JX.install('Workflow', {
return;
}
event.prevent();
// Get the button (which is sometimes actually another tag, like an <a />)
// which triggered the event. In particular, this makes sure we get the
// right node if there is a <button> with an <img /> inside it or
// or something similar.
var t = event.getNode('jx-workflow-button') ||
event.getNode('tag:button');
// If this button disables workflow (normally, because it is a file
// download button) let the event through without modification.
if (JX.Stratcom.getData(t).disableWorkflow) {
return;
}
event.prevent();
if (t.name == '__cancel__' || t.name == '__close__') {
JX.Workflow._pop();
} else {