@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.

Allow Herald "diff" rules to reject content before it is written

Summary: Fixes T5915. Occasionally, users derp up and diff private key material. Adding a pre-write Herald phase enables configuration of a partial layer of protection that will reject these changes before they hit disk, provided they can be detected by, e.g., filename.

Test Plan:
- Added a rule with checks on every field, verified they looked fine in the transcript.
- Created some revisions to test those changes (I have a bunch of revision rules locally).
- Verified rejects don't write transcripts to the database.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5915

Differential Revision: https://secure.phabricator.com/D10305

+364 -81
+9 -1
src/__phutil_library_map__.php
··· 247 247 'DifferentialDependsOnField' => 'applications/differential/customfield/DifferentialDependsOnField.php', 248 248 'DifferentialDiff' => 'applications/differential/storage/DifferentialDiff.php', 249 249 'DifferentialDiffCreateController' => 'applications/differential/controller/DifferentialDiffCreateController.php', 250 + 'DifferentialDiffCreationRejectException' => 'applications/differential/exception/DifferentialDiffCreationRejectException.php', 251 + 'DifferentialDiffEditor' => 'applications/differential/editor/DifferentialDiffEditor.php', 250 252 'DifferentialDiffPHIDType' => 'applications/differential/phid/DifferentialDiffPHIDType.php', 251 253 'DifferentialDiffProperty' => 'applications/differential/storage/DifferentialDiffProperty.php', 252 254 'DifferentialDiffQuery' => 'applications/differential/query/DifferentialDiffQuery.php', ··· 748 750 'HeraldController' => 'applications/herald/controller/HeraldController.php', 749 751 'HeraldCustomAction' => 'applications/herald/extension/HeraldCustomAction.php', 750 752 'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php', 753 + 'HeraldDifferentialAdapter' => 'applications/herald/adapter/HeraldDifferentialAdapter.php', 754 + 'HeraldDifferentialDiffAdapter' => 'applications/herald/adapter/HeraldDifferentialDiffAdapter.php', 751 755 'HeraldDifferentialRevisionAdapter' => 'applications/herald/adapter/HeraldDifferentialRevisionAdapter.php', 752 756 'HeraldDisableController' => 'applications/herald/controller/HeraldDisableController.php', 753 757 'HeraldEditLogQuery' => 'applications/herald/query/HeraldEditLogQuery.php', ··· 2998 3002 'PhabricatorDestructibleInterface', 2999 3003 ), 3000 3004 'DifferentialDiffCreateController' => 'DifferentialController', 3005 + 'DifferentialDiffCreationRejectException' => 'Exception', 3006 + 'DifferentialDiffEditor' => 'PhabricatorEditor', 3001 3007 'DifferentialDiffPHIDType' => 'PhabricatorPHIDType', 3002 3008 'DifferentialDiffProperty' => 'DifferentialDAO', 3003 3009 'DifferentialDiffQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', ··· 3529 3535 'HeraldCondition' => 'HeraldDAO', 3530 3536 'HeraldController' => 'PhabricatorController', 3531 3537 'HeraldDAO' => 'PhabricatorLiskDAO', 3532 - 'HeraldDifferentialRevisionAdapter' => 'HeraldAdapter', 3538 + 'HeraldDifferentialAdapter' => 'HeraldAdapter', 3539 + 'HeraldDifferentialDiffAdapter' => 'HeraldDifferentialAdapter', 3540 + 'HeraldDifferentialRevisionAdapter' => 'HeraldDifferentialAdapter', 3533 3541 'HeraldDisableController' => 'HeraldController', 3534 3542 'HeraldEditLogQuery' => 'PhabricatorOffsetPagedQuery', 3535 3543 'HeraldInvalidActionException' => 'Exception',
+5 -1
src/applications/differential/conduit/DifferentialCreateDiffConduitAPIMethod.php
··· 162 162 break; 163 163 } 164 164 165 - $diff->save(); 165 + id(new DifferentialDiffEditor()) 166 + ->setActor($viewer) 167 + ->setContentSource( 168 + PhabricatorContentSource::newFromConduitRequest($request)) 169 + ->saveDiff($diff); 166 170 167 171 // If we didn't get an explicit `repositoryPHID` (which means the client is 168 172 // old, or couldn't figure out which repository the working copy belongs
+5 -1
src/applications/differential/conduit/DifferentialCreateRawDiffConduitAPIMethod.php
··· 59 59 $diff->setRepositoryPHID($repository->getPHID()); 60 60 } 61 61 62 - $diff->save(); 62 + id(new DifferentialDiffEditor()) 63 + ->setActor($viewer) 64 + ->setContentSource( 65 + PhabricatorContentSource::newFromConduitRequest($request)) 66 + ->saveDiff($diff); 63 67 64 68 return $this->buildDiffInfoDictionary($diff); 65 69 }
+80
src/applications/differential/editor/DifferentialDiffEditor.php
··· 1 + <?php 2 + 3 + final class DifferentialDiffEditor extends PhabricatorEditor { 4 + 5 + private $contentSource; 6 + 7 + public function setContentSource($content_source) { 8 + $this->contentSource = $content_source; 9 + return $this; 10 + } 11 + 12 + public function getContentSource() { 13 + return $this->contentSource; 14 + } 15 + 16 + public function saveDiff(DifferentialDiff $diff) { 17 + $actor = $this->requireActor(); 18 + 19 + // Generate a PHID first, so the transcript will point at the object if 20 + // we deicde to preserve it. 21 + $phid = $diff->generatePHID(); 22 + $diff->setPHID($phid); 23 + 24 + $adapter = id(new HeraldDifferentialDiffAdapter()) 25 + ->setDiff($diff); 26 + 27 + $adapter->setContentSource($this->getContentSource()); 28 + $adapter->setIsNewObject(true); 29 + 30 + $engine = new HeraldEngine(); 31 + 32 + $rules = $engine->loadRulesForAdapter($adapter); 33 + $rules = mpull($rules, null, 'getID'); 34 + 35 + $effects = $engine->applyRules($rules, $adapter); 36 + 37 + $blocking_effect = null; 38 + foreach ($effects as $effect) { 39 + if ($effect->getAction() == HeraldAdapter::ACTION_BLOCK) { 40 + $blocking_effect = $effect; 41 + break; 42 + } 43 + } 44 + 45 + if ($blocking_effect) { 46 + $rule = idx($rules, $effect->getRuleID()); 47 + if ($rule && strlen($rule->getName())) { 48 + $rule_name = $rule->getName(); 49 + } else { 50 + $rule_name = pht('Unnamed Herald Rule'); 51 + } 52 + 53 + $message = $effect->getTarget(); 54 + if (!strlen($message)) { 55 + $message = pht('(None.)'); 56 + } 57 + 58 + throw new DifferentialDiffCreationRejectException( 59 + pht( 60 + "Creation of this diff was rejected by Herald rule %s.\n". 61 + " Rule: %s\n". 62 + "Reason: %s", 63 + 'H'.$effect->getRuleID(), 64 + $rule_name, 65 + $message)); 66 + } 67 + 68 + $diff->save(); 69 + 70 + // NOTE: We only save the transcript if we didn't block the diff. 71 + // Otherwise, we might save some of the diff's content in the transcript 72 + // table, which would defeat the purpose of allowing rules to block 73 + // storage of key material. 74 + 75 + $engine->applyEffects($effects, $adapter, $rules); 76 + $xscript = $engine->getTranscript(); 77 + 78 + } 79 + 80 + }
+5
src/applications/differential/exception/DifferentialDiffCreationRejectException.php
··· 1 + <?php 2 + 3 + final class DifferentialDiffCreationRejectException extends Exception { 4 + 5 + }
+93
src/applications/herald/adapter/HeraldDifferentialAdapter.php
··· 1 + <?php 2 + 3 + abstract class HeraldDifferentialAdapter extends HeraldAdapter { 4 + 5 + private $repository = false; 6 + private $diff; 7 + 8 + abstract protected function loadChangesets(); 9 + abstract protected function loadChangesetsWithHunks(); 10 + 11 + public function getDiff() { 12 + return $this->diff; 13 + } 14 + 15 + public function setDiff(DifferentialDiff $diff) { 16 + $this->diff = $diff; 17 + return $this; 18 + } 19 + 20 + public function loadRepository() { 21 + if ($this->repository === false) { 22 + $repository_phid = $this->getObject()->getRepositoryPHID(); 23 + if ($repository_phid) { 24 + $repository = id(new PhabricatorRepositoryQuery()) 25 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 26 + ->withPHIDs(array($repository_phid)) 27 + ->needProjectPHIDs(true) 28 + ->executeOne(); 29 + } 30 + $this->repository = false; 31 + } 32 + 33 + return $this->repository; 34 + } 35 + 36 + 37 + protected function loadAffectedPaths() { 38 + $changesets = $this->loadChangesets(); 39 + 40 + $paths = array(); 41 + foreach ($changesets as $changeset) { 42 + $paths[] = $this->getAbsoluteRepositoryPathForChangeset($changeset); 43 + } 44 + 45 + return $paths; 46 + } 47 + 48 + protected function getAbsoluteRepositoryPathForChangeset( 49 + DifferentialChangeset $changeset) { 50 + 51 + $repository = $this->loadRepository(); 52 + if (!$repository) { 53 + return '/'.ltrim($changeset->getFilename(), '/'); 54 + } 55 + 56 + $diff = $this->getDiff(); 57 + 58 + return $changeset->getAbsoluteRepositoryPath($repository, $diff); 59 + } 60 + 61 + protected function loadContentDictionary() { 62 + $add_lines = DifferentialHunk::FLAG_LINES_ADDED; 63 + $rem_lines = DifferentialHunk::FLAG_LINES_REMOVED; 64 + $mask = ($add_lines | $rem_lines); 65 + return $this->loadContentWithMask($mask); 66 + } 67 + 68 + protected function loadAddedContentDictionary() { 69 + return $this->loadContentWithMask(DifferentialHunk::FLAG_LINES_ADDED); 70 + } 71 + 72 + protected function loadRemovedContentDictionary() { 73 + return $this->loadContentWithMask(DifferentialHunk::FLAG_LINES_REMOVED); 74 + } 75 + 76 + protected function loadContentWithMask($mask) { 77 + $changesets = $this->loadChangesetsWithHunks(); 78 + 79 + $dict = array(); 80 + foreach ($changesets as $changeset) { 81 + $content = array(); 82 + foreach ($changeset->getHunks() as $hunk) { 83 + $content[] = $hunk->getContentWithMask($mask); 84 + } 85 + 86 + $path = $this->getAbsoluteRepositoryPathForChangeset($changeset); 87 + $dict[$path] = implode("\n", $content); 88 + } 89 + 90 + return $dict; 91 + } 92 + 93 + }
+164
src/applications/herald/adapter/HeraldDifferentialDiffAdapter.php
··· 1 + <?php 2 + 3 + final class HeraldDifferentialDiffAdapter extends HeraldDifferentialAdapter { 4 + 5 + public function getAdapterApplicationClass() { 6 + return 'PhabricatorDifferentialApplication'; 7 + } 8 + 9 + protected function loadChangesets() { 10 + return $this->loadChangesetsWithHunks(); 11 + } 12 + 13 + protected function loadChangesetsWithHunks() { 14 + return $this->getDiff()->getChangesets(); 15 + } 16 + 17 + public function getObject() { 18 + return $this->getDiff(); 19 + } 20 + 21 + public function getAdapterContentType() { 22 + return 'differential.diff'; 23 + } 24 + 25 + public function getAdapterContentName() { 26 + return pht('Differential Diffs'); 27 + } 28 + 29 + public function getAdapterContentDescription() { 30 + return pht( 31 + "React to new diffs being uploaded, before writes occur.\n". 32 + "These rules can reject diffs before they are written to permanent ". 33 + "storage, to prevent users from accidentally uploading private keys or ". 34 + "other sensitive information."); 35 + } 36 + 37 + public function supportsRuleType($rule_type) { 38 + switch ($rule_type) { 39 + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: 40 + return true; 41 + case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: 42 + case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: 43 + default: 44 + return false; 45 + } 46 + } 47 + 48 + public function getFields() { 49 + return array_merge( 50 + array( 51 + self::FIELD_AUTHOR, 52 + self::FIELD_AUTHOR_PROJECTS, 53 + self::FIELD_REPOSITORY, 54 + self::FIELD_REPOSITORY_PROJECTS, 55 + self::FIELD_DIFF_FILE, 56 + self::FIELD_DIFF_CONTENT, 57 + self::FIELD_DIFF_ADDED_CONTENT, 58 + self::FIELD_DIFF_REMOVED_CONTENT, 59 + ), 60 + parent::getFields()); 61 + } 62 + 63 + public function getRepetitionOptions() { 64 + return array( 65 + HeraldRepetitionPolicyConfig::FIRST, 66 + ); 67 + } 68 + 69 + public function getPHID() { 70 + return $this->getObject()->getPHID(); 71 + } 72 + 73 + public function getHeraldName() { 74 + return pht('New Diff'); 75 + } 76 + 77 + public function getActionNameMap($rule_type) { 78 + return array( 79 + self::ACTION_BLOCK => pht('Block diff with message'), 80 + ); 81 + } 82 + 83 + public function getHeraldField($field) { 84 + switch ($field) { 85 + case self::FIELD_AUTHOR: 86 + return $this->getObject()->getAuthorPHID(); 87 + break; 88 + case self::FIELD_AUTHOR_PROJECTS: 89 + $author_phid = $this->getHeraldField(self::FIELD_AUTHOR); 90 + if (!$author_phid) { 91 + return array(); 92 + } 93 + 94 + $projects = id(new PhabricatorProjectQuery()) 95 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 96 + ->withMemberPHIDs(array($author_phid)) 97 + ->execute(); 98 + 99 + return mpull($projects, 'getPHID'); 100 + case self::FIELD_DIFF_FILE: 101 + return $this->loadAffectedPaths(); 102 + case self::FIELD_REPOSITORY: 103 + $repository = $this->loadRepository(); 104 + if (!$repository) { 105 + return null; 106 + } 107 + return $repository->getPHID(); 108 + case self::FIELD_REPOSITORY_PROJECTS: 109 + $repository = $this->loadRepository(); 110 + if (!$repository) { 111 + return array(); 112 + } 113 + return $repository->getProjectPHIDs(); 114 + case self::FIELD_DIFF_CONTENT: 115 + return $this->loadContentDictionary(); 116 + case self::FIELD_DIFF_ADDED_CONTENT: 117 + return $this->loadAddedContentDictionary(); 118 + case self::FIELD_DIFF_REMOVED_CONTENT: 119 + return $this->loadRemovedContentDictionary(); 120 + } 121 + 122 + return parent::getHeraldField($field); 123 + } 124 + 125 + public function getActions($rule_type) { 126 + switch ($rule_type) { 127 + case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: 128 + return array_merge( 129 + array( 130 + self::ACTION_BLOCK, 131 + self::ACTION_NOTHING, 132 + ), 133 + parent::getActions($rule_type)); 134 + } 135 + } 136 + 137 + public function applyHeraldEffects(array $effects) { 138 + assert_instances_of($effects, 'HeraldEffect'); 139 + 140 + $result = array(); 141 + foreach ($effects as $effect) { 142 + $action = $effect->getAction(); 143 + switch ($action) { 144 + case self::ACTION_NOTHING: 145 + $result[] = new HeraldApplyTranscript( 146 + $effect, 147 + true, 148 + pht('Did nothing.')); 149 + break; 150 + case self::ACTION_BLOCK: 151 + $result[] = new HeraldApplyTranscript( 152 + $effect, 153 + true, 154 + pht('Blocked diff.')); 155 + break; 156 + default: 157 + throw new Exception(pht('No rules to handle action "%s"!', $action)); 158 + } 159 + } 160 + 161 + return $result; 162 + } 163 + 164 + }
+3 -78
src/applications/herald/adapter/HeraldDifferentialRevisionAdapter.php
··· 1 1 <?php 2 2 3 - final class HeraldDifferentialRevisionAdapter extends HeraldAdapter { 3 + final class HeraldDifferentialRevisionAdapter 4 + extends HeraldDifferentialAdapter { 4 5 5 6 protected $revision; 6 - protected $diff; 7 7 8 8 protected $explicitCCs; 9 9 protected $explicitReviewers; ··· 17 17 protected $buildPlans = array(); 18 18 protected $requiredSignatureDocumentPHIDs = array(); 19 19 20 - protected $repository; 21 20 protected $affectedPackages; 22 21 protected $changesets; 23 22 private $haveHunks; ··· 160 159 return $this->revision->getTitle(); 161 160 } 162 161 163 - public function loadRepository() { 164 - if ($this->repository === null) { 165 - $this->repository = false; 166 - $repository_phid = $this->getObject()->getRepositoryPHID(); 167 - if ($repository_phid) { 168 - $repository = id(new PhabricatorRepositoryQuery()) 169 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 170 - ->withPHIDs(array($repository_phid)) 171 - ->needProjectPHIDs(true) 172 - ->executeOne(); 173 - if ($repository) { 174 - $this->repository = $repository; 175 - } 176 - } 177 - } 178 - 179 - return $this->repository; 180 - } 181 - 182 162 protected function loadChangesets() { 183 163 if ($this->changesets === null) { 184 164 $this->changesets = $this->diff->loadChangesets(); ··· 186 166 return $this->changesets; 187 167 } 188 168 189 - private function loadChangesetsWithHunks() { 169 + protected function loadChangesetsWithHunks() { 190 170 $changesets = $this->loadChangesets(); 191 171 192 172 if ($changesets && !$this->haveHunks) { ··· 200 180 } 201 181 202 182 return $changesets; 203 - } 204 - 205 - protected function loadAffectedPaths() { 206 - $changesets = $this->loadChangesets(); 207 - 208 - $paths = array(); 209 - foreach ($changesets as $changeset) { 210 - $paths[] = $this->getAbsoluteRepositoryPathForChangeset($changeset); 211 - } 212 - return $paths; 213 - } 214 - 215 - protected function getAbsoluteRepositoryPathForChangeset( 216 - DifferentialChangeset $changeset) { 217 - 218 - $repository = $this->loadRepository(); 219 - if (!$repository) { 220 - return '/'.ltrim($changeset->getFilename(), '/'); 221 - } 222 - 223 - $diff = $this->diff; 224 - 225 - return $changeset->getAbsoluteRepositoryPath($repository, $diff); 226 - } 227 - 228 - protected function loadContentDictionary() { 229 - $add_lines = DifferentialHunk::FLAG_LINES_ADDED; 230 - $rem_lines = DifferentialHunk::FLAG_LINES_REMOVED; 231 - $mask = ($add_lines | $rem_lines); 232 - return $this->loadContentWithMask($mask); 233 - } 234 - 235 - protected function loadAddedContentDictionary() { 236 - return $this->loadContentWithMask(DifferentialHunk::FLAG_LINES_ADDED); 237 - } 238 - 239 - protected function loadRemovedContentDictionary() { 240 - return $this->loadContentWithMask(DifferentialHunk::FLAG_LINES_REMOVED); 241 - } 242 - 243 - private function loadContentWithMask($mask) { 244 - $changesets = $this->loadChangesetsWithHunks(); 245 - 246 - $dict = array(); 247 - foreach ($changesets as $changeset) { 248 - $content = array(); 249 - foreach ($changeset->getHunks() as $hunk) { 250 - $content[] = $hunk->getContentWithMask($mask); 251 - } 252 - 253 - $path = $this->getAbsoluteRepositoryPathForChangeset($changeset); 254 - $dict[$path] = implode("\n", $content); 255 - } 256 - 257 - return $dict; 258 183 } 259 184 260 185 public function loadAffectedPackages() {