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

Move sender validation into MailReceiver classes

Summary:
Ref T1205. Finally able to delete a big chunk of this nastiness.

Make MailReceivers responsible for validating senders. For object creation receivers (bugs, conpherences) this just means that users must not be disabled. For other receivers the senders must be able to see the objects, have the right hashes, etc., according to policy.

Test Plan: Added a bunch of test cases (everything except policy). Verified behavior via the Receive test console.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1205

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

+294 -141
+2
src/__phutil_library_map__.php
··· 1185 1185 'PhabricatorObjectItemView' => 'view/layout/PhabricatorObjectItemView.php', 1186 1186 'PhabricatorObjectListView' => 'view/control/PhabricatorObjectListView.php', 1187 1187 'PhabricatorObjectMailReceiver' => 'applications/metamta/receiver/PhabricatorObjectMailReceiver.php', 1188 + 'PhabricatorObjectMailReceiverTestCase' => 'applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php', 1188 1189 'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php', 1189 1190 'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php', 1190 1191 'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php', ··· 2920 2921 'PhabricatorObjectItemView' => 'AphrontTagView', 2921 2922 'PhabricatorObjectListView' => 'AphrontView', 2922 2923 'PhabricatorObjectMailReceiver' => 'PhabricatorMailReceiver', 2924 + 'PhabricatorObjectMailReceiverTestCase' => 'PhabricatorTestCase', 2923 2925 'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery', 2924 2926 'PhabricatorOwnersConfigOptions' => 'PhabricatorApplicationConfigOptions', 2925 2927 'PhabricatorOwnersController' => 'PhabricatorController',
+4 -1
src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php
··· 72 72 public function getProjectPHIDs() { 73 73 $projects = array(); 74 74 for ($i = 0; $i < rand(1, 4);$i++) { 75 - $projects[] = $this->loadOneRandom("PhabricatorProject")->getPHID(); 75 + $project = $this->loadOneRandom("PhabricatorProject"); 76 + if ($project) { 77 + $projects[] = $project->getPHID(); 78 + } 76 79 } 77 80 return $projects; 78 81 }
+6
src/applications/metamta/constants/MetaMTAReceivedMailStatus.php
··· 8 8 const STATUS_NO_RECEIVERS = 'err:no-receivers'; 9 9 const STATUS_ABUNDANT_RECEIVERS = 'err:multiple-receivers'; 10 10 const STATUS_UNKNOWN_SENDER = 'err:unknown-sender'; 11 + const STATUS_DISABLED_SENDER = 'err:disabled-sender'; 12 + const STATUS_NO_PUBLIC_MAIL = 'err:no-public-mail'; 13 + const STATUS_USER_MISMATCH = 'err:bad-user'; 14 + const STATUS_POLICY_PROBLEM = 'err:policy'; 15 + const STATUS_NO_SUCH_OBJECT = 'err:not-found'; 16 + const STATUS_HASH_MISMATCH = 'err:bad-hash'; 11 17 12 18 }
+16
src/applications/metamta/receiver/PhabricatorMailReceiver.php
··· 6 6 abstract public function isEnabled(); 7 7 abstract public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail); 8 8 9 + public function validateSender( 10 + PhabricatorMetaMTAReceivedMail $mail, 11 + PhabricatorUser $sender) { 12 + 13 + if ($sender->getIsDisabled()) { 14 + throw new PhabricatorMetaMTAReceivedMailProcessingException( 15 + MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER, 16 + pht( 17 + "Sender '%s' has a disabled user account.", 18 + $sender->getUsername())); 19 + } 20 + 21 + 22 + return; 23 + } 24 + 9 25 /** 10 26 * Identifies the sender's user account for a piece of received mail. Note 11 27 * that this method does not validate that the sender is who they say they
+105 -5
src/applications/metamta/receiver/PhabricatorObjectMailReceiver.php
··· 2 2 3 3 abstract class PhabricatorObjectMailReceiver extends PhabricatorMailReceiver { 4 4 5 + /** 6 + * Return a regular expression fragment which matches the name of an 7 + * object which can receive mail. For example, Differential uses: 8 + * 9 + * D[1-9]\d* 10 + * 11 + * ...to match `D123`, etc., identifying Differential Revisions. 12 + * 13 + * @return string Regular expression fragment. 14 + */ 5 15 abstract protected function getObjectPattern(); 6 16 7 - final public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) { 8 - $regexp = $this->getAddressRegexp(); 17 + 18 + /** 19 + * Load the object receiving mail, based on an identifying pattern. Normally 20 + * this pattern is some sort of object ID. 21 + * 22 + * @param string A string matched by @{method:getObjectPattern} 23 + * fragment. 24 + * @param PhabricatorUser The viewing user. 25 + * @return void 26 + */ 27 + abstract protected function loadObject($pattern, PhabricatorUser $viewer); 28 + 29 + 30 + public function validateSender( 31 + PhabricatorMetaMTAReceivedMail $mail, 32 + PhabricatorUser $sender) { 33 + parent::validateSender($mail, $sender); 9 34 10 35 foreach ($mail->getToAddresses() as $address) { 11 - $address = self::stripMailboxPrefix($address); 12 - $local = id(new PhutilEmailAddress($address))->getLocalPart(); 13 - if (preg_match($regexp, $local)) { 36 + $parts = $this->matchObjectAddress($address); 37 + if ($parts) { 38 + break; 39 + } 40 + } 41 + 42 + try { 43 + $object = $this->loadObject($parts['pattern'], $sender); 44 + } catch (PhabricatorPolicyException $policy_exception) { 45 + throw new PhabricatorMetaMTAReceivedMailProcessingException( 46 + MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM, 47 + pht( 48 + "This mail is addressed to an object you are not permitted ". 49 + "to see: %s", 50 + $policy_exception->getMessage())); 51 + } 52 + 53 + if (!$object) { 54 + throw new PhabricatorMetaMTAReceivedMailProcessingException( 55 + MetaMTAReceivedMailStatus::STATUS_NO_SUCH_OBJECT, 56 + pht( 57 + "This mail is addressed to an object ('%s'), but that object ". 58 + "does not exist.", 59 + $parts['pattern'])); 60 + } 61 + 62 + $sender_identifier = $parts['sender']; 63 + 64 + if ($sender_identifier === 'public') { 65 + if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) { 66 + throw new PhabricatorMetaMTAReceivedMailProcessingException( 67 + MetaMTAReceivedMailStatus::STATUS_NO_PUBLIC_MAIL, 68 + pht( 69 + "This mail is addressed to an object's public address, but ". 70 + "public replies are not enabled (`metamta.public-replies`).")); 71 + } 72 + $check_phid = $object->getPHID(); 73 + } else { 74 + if ($sender_identifier != $sender->getID()) { 75 + throw new PhabricatorMetaMTAReceivedMailProcessingException( 76 + MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH, 77 + pht( 78 + "This mail is addressed to an object's private address, but ". 79 + "the sending user and the private address owner are not the ". 80 + "same user.")); 81 + } 82 + $check_phid = $sender->getPHID(); 83 + } 84 + 85 + $expect_hash = self::computeMailHash($object->getMailKey(), $check_phid); 86 + 87 + if ($expect_hash != $parts['hash']) { 88 + throw new PhabricatorMetaMTAReceivedMailProcessingException( 89 + MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH, 90 + pht( 91 + "The hash in this object's address does not match the expected ". 92 + "value.")); 93 + } 94 + } 95 + 96 + 97 + final public function canAcceptMail(PhabricatorMetaMTAReceivedMail $mail) { 98 + foreach ($mail->getToAddresses() as $address) { 99 + if ($this->matchObjectAddress($address)) { 14 100 return true; 15 101 } 16 102 } 17 103 18 104 return false; 105 + } 106 + 107 + private function matchObjectAddress($address) { 108 + $regexp = $this->getAddressRegexp(); 109 + 110 + $address = self::stripMailboxPrefix($address); 111 + $local = id(new PhutilEmailAddress($address))->getLocalPart(); 112 + 113 + $matches = null; 114 + if (!preg_match($regexp, $local, $matches)) { 115 + return false; 116 + } 117 + 118 + return $matches; 19 119 } 20 120 21 121 private function getAddressRegexp() {
+128
src/applications/metamta/receiver/__tests__/PhabricatorObjectMailReceiverTestCase.php
··· 1 + <?php 2 + 3 + final class PhabricatorObjectMailReceiverTestCase 4 + extends PhabricatorTestCase { 5 + 6 + protected function getPhabricatorTestCaseConfiguration() { 7 + return array( 8 + self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true, 9 + ); 10 + } 11 + 12 + public function testDropUnconfiguredPublicMail() { 13 + list($task, $user, $mail) = $this->buildMail('public'); 14 + 15 + $env = PhabricatorEnv::beginScopedEnv(); 16 + $env->overrideEnvConfig('metamta.public-replies', false); 17 + 18 + $mail->save(); 19 + $mail->processReceivedMail(); 20 + 21 + $this->assertEqual( 22 + MetaMTAReceivedMailStatus::STATUS_NO_PUBLIC_MAIL, 23 + $mail->getStatus()); 24 + } 25 + 26 + /* 27 + 28 + TODO: Tasks don't support policies yet. Implement this once they do. 29 + 30 + public function testDropPolicyViolationMail() { 31 + list($task, $user, $mail) = $this->buildMail('public'); 32 + 33 + // TODO: Set task policy to "no one" here. 34 + 35 + $mail->save(); 36 + $mail->processReceivedMail(); 37 + 38 + $this->assertEqual( 39 + MetaMTAReceivedMailStatus::STATUS_POLICY_PROBLEM, 40 + $mail->getStatus()); 41 + } 42 + 43 + */ 44 + 45 + public function testDropInvalidObjectMail() { 46 + list($task, $user, $mail) = $this->buildMail('404'); 47 + 48 + $mail->save(); 49 + $mail->processReceivedMail(); 50 + 51 + $this->assertEqual( 52 + MetaMTAReceivedMailStatus::STATUS_NO_SUCH_OBJECT, 53 + $mail->getStatus()); 54 + } 55 + 56 + public function testDropUserMismatchMail() { 57 + list($task, $user, $mail) = $this->buildMail('baduser'); 58 + 59 + $mail->save(); 60 + $mail->processReceivedMail(); 61 + 62 + $this->assertEqual( 63 + MetaMTAReceivedMailStatus::STATUS_USER_MISMATCH, 64 + $mail->getStatus()); 65 + } 66 + 67 + public function testDropHashMismatchMail() { 68 + list($task, $user, $mail) = $this->buildMail('badhash'); 69 + 70 + $mail->save(); 71 + $mail->processReceivedMail(); 72 + 73 + $this->assertEqual( 74 + MetaMTAReceivedMailStatus::STATUS_HASH_MISMATCH, 75 + $mail->getStatus()); 76 + } 77 + 78 + private function buildMail($style) { 79 + 80 + // TODO: Clean up test data generators so that we don't need to guarantee 81 + // the existence of a user. 82 + $this->generateNewTestUser(); 83 + 84 + $task = id(new PhabricatorManiphestTaskTestDataGenerator())->generate(); 85 + $user = $this->generateNewTestUser(); 86 + 87 + $is_public = ($style === 'public'); 88 + $is_bad_hash = ($style == 'badhash'); 89 + $is_bad_user = ($style == 'baduser'); 90 + $is_404_object = ($style == '404'); 91 + 92 + if ($is_public) { 93 + $user_identifier = 'public'; 94 + } else if ($is_bad_user) { 95 + $user_identifier = $user->getID() + 1; 96 + } else { 97 + $user_identifier = $user->getID(); 98 + } 99 + 100 + if ($is_bad_hash) { 101 + $hash = PhabricatorObjectMailReceiver::computeMailHash('x', 'y'); 102 + } else { 103 + $hash = PhabricatorObjectMailReceiver::computeMailHash( 104 + $task->getMailKey(), 105 + $is_public ? $task->getPHID() : $user->getPHID()); 106 + } 107 + 108 + if ($is_404_object) { 109 + $task_identifier = 'T'.($task->getID() + 1); 110 + } else { 111 + $task_identifier = 'T'.$task->getID(); 112 + } 113 + 114 + $to = $task_identifier.'+'.$user_identifier.'+'.$hash.'@example.com'; 115 + 116 + $mail = new PhabricatorMetaMTAReceivedMail(); 117 + $mail->setHeaders( 118 + array( 119 + 'Message-ID' => 'test@example.com', 120 + 'From' => $user->loadPrimaryEmail()->getAddress(), 121 + 'To' => $to, 122 + )); 123 + 124 + return array($task, $user, $mail); 125 + } 126 + 127 + 128 + }
+6 -135
src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
··· 176 176 177 177 $receiver = $this->loadReceiver(); 178 178 $sender = $receiver->loadSender($this); 179 + $receiver->validateSender($this, $sender); 179 180 180 181 } catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) { 181 182 $this ··· 197 198 198 199 $from = idx($this->headers, 'from'); 199 200 200 - // TODO -- make this a switch statement / better if / when we add more 201 - // public create email addresses! 202 - $create_task = PhabricatorEnv::getEnvConfig( 203 - 'metamta.maniphest.public-create-email'); 204 - 205 - if ($create_task && $to == $create_task) { 201 + // TODO: Move this into `ManiphestCreateMailReceiver`. 202 + if ($receiver instanceof ManiphestCreateMailReceiver) { 206 203 $receiver = new ManiphestTask(); 207 204 208 - $user = $this->lookupSender(); 209 - if ($user) { 210 - $this->setAuthorPHID($user->getPHID()); 211 - } else { 212 - $allow_email_users = PhabricatorEnv::getEnvConfig( 213 - 'phabricator.allow-email-users'); 214 - 215 - if ($allow_email_users) { 216 - $email = new PhutilEmailAddress($from); 217 - 218 - $xuser = id(new PhabricatorExternalAccount())->loadOneWhere( 219 - 'accountType = %s AND accountDomain IS NULL and accountID = %s', 220 - 'email', 221 - $email->getAddress()); 222 - 223 - if (!$xuser) { 224 - $xuser = new PhabricatorExternalAccount(); 225 - $xuser->setAccountID($email->getAddress()); 226 - $xuser->setAccountType('email'); 227 - $xuser->setDisplayName($email->getDisplayName()); 228 - $xuser->save(); 229 - } 230 - 231 - $user = $xuser->getPhabricatorUser(); 232 - } else { 233 - $default_author = PhabricatorEnv::getEnvConfig( 234 - 'metamta.maniphest.default-public-author'); 235 - 236 - if ($default_author) { 237 - $user = id(new PhabricatorUser())->loadOneWhere( 238 - 'username = %s', 239 - $default_author); 240 - 241 - if (!$user) { 242 - throw new Exception( 243 - "Phabricator is misconfigured, the configuration key ". 244 - "'metamta.maniphest.default-public-author' is set to user ". 245 - "'{$default_author}' but that user does not exist."); 246 - } 247 - 248 - } else { 249 - // TODO: We should probably bounce these since from the user's 250 - // perspective their email vanishes into a black hole. 251 - return $this->setMessage("Invalid public user '{$from}'.")->save(); 252 - } 253 - } 254 - 255 - } 205 + $user = $sender; 256 206 257 207 $receiver->setAuthorPHID($user->getPHID()); 258 208 $receiver->setOriginalEmailSource($from); ··· 275 225 276 226 // means we're creating a conpherence...! 277 227 if ($user_phids) { 278 - // we must have a valid user who created this conpherence 279 - $user = $this->lookupSender(); 280 - if (!$user) { 281 - return $this->setMessage("Invalid public user '{$from}'.")->save(); 282 - } 228 + $user = $sender; 283 229 284 230 $conpherence = id(new ConpherenceReplyHandler()) 285 231 ->setMailReceiver(new ConpherenceThread()) ··· 293 239 return $this->save(); 294 240 } 295 241 296 - if ($user_id == 'public') { 297 - if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) { 298 - return $this->setMessage("Public replies not enabled.")->save(); 299 - } 300 - 301 - $user = $this->lookupSender(); 302 - 303 - if (!$user) { 304 - return $this->setMessage("Invalid public user '{$from}'.")->save(); 305 - } 306 - 307 - $use_user_hash = false; 308 - } else { 309 - $user = id(new PhabricatorUser())->load($user_id); 310 - if (!$user) { 311 - return $this->setMessage("Invalid private user '{$user_id}'.")->save(); 312 - } 313 - 314 - $use_user_hash = true; 315 - } 316 - 317 - if ($user->getIsDisabled()) { 318 - return $this->setMessage("User '{$user_id}' is disabled")->save(); 319 - } 320 - 242 + $user = $sender; 321 243 $this->setAuthorPHID($user->getPHID()); 322 244 323 245 $receiver = self::loadReceiverObject($receiver_name); ··· 326 248 } 327 249 328 250 $this->setRelatedPHID($receiver->getPHID()); 329 - 330 - if ($use_user_hash) { 331 - // This is a private reply-to address, check that the user hash is 332 - // correct. 333 - $check_phid = $user->getPHID(); 334 - } else { 335 - // This is a public reply-to address, check that the object hash is 336 - // correct. 337 - $check_phid = $receiver->getPHID(); 338 - } 339 - 340 - $expect_hash = PhabricatorObjectMailReceiver::computeMailHash( 341 - $receiver->getMailKey(), 342 - $check_phid); 343 - 344 - if ($expect_hash != $hash) { 345 - return $this->setMessage("Invalid mail hash!")->save(); 346 - } 347 251 348 252 if ($receiver instanceof ManiphestTask) { 349 253 $editor = new ManiphestTransactionEditor(); ··· 429 333 $raw_addresses[] = $this->getRawEmailAddress($address); 430 334 } 431 335 return array_filter($raw_addresses); 432 - } 433 - 434 - private function lookupSender() { 435 - $from = idx($this->headers, 'from'); 436 - $from = $this->getRawEmailAddress($from); 437 - 438 - $user = PhabricatorUser::loadOneWithEmailAddress($from); 439 - 440 - // If Phabricator is configured to allow "Reply-To" authentication, try 441 - // the "Reply-To" address if we failed to match the "From" address. 442 - $config_key = 'metamta.insecure-auth-with-reply-to'; 443 - $allow_reply_to = PhabricatorEnv::getEnvConfig($config_key); 444 - 445 - if (!$user && $allow_reply_to) { 446 - $reply_to = idx($this->headers, 'reply-to'); 447 - $reply_to = $this->getRawEmailAddress($reply_to); 448 - if ($reply_to) { 449 - $user = PhabricatorUser::loadOneWithEmailAddress($reply_to); 450 - } 451 - } 452 - 453 - $allow_email_users = PhabricatorEnv::getEnvConfig( 454 - 'phabricator.allow-email-users'); 455 - 456 - if (!$user && $allow_email_users) { 457 - $xusr = id(new PhabricatorExternalAccount())->loadOneWhere( 458 - 'accountType = %s AND accountDomain IS NULL and accountID = %s', 459 - 'email', $from); 460 - 461 - $user = $xusr->getPhabricatorUser(); 462 - } 463 - 464 - return $user; 465 336 } 466 337 467 338 /**
+27
src/applications/metamta/storage/__tests__/PhabricatorMetaMTAReceivedMailTestCase.php
··· 87 87 $mail->getStatus()); 88 88 } 89 89 90 + 91 + public function testDropDisabledSenderMail() { 92 + $env = PhabricatorEnv::beginScopedEnv(); 93 + $env->overrideEnvConfig( 94 + 'metamta.maniphest.public-create-email', 95 + 'bugs@example.com'); 96 + 97 + $user = $this->generateNewTestUser() 98 + ->setIsDisabled(true) 99 + ->save(); 100 + 101 + $mail = new PhabricatorMetaMTAReceivedMail(); 102 + $mail->setHeaders( 103 + array( 104 + 'Message-ID' => 'test@example.com', 105 + 'From' => $user->loadPrimaryEmail()->getAddress(), 106 + 'To' => 'bugs@example.com', 107 + )); 108 + $mail->save(); 109 + 110 + $mail->processReceivedMail(); 111 + 112 + $this->assertEqual( 113 + MetaMTAReceivedMailStatus::STATUS_DISABLED_SENDER, 114 + $mail->getStatus()); 115 + } 116 + 90 117 }