diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -139,6 +139,10 @@ return 'F'.$this->getID(); } + public function scrambleSecret() { + return $this->setSecretKey($this->generateSecretKey()); + } + public static function readUploadedFileData($spec) { if (!$spec) { throw new Exception(pht('No file was uploaded!')); diff --git a/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php b/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php --- a/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php +++ b/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php @@ -8,6 +8,133 @@ ); } + public function testFileDirectScramble() { + // Changes to a file's view policy should scramble the file secret. + + $engine = new PhabricatorTestStorageEngine(); + $data = Filesystem::readRandomCharacters(64); + + $author = $this->generateNewTestUser(); + + $params = array( + 'name' => 'test.dat', + 'viewPolicy' => PhabricatorPolicies::POLICY_USER, + 'authorPHID' => $author->getPHID(), + 'storageEngines' => array( + $engine, + ), + ); + + $file = PhabricatorFile::newFromFileData($data, $params); + + $secret1 = $file->getSecretKey(); + + // First, change the name: this should not scramble the secret. + $xactions = array(); + $xactions[] = id(new PhabricatorFileTransaction()) + ->setTransactionType(PhabricatorFileTransaction::TYPE_NAME) + ->setNewValue('test.dat2'); + + $engine = id(new PhabricatorFileEditor()) + ->setActor($author) + ->setContentSource($this->newContentSource()) + ->applyTransactions($file, $xactions); + + $file = $file->reload(); + + $secret2 = $file->getSecretKey(); + + $this->assertEqual( + $secret1, + $secret2, + pht('No secret scramble on non-policy edit.')); + + // Now, change the view policy. This should scramble the secret. + $xactions = array(); + $xactions[] = id(new PhabricatorFileTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($author->getPHID()); + + $engine = id(new PhabricatorFileEditor()) + ->setActor($author) + ->setContentSource($this->newContentSource()) + ->applyTransactions($file, $xactions); + + $file = $file->reload(); + $secret3 = $file->getSecretKey(); + + $this->assertTrue( + ($secret1 !== $secret3), + pht('Changing file view policy should scramble secret.')); + } + + public function testFileIndirectScramble() { + // When a file is attached to an object like a task and the task view + // policy changes, the file secret should be scrambled. This invalidates + // old URIs if tasks get locked down. + + $engine = new PhabricatorTestStorageEngine(); + $data = Filesystem::readRandomCharacters(64); + + $author = $this->generateNewTestUser(); + + $params = array( + 'name' => 'test.dat', + 'viewPolicy' => $author->getPHID(), + 'authorPHID' => $author->getPHID(), + 'storageEngines' => array( + $engine, + ), + ); + + $file = PhabricatorFile::newFromFileData($data, $params); + $secret1 = $file->getSecretKey(); + + $task = ManiphestTask::initializeNewTask($author); + + $xactions = array(); + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_TITLE) + ->setNewValue(pht('File Scramble Test Task')); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION) + ->setNewValue('{'.$file->getMonogram().'}'); + + id(new ManiphestTransactionEditor()) + ->setActor($author) + ->setContentSource($this->newContentSource()) + ->applyTransactions($task, $xactions); + + $file = $file->reload(); + $secret2 = $file->getSecretKey(); + + $this->assertEqual( + $secret1, + $secret2, + pht( + 'File policy should not scramble when attached to '. + 'newly created object.')); + + $xactions = array(); + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($author->getPHID()); + + id(new ManiphestTransactionEditor()) + ->setActor($author) + ->setContentSource($this->newContentSource()) + ->applyTransactions($task, $xactions); + + $file = $file->reload(); + $secret3 = $file->getSecretKey(); + + $this->assertTrue( + ($secret1 !== $secret3), + pht('Changing attached object view policy should scramble secret.')); + } + + public function testFileVisibility() { $engine = new PhabricatorTestStorageEngine(); $data = Filesystem::readRandomCharacters(64); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -678,6 +678,10 @@ $editor->save(); break; + case PhabricatorTransactions::TYPE_VIEW_POLICY: + case PhabricatorTransactions::TYPE_SPACE: + $this->scrambleFileSecrets($object); + break; } } @@ -914,7 +918,6 @@ } $xactions = $this->applyFinalEffects($object, $xactions); - if ($read_locking) { $object->endReadLocking(); $read_locking = false; @@ -3471,4 +3474,64 @@ return $phids; } + /** + * When the view policy for an object is changed, scramble the secret keys + * for attached files to invalidate existing URIs. + */ + private function scrambleFileSecrets($object) { + // If this is a newly created object, we don't need to scramble anything + // since it couldn't have been previously published. + if ($this->getIsNewObject()) { + return; + } + + // If the object is a file itself, scramble it. + if ($object instanceof PhabricatorFile) { + if ($this->shouldScramblePolicy($object->getViewPolicy())) { + $object->scrambleSecret(); + $object->save(); + } + } + + $phid = $object->getPHID(); + + $attached_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $phid, + PhabricatorObjectHasFileEdgeType::EDGECONST); + if (!$attached_phids) { + return; + } + + $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); + + $files = id(new PhabricatorFileQuery()) + ->setViewer($omnipotent_viewer) + ->withPHIDs($attached_phids) + ->execute(); + foreach ($files as $file) { + $view_policy = $file->getViewPolicy(); + if ($this->shouldScramblePolicy($view_policy)) { + $file->scrambleSecret(); + $file->save(); + } + } + } + + + /** + * Check if a policy is strong enough to justify scrambling. Objects which + * are set to very open policies don't need to scramble their files, and + * files with very open policies don't need to be scrambled when associated + * objects change. + */ + private function shouldScramblePolicy($policy) { + switch ($policy) { + case PhabricatorPolicies::POLICY_PUBLIC: + case PhabricatorPolicies::POLICY_USER: + return false; + } + + return true; + } + }