mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 23:02:42 +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:
parent
d27cd5fb99
commit
44fc671b3f
6 changed files with 205 additions and 30 deletions
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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())
|
||||
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
|
||||
->setSpriteIcon('new');
|
||||
$upload_icon = id(new PHUIIconView())
|
||||
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
|
||||
->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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
32
src/infrastructure/util/PhabricatorSSHKeyGenerator.php
Normal file
32
src/infrastructure/util/PhabricatorSSHKeyGenerator.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
11
webroot/rsrc/externals/javelin/lib/Workflow.js
vendored
11
webroot/rsrc/externals/javelin/lib/Workflow.js
vendored
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue