Reject dangerous changes in Git repositories by default

Summary: Ref T4189. This adds a per-repository "dangerous changes" flag, which defaults to off. This flag must be enabled to do non-appending branch mutation (delete branches / rewrite history).

Test Plan:
With flag on and off, performed various safe and dangerous pushes.

  >>> orbital ~/repos/POEMS $ git push origin :blarp
  remote: +---------------------------------------------------------------+
  remote: |      * * * PUSH REJECTED BY EVIL DRAGON BUREAUCRATS * * *     |
  remote: +---------------------------------------------------------------+
  remote:             \
  remote:              \                    ^    /^
  remote:               \                  / \  // \
  remote:                \   |\___/|      /   \//  .\
  remote:                 \  /V  V  \__  /    //  | \ \           *----*
  remote:                   /     /  \/_/    //   |  \  \          \   |
  remote:                   @___@`    \/_   //    |   \   \         \/\ \
  remote:                  0/0/|       \/_ //     |    \    \         \  \
  remote:              0/0/0/0/|        \///      |     \     \       |  |
  remote:           0/0/0/0/0/_|_ /   (  //       |      \     _\     |  /
  remote:        0/0/0/0/0/0/`/,_ _ _/  ) ; -.    |    _ _\.-~       /   /
  remote:                    ,-}        _      *-.|.-~-.           .~    ~
  remote:   \     \__/        `/\      /                 ~-. _ .-~      /
  remote:    \____(Oo)           *.   }            {                   /
  remote:    (    (--)          .----~-.\        \-`                 .~
  remote:    //__\\  \ DENIED!  ///.----..<        \             _ -~
  remote:   //    \\               ///-._ _ _ _ _ _ _{^ - - - - ~
  remote:
  remote:
  remote: DANGEROUS CHANGE: The change you're attempting to push deletes the branch 'blarp'.
  remote: Dangerous change protection is enabled for this repository.
  remote: Edit the repository configuration before making dangerous changes.
  remote:
  To ssh://dweller@localhost/diffusion/POEMS/
   ! [remote rejected] blarp (pre-receive hook declined)
  error: failed to push some refs to 'ssh://dweller@localhost/diffusion/POEMS/'

Reviewers: btrahan

Reviewed By: btrahan

CC: aran, chad, richardvanvelzen

Maniphest Tasks: T4189

Differential Revision: https://secure.phabricator.com/D7689
This commit is contained in:
epriestley
2013-12-03 10:28:39 -08:00
parent 632e1ceda6
commit d2e9aee16d
10 changed files with 252 additions and 2 deletions

View File

@@ -70,6 +70,7 @@ final class PhabricatorApplicationDiffusion extends PhabricatorApplication {
'basic/' => 'DiffusionRepositoryEditBasicController',
'encoding/' => 'DiffusionRepositoryEditEncodingController',
'activate/' => 'DiffusionRepositoryEditActivateController',
'dangerous/' => 'DiffusionRepositoryEditDangerousController',
'policy/' => 'DiffusionRepositoryEditPolicyController',
'branches/' => 'DiffusionRepositoryEditBranchesController',
'subversion/' => 'DiffusionRepositoryEditSubversionController',

View File

@@ -0,0 +1,78 @@
<?php
final class DiffusionRepositoryEditDangerousController
extends DiffusionRepositoryEditController {
public function processRequest() {
$request = $this->getRequest();
$viewer = $request->getUser();
$drequest = $this->diffusionRequest;
$repository = $drequest->getRepository();
$repository = id(new PhabricatorRepositoryQuery())
->setViewer($viewer)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
))
->withIDs(array($repository->getID()))
->executeOne();
if (!$repository) {
return new Aphront404Response();
}
if (!$repository->canAllowDangerousChanges()) {
return new Aphront400Response();
}
$edit_uri = $this->getRepositoryControllerURI($repository, 'edit/');
if ($request->isFormPost()) {
$xaction = id(new PhabricatorRepositoryTransaction())
->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DANGEROUS)
->setNewValue(!$repository->shouldAllowDangerousChanges());
$editor = id(new PhabricatorRepositoryEditor())
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request)
->setActor($viewer)
->applyTransactions($repository, array($xaction));
return id(new AphrontReloadResponse())->setURI($edit_uri);
}
$dialog = id(new AphrontDialogView())
->setUser($viewer);
$force = phutil_tag('tt', array(), '--force');
if ($repository->shouldAllowDangerousChanges()) {
$dialog
->setTitle(pht('Prevent Dangerous changes?'))
->appendChild(
pht(
'It will no longer be possible to delete branches from this '.
'repository, or %s push to this repository.',
$force))
->addSubmitButton(pht('Prevent Dangerous Changes'))
->addCancelButton($edit_uri);
} else {
$dialog
->setTitle(pht('Allow Dangerous Changes?'))
->appendChild(
pht(
'If you allow dangerous changes, it will be possible to delete '.
'branches and %s push this repository. These operations can '.
'alter a repository in a way that is difficult to recover from.',
$force))
->addSubmitButton(pht('Allow Dangerous Changes'))
->addCancelButton($edit_uri);
}
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
}

View File

@@ -577,6 +577,25 @@ final class DiffusionRepositoryEditMainController
$this->getRepositoryControllerURI($repository, 'edit/hosting/'));
$view->addAction($edit);
if ($repository->canAllowDangerousChanges()) {
if ($repository->shouldAllowDangerousChanges()) {
$changes = id(new PhabricatorActionView())
->setIcon('blame')
->setName(pht('Prevent Dangerous Changes'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/dangerous/'))
->setWorkflow(true);
} else {
$changes = id(new PhabricatorActionView())
->setIcon('warning')
->setName(pht('Allow Dangerous Changes'))
->setHref(
$this->getRepositoryControllerURI($repository, 'edit/dangerous/'))
->setWorkflow(true);
}
$view->addAction($changes);
}
return $view;
}
@@ -611,6 +630,18 @@ final class DiffusionRepositoryEditMainController
PhabricatorRepository::getProtocolAvailabilityName(
$repository->getServeOverSSH())));
if ($repository->canAllowDangerousChanges()) {
if ($repository->shouldAllowDangerousChanges()) {
$description = pht('Allowed');
} else {
$description = pht('Not Allowed');
}
$view->addProperty(
pht('Dangerous Changes'),
$description);
}
return $view;
}

View File

@@ -67,6 +67,8 @@ final class DiffusionCommitHookEngine extends Phobject {
private function executeGitHook() {
$updates = $this->parseGitUpdates($this->getStdin());
$this->rejectGitDangerousChanges($updates);
// TODO: Do cheap checks: non-ff commits, mutating refs without access,
// creating or deleting things you can't touch. We can do all non-content
// checks here.
@@ -101,8 +103,10 @@ final class DiffusionCommitHookEngine extends Phobject {
if (preg_match('(^refs/heads/)', $update['ref'])) {
$update['type'] = 'branch';
$update['ref.short'] = substr($update['ref'], strlen('refs/heads/'));
} else if (preg_match('(^refs/tags/)', $update['ref'])) {
$update['type'] = 'tag';
$update['ref.short'] = substr($update['ref'], strlen('refs/tags/'));
} else {
$update['type'] = 'unknown';
}
@@ -159,7 +163,7 @@ final class DiffusionCommitHookEngine extends Phobject {
private function findGitNewCommits(array $updates) {
$futures = array();
foreach ($updates as $key => $update) {
if ($update['type'] == 'delete') {
if ($update['operation'] == 'delete') {
// Deleting a branch or tag can never create any new commits.
continue;
}
@@ -183,6 +187,61 @@ final class DiffusionCommitHookEngine extends Phobject {
return $updates;
}
private function rejectGitDangerousChanges(array $updates) {
$repository = $this->getRepository();
if ($repository->shouldAllowDangerousChanges()) {
return;
}
foreach ($updates as $update) {
if ($update['type'] != 'branch') {
// For now, we don't consider deleting or moving tags to be a
// "dangerous" update. It's way harder to get wrong and should be easy
// to recover from once we have better logging.
continue;
}
if ($update['operation'] == 'create') {
// Creating a branch is never dangerous.
continue;
}
if ($update['operation'] == 'change') {
if ($update['old'] == $update['merge-base']) {
// This is a fast-forward update to an existing branch.
// These are safe.
continue;
}
}
// We either have a branch deletion or a non fast-forward branch update.
// Format a message and reject the push.
if ($update['operation'] == 'delete') {
$message = pht(
"DANGEROUS CHANGE: The change you're attempting to push deletes ".
"the branch '%s'.",
$update['ref.short']);
} else {
$message = pht(
"DANGEROUS CHANGE: The change you're attempting to push updates ".
"the branch '%s' from '%s' to '%s', but this is not a fast-forward. ".
"Pushes which rewrite published branch history are dangerous.",
$update['ref.short'],
$update['old.short'],
$update['new.short']);
}
$boilerplate = pht(
"Dangerous change protection is enabled for this repository.\n".
"Edit the repository configuration before making dangerous changes.");
$message = $message."\n".$boilerplate;
throw new DiffusionCommitHookRejectException($message);
}
}
private function executeSubversionHook() {
// TODO: Do useful things here, too.

View File

@@ -0,0 +1,5 @@
<?php
final class DiffusionCommitHookRejectException extends Exception {
}