@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+205 -30
+14 -14
resources/celerity/map.php
··· 14 14 'differential.pkg.js' => '11a5b750', 15 15 'diffusion.pkg.css' => '3783278d', 16 16 'diffusion.pkg.js' => '5b4010f4', 17 - 'javelin.pkg.js' => '5b0f988e', 17 + 'javelin.pkg.js' => '65fa3049', 18 18 'maniphest.pkg.css' => 'f1887d71', 19 19 'maniphest.pkg.js' => '2fe8af22', 20 20 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', ··· 208 208 'rsrc/externals/javelin/lib/Resource.js' => '356de121', 209 209 'rsrc/externals/javelin/lib/URI.js' => 'd9a9b862', 210 210 'rsrc/externals/javelin/lib/Vector.js' => '403a3dce', 211 - 'rsrc/externals/javelin/lib/Workflow.js' => 'd16edeae', 211 + 'rsrc/externals/javelin/lib/Workflow.js' => 'f28bf201', 212 212 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', 213 213 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', 214 214 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '2295d074', ··· 663 663 'javelin-view-interpreter' => '0c33c1a0', 664 664 'javelin-view-renderer' => '6c2b09a2', 665 665 'javelin-view-visitor' => 'efe49472', 666 - 'javelin-workflow' => 'd16edeae', 666 + 'javelin-workflow' => 'f28bf201', 667 667 'legalpad-document-css' => 'cd275275', 668 668 'lightbox-attachment-css' => '7acac05d', 669 669 'maniphest-batch-editor' => '8f380ebc', ··· 1742 1742 4 => 'javelin-fx', 1743 1743 5 => 'javelin-util', 1744 1744 ), 1745 - 'd16edeae' => 1746 - array( 1747 - 0 => 'javelin-stratcom', 1748 - 1 => 'javelin-request', 1749 - 2 => 'javelin-dom', 1750 - 3 => 'javelin-vector', 1751 - 4 => 'javelin-install', 1752 - 5 => 'javelin-util', 1753 - 6 => 'javelin-mask', 1754 - 7 => 'javelin-uri', 1755 - ), 1756 1745 'd254d646' => 1757 1746 array( 1758 1747 0 => 'javelin-util', ··· 1879 1868 3 => 'javelin-install', 1880 1869 4 => 'javelin-request', 1881 1870 5 => 'javelin-workflow', 1871 + ), 1872 + 'f28bf201' => 1873 + array( 1874 + 0 => 'javelin-stratcom', 1875 + 1 => 'javelin-request', 1876 + 2 => 'javelin-dom', 1877 + 3 => 'javelin-vector', 1878 + 4 => 'javelin-install', 1879 + 5 => 'javelin-util', 1880 + 6 => 'javelin-mask', 1881 + 7 => 'javelin-uri', 1882 1882 ), 1883 1883 'f42bb8c6' => 1884 1884 array(
+2
src/__phutil_library_map__.php
··· 1958 1958 'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php', 1959 1959 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 1960 1960 'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php', 1961 + 'PhabricatorSSHKeyGenerator' => 'infrastructure/util/PhabricatorSSHKeyGenerator.php', 1961 1962 'PhabricatorSSHLog' => 'infrastructure/log/PhabricatorSSHLog.php', 1962 1963 'PhabricatorSSHPassthruCommand' => 'infrastructure/ssh/PhabricatorSSHPassthruCommand.php', 1963 1964 'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php', ··· 4748 4749 'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase', 4749 4750 'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO', 4750 4751 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', 4752 + 'PhabricatorSSHKeyGenerator' => 'Phobject', 4751 4753 'PhabricatorSSHLog' => 'Phobject', 4752 4754 'PhabricatorSSHPassthruCommand' => 'Phobject', 4753 4755 'PhabricatorSSHWorkflow' => 'PhabricatorManagementWorkflow',
+112 -9
src/applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php
··· 23 23 24 24 $user = $request->getUser(); 25 25 26 + $generate = $request->getStr('generate'); 27 + if ($generate) { 28 + return $this->processGenerate($request); 29 + } 30 + 26 31 $edit = $request->getStr('edit'); 27 32 $delete = $request->getStr('delete'); 28 33 if (!$edit && !$delete) { ··· 220 225 $panel = new PHUIObjectBoxView(); 221 226 $header = new PHUIHeaderView(); 222 227 223 - $icon = id(new PHUIIconView()) 224 - ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) 225 - ->setSpriteIcon('new'); 228 + $upload_icon = id(new PHUIIconView()) 229 + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) 230 + ->setSpriteIcon('upload'); 231 + $upload_button = id(new PHUIButtonView()) 232 + ->setText(pht('Upload Public Key')) 233 + ->setHref($this->getPanelURI('?edit=true')) 234 + ->setTag('a') 235 + ->setIcon($upload_icon); 226 236 227 - $button = new PHUIButtonView(); 228 - $button->setText(pht('Add New Public Key')); 229 - $button->setHref($this->getPanelURI('?edit=true')); 230 - $button->setTag('a'); 231 - $button->setIcon($icon); 237 + try { 238 + PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); 239 + $can_generate = true; 240 + } catch (Exception $ex) { 241 + $can_generate = false; 242 + } 243 + 244 + $generate_icon = id(new PHUIIconView()) 245 + ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) 246 + ->setSpriteIcon('lock'); 247 + $generate_button = id(new PHUIButtonView()) 248 + ->setText(pht('Generate Keypair')) 249 + ->setHref($this->getPanelURI('?generate=true')) 250 + ->setTag('a') 251 + ->setWorkflow(true) 252 + ->setDisabled(!$can_generate) 253 + ->setIcon($generate_icon); 232 254 233 255 $header->setHeader(pht('SSH Public Keys')); 234 - $header->addActionLink($button); 256 + $header->addActionLink($generate_button); 257 + $header->addActionLink($upload_button); 235 258 236 259 $panel->setHeader($header); 237 260 $panel->appendChild($table); ··· 263 286 $name))) 264 287 ->addSubmitButton(pht('Delete Public Key')) 265 288 ->addCancelButton($this->getPanelURI()); 289 + 290 + return id(new AphrontDialogResponse()) 291 + ->setDialog($dialog); 292 + } 293 + 294 + private function processGenerate( 295 + AphrontRequest $request) { 296 + $viewer = $request->getUser(); 297 + 298 + if ($request->isFormPost()) { 299 + $keys = PhabricatorSSHKeyGenerator::generateKeypair(); 300 + list($public_key, $private_key) = $keys; 301 + 302 + $file = PhabricatorFile::buildFromFileDataOrHash( 303 + $private_key, 304 + array( 305 + 'name' => 'id_rsa_phabricator.key', 306 + 'ttl' => time() + (60 * 10), 307 + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, 308 + )); 309 + 310 + $key = id(new PhabricatorUserSSHKey()) 311 + ->setUserPHID($viewer->getPHID()) 312 + ->setName('id_rsa_phabricator') 313 + ->setKeyType('rsa') 314 + ->setKeyBody($public_key) 315 + ->setKeyHash(md5($public_key)) 316 + ->setKeyComment(pht('Generated Key')) 317 + ->save(); 318 + 319 + // NOTE: We're disabling workflow on submit so the download works. We're 320 + // disabling workflow on cancel so the page reloads, showing the new 321 + // key. 322 + 323 + $dialog = id(new AphrontDialogView()) 324 + ->setTitle(pht('Download Private Key')) 325 + ->setUser($viewer) 326 + ->setDisableWorkflowOnCancel(true) 327 + ->setDisableWorkflowOnSubmit(true) 328 + ->setSubmitURI($file->getDownloadURI()) 329 + ->appendParagraph( 330 + pht( 331 + 'Successfully generated a new keypair.')) 332 + ->appendParagraph( 333 + pht( 334 + 'The public key has been associated with your Phabricator '. 335 + 'account. Use the button below to download the private key.')) 336 + ->appendParagraph( 337 + pht( 338 + 'After you download the private key, it will be destroyed. '. 339 + 'You will not be able to retrieve it if you lose your copy.')) 340 + ->addSubmitButton(pht('Download Private Key')) 341 + ->addCancelButton($this->getPanelURI(), pht('Done')); 342 + 343 + return id(new AphrontDialogResponse()) 344 + ->setDialog($dialog); 345 + } 346 + 347 + $dialog = id(new AphrontDialogView()) 348 + ->setUser($viewer) 349 + ->addCancelButton($this->getPanelURI()); 350 + 351 + try { 352 + PhabricatorSSHKeyGenerator::assertCanGenerateKeypair(); 353 + $dialog 354 + ->addHiddenInput('generate', true) 355 + ->setTitle(pht('Generate New Keypair')) 356 + ->appendParagraph( 357 + pht( 358 + "This will generate an SSH keypair, associate the public key ". 359 + "with your account, and let you download the private key.")) 360 + ->appendParagraph( 361 + pht( 362 + "Phabricator will not retain a copy of the private key.")) 363 + ->addSubmitButton(pht('Generate Keypair')); 364 + } catch (Exception $ex) { 365 + $dialog 366 + ->setTitle(pht('Unable to Generate Keys')) 367 + ->appendParagraph($ex->getMessage()); 368 + } 266 369 267 370 return id(new AphrontDialogResponse()) 268 371 ->setDialog($dialog);
+32
src/infrastructure/util/PhabricatorSSHKeyGenerator.php
··· 1 + <?php 2 + 3 + final class PhabricatorSSHKeyGenerator extends Phobject { 4 + 5 + public static function assertCanGenerateKeypair() { 6 + $binary = 'ssh-keygen'; 7 + if (!Filesystem::resolveBinary($binary)) { 8 + throw new Exception( 9 + pht( 10 + 'Can not generate keys: unable to find "%s" in PATH!', 11 + $binary)); 12 + } 13 + } 14 + 15 + public static function generateKeypair() { 16 + self::assertCanGenerateKeypair(); 17 + 18 + $tempfile = new TempFile(); 19 + $keyfile = dirname($tempfile).DIRECTORY_SEPARATOR.'keytext'; 20 + 21 + execx( 22 + 'ssh-keygen -t rsa -N %s -f %s', 23 + '', 24 + $keyfile); 25 + 26 + $public_key = Filesystem::readFile($keyfile.'.pub'); 27 + $private_key = Filesystem::readFile($keyfile); 28 + 29 + return array($public_key, $private_key); 30 + } 31 + 32 + }
+36 -5
src/view/AphrontDialogView.php
··· 15 15 private $footers = array(); 16 16 private $isStandalone; 17 17 private $method = 'POST'; 18 + private $disableWorkflowOnSubmit; 19 + private $disableWorkflowOnCancel; 20 + private $width = 'default'; 18 21 22 + const WIDTH_DEFAULT = 'default'; 23 + const WIDTH_FORM = 'form'; 24 + const WIDTH_FULL = 'full'; 19 25 20 26 public function setMethod($method) { 21 27 $this->method = $method; ··· 30 36 public function getIsStandalone() { 31 37 return $this->isStandalone; 32 38 } 33 - 34 - private $width = 'default'; 35 - const WIDTH_DEFAULT = 'default'; 36 - const WIDTH_FORM = 'form'; 37 - const WIDTH_FULL = 'full'; 38 39 39 40 public function setSubmitURI($uri) { 40 41 $this->submitURI = $uri; ··· 121 122 $paragraph)); 122 123 } 123 124 125 + public function setDisableWorkflowOnSubmit($disable_workflow_on_submit) { 126 + $this->disableWorkflowOnSubmit = $disable_workflow_on_submit; 127 + return $this; 128 + } 129 + 130 + public function getDisableWorkflowOnSubmit() { 131 + return $this->disableWorkflowOnSubmit; 132 + } 133 + 134 + public function setDisableWorkflowOnCancel($disable_workflow_on_cancel) { 135 + $this->disableWorkflowOnCancel = $disable_workflow_on_cancel; 136 + return $this; 137 + } 138 + 139 + public function getDisableWorkflowOnCancel() { 140 + return $this->disableWorkflowOnCancel; 141 + } 142 + 124 143 final public function render() { 125 144 require_celerity_resource('aphront-dialog-view-css'); 126 145 127 146 $buttons = array(); 128 147 if ($this->submitButton) { 148 + $meta = array(); 149 + if ($this->disableWorkflowOnSubmit) { 150 + $meta['disableWorkflow'] = true; 151 + } 152 + 129 153 $buttons[] = javelin_tag( 130 154 'button', 131 155 array( 132 156 'name' => '__submit__', 133 157 'sigil' => '__default__', 134 158 'type' => 'submit', 159 + 'meta' => $meta, 135 160 ), 136 161 $this->submitButton); 137 162 } 138 163 139 164 if ($this->cancelURI) { 165 + $meta = array(); 166 + if ($this->disableWorkflowOnCancel) { 167 + $meta['disableWorkflow'] = true; 168 + } 169 + 140 170 $buttons[] = javelin_tag( 141 171 'a', 142 172 array( ··· 144 174 'class' => 'button grey', 145 175 'name' => '__cancel__', 146 176 'sigil' => 'jx-workflow-button', 177 + 'meta' => $meta, 147 178 ), 148 179 $this->cancelText); 149 180 }
+9 -2
webroot/rsrc/externals/javelin/lib/Workflow.js
··· 88 88 return; 89 89 } 90 90 91 - event.prevent(); 92 - 93 91 // Get the button (which is sometimes actually another tag, like an <a />) 94 92 // which triggered the event. In particular, this makes sure we get the 95 93 // right node if there is a <button> with an <img /> inside it or 96 94 // or something similar. 97 95 var t = event.getNode('jx-workflow-button') || 98 96 event.getNode('tag:button'); 97 + 98 + // If this button disables workflow (normally, because it is a file 99 + // download button) let the event through without modification. 100 + if (JX.Stratcom.getData(t).disableWorkflow) { 101 + return; 102 + } 103 + 104 + event.prevent(); 105 + 99 106 if (t.name == '__cancel__' || t.name == '__close__') { 100 107 JX.Workflow._pop(); 101 108 } else {