diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -14,7 +14,7 @@ '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 @@ '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 @@ '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 @@ 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 @@ 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', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1958,6 +1958,7 @@ '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 @@ 'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', + 'PhabricatorSSHKeyGenerator' => 'Phobject', 'PhabricatorSSHLog' => 'Phobject', 'PhabricatorSSHPassthruCommand' => 'Phobject', 'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow', diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php --- a/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php @@ -23,6 +23,11 @@ $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 @@ $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); + + try { + PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); + $can_generate = true; + } catch (Exception $ex) { + $can_generate = false; + } - $button = new PHUIButtonView(); - $button->setText(pht('Add New Public Key')); - $button->setHref($this->getPanelURI('?edit=true')); - $button->setTag('a'); - $button->setIcon($icon); + $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 @@ ->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); + } + } diff --git a/src/infrastructure/util/PhabricatorSSHKeyGenerator.php b/src/infrastructure/util/PhabricatorSSHKeyGenerator.php new file mode 100644 --- /dev/null +++ b/src/infrastructure/util/PhabricatorSSHKeyGenerator.php @@ -0,0 +1,32 @@ +method = $method; @@ -31,11 +37,6 @@ 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 @@ $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 @@ 'class' => 'button grey', 'name' => '__cancel__', 'sigil' => 'jx-workflow-button', + 'meta' => $meta, ), $this->cancelText); } diff --git a/webroot/rsrc/externals/javelin/lib/Workflow.js b/webroot/rsrc/externals/javelin/lib/Workflow.js --- a/webroot/rsrc/externals/javelin/lib/Workflow.js +++ b/webroot/rsrc/externals/javelin/lib/Workflow.js @@ -88,14 +88,21 @@ return; } - event.prevent(); - // Get the button (which is sometimes actually another tag, like an ) // which triggered the event. In particular, this makes sure we get the // right node if there is a