1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-18 12:52:42 +01:00

Allow different policy rules for different types of objects

Summary:
Ref T5681. Policy rules can now select objects they can apply to, so a rule like "task author" only shows up where it makes sense (when defining task policies).

This will let us define rules like "members of thread" in Conpherence, "subscribers", etc., to make custom policies more flexible.

Notes:

  - Per D13251, we need to do a little work to get the right options for policies like "Maniphest > Default View Policy". This should allow "task" policies.
  - This implements a "task author" policy as a simple example.
  - The `willApplyRule()` signature now accepts `$objects` to support bulk-loading things like subscribers.

Test Plan:
  - Defined a task to be "visible to: task author", verified author could see it and other users could not.
  - `var_dump()`'d willApplyRule() inputs, verified they were correct (exactly the objects which use the rule).
  - Set `default view policy` to a task-specific policy.
  - Verified that other policies like "Can Use Bulk Editor" don't have these options.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T5681

Differential Revision: https://secure.phabricator.com/D13252
This commit is contained in:
epriestley 2015-06-13 15:44:03 -07:00
parent ee8de4a9ec
commit 7f98a8575d
16 changed files with 220 additions and 41 deletions

View file

@ -391,7 +391,7 @@ return array(
'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '3f5d6dbf', 'rsrc/js/application/phortune/behavior-stripe-payment-form.js' => '3f5d6dbf',
'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'fc91ab6c', 'rsrc/js/application/phortune/behavior-test-payment-form.js' => 'fc91ab6c',
'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef',
'rsrc/js/application/policy/behavior-policy-control.js' => '9a340b3d', 'rsrc/js/application/policy/behavior-policy-control.js' => '7d470398',
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c',
'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b', 'rsrc/js/application/ponder/behavior-votebox.js' => '4e9b766b',
'rsrc/js/application/projects/behavior-project-boards.js' => 'ba4fa35c', 'rsrc/js/application/projects/behavior-project-boards.js' => 'ba4fa35c',
@ -624,7 +624,7 @@ return array(
'javelin-behavior-pholio-mock-view' => 'fbe497e7', 'javelin-behavior-pholio-mock-view' => 'fbe497e7',
'javelin-behavior-phui-dropdown-menu' => '54733475', 'javelin-behavior-phui-dropdown-menu' => '54733475',
'javelin-behavior-phui-object-box-tabs' => '2bfa2836', 'javelin-behavior-phui-object-box-tabs' => '2bfa2836',
'javelin-behavior-policy-control' => '9a340b3d', 'javelin-behavior-policy-control' => '7d470398',
'javelin-behavior-policy-rule-editor' => '5e9f347c', 'javelin-behavior-policy-rule-editor' => '5e9f347c',
'javelin-behavior-ponder-votebox' => '4e9b766b', 'javelin-behavior-ponder-votebox' => '4e9b766b',
'javelin-behavior-project-boards' => 'ba4fa35c', 'javelin-behavior-project-boards' => 'ba4fa35c',
@ -1413,6 +1413,15 @@ return array(
'javelin-request', 'javelin-request',
'javelin-router', 'javelin-router',
), ),
'7d470398' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'javelin-workflow',
),
'7e41274a' => array( '7e41274a' => array(
'javelin-install', 'javelin-install',
), ),
@ -1575,15 +1584,6 @@ return array(
'javelin-behavior-device', 'javelin-behavior-device',
'phabricator-title', 'phabricator-title',
), ),
'9a340b3d' => array(
'javelin-behavior',
'javelin-dom',
'javelin-util',
'phuix-dropdown-menu',
'phuix-action-list-view',
'phuix-action-view',
'javelin-workflow',
),
'9f36c42d' => array( '9f36c42d' => array(
'javelin-behavior', 'javelin-behavior',
'javelin-stratcom', 'javelin-stratcom',

View file

@ -1051,6 +1051,7 @@ phutil_register_library_map(array(
'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php', 'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php',
'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php', 'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php',
'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php', 'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php',
'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php',
'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php', 'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php',
'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php', 'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php',
'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php', 'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php',
@ -4412,6 +4413,7 @@ phutil_register_library_map(array(
'PhabricatorProjectInterface', 'PhabricatorProjectInterface',
'PhabricatorSpacesInterface', 'PhabricatorSpacesInterface',
), ),
'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule',
'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource',
'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType',
'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType',

View file

@ -583,6 +583,12 @@ abstract class PhabricatorApplication implements PhabricatorPolicyInterface {
} }
public function getCapabilityTemplatePHIDType($capability) { public function getCapabilityTemplatePHIDType($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
case PhabricatorPolicyCapability::CAN_EDIT:
return null;
}
$spec = $this->getCustomCapabilitySpecification($capability); $spec = $this->getCustomCapabilitySpecification($capability);
return idx($spec, 'template'); return idx($spec, 'template');
} }

View file

@ -0,0 +1,31 @@
<?php
final class ManiphestTaskAuthorPolicyRule
extends PhabricatorPolicyRule {
public function getRuleDescription() {
return pht('task author');
}
public function canApplyToObject(PhabricatorPolicyInterface $object) {
return ($object instanceof ManiphestTask);
}
public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object) {
$viewer_phid = $viewer->getPHID();
if (!$viewer_phid) {
return false;
}
return ($object->getAuthorPHID() == $viewer_phid);
}
public function getValueControlType() {
return self::CONTROL_TYPE_NONE;
}
}

View file

@ -124,8 +124,7 @@ final class PhabricatorApplicationEditController
->setValue(idx($descriptions, $capability)) ->setValue(idx($descriptions, $capability))
->setCaption($caption)); ->setCaption($caption));
} else { } else {
$form->appendChild( $control = id(new AphrontFormPolicyControl())
id(new AphrontFormPolicyControl())
->setUser($user) ->setUser($user)
->setDisabled($locked) ->setDisabled($locked)
->setCapability($capability) ->setCapability($capability)
@ -133,7 +132,14 @@ final class PhabricatorApplicationEditController
->setPolicies($policies) ->setPolicies($policies)
->setLabel($label) ->setLabel($label)
->setName('policy:'.$capability) ->setName('policy:'.$capability)
->setCaption($caption)); ->setCaption($caption);
$template = $application->getCapabilityTemplatePHIDType($capability);
if ($template) {
$control->setTemplatePHIDType($template);
}
$form->appendControl($control);
} }
} }

View file

@ -19,7 +19,15 @@ final class PhabricatorPolicyApplication extends PhabricatorApplication {
'/policy/' => array( '/policy/' => array(
'explain/(?P<phid>[^/]+)/(?P<capability>[^/]+)/' 'explain/(?P<phid>[^/]+)/(?P<capability>[^/]+)/'
=> 'PhabricatorPolicyExplainController', => 'PhabricatorPolicyExplainController',
'edit/(?:(?P<phid>[^/]+)/)?' => 'PhabricatorPolicyEditController', 'edit/'.
'(?:'.
'object/(?P<objectPHID>[^/]+)'.
'|'.
'type/(?P<objectType>[^/]+)'.
'|'.
'template/(?P<templateType>[^/]+)'.
')/'.
'(?:(?P<phid>[^/]+)/)?' => 'PhabricatorPolicyEditController',
), ),
); );
} }

View file

@ -4,8 +4,37 @@ final class PhabricatorPolicyEditController
extends PhabricatorPolicyController { extends PhabricatorPolicyController {
public function handleRequest(AphrontRequest $request) { public function handleRequest(AphrontRequest $request) {
$request = $this->getRequest(); $viewer = $this->getViewer();
$viewer = $request->getUser();
// TODO: This doesn't do anything yet, but sets up template policies; see
// T6860.
$is_template = false;
$object_phid = $request->getURIData('objectPHID');
if ($object_phid) {
$object = id(new PhabricatorObjectQuery())
->setViewer($viewer)
->withPHIDs(array($object_phid))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
} else {
$object_type = $request->getURIData('objectType');
if (!$object_type) {
$object_type = $request->getURIData('templateType');
$is_template = true;
}
$phid_types = PhabricatorPHIDType::getAllInstalledTypes($viewer);
if (empty($phid_types[$object_type])) {
return new Aphront404Response();
}
$object = $phid_types[$object_type]->newObject();
if (!$object) {
return new Aphront404Response();
}
}
$action_options = array( $action_options = array(
PhabricatorPolicy::ACTION_ALLOW => pht('Allow'), PhabricatorPolicy::ACTION_ALLOW => pht('Allow'),
@ -15,6 +44,13 @@ final class PhabricatorPolicyEditController
$rules = id(new PhutilSymbolLoader()) $rules = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorPolicyRule') ->setAncestorClass('PhabricatorPolicyRule')
->loadObjects(); ->loadObjects();
foreach ($rules as $key => $rule) {
if (!$rule->canApplyToObject($object)) {
unset($rules[$key]);
}
}
$rules = msort($rules, 'getRuleOrder'); $rules = msort($rules, 'getRuleOrder');
$default_rule = array( $default_rule = array(

View file

@ -149,13 +149,13 @@ final class PhabricatorPolicyFilter {
} }
if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
$need_policies[$policy] = $policy; $need_policies[$policy][] = $object;
} }
} }
} }
if ($need_policies) { if ($need_policies) {
$this->loadCustomPolicies(array_keys($need_policies)); $this->loadCustomPolicies($need_policies);
} }
// If we need projects, check if any of the projects we need are also the // If we need projects, check if any of the projects we need are also the
@ -500,7 +500,7 @@ final class PhabricatorPolicyFilter {
$this->rejectObject($object, $policy, $capability); $this->rejectObject($object, $policy, $capability);
} }
} else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { } else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
if ($this->checkCustomPolicy($policy)) { if ($this->checkCustomPolicy($policy, $object)) {
return true; return true;
} else { } else {
$this->rejectObject($object, $policy, $capability); $this->rejectObject($object, $policy, $capability);
@ -573,30 +573,47 @@ final class PhabricatorPolicyFilter {
throw $exception; throw $exception;
} }
private function loadCustomPolicies(array $phids) { private function loadCustomPolicies(array $map) {
$viewer = $this->viewer; $viewer = $this->viewer;
$viewer_phid = $viewer->getPHID(); $viewer_phid = $viewer->getPHID();
$custom_policies = id(new PhabricatorPolicyQuery()) $custom_policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer) ->setViewer($viewer)
->withPHIDs($phids) ->withPHIDs(array_keys($map))
->execute(); ->execute();
$custom_policies = mpull($custom_policies, null, 'getPHID'); $custom_policies = mpull($custom_policies, null, 'getPHID');
$classes = array(); $classes = array();
$values = array(); $values = array();
foreach ($custom_policies as $policy) { $objects = array();
foreach ($custom_policies as $policy_phid => $policy) {
foreach ($policy->getCustomRuleClasses() as $class) { foreach ($policy->getCustomRuleClasses() as $class) {
$classes[$class] = $class; $classes[$class] = $class;
$values[$class][] = $policy->getCustomRuleValues($class); $values[$class][] = $policy->getCustomRuleValues($class);
foreach (idx($map, $policy_phid, array()) as $object) {
$objects[$class][] = $object;
}
} }
} }
foreach ($classes as $class => $ignored) { foreach ($classes as $class => $ignored) {
$object = newv($class, array()); $rule_object = newv($class, array());
$object->willApplyRules($viewer, array_mergev($values[$class]));
$classes[$class] = $object; // Filter out any objects which the rule can't apply to.
$target_objects = idx($objects, $class, array());
foreach ($target_objects as $key => $target_object) {
if (!$rule_object->canApplyToObject($target_object)) {
unset($target_objects[$key]);
}
}
$rule_object->willApplyRules(
$viewer,
array_mergev($values[$class]),
$target_objects);
$classes[$class] = $rule_object;
} }
foreach ($custom_policies as $policy) { foreach ($custom_policies as $policy) {
@ -610,7 +627,10 @@ final class PhabricatorPolicyFilter {
$this->customPolicies[$viewer->getPHID()] += $custom_policies; $this->customPolicies[$viewer->getPHID()] += $custom_policies;
} }
private function checkCustomPolicy($policy_phid) { private function checkCustomPolicy(
$policy_phid,
PhabricatorPolicyInterface $object) {
$viewer = $this->viewer; $viewer = $this->viewer;
$viewer_phid = $viewer->getPHID(); $viewer_phid = $viewer->getPHID();
@ -623,14 +643,19 @@ final class PhabricatorPolicyFilter {
$objects = $policy->getRuleObjects(); $objects = $policy->getRuleObjects();
$action = null; $action = null;
foreach ($policy->getRules() as $rule) { foreach ($policy->getRules() as $rule) {
$object = idx($objects, idx($rule, 'rule')); $rule_object = idx($objects, idx($rule, 'rule'));
if (!$object) { if (!$rule_object) {
// Reject, this policy has a bogus rule. // Reject, this policy has a bogus rule.
return false; return false;
} }
if (!$rule_object->canApplyToObject($object)) {
// Reject, this policy rule can't be applied to the given object.
return false;
}
// If the user matches this rule, use this action. // If the user matches this rule, use this action.
if ($object->applyRule($viewer, idx($rule, 'value'))) { if ($rule_object->applyRule($viewer, idx($rule, 'value'), $object)) {
$action = idx($rule, 'action'); $action = idx($rule, 'action');
break; break;
} }

View file

@ -6,7 +6,10 @@ final class PhabricatorAdministratorsPolicyRule extends PhabricatorPolicyRule {
return pht('administrators'); return pht('administrators');
} }
public function applyRule(PhabricatorUser $viewer, $value) { public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object) {
return $viewer->getIsAdmin(); return $viewer->getIsAdmin();
} }

View file

@ -9,7 +9,11 @@ final class PhabricatorLegalpadSignaturePolicyRule
return pht('signers of legalpad documents'); return pht('signers of legalpad documents');
} }
public function willApplyRules(PhabricatorUser $viewer, array $values) { public function willApplyRules(
PhabricatorUser $viewer,
array $values,
array $objects) {
$values = array_unique(array_filter(array_mergev($values))); $values = array_unique(array_filter(array_mergev($values)));
if (!$values) { if (!$values) {
return; return;
@ -26,12 +30,17 @@ final class PhabricatorLegalpadSignaturePolicyRule
$this->signatures = mpull($documents, 'getPHID', 'getPHID'); $this->signatures = mpull($documents, 'getPHID', 'getPHID');
} }
public function applyRule(PhabricatorUser $viewer, $value) { public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object) {
foreach ($value as $document_phid) { foreach ($value as $document_phid) {
if (!isset($this->signatures[$document_phid])) { if (!isset($this->signatures[$document_phid])) {
return false; return false;
} }
} }
return true; return true;
} }

View file

@ -11,7 +11,11 @@ final class PhabricatorLunarPhasePolicyRule extends PhabricatorPolicyRule {
return pht('when the moon'); return pht('when the moon');
} }
public function applyRule(PhabricatorUser $viewer, $value) { public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object) {
$moon = new PhutilLunarPhase(PhabricatorTime::getNow()); $moon = new PhutilLunarPhase(PhabricatorTime::getNow());
switch ($value) { switch ($value) {

View file

@ -8,9 +8,15 @@ abstract class PhabricatorPolicyRule {
const CONTROL_TYPE_NONE = 'none'; const CONTROL_TYPE_NONE = 'none';
abstract public function getRuleDescription(); abstract public function getRuleDescription();
abstract public function applyRule(PhabricatorUser $viewer, $value); abstract public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object);
public function willApplyRules(PhabricatorUser $viewer, array $values) { public function willApplyRules(
PhabricatorUser $viewer,
array $values,
array $objects) {
return; return;
} }
@ -22,6 +28,16 @@ abstract class PhabricatorPolicyRule {
return null; return null;
} }
/**
* Return `true` if this rule can be applied to the given object.
*
* Some policy rules may only operation on certain kinds of objects. For
* example, a "task author" rule
*/
public function canApplyToObject(PhabricatorPolicyInterface $object) {
return true;
}
protected function getDatasourceTemplate( protected function getDatasourceTemplate(
PhabricatorTypeaheadDatasource $datasource) { PhabricatorTypeaheadDatasource $datasource) {
return array( return array(

View file

@ -8,7 +8,11 @@ final class PhabricatorProjectsPolicyRule extends PhabricatorPolicyRule {
return pht('members of projects'); return pht('members of projects');
} }
public function willApplyRules(PhabricatorUser $viewer, array $values) { public function willApplyRules(
PhabricatorUser $viewer,
array $values,
array $objects) {
$values = array_unique(array_filter(array_mergev($values))); $values = array_unique(array_filter(array_mergev($values)));
if (!$values) { if (!$values) {
return; return;
@ -24,12 +28,17 @@ final class PhabricatorProjectsPolicyRule extends PhabricatorPolicyRule {
} }
} }
public function applyRule(PhabricatorUser $viewer, $value) { public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object) {
foreach ($value as $project_phid) { foreach ($value as $project_phid) {
if (isset($this->memberships[$viewer->getPHID()][$project_phid])) { if (isset($this->memberships[$viewer->getPHID()][$project_phid])) {
return true; return true;
} }
} }
return false; return false;
} }

View file

@ -6,12 +6,17 @@ final class PhabricatorUsersPolicyRule extends PhabricatorPolicyRule {
return pht('users'); return pht('users');
} }
public function applyRule(PhabricatorUser $viewer, $value) { public function applyRule(
PhabricatorUser $viewer,
$value,
PhabricatorPolicyInterface $object) {
foreach ($value as $phid) { foreach ($value as $phid) {
if ($phid == $viewer->getPHID()) { if ($phid == $viewer->getPHID()) {
return true; return true;
} }
} }
return false; return false;
} }

View file

@ -6,6 +6,7 @@ final class AphrontFormPolicyControl extends AphrontFormControl {
private $capability; private $capability;
private $policies; private $policies;
private $spacePHID; private $spacePHID;
private $templatePHIDType;
public function setPolicyObject(PhabricatorPolicyInterface $object) { public function setPolicyObject(PhabricatorPolicyInterface $object) {
$this->object = $object; $this->object = $object;
@ -27,6 +28,11 @@ final class AphrontFormPolicyControl extends AphrontFormControl {
return $this->spacePHID; return $this->spacePHID;
} }
public function setTemplatePHIDType($type) {
$this->templatePHIDType = $type;
return $this;
}
public function setCapability($capability) { public function setCapability($capability) {
$this->capability = $capability; $this->capability = $capability;
@ -178,6 +184,18 @@ final class AphrontFormPolicyControl extends AphrontFormControl {
} }
if ($this->templatePHIDType) {
$context_path = 'template/'.$this->templatePHIDType.'/';
} else {
$object_phid = $this->object->getPHID();
if ($object_phid) {
$context_path = 'object/'.$object_phid.'/';
} else {
$object_type = phid_get_type($this->object->generatePHID());
$context_path = 'type/'.$object_type.'/';
}
}
Javelin::initBehavior( Javelin::initBehavior(
'policy-control', 'policy-control',
array( array(
@ -190,6 +208,7 @@ final class AphrontFormPolicyControl extends AphrontFormControl {
'labels' => $labels, 'labels' => $labels,
'value' => $this->getValue(), 'value' => $this->getValue(),
'capability' => $this->capability, 'capability' => $this->capability,
'editURI' => '/policy/edit/'.$context_path,
'customPlaceholder' => $this->getCustomPolicyPlaceholder(), 'customPlaceholder' => $this->getCustomPolicyPlaceholder(),
)); ));

View file

@ -101,7 +101,7 @@ JX.behavior('policy-control', function(config) {
* Get the workflow URI to create or edit a policy with a given PHID. * Get the workflow URI to create or edit a policy with a given PHID.
*/ */
var get_custom_uri = function(phid, capability) { var get_custom_uri = function(phid, capability) {
var uri = '/policy/edit/'; var uri = config.editURI;
if (phid != config.customPlaceholder) { if (phid != config.customPlaceholder) {
uri += phid + '/'; uri += phid + '/';
} }