diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'af983028', + 'core.pkg.css' => '5a4a5010', 'core.pkg.js' => '73a06a9f', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '0b037a4f', @@ -24,7 +24,7 @@ 'rsrc/audio/basic/ting.mp3' => 'a6b6540e', 'rsrc/css/aphront/aphront-bars.css' => '4a327b4a', 'rsrc/css/aphront/dark-console.css' => '7f06cda2', - 'rsrc/css/aphront/dialog-view.css' => 'b70c70df', + 'rsrc/css/aphront/dialog-view.css' => '874f5c06', 'rsrc/css/aphront/list-filter-view.css' => 'feb64255', 'rsrc/css/aphront/multi-column.css' => 'fbc00ba3', 'rsrc/css/aphront/notification.css' => '30240bd2', @@ -530,7 +530,7 @@ 'almanac-css' => '2e050f4f', 'aphront-bars' => '4a327b4a', 'aphront-dark-console-css' => '7f06cda2', - 'aphront-dialog-view-css' => 'b70c70df', + 'aphront-dialog-view-css' => '874f5c06', 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php --- a/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditDeleteController.php @@ -17,32 +17,31 @@ ->setRepository($repository) ->getPanelURI(); - $dialog = new AphrontDialogView(); - $text_1 = pht( - 'If you really want to delete the repository, run this command from '. - 'the command line:'); - $command = csprintf( - 'phabricator/ $ ./bin/remove destroy %R', - $repository->getMonogram()); - $text_2 = pht( - 'Repositories touch many objects and as such deletes are '. - 'prohibitively expensive to run from the web UI.'); - $body = phutil_tag( - 'div', - array( - 'class' => 'phabricator-remarkup', - ), - array( - phutil_tag('p', array(), $text_1), - phutil_tag('p', array(), - phutil_tag('tt', array(), $command)), - phutil_tag('p', array(), $text_2), - )); + $doc_uri = PhabricatorEnv::getDoclink( + 'Permanently Destroying Data'); return $this->newDialog() - ->setTitle(pht('Really want to delete the repository?')) - ->appendChild($body) - ->addCancelButton($panel_uri, pht('Okay')); + ->setTitle(pht('Delete Repository')) + ->appendParagraph( + pht( + 'To permanently destroy this repository, run this command from '. + 'the command line:')) + ->appendCommand( + csprintf( + 'phabricator/ $ ./bin/remove destroy %R', + $repository->getMonogram())) + ->appendParagraph( + pht( + 'Repositories can not be permanently destroyed from the web '. + 'interface. See %s in the documentation for more information.', + phutil_tag( + 'a', + array( + 'href' => $doc_uri, + 'target' => '_blank', + ), + pht('Permanently Destroying Data')))) + ->addCancelButton($panel_uri, pht('Close')); } } diff --git a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php --- a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php @@ -155,8 +155,6 @@ ->setName(pht('Delete Repository')) ->setHref($delete_uri) ->setIcon('fa-times') - ->setColor(PhabricatorActionView::RED) - ->setDisabled(true) ->setWorkflow(true)); return $this->newCurtainView() diff --git a/src/applications/people/controller/PhabricatorPeopleDeleteController.php b/src/applications/people/controller/PhabricatorPeopleDeleteController.php --- a/src/applications/people/controller/PhabricatorPeopleDeleteController.php +++ b/src/applications/people/controller/PhabricatorPeopleDeleteController.php @@ -17,58 +17,35 @@ $manage_uri = $this->getApplicationURI("manage/{$id}/"); - if ($user->getPHID() == $viewer->getPHID()) { - return $this->buildDeleteSelfResponse($manage_uri); - } - - $str1 = pht( - 'Be careful when deleting users! This will permanently and '. - 'irreversibly destroy this user account.'); - - $str2 = pht( - 'If this user interacted with anything, it is generally better to '. - 'disable them, not delete them. If you delete them, it will no longer '. - 'be possible to (for example) search for objects they created, and you '. - 'will lose other information about their history. Disabling them '. - 'instead will prevent them from logging in, but will not destroy any of '. - 'their data.'); - - $str3 = pht( - 'It is generally safe to delete newly created users (and test users and '. - 'so on), but less safe to delete established users. If possible, '. - 'disable them instead.'); - - $str4 = pht('To permanently destroy this user, run this command:'); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendRemarkupInstructions( - csprintf( - " phabricator/ $ ./bin/remove destroy %R\n", - '@'.$user->getUsername())); - - return $this->newDialog() - ->setWidth(AphrontDialogView::WIDTH_FORM) - ->setTitle(pht('Permanently Delete User')) - ->setShortTitle(pht('Delete User')) - ->appendParagraph($str1) - ->appendParagraph($str2) - ->appendParagraph($str3) - ->appendParagraph($str4) - ->appendChild($form->buildLayoutView()) - ->addCancelButton($manage_uri, pht('Close')); - } + $doc_uri = PhabricatorEnv::getDoclink( + 'Permanently Destroying Data'); - private function buildDeleteSelfResponse($cancel_uri) { return $this->newDialog() - ->setTitle(pht('You Shall Journey No Farther')) + ->setTitle(pht('Delete User')) + ->appendParagraph( + pht( + 'To permanently destroy this user, run this command from the '. + 'command line:')) + ->appendCommand( + csprintf( + 'phabricator/ $ ./bin/remove destroy %R', + $user->getMonogram())) ->appendParagraph( pht( - 'As you stare into the gaping maw of the abyss, something '. - 'holds you back.')) - ->appendParagraph(pht('You can not delete your own account.')) - ->addCancelButton($cancel_uri, pht('Turn Back')); + 'Unless you have a very good reason to delete this user, consider '. + 'disabling them instead.')) + ->appendParagraph( + pht( + 'Users can not be permanently destroyed from the web interface. '. + 'See %s in the documentation for more information.', + phutil_tag( + 'a', + array( + 'href' => $doc_uri, + 'target' => '_blank', + ), + pht('Permanently Destroying Data')))) + ->addCancelButton($manage_uri, pht('Close')); } - } diff --git a/src/docs/user/field/permanently_destroying_data.diviner b/src/docs/user/field/permanently_destroying_data.diviner new file mode 100644 --- /dev/null +++ b/src/docs/user/field/permanently_destroying_data.diviner @@ -0,0 +1,92 @@ +@title Permanently Destroying Data +@group fieldmanual + +How to permanently destroy data and manage leaked secrets. + +Overview +======== + +Phabricator intentionally makes it difficult to permanently destroy data, but +provides a command-line tool for destroying objects if you're certain that +you want to destroy something. + +**Disable vs Destroy**: Most kinds of objects can be disabled, deactivated, +closed, or archived. These operations place them in inactive states and +preserve their transaction history. + +(NOTE) Disabling (rather than destroying) objects is strongly recommended +unless you have a very good reason to want to permanently destroy an object. + + +Destroying Data +=============== + +To permanently destroy an object, run this command from the command line: + +``` +phabricator/ $ ./bin/remove destroy +``` + +The `` may be an object monogram or PHID. For instance, you can use +`@alice` to destroy a particular user, or `T123` to destroy a particular task. + +(IMPORTANT) This operation is permanent and can not be undone. + + +CLI Access Required +=================== + +In almost all cases, Phabricator requires operational access from the CLI to +permanently destroy data. One major reason for this requirement is that it +limits the reach of an attacker who compromises a privileged account. + +The web UI is generally append-only and actions generally leave an audit +trail, usually in the transaction log. Thus, an attacker who compromises an +account but only gains access to the web UI usually can not do much permanent +damage and usually can not hide their actions or cover their tracks. + +Another reason that destroying data is hard is simply that it's permanent and +can not be undone, so there's no way to recover from mistakes. + + +Leaked Secrets +============== + +Sometimes you may want to destroy an object because it has leaked a secret, +like an API key or another credential. For example, an engineer might +accidentally send a change for review which includes a sensitive private key. + +No Phabricator command can rewind time, and once data is written to Phabricator +the cat is often out of the bag: it has often been transmitted to external +systems which Phabricator can not interact with via email, webhooks, API calls, +repository mirroring, CDN caching, and so on. You can try to clean up the mess, +but you're generally already too late. + +The `bin/remove destroy` command will make a reasonable attempt to completely +destroy objects, but this is just an attempt. It can not unsend email or uncall +the API, and no command can rewind time and undo a leak. + +**Revoking Credentials**: If Phabricator credentials were accidentally +disclosed, you can revoke them so they no longer function. See +@{article:Revoking Credentials} for more information. + + +Preventing Leaks +================ + +Because time can not be rewound, it is best to prevent sensitive data from +leaking in the first place. Phabricator supports some technical measures that +can make it more difficult to accidentally disclose secrets: + +**Differential Diff Herald Rules**: You can write "Differential Diff" rules +in Herald that reject diffs before they are written to disk by using the +"Block diff with message" action. + +These rules can reject diffs based on affected file names or file content. +This is a coarse tool, but rejecting diffs which contain strings like +`BEGIN RSA PRIVATE KEY` may make it more difficult to accidentally disclose +certain secrets. + +**Commit Content Herald Rules**: For hosted repositories, you can write +"Commit Hook: Commit Content" rules in Herald which reject pushes that contain +commit which match certain rules (like file name or file content rules). diff --git a/src/docs/user/userguide/diffusion_managing.diviner b/src/docs/user/userguide/diffusion_managing.diviner --- a/src/docs/user/userguide/diffusion_managing.diviner +++ b/src/docs/user/userguide/diffusion_managing.diviner @@ -169,8 +169,8 @@ Basics: Delete Repository ========================= -Repositories can not be deleted from the web UI, so this option is always -disabled. Clicking it gives you information about how to delete a repository. +Repositories can not be deleted from the web UI, so this option only gives you +information about how to delete a repository. Repositories can only be deleted from the command line, with `bin/remove`: @@ -178,9 +178,8 @@ $ ./bin/remove destroy ``` -WARNING: This command will issue you a dire warning about the severity of the -action you are taking. Heed this warning. You are **strongly discouraged** from -destroying repositories. Instead, deactivate them. +This command will permanently destroy the repository. For more information +about destroying things, see @{article:Permanently Destroying Data}. Policies diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -161,15 +161,36 @@ } public function appendParagraph($paragraph) { - return $this->appendChild( - phutil_tag( - 'p', - array( - 'class' => 'aphront-dialog-view-paragraph', - ), - $paragraph)); + return $this->appendParagraphTag($paragraph); } + public function appendCommand($command) { + $command_tag = phutil_tag('tt', array(), $command); + return $this->appendParagraphTag( + $command_tag, + 'aphront-dialog-view-command'); + } + + private function appendParagraphTag($content, $classes = null) { + if ($classes) { + $classes = (array)$classes; + } else { + $classes = array(); + } + + array_unshift($classes, 'aphront-dialog-view-paragraph'); + + $paragraph_tag = phutil_tag( + 'p', + array( + 'class' => implode(' ', $classes), + ), + $content); + + return $this->appendChild($paragraph_tag); + } + + public function appendList(array $items) { $listitems = array(); foreach ($items as $item) { diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -158,6 +158,11 @@ margin-top: 16px; } +.aphront-dialog-view-command { + padding: 8px 16px; + background: {$greybackground}; +} + .device-desktop .aphront-dialog-flush .phui-oi-list-view { margin: 0; padding: 0;