1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-20 12:30:56 +01:00

Add a "Subscribers" object policy

Summary:
Ref T5681. Getting this to work correctly is a bit tricky, mostly because of the policy checks we do prior to applying an edit.

I think I came up with a mostly-reasonable approach, although it's a little bit gross. It uses `spl_object_hash()` so it shouldn't be able to do anything bad/dangerous (the hints are strictly bound to the hinted object, which is a clone that we destroy moments later).

Test Plan:
  - Added + ran a unit test.
  - Created a task with a "Subscribers" policy with me as a subscriber (without the hint stuff, this isn't possible: since you aren't a subscriber *yet*, you get a "you won't be able to see it" error).
  - Unsubscribed from a task with a "Subscribers" policy, was immediately unable to see it.
  - Created a task with a "subscribers" policy and a project subscriber with/without me as a member (error / success, respectively).

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5681

Differential Revision: https://secure.phabricator.com/D13259
This commit is contained in:
epriestley 2015-06-13 15:45:17 -07:00
parent 893c7a26c1
commit 3de3a72dd8
5 changed files with 213 additions and 0 deletions

View file

@ -2645,6 +2645,7 @@ phutil_register_library_map(array(
'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php',
'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php',
'PhabricatorSubscriptionsSubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsSubscribeEmailCommand.php',
'PhabricatorSubscriptionsSubscribersPolicyRule' => 'applications/subscriptions/policyrule/PhabricatorSubscriptionsSubscribersPolicyRule.php',
'PhabricatorSubscriptionsTransactionController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsTransactionController.php',
'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php',
'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php',
@ -6166,6 +6167,7 @@ phutil_register_library_map(array(
'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor',
'PhabricatorSubscriptionsListController' => 'PhabricatorController',
'PhabricatorSubscriptionsSubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',
'PhabricatorSubscriptionsSubscribersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorSubscriptionsTransactionController' => 'PhabricatorController',
'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener',
'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand',

View file

@ -208,5 +208,32 @@ final class PhabricatorPolicyDataTestCase extends PhabricatorTestCase {
PhabricatorPolicyCapability::CAN_VIEW));
}
public function testObjectPolicyRuleSubscribers() {
$author = $this->generateNewTestUser();
$rule = new PhabricatorSubscriptionsSubscribersPolicyRule();
$task = ManiphestTask::initializeNewTask($author);
$task->setViewPolicy($rule->getObjectPolicyFullKey());
$task->save();
$this->assertFalse(
PhabricatorPolicyFilter::hasCapability(
$author,
$task,
PhabricatorPolicyCapability::CAN_VIEW));
id(new PhabricatorSubscriptionsEditor())
->setActor($author)
->setObject($task)
->subscribeExplicit(array($author->getPHID()))
->save();
$this->assertTrue(
PhabricatorPolicyFilter::hasCapability(
$author,
$task,
PhabricatorPolicyCapability::CAN_VIEW));
}
}

View file

@ -97,6 +97,56 @@ abstract class PhabricatorPolicyRule {
}
/* -( Transaction Hints )-------------------------------------------------- */
/**
* Tell policy rules about upcoming transaction effects.
*
* Before transaction effects are applied, we try to stop users from making
* edits which will lock them out of objects. We can't do this perfectly,
* since they can set a policy to "the moon is full" moments before it wanes,
* but we try to prevent as many mistakes as possible.
*
* Some policy rules depend on complex checks against object state which
* we can't set up ahead of time. For example, subscriptions require database
* writes.
*
* In cases like this, instead of doing writes, you can pass a hint about an
* object to a policy rule. The rule can then look for hints and use them in
* rendering a verdict about whether the user will be able to see the object
* or not after applying the policy change.
*
* @param PhabricatorPolicyInterface Object to pass a hint about.
* @param PhabricatorPolicyRule Rule to pass hint to.
* @param wild Hint.
* @return void
*/
public static function passTransactionHintToRule(
PhabricatorPolicyInterface $object,
PhabricatorPolicyRule $rule,
$hint) {
$cache = PhabricatorCaches::getRequestCache();
$cache->setKey(self::getObjectPolicyCacheKey($object, $rule), $hint);
}
protected function getTransactionHint(
PhabricatorPolicyInterface $object) {
$cache = PhabricatorCaches::getRequestCache();
return $cache->getKey(self::getObjectPolicyCacheKey($object, $this));
}
private static function getObjectPolicyCacheKey(
PhabricatorPolicyInterface $object,
PhabricatorPolicyRule $rule) {
$hash = spl_object_hash($object);
$rule = get_class($rule);
return 'policycache.'.$hash.'.'.$rule;
}
/* -( Implementing Object Policies )--------------------------------------- */

View file

@ -0,0 +1,121 @@
<?php
final class PhabricatorSubscriptionsSubscribersPolicyRule
extends PhabricatorPolicyRule {
private $subscribed = array();
private $sourcePHIDs = array();
public function getObjectPolicyKey() {
return 'subscriptions.subscribers';
}
public function getObjectPolicyName() {
return pht('Subscribers');
}
public function getPolicyExplanation() {
return pht('Subscribers can take this action.');
}
public function getRuleDescription() {
return pht('subscribers');
}
public function canApplyToObject(PhabricatorPolicyInterface $object) {
return ($object instanceof PhabricatorSubscribableInterface);
}
public function willApplyRules(
PhabricatorUser $viewer,
array $values,
array $objects) {
// We want to let the user see the object if they're a subscriber or
// a member of any project which is a subscriber. Additionally, because
// subscriber state is complex, we need to read hints passed from
// the TransactionEditor to predict policy state after transactions apply.
$viewer_phid = $viewer->getPHID();
if (!$viewer_phid) {
return;
}
if (empty($this->subscribed[$viewer_phid])) {
$this->subscribed[$viewer_phid] = array();
}
// Load the project PHIDs the user is a member of.
if (!isset($this->sourcePHIDs[$viewer_phid])) {
$source_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$viewer_phid,
PhabricatorProjectMemberOfProjectEdgeType::EDGECONST);
$source_phids[] = $viewer_phid;
$this->sourcePHIDs[$viewer_phid] = $source_phids;
}
// Look for transaction hints.
foreach ($objects as $key => $object) {
$cache = $this->getTransactionHint($object);
if ($cache === null) {
// We don't have a hint for this object, so we'll deal with it below.
continue;
}
// We have a hint, so use that as the source of truth.
unset($objects[$key]);
foreach ($this->sourcePHIDs[$viewer_phid] as $source_phid) {
if (isset($cache[$source_phid])) {
$this->subscribed[$viewer_phid][$object->getPHID()] = true;
break;
}
}
}
$phids = mpull($objects, 'getPHID');
if (!$phids) {
return;
}
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($this->sourcePHIDs[$viewer_phid])
->withEdgeTypes(
array(
PhabricatorSubscribedToObjectEdgeType::EDGECONST,
))
->withDestinationPHIDs($phids);
$edge_query->execute();
$subscribed = $edge_query->getDestinationPHIDs();
if (!$subscribed) {
return;
}
$this->subscribed[$viewer_phid] += array_fill_keys($subscribed, true);
}
public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object) {
$viewer_phid = $viewer->getPHID();
if (!$viewer_phid) {
return false;
}
if ($object->isAutomaticallySubscribed($viewer_phid)) {
return true;
}
$subscribed = idx($this->subscribed, $viewer_phid);
return isset($subscribed[$object->getPHID()]);
}
public function getValueControlType() {
return self::CONTROL_TYPE_NONE;
}
}

View file

@ -2087,6 +2087,19 @@ abstract class PhabricatorApplicationTransactionEditor
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case PhabricatorTransactions::TYPE_SUBSCRIBERS:
$clone_xaction = clone $xaction;
$clone_xaction->setOldValue(array_values($this->subscribers));
$clone_xaction->setNewValue(
$this->getPHIDTransactionNewValue(
$clone_xaction));
PhabricatorPolicyRule::passTransactionHintToRule(
$copy,
new PhabricatorSubscriptionsSubscribersPolicyRule(),
array_fuse($clone_xaction->getNewValue()));
break;
case PhabricatorTransactions::TYPE_SPACE:
$space_phid = $this->getTransactionNewValue($object, $xaction);
$copy->setSpacePHID($space_phid);