diff --git a/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php index 81aaff88db..145cbe1f90 100644 --- a/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php +++ b/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php @@ -1,147 +1,147 @@ true, ); } public function testProjectPolicyMembership() { $author = $this->generateNewTestUser(); $proj_a = id(new PhabricatorProject()) ->setName('A') ->setAuthorPHID($author->getPHID()) ->save(); $proj_b = id(new PhabricatorProject()) ->setName('B') ->setAuthorPHID($author->getPHID()) ->save(); $proj_a->setViewPolicy($proj_b->getPHID())->save(); $proj_b->setViewPolicy($proj_a->getPHID())->save(); $user = new PhabricatorUser(); $results = id(new PhabricatorProjectQuery()) ->setViewer($user) ->execute(); $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, + 'action' => PhabricatorPolicy::ACTION_ALLOW, '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, + 'action' => PhabricatorPolicy::ACTION_ALLOW, '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, + 'action' => PhabricatorPolicy::ACTION_ALLOW, '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); } } diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php index ef6108ceaa..b962049d71 100644 --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -1,406 +1,406 @@ setViewer($user); $filter->requireCapabilities(array($capability)); $filter->raisePolicyExceptions(true); $filter->apply(array($object)); } public static function hasCapability( PhabricatorUser $user, PhabricatorPolicyInterface $object, $capability) { $filter = new PhabricatorPolicyFilter(); $filter->setViewer($user); $filter->requireCapabilities(array($capability)); $result = $filter->apply(array($object)); return (count($result) == 1); } public function setViewer(PhabricatorUser $user) { $this->viewer = $user; return $this; } public function requireCapabilities(array $capabilities) { $this->capabilities = $capabilities; return $this; } public function raisePolicyExceptions($raise) { $this->raisePolicyExceptions = $raise; return $this; } public function apply(array $objects) { assert_instances_of($objects, 'PhabricatorPolicyInterface'); $viewer = $this->viewer; $capabilities = $this->capabilities; if (!$viewer || !$capabilities) { throw new Exception( 'Call setViewer() and requireCapabilities() before apply()!'); } // If the viewer is omnipotent, short circuit all the checks and just // return the input unmodified. This is an optimization; we know the // result already. if ($viewer->isOmnipotent()) { return $objects; } $filtered = array(); $viewer_phid = $viewer->getPHID(); if (empty($this->userProjects[$viewer_phid])) { $this->userProjects[$viewer_phid] = array(); } $need_projects = array(); $need_policies = array(); foreach ($objects as $key => $object) { $object_capabilities = $object->getCapabilities(); foreach ($capabilities as $capability) { if (!in_array($capability, $object_capabilities)) { throw new Exception( "Testing for capability '{$capability}' on an object which does ". "not have that capability!"); } $policy = $object->getPolicy($capability); $type = phid_get_type($policy); 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. if ($need_projects) { foreach ($objects as $object) { if ($object instanceof PhabricatorProject) { $project_phid = $object->getPHID(); if (isset($need_projects[$project_phid])) { $is_member = $object->isUserMember($viewer_phid); $this->userProjects[$viewer_phid][$project_phid] = $is_member; unset($need_projects[$project_phid]); } } } } if ($need_projects) { $need_projects = array_unique($need_projects); // NOTE: We're using the omnipotent user here to avoid a recursive // descent into madness. We don't actually need to know if the user can // see these projects or not, since: the check is "user is member of // project", not "user can see project"; and membership implies // visibility anyway. Without this, we may load other projects and // re-enter the policy filter and generally create a huge mess. $projects = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withMemberPHIDs(array($viewer->getPHID())) ->withPHIDs($need_projects) ->execute(); foreach ($projects as $project) { $this->userProjects[$viewer_phid][$project->getPHID()] = true; } } foreach ($objects as $key => $object) { $object_capabilities = $object->getCapabilities(); foreach ($capabilities as $capability) { if (!$this->checkCapability($object, $capability)) { // If we're missing any capability, move on to the next object. continue 2; } // If we make it here, we have all of the required capabilities. $filtered[$key] = $object; } } return $filtered; } private function checkCapability( PhabricatorPolicyInterface $object, $capability) { $policy = $object->getPolicy($capability); if (!$policy) { // TODO: Formalize this somehow? $policy = PhabricatorPolicies::POLICY_USER; } if ($policy == PhabricatorPolicies::POLICY_PUBLIC) { // If the object is set to "public" but that policy is disabled for this // install, restrict the policy to "user". if (!PhabricatorEnv::getEnvConfig('policy.allow-public')) { $policy = PhabricatorPolicies::POLICY_USER; } // If the object is set to "public" but the capability is not a public // capability, restrict the policy to "user". $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { $policy = PhabricatorPolicies::POLICY_USER; } } $viewer = $this->viewer; if ($viewer->isOmnipotent()) { return true; } if ($object->hasAutomaticCapability($capability, $viewer)) { return true; } switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return true; case PhabricatorPolicies::POLICY_USER: if ($viewer->getPHID()) { return true; } else { $this->rejectObject($object, $policy, $capability); } break; case PhabricatorPolicies::POLICY_ADMIN: if ($viewer->getIsAdmin()) { return true; } else { $this->rejectObject($object, $policy, $capability); } break; case PhabricatorPolicies::POLICY_NOONE: $this->rejectObject($object, $policy, $capability); break; default: $type = phid_get_type($policy); if ($type == PhabricatorProjectPHIDTypeProject::TYPECONST) { if (!empty($this->userProjects[$viewer->getPHID()][$policy])) { return true; } else { $this->rejectObject($object, $policy, $capability); } } else if ($type == PhabricatorPeoplePHIDTypeUser::TYPECONST) { if ($viewer->getPHID() == $policy) { return true; } 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); } } return false; } public function rejectObject( PhabricatorPolicyInterface $object, $policy, $capability) { if (!$this->raisePolicyExceptions) { return; } $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); $rejection = null; if ($capobj) { $rejection = $capobj->describeCapabilityRejection(); $capability_name = $capobj->getCapabilityName(); } else { $capability_name = $capability; } if (!$rejection) { // We couldn't find the capability object, or it doesn't provide a // tailored rejection string. $rejection = pht( 'You do not have the required capability ("%s") to do whatever you '. 'are trying to do.', $capability); } $more = PhabricatorPolicy::getPolicyExplanation($this->viewer, $policy); $exceptions = $object->describeAutomaticCapability($capability); $details = array_filter(array_merge(array($more), (array)$exceptions)); // NOTE: Not every policy object has a PHID, just pull an arbitrary // "unknown object" handle if this fails. We're just using this to provide // a better error message if we can. $phid = '?'; if (($object instanceof PhabricatorLiskDAO) || (method_exists($object, 'getPHID'))) { try { $phid = $object->getPHID(); } catch (Exception $ignored) { // Ignore. } } $handle = id(new PhabricatorHandleQuery()) ->setViewer($this->viewer) ->withPHIDs(array($phid)) ->executeOne(); $object_name = pht( '%s %s', $handle->getTypeName(), $handle->getObjectName()); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if ($is_serious) { $title = pht( 'Access Denied: %s', $object_name); } else { $title = pht( 'You Shall Not Pass: %s', $object_name); } $full_message = pht( '[%s] (%s) %s // %s', $title, $capability_name, $rejection, implode(' ', $details)); $exception = id(new PhabricatorPolicyException($full_message)) ->setTitle($title) ->setRejection($rejection) ->setCapabilityName($capability_name) ->setMoreInfo($details); 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) { + if ($action === PhabricatorPolicy::ACTION_ALLOW) { return true; } return false; } } diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php index e14f49c6f9..007aa9d99b 100644 --- a/src/applications/policy/storage/PhabricatorPolicy.php +++ b/src/applications/policy/storage/PhabricatorPolicy.php @@ -1,284 +1,284 @@ true, self::CONFIG_SERIALIZATION => array( 'rules' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPolicyPHIDTypePolicy::TYPECONST); } public static function newFromPolicyAndHandle( $policy_identifier, PhabricatorObjectHandle $handle = null) { $is_global = PhabricatorPolicyQuery::isGlobalPolicy($policy_identifier); if ($is_global) { return PhabricatorPolicyQuery::getGlobalPolicy($policy_identifier); } if (!$handle) { throw new Exception( "Policy identifier is an object PHID ('{$policy_identifier}'), but no ". "object handle was provided. A handle must be provided for object ". "policies."); } $handle_phid = $handle->getPHID(); if ($policy_identifier != $handle_phid) { throw new Exception( "Policy identifier is an object PHID ('{$policy_identifier}'), but ". "the provided handle has a different PHID ('{$handle_phid}'). The ". "handle must correspond to the policy identifier."); } $policy = id(new PhabricatorPolicy()) ->setPHID($policy_identifier) ->setHref($handle->getURI()); $phid_type = phid_get_type($policy_identifier); switch ($phid_type) { case PhabricatorProjectPHIDTypeProject::TYPECONST: $policy->setType(PhabricatorPolicyType::TYPE_PROJECT); $policy->setName($handle->getName()); break; default: $policy->setType(PhabricatorPolicyType::TYPE_MASKED); $policy->setName($handle->getFullName()); break; } $policy->makeEphemeral(); return $policy; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setHref($href) { $this->href = $href; return $this; } public function getHref() { return $this->href; } public function getIcon() { switch ($this->getType()) { case PhabricatorPolicyType::TYPE_GLOBAL: static $map = array( PhabricatorPolicies::POLICY_PUBLIC => 'policy-public', PhabricatorPolicies::POLICY_USER => 'policy-all', PhabricatorPolicies::POLICY_ADMIN => 'policy-admin', PhabricatorPolicies::POLICY_NOONE => 'policy-noone', ); return idx($map, $this->getPHID(), 'policy-unknown'); break; case PhabricatorPolicyType::TYPE_PROJECT: return 'policy-project'; break; case PhabricatorPolicyType::TYPE_MASKED: return 'policy-custom'; break; default: return 'policy-unknown'; break; } } public function getSortKey() { return sprintf( '%02d%s', PhabricatorPolicyType::getPolicyTypeOrder($this->getType()), $this->getSortName()); } private function getSortName() { if ($this->getType() == PhabricatorPolicyType::TYPE_GLOBAL) { static $map = array( PhabricatorPolicies::POLICY_PUBLIC => 0, PhabricatorPolicies::POLICY_USER => 1, PhabricatorPolicies::POLICY_ADMIN => 2, PhabricatorPolicies::POLICY_NOONE => 3, ); return idx($map, $this->getPHID()); } return $this->getName(); } public static function getPolicyExplanation( PhabricatorUser $viewer, $policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return pht('This object is public.'); case PhabricatorPolicies::POLICY_USER: return pht('Logged in users can take this action.'); case PhabricatorPolicies::POLICY_ADMIN: return pht('Administrators can take this action.'); case PhabricatorPolicies::POLICY_NOONE: return pht('By default, no one can take this action.'); default: $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($policy)) ->executeOne(); $type = phid_get_type($policy); if ($type == PhabricatorProjectPHIDTypeProject::TYPECONST) { return pht( 'Members of the project "%s" can take this action.', $handle->getFullName()); } else if ($type == PhabricatorPeoplePHIDTypeUser::TYPECONST) { return pht( '%s can take this action.', $handle->getFullName()); } else { return pht( 'This object has an unknown or invalid policy setting ("%s").', $policy); } } } public function getFullName() { switch ($this->getType()) { case PhabricatorPolicyType::TYPE_PROJECT: return pht('Project: %s', $this->getName()); case PhabricatorPolicyType::TYPE_MASKED: return pht('Other: %s', $this->getName()); default: return $this->getName(); } } public function renderDescription($icon=false) { $img = null; if ($icon) { $img = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_STATUS) ->setSpriteIcon($this->getIcon()); } if ($this->getHref()) { $desc = phutil_tag( 'a', array( 'href' => $this->getHref(), 'class' => 'policy-link', ), array( $img, $this->getName(), )); } else { if ($img) { $desc = array($img, $this->getName()); } else { $desc = $this->getName(); } } switch ($this->getType()) { case PhabricatorPolicyType::TYPE_PROJECT: return pht('%s (Project)', $desc); case PhabricatorPolicyType::TYPE_MASKED: return pht( '%s (You do not have permission to view policy details.)', $desc); default: return $desc; } } /** * Return a list of custom rule classes (concrete subclasses of * @{class:PhabricatorPolicyRule}) this policy uses. * * @return list 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 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); } }