1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-23 15:22:41 +01:00

Allow custom policies to be loaded and exeucuted by the policy filter

Summary: Ref T603. Adds code to actually execute custom policies. (There's still no way to select them in the UI.)

Test Plan:
  - Added and executed unit tests.
  - Edited policies in existing applications.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T603

Differential Revision: https://secure.phabricator.com/D7292
This commit is contained in:
epriestley 2013-10-14 11:46:14 -07:00
parent 4733a8ef14
commit 67b17239b8
6 changed files with 354 additions and 77 deletions

View file

@ -20,7 +20,7 @@ final class ManiphestTask extends ManiphestDAO
protected $subpriority = 0;
protected $title = '';
protected $originalTitle;
protected $originalTitle = '';
protected $description = '';
protected $originalEmailSource;
protected $mailKey;

View file

@ -31,4 +31,117 @@ final class PhabricatorPolicyDataTestCase extends PhabricatorTestCase {
$this->assertEqual(0, count($results));
}
public function testCustomPolicyRuleUser() {
$user_a = $this->generateNewTestUser();
$user_b = $this->generateNewTestUser();
$author = $this->generateNewTestUser();
$policy = id(new PhabricatorPolicy())
->setRules(
array(
array(
'action' => PhabricatorPolicy::ACTION_ACCEPT,
'rule' => 'PhabricatorPolicyRuleUsers',
'value' => array($user_a->getPHID()),
),
))
->save();
$task = ManiphestTask::initializeNewTask($author);
$task->setViewPolicy($policy->getPHID());
$task->save();
$can_a_view = PhabricatorPolicyFilter::hasCapability(
$user_a,
$task,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertEqual(true, $can_a_view);
$can_b_view = PhabricatorPolicyFilter::hasCapability(
$user_b,
$task,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertEqual(false, $can_b_view);
}
public function testCustomPolicyRuleAdministrators() {
$user_a = $this->generateNewTestUser();
$user_a->setIsAdmin(true)->save();
$user_b = $this->generateNewTestUser();
$author = $this->generateNewTestUser();
$policy = id(new PhabricatorPolicy())
->setRules(
array(
array(
'action' => PhabricatorPolicy::ACTION_ACCEPT,
'rule' => 'PhabricatorPolicyRuleAdministrators',
'value' => null,
),
))
->save();
$task = ManiphestTask::initializeNewTask($author);
$task->setViewPolicy($policy->getPHID());
$task->save();
$can_a_view = PhabricatorPolicyFilter::hasCapability(
$user_a,
$task,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertEqual(true, $can_a_view);
$can_b_view = PhabricatorPolicyFilter::hasCapability(
$user_b,
$task,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertEqual(false, $can_b_view);
}
public function testCustomPolicyRuleLunarPhase() {
$user_a = $this->generateNewTestUser();
$author = $this->generateNewTestUser();
$policy = id(new PhabricatorPolicy())
->setRules(
array(
array(
'action' => PhabricatorPolicy::ACTION_ACCEPT,
'rule' => 'PhabricatorPolicyRuleLunarPhase',
'value' => 'new',
),
))
->save();
$task = ManiphestTask::initializeNewTask($author);
$task->setViewPolicy($policy->getPHID());
$task->save();
$time_a = PhabricatorTime::pushTime(934354800, 'UTC');
$can_a_view = PhabricatorPolicyFilter::hasCapability(
$user_a,
$task,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertEqual(true, $can_a_view);
unset($time_a);
$time_b = PhabricatorTime::pushTime(1116745200, 'UTC');
$can_a_view = PhabricatorPolicyFilter::hasCapability(
$user_a,
$task,
PhabricatorPolicyCapability::CAN_VIEW);
$this->assertEqual(false, $can_a_view);
unset($time_b);
}
}

View file

@ -7,6 +7,7 @@ final class PhabricatorPolicyFilter {
private $capabilities;
private $raisePolicyExceptions;
private $userProjects;
private $customPolicies = array();
public static function mustRetainCapability(
PhabricatorUser $user,
@ -85,6 +86,7 @@ final class PhabricatorPolicyFilter {
}
$need_projects = array();
$need_policies = array();
foreach ($objects as $key => $object) {
$object_capabilities = $object->getCapabilities();
foreach ($capabilities as $capability) {
@ -99,9 +101,17 @@ final class PhabricatorPolicyFilter {
if ($type == PhabricatorProjectPHIDTypeProject::TYPECONST) {
$need_projects[$policy] = $policy;
}
if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
$need_policies[$policy] = $policy;
}
}
}
if ($need_policies) {
$this->loadCustomPolicies(array_keys($need_policies));
}
// If we need projects, check if any of the projects we need are also the
// objects we're filtering. Because of how project rules work, this is a
// common case.
@ -225,6 +235,12 @@ final class PhabricatorPolicyFilter {
} else {
$this->rejectObject($object, $policy, $capability);
}
} else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
if ($this->checkCustomPolicy($policy)) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else {
// Reject objects with unknown policies.
$this->rejectObject($object, false, $capability);
@ -316,4 +332,75 @@ final class PhabricatorPolicyFilter {
throw $exception;
}
private function loadCustomPolicies(array $phids) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$custom_policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->withPHIDs($phids)
->execute();
$custom_policies = mpull($custom_policies, null, 'getPHID');
$classes = array();
$values = array();
foreach ($custom_policies as $policy) {
foreach ($policy->getCustomRuleClasses() as $class) {
$classes[$class] = $class;
$values[$class][] = $policy->getCustomRuleValues($class);
}
}
foreach ($classes as $class => $ignored) {
$object = newv($class, array());
$object->willApplyRules($viewer, array_mergev($values[$class]));
$classes[$class] = $object;
}
foreach ($custom_policies as $policy) {
$policy->attachRuleObjects($classes);
}
if (empty($this->customPolicies[$viewer_phid])) {
$this->customPolicies[$viewer_phid] = array();
}
$this->customPolicies[$viewer->getPHID()] += $custom_policies;
}
private function checkCustomPolicy($policy_phid) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$policy = $this->customPolicies[$viewer_phid][$policy_phid];
$objects = $policy->getRuleObjects();
$action = null;
foreach ($policy->getRules() as $rule) {
$object = idx($objects, idx($rule, 'rule'));
if (!$object) {
// Reject, this policy has a bogus rule.
return false;
}
// If the user matches this rule, use this action.
if ($object->applyRule($viewer, idx($rule, 'value'))) {
$action = idx($rule, 'action');
break;
}
}
if ($action === null) {
$action = $policy->getDefaultAction();
}
if ($action === PhabricatorPolicy::ACTION_ACCEPT) {
return true;
}
return false;
}
}

View file

@ -26,31 +26,19 @@ final class PhabricatorPolicyQuery extends PhabricatorQuery {
PhabricatorPolicyInterface $object) {
$results = array();
$policies = null;
$global = self::getGlobalPolicies();
$capabilities = $object->getCapabilities();
foreach ($capabilities as $capability) {
$policy = $object->getPolicy($capability);
if (!$policy) {
continue;
}
$map = array();
foreach ($object->getCapabilities() as $capability) {
$map[$capability] = $object->getPolicy($capability);
}
if (isset($global[$policy])) {
$results[$capability] = $global[$policy];
continue;
}
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->withPHIDs($map)
->execute();
if ($policies === null) {
// This slightly overfetches data, but it shouldn't generally
// be a problem.
$policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->setObject($object)
->execute();
}
$results[$capability] = $policies[$policy];
foreach ($map as $capability => $phid) {
$results[$capability] = $policies[$phid];
}
return $results;
@ -75,72 +63,65 @@ final class PhabricatorPolicyQuery extends PhabricatorQuery {
throw new Exception('Call setViewer() before execute()!');
}
$results = $this->getGlobalPolicies();
if ($this->object && $this->phids) {
throw new Exception(
"You can not issue a policy query with both setObject() and ".
"setPHIDs().");
} else if ($this->object) {
$phids = $this->loadObjectPolicyPHIDs();
} else {
$phids = $this->phids;
}
if ($this->viewer->getPHID()) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->viewer)
->withMemberPHIDs(array($this->viewer->getPHID()))
->execute();
if ($projects) {
foreach ($projects as $project) {
$results[] = id(new PhabricatorPolicy())
->setType(PhabricatorPolicyType::TYPE_PROJECT)
->setPHID($project->getPHID())
->setHref('/project/view/'.$project->getID().'/')
->setName($project->getName());
}
$phids = array_fuse($phids);
$results = array();
// First, load global policies.
foreach ($this->getGlobalPolicies() as $phid => $policy) {
if (isset($phids[$phid])) {
$results[$phid] = $policy;
unset($phids[$phid]);
}
}
$results = mpull($results, null, 'getPHID');
$other_policies = array();
if ($this->object) {
$capabilities = $this->object->getCapabilities();
foreach ($capabilities as $capability) {
$policy = $this->object->getPolicy($capability);
if (!$policy) {
continue;
// If we still need policies, we're going to have to fetch data. Bucket
// the remaining policies into rule-based policies and handle-based
// policies.
if ($phids) {
$rule_policies = array();
$handle_policies = array();
foreach ($phids as $phid) {
$phid_type = phid_get_type($phid);
if ($phid_type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
$rule_policies[$phid] = $phid;
} else {
$handle_policies[$phid] = $phid;
}
$other_policies[$policy] = $policy;
}
}
// If this install doesn't have "Public" enabled, remove it as an option
// unless the object already has a "Public" policy. In this case we retain
// the policy but enforce it as thought it was "All Users".
$show_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
if (!$show_public &&
empty($other_policies[PhabricatorPolicies::POLICY_PUBLIC])) {
unset($results[PhabricatorPolicies::POLICY_PUBLIC]);
}
if ($handle_policies) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->viewer)
->withPHIDs($handle_policies)
->execute();
foreach ($handle_policies as $phid) {
$results[$phid] = PhabricatorPolicy::newFromPolicyAndHandle(
$phid,
$handles[$phid]);
}
}
$other_policies = array_diff_key($other_policies, $results);
if ($other_policies) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->viewer)
->withPHIDs($other_policies)
->execute();
foreach ($other_policies as $phid) {
$results[$phid] = PhabricatorPolicy::newFromPolicyAndHandle(
$phid,
$handles[$phid]);
if ($rule_policies) {
$rules = id(new PhabricatorPolicy())->loadAllWhere(
'phid IN (%Ls)',
$rule_policies);
$results += mpull($rules, null, 'getPHID');
}
}
$results = msort($results, 'getSortKey');
if ($this->phids) {
$phids = array_fuse($this->phids);
foreach ($results as $key => $result) {
if (empty($phids[$result->getPHID()])) {
unset($results[$key]);
}
}
}
return $results;
}
@ -196,5 +177,43 @@ final class PhabricatorPolicyQuery extends PhabricatorQuery {
}
}
private function loadObjectPolicyPHIDs() {
$phids = array();
if ($this->viewer->getPHID()) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->viewer)
->withMemberPHIDs(array($this->viewer->getPHID()))
->execute();
foreach ($projects as $project) {
$phids[] = $project->getPHID();
}
}
$capabilities = $this->object->getCapabilities();
foreach ($capabilities as $capability) {
$policy = $this->object->getPolicy($capability);
if (!$policy) {
continue;
}
$phids[] = $policy;
}
// If this install doesn't have "Public" enabled, don't include it as an
// option unless the object already has a "Public" policy. In this case we
// retain the policy but enforce it as though it was "All Users".
$show_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
foreach ($this->getGlobalPolicies() as $phid => $policy) {
if ($phid == PhabricatorPolicies::POLICY_PUBLIC) {
if (!$show_public) {
continue;
}
}
$phids[] = $phid;
}
return $phids;
}
}

View file

@ -8,7 +8,12 @@ final class PhabricatorPolicyRuleUsers
}
public function applyRule(PhabricatorUser $viewer, $value) {
return isset($value[$viewer->getPHID()]);
foreach ($value as $phid) {
if ($phid == $viewer->getPHID()) {
return true;
}
}
return false;
}
public function getValueControlType() {

View file

@ -14,6 +14,8 @@ final class PhabricatorPolicy
protected $rules = array();
protected $defaultAction = self::ACTION_DENY;
private $ruleObjects = self::ATTACHABLE;
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
@ -228,4 +230,55 @@ final class PhabricatorPolicy
return $desc;
}
}
/**
* Return a list of custom rule classes (concrete subclasses of
* @{class:PhabricatorPolicyRule}) this policy uses.
*
* @return list<string> List of class names.
*/
public function getCustomRuleClasses() {
$classes = array();
foreach ($this->getRules() as $rule) {
$class = idx($rule, 'rule');
try {
if (class_exists($class)) {
$classes[$class] = $class;
}
} catch (Exception $ex) {
continue;
}
}
return array_keys($classes);
}
/**
* Return a list of all values used by a given rule class to implement this
* policy. This is used to bulk load data (like project memberships) in order
* to apply policy filters efficiently.
*
* @param string Policy rule classname.
* @return list<wild> List of values used in this policy.
*/
public function getCustomRuleValues($rule_class) {
$values = array();
foreach ($this->getRules() as $rule) {
if ($rule['rule'] == $rule_class) {
$values[] = $rule['value'];
}
}
return $values;
}
public function attachRuleObjects(array $objects) {
$this->ruleObjects = $objects;
return $this;
}
public function getRuleObjects() {
return $this->assertAttached($this->ruleObjects);
}
}