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:
parent
d27cd5fb99
commit
44fc671b3f
6 changed files with 205 additions and 30 deletions
|
@ -14,7 +14,7 @@ return array(
|
||||||
'differential.pkg.js' => '11a5b750',
|
'differential.pkg.js' => '11a5b750',
|
||||||
'diffusion.pkg.css' => '3783278d',
|
'diffusion.pkg.css' => '3783278d',
|
||||||
'diffusion.pkg.js' => '5b4010f4',
|
'diffusion.pkg.js' => '5b4010f4',
|
||||||
'javelin.pkg.js' => '5b0f988e',
|
'javelin.pkg.js' => '65fa3049',
|
||||||
'maniphest.pkg.css' => 'f1887d71',
|
'maniphest.pkg.css' => 'f1887d71',
|
||||||
'maniphest.pkg.js' => '2fe8af22',
|
'maniphest.pkg.js' => '2fe8af22',
|
||||||
'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
|
'rsrc/css/aphront/aphront-bars.css' => '231ac33c',
|
||||||
|
@ -208,7 +208,7 @@ return array(
|
||||||
'rsrc/externals/javelin/lib/Resource.js' => '356de121',
|
'rsrc/externals/javelin/lib/Resource.js' => '356de121',
|
||||||
'rsrc/externals/javelin/lib/URI.js' => 'd9a9b862',
|
'rsrc/externals/javelin/lib/URI.js' => 'd9a9b862',
|
||||||
'rsrc/externals/javelin/lib/Vector.js' => '403a3dce',
|
'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__/Cookie.js' => '5ed109e8',
|
||||||
'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
|
'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
|
||||||
'rsrc/externals/javelin/lib/__tests__/JSON.js' => '2295d074',
|
'rsrc/externals/javelin/lib/__tests__/JSON.js' => '2295d074',
|
||||||
|
@ -663,7 +663,7 @@ return array(
|
||||||
'javelin-view-interpreter' => '0c33c1a0',
|
'javelin-view-interpreter' => '0c33c1a0',
|
||||||
'javelin-view-renderer' => '6c2b09a2',
|
'javelin-view-renderer' => '6c2b09a2',
|
||||||
'javelin-view-visitor' => 'efe49472',
|
'javelin-view-visitor' => 'efe49472',
|
||||||
'javelin-workflow' => 'd16edeae',
|
'javelin-workflow' => 'f28bf201',
|
||||||
'legalpad-document-css' => 'cd275275',
|
'legalpad-document-css' => 'cd275275',
|
||||||
'lightbox-attachment-css' => '7acac05d',
|
'lightbox-attachment-css' => '7acac05d',
|
||||||
'maniphest-batch-editor' => '8f380ebc',
|
'maniphest-batch-editor' => '8f380ebc',
|
||||||
|
@ -1742,17 +1742,6 @@ return array(
|
||||||
4 => 'javelin-fx',
|
4 => 'javelin-fx',
|
||||||
5 => 'javelin-util',
|
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' =>
|
'd254d646' =>
|
||||||
array(
|
array(
|
||||||
0 => 'javelin-util',
|
0 => 'javelin-util',
|
||||||
|
@ -1880,6 +1869,17 @@ return array(
|
||||||
4 => 'javelin-request',
|
4 => 'javelin-request',
|
||||||
5 => 'javelin-workflow',
|
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' =>
|
'f42bb8c6' =>
|
||||||
array(
|
array(
|
||||||
0 => 'javelin-stratcom',
|
0 => 'javelin-stratcom',
|
||||||
|
|
|
@ -1958,6 +1958,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php',
|
'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php',
|
||||||
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
|
'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php',
|
||||||
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
|
'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php',
|
||||||
|
'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php',
|
||||||
'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php',
|
'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php',
|
||||||
'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
|
'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php',
|
||||||
'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
|
'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
|
||||||
|
@ -4748,6 +4749,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
|
'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
|
||||||
'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO',
|
'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO',
|
||||||
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
|
'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine',
|
||||||
|
'PhabricatorSSHKeyGenerator' => 'Phobject',
|
||||||
'PhabricatorSSHLog' => 'Phobject',
|
'PhabricatorSSHLog' => 'Phobject',
|
||||||
'PhabricatorSSHPassthruCommand' => 'Phobject',
|
'PhabricatorSSHPassthruCommand' => 'Phobject',
|
||||||
'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow',
|
'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow',
|
||||||
|
|
|
@ -23,6 +23,11 @@ final class PhabricatorSettingsPanelSSHKeys
|
||||||
|
|
||||||
$user = $request->getUser();
|
$user = $request->getUser();
|
||||||
|
|
||||||
|
$generate = $request->getStr('generate');
|
||||||
|
if ($generate) {
|
||||||
|
return $this->processGenerate($request);
|
||||||
|
}
|
||||||
|
|
||||||
$edit = $request->getStr('edit');
|
$edit = $request->getStr('edit');
|
||||||
$delete = $request->getStr('delete');
|
$delete = $request->getStr('delete');
|
||||||
if (!$edit && !$delete) {
|
if (!$edit && !$delete) {
|
||||||
|
@ -220,18 +225,36 @@ final class PhabricatorSettingsPanelSSHKeys
|
||||||
$panel = new PHUIObjectBoxView();
|
$panel = new PHUIObjectBoxView();
|
||||||
$header = new PHUIHeaderView();
|
$header = new PHUIHeaderView();
|
||||||
|
|
||||||
$icon = id(new PHUIIconView())
|
$upload_icon = id(new PHUIIconView())
|
||||||
->setSpriteSheet(PHUIIconView::SPRITE_ICONS)
|
->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();
|
try {
|
||||||
$button->setText(pht('Add New Public Key'));
|
PhabricatorSSHKeyGenerator::assertCanGenerateKeypair();
|
||||||
$button->setHref($this->getPanelURI('?edit=true'));
|
$can_generate = true;
|
||||||
$button->setTag('a');
|
} catch (Exception $ex) {
|
||||||
$button->setIcon($icon);
|
$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->setHeader(pht('SSH Public Keys'));
|
||||||
$header->addActionLink($button);
|
$header->addActionLink($generate_button);
|
||||||
|
$header->addActionLink($upload_button);
|
||||||
|
|
||||||
$panel->setHeader($header);
|
$panel->setHeader($header);
|
||||||
$panel->appendChild($table);
|
$panel->appendChild($table);
|
||||||
|
@ -268,4 +291,84 @@ final class PhabricatorSettingsPanelSSHKeys
|
||||||
->setDialog($dialog);
|
->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 $footers = array();
|
||||||
private $isStandalone;
|
private $isStandalone;
|
||||||
private $method = 'POST';
|
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) {
|
public function setMethod($method) {
|
||||||
$this->method = $method;
|
$this->method = $method;
|
||||||
|
@ -31,11 +37,6 @@ final class AphrontDialogView extends AphrontView {
|
||||||
return $this->isStandalone;
|
return $this->isStandalone;
|
||||||
}
|
}
|
||||||
|
|
||||||
private $width = 'default';
|
|
||||||
const WIDTH_DEFAULT = 'default';
|
|
||||||
const WIDTH_FORM = 'form';
|
|
||||||
const WIDTH_FULL = 'full';
|
|
||||||
|
|
||||||
public function setSubmitURI($uri) {
|
public function setSubmitURI($uri) {
|
||||||
$this->submitURI = $uri;
|
$this->submitURI = $uri;
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -121,22 +122,51 @@ final class AphrontDialogView extends AphrontView {
|
||||||
$paragraph));
|
$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() {
|
final public function render() {
|
||||||
require_celerity_resource('aphront-dialog-view-css');
|
require_celerity_resource('aphront-dialog-view-css');
|
||||||
|
|
||||||
$buttons = array();
|
$buttons = array();
|
||||||
if ($this->submitButton) {
|
if ($this->submitButton) {
|
||||||
|
$meta = array();
|
||||||
|
if ($this->disableWorkflowOnSubmit) {
|
||||||
|
$meta['disableWorkflow'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
$buttons[] = javelin_tag(
|
$buttons[] = javelin_tag(
|
||||||
'button',
|
'button',
|
||||||
array(
|
array(
|
||||||
'name' => '__submit__',
|
'name' => '__submit__',
|
||||||
'sigil' => '__default__',
|
'sigil' => '__default__',
|
||||||
'type' => 'submit',
|
'type' => 'submit',
|
||||||
|
'meta' => $meta,
|
||||||
),
|
),
|
||||||
$this->submitButton);
|
$this->submitButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->cancelURI) {
|
if ($this->cancelURI) {
|
||||||
|
$meta = array();
|
||||||
|
if ($this->disableWorkflowOnCancel) {
|
||||||
|
$meta['disableWorkflow'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
$buttons[] = javelin_tag(
|
$buttons[] = javelin_tag(
|
||||||
'a',
|
'a',
|
||||||
array(
|
array(
|
||||||
|
@ -144,6 +174,7 @@ final class AphrontDialogView extends AphrontView {
|
||||||
'class' => 'button grey',
|
'class' => 'button grey',
|
||||||
'name' => '__cancel__',
|
'name' => '__cancel__',
|
||||||
'sigil' => 'jx-workflow-button',
|
'sigil' => 'jx-workflow-button',
|
||||||
|
'meta' => $meta,
|
||||||
),
|
),
|
||||||
$this->cancelText);
|
$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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.prevent();
|
|
||||||
|
|
||||||
// Get the button (which is sometimes actually another tag, like an <a />)
|
// Get the button (which is sometimes actually another tag, like an <a />)
|
||||||
// which triggered the event. In particular, this makes sure we get the
|
// 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
|
// right node if there is a <button> with an <img /> inside it or
|
||||||
// or something similar.
|
// or something similar.
|
||||||
var t = event.getNode('jx-workflow-button') ||
|
var t = event.getNode('jx-workflow-button') ||
|
||||||
event.getNode('tag: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__') {
|
if (t.name == '__cancel__' || t.name == '__close__') {
|
||||||
JX.Workflow._pop();
|
JX.Workflow._pop();
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue