diff --git a/src/applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php b/src/applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php --- a/src/applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php +++ b/src/applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php @@ -3,6 +3,18 @@ final class ManiphestTaskAuthorPolicyRule extends PhabricatorPolicyRule { + public function getObjectPolicyKey() { + return 'maniphest.author'; + } + + public function getObjectPolicyName() { + return pht('Task Author'); + } + + public function getPolicyExplanation() { + return pht('The author of this task can take this action.'); + } + public function getRuleDescription() { return pht('task author'); } diff --git a/src/applications/meta/controller/PhabricatorApplicationEditController.php b/src/applications/meta/controller/PhabricatorApplicationEditController.php --- a/src/applications/meta/controller/PhabricatorApplicationEditController.php +++ b/src/applications/meta/controller/PhabricatorApplicationEditController.php @@ -136,6 +136,20 @@ $template = $application->getCapabilityTemplatePHIDType($capability); if ($template) { + $phid_types = PhabricatorPHIDType::getAllTypes(); + $phid_type = idx($phid_types, $template); + if ($phid_type) { + $template_object = $phid_type->newObject(); + if ($template_object) { + $template_policies = id(new PhabricatorPolicyQuery()) + ->setViewer($user) + ->setObject($template_object) + ->execute(); + $control->setPolicies($template_policies); + $control->setTemplateObject($template_object); + } + } + $control->setTemplatePHIDType($template); } diff --git a/src/applications/policy/constants/PhabricatorPolicyType.php b/src/applications/policy/constants/PhabricatorPolicyType.php --- a/src/applications/policy/constants/PhabricatorPolicyType.php +++ b/src/applications/policy/constants/PhabricatorPolicyType.php @@ -3,6 +3,7 @@ final class PhabricatorPolicyType extends PhabricatorPolicyConstants { const TYPE_GLOBAL = 'global'; + const TYPE_OBJECT = 'object'; const TYPE_USER = 'user'; const TYPE_CUSTOM = 'custom'; const TYPE_PROJECT = 'project'; @@ -11,9 +12,10 @@ public static function getPolicyTypeOrder($type) { static $map = array( self::TYPE_GLOBAL => 0, - self::TYPE_USER => 1, - self::TYPE_CUSTOM => 2, - self::TYPE_PROJECT => 3, + self::TYPE_OBJECT => 1, + self::TYPE_USER => 2, + self::TYPE_CUSTOM => 3, + self::TYPE_PROJECT => 4, self::TYPE_MASKED => 9, ); return idx($map, $type, 9); @@ -23,6 +25,8 @@ switch ($type) { case self::TYPE_GLOBAL: return pht('Basic Policies'); + case self::TYPE_OBJECT: + return pht('Object Policies'); case self::TYPE_USER: return pht('User Policies'); case self::TYPE_CUSTOM: diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -8,6 +8,7 @@ private $raisePolicyExceptions; private $userProjects; private $customPolicies = array(); + private $objectPolicies = array(); private $forcedPolicy; public static function mustRetainCapability( @@ -131,6 +132,7 @@ $need_projects = array(); $need_policies = array(); + $need_objpolicies = array(); foreach ($objects as $key => $object) { $object_capabilities = $object->getCapabilities(); foreach ($capabilities as $capability) { @@ -143,17 +145,29 @@ } $policy = $this->getObjectPolicy($object, $capability); + + if (PhabricatorPolicyQuery::isObjectPolicy($policy)) { + $need_objpolicies[$policy][] = $object; + continue; + } + $type = phid_get_type($policy); if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) { $need_projects[$policy] = $policy; + continue; } if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { $need_policies[$policy][] = $object; + continue; } } } + if ($need_objpolicies) { + $this->loadObjectPolicies($need_objpolicies); + } + if ($need_policies) { $this->loadCustomPolicies($need_policies); } @@ -486,6 +500,15 @@ $this->rejectObject($object, $policy, $capability); break; default: + if (PhabricatorPolicyQuery::isObjectPolicy($policy)) { + if ($this->checkObjectPolicy($policy, $object)) { + return true; + } else { + $this->rejectObject($object, $policy, $capability); + break; + } + } + $type = phid_get_type($policy); if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) { if (!empty($this->userProjects[$viewer->getPHID()][$policy])) { @@ -573,6 +596,32 @@ throw $exception; } + private function loadObjectPolicies(array $map) { + $viewer = $this->viewer; + $viewer_phid = $viewer->getPHID(); + + $rules = PhabricatorPolicyQuery::getObjectPolicyRules(null); + + $results = array(); + foreach ($map as $key => $object_list) { + $rule = idx($rules, $key); + if (!$rule) { + continue; + } + + foreach ($object_list as $object_key => $object) { + if (!$rule->canApplyToObject($object)) { + unset($object_list[$object_key]); + } + } + + $rule->willApplyRules($viewer, array(), $object_list); + $results[$key] = $rule; + } + + $this->objectPolicies[$viewer_phid] = $results; + } + private function loadCustomPolicies(array $map) { $viewer = $this->viewer; $viewer_phid = $viewer->getPHID(); @@ -627,6 +676,24 @@ $this->customPolicies[$viewer->getPHID()] += $custom_policies; } + private function checkObjectPolicy( + $policy_phid, + PhabricatorPolicyInterface $object) { + $viewer = $this->viewer; + $viewer_phid = $viewer->getPHID(); + + $rule = idx($this->objectPolicies[$viewer_phid], $policy_phid); + if (!$rule) { + return false; + } + + if (!$rule->canApplyToObject($object)) { + return false; + } + + return $rule->applyRule($viewer, null, $object); + } + private function checkCustomPolicy( $policy_phid, PhabricatorPolicyInterface $object) { diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php --- a/src/applications/policy/query/PhabricatorPolicyQuery.php +++ b/src/applications/policy/query/PhabricatorPolicyQuery.php @@ -6,6 +6,8 @@ private $object; private $phids; + const OBJECT_POLICY_PREFIX = 'obj.'; + public function setObject(PhabricatorPolicyInterface $object) { $this->object = $object; return $this; @@ -71,7 +73,15 @@ $results = array(); // First, load global policies. - foreach ($this->getGlobalPolicies() as $phid => $policy) { + foreach (self::getGlobalPolicies() as $phid => $policy) { + if (isset($phids[$phid])) { + $results[$phid] = $policy; + unset($phids[$phid]); + } + } + + // Now, load object policies. + foreach (self::getObjectPolicies($this->object) as $phid => $policy) { if (isset($phids[$phid])) { $results[$phid] = $policy; unset($phids[$phid]); @@ -212,7 +222,7 @@ // 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) { + foreach (self::getGlobalPolicies() as $phid => $policy) { if ($phid == PhabricatorPolicies::POLICY_PUBLIC) { if (!$show_public) { continue; @@ -221,6 +231,10 @@ $phids[] = $phid; } + foreach (self::getObjectPolicies($this->object) as $phid => $policy) { + $phids[] = $phid; + } + return $phids; } @@ -234,4 +248,99 @@ return 'PhabricatorPolicyApplication'; } + public static function isSpecialPolicy($identifier) { + if (self::isObjectPolicy($identifier)) { + return true; + } + + if (self::isGlobalPolicy($identifier)) { + return true; + } + + return false; + } + + +/* -( Object Policies )---------------------------------------------------- */ + + + public static function isObjectPolicy($identifier) { + $prefix = self::OBJECT_POLICY_PREFIX; + return !strncmp($identifier, $prefix, strlen($prefix)); + } + + public static function getObjectPolicy($identifier) { + if (!self::isObjectPolicy($identifier)) { + return null; + } + + $policies = self::getObjectPolicies(null); + return idx($policies, $identifier); + } + + public static function getObjectPolicyRule($identifier) { + if (!self::isObjectPolicy($identifier)) { + return null; + } + + $rules = self::getObjectPolicyRules(null); + return idx($rules, $identifier); + } + + public static function getObjectPolicies($object) { + $rule_map = self::getObjectPolicyRules($object); + + $results = array(); + foreach ($rule_map as $key => $rule) { + $results[$key] = id(new PhabricatorPolicy()) + ->setType(PhabricatorPolicyType::TYPE_OBJECT) + ->setPHID($key) + ->setIcon($rule->getObjectPolicyIcon()) + ->setName($rule->getObjectPolicyName()) + ->setShortName($rule->getObjectPolicyShortName()) + ->makeEphemeral(); + } + + return $results; + } + + public static function getObjectPolicyRules($object) { + $rules = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorPolicyRule') + ->loadObjects(); + + $results = array(); + foreach ($rules as $rule) { + $key = $rule->getObjectPolicyKey(); + if (!$key) { + continue; + } + + $full_key = self::OBJECT_POLICY_PREFIX.$key; + if (isset($results[$full_key])) { + throw new Exception( + pht( + 'Two policy rules (of classes "%s" and "%s") define the same '. + 'object policy key ("%s"), but each object policy rule must use '. + 'a unique key.', + get_class($rule), + get_class($results[$full_key]), + $key)); + } + + $results[$full_key] = $rule; + } + + if ($object !== null) { + foreach ($results as $key => $rule) { + if (!$rule->canApplyToObject($object)) { + unset($results[$key]); + } + } + } + + return $results; + } + + } diff --git a/src/applications/policy/rule/PhabricatorPolicyRule.php b/src/applications/policy/rule/PhabricatorPolicyRule.php --- a/src/applications/policy/rule/PhabricatorPolicyRule.php +++ b/src/applications/policy/rule/PhabricatorPolicyRule.php @@ -1,5 +1,8 @@ getObjectPolicyName(); + } + + public function getObjectPolicyIcon() { + return 'fa-cube'; + } + + public function getPolicyExplanation() { + throw new PhutilMethodNotImplementedException(); + } + } diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php --- a/src/applications/policy/storage/PhabricatorPolicy.php +++ b/src/applications/policy/storage/PhabricatorPolicy.php @@ -54,6 +54,11 @@ return PhabricatorPolicyQuery::getGlobalPolicy($policy_identifier); } + $policy = PhabricatorPolicyQuery::getObjectPolicy($policy_identifier); + if ($policy) { + return $policy; + } + if (!$handle) { throw new Exception( pht( @@ -158,7 +163,16 @@ return $this->workflow; } + public function setIcon($icon) { + $this->icon = $icon; + return $this; + } + public function getIcon() { + if ($this->icon) { + return $this->icon; + } + switch ($this->getType()) { case PhabricatorPolicyType::TYPE_GLOBAL: static $map = array( @@ -204,6 +218,11 @@ PhabricatorUser $viewer, $policy) { + $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy); + if ($rule) { + return $rule->getPolicyExplanation(); + } + switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return pht('This object is public.'); diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -243,10 +243,10 @@ case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: - if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) { + if (!PhabricatorPolicyQuery::isSpecialPolicy($old)) { $phids[] = array($old); } - if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) { + if (!PhabricatorPolicyQuery::isSpecialPolicy($new)) { $phids[] = array($new); } break; diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php --- a/src/view/form/control/AphrontFormPolicyControl.php +++ b/src/view/form/control/AphrontFormPolicyControl.php @@ -7,6 +7,7 @@ private $policies; private $spacePHID; private $templatePHIDType; + private $templateObject; public function setPolicyObject(PhabricatorPolicyInterface $object) { $this->object = $object; @@ -33,6 +34,11 @@ return $this; } + public function setTemplateObject($object) { + $this->templateObject = $object; + return $this; + } + public function setCapability($capability) { $this->capability = $capability; @@ -64,9 +70,31 @@ protected function getOptions() { $capability = $this->capability; + $policies = $this->policies; + + // Exclude object policies which don't make sense here. This primarily + // filters object policies associated from template capabilities (like + // "Default Task View Policy" being set to "Task Author") so they aren't + // made available on non-template capabilities (like "Can Bulk Edit"). + foreach ($policies as $key => $policy) { + if ($policy->getType() != PhabricatorPolicyType::TYPE_OBJECT) { + continue; + } + + $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy->getPHID()); + if (!$rule) { + continue; + } + + $target = nonempty($this->templateObject, $this->object); + if (!$rule->canApplyToObject($target)) { + unset($policies[$key]); + continue; + } + } $options = array(); - foreach ($this->policies as $policy) { + foreach ($policies as $policy) { if ($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) { // Never expose "Public" for capabilities which don't support it. $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); @@ -74,6 +102,7 @@ continue; } } + $policy_short_name = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(28) ->truncateString($policy->getName()); @@ -122,6 +151,7 @@ $options, array( PhabricatorPolicyType::TYPE_GLOBAL, + PhabricatorPolicyType::TYPE_OBJECT, PhabricatorPolicyType::TYPE_USER, PhabricatorPolicyType::TYPE_CUSTOM, PhabricatorPolicyType::TYPE_PROJECT,