diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index b15544429d..171f504b7b 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -1,276 +1,276 @@ setViewer($actor) ->withClasses(array('PhabricatorApplicationManiphest')) ->executeOne(); $view_policy = $app->getPolicy(ManiphestCapabilityDefaultView::CAPABILITY); $edit_policy = $app->getPolicy(ManiphestCapabilityDefaultEdit::CAPABILITY); return id(new ManiphestTask()) ->setPriority(ManiphestTaskPriority::getDefaultPriority()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'ccPHIDs' => self::SERIALIZATION_JSON, 'attached' => self::SERIALIZATION_JSON, 'projectPHIDs' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function loadDependsOnTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK); } public function loadDependedOnByTaskPHIDs() { return PhabricatorEdgeQuery::loadDestinationPHIDs( $this->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK); } public function getAttachedPHIDs($type) { return array_keys(idx($this->attached, $type, array())); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(ManiphestPHIDTypeTask::TYPECONST); } public function getCCPHIDs() { return array_values(nonempty($this->ccPHIDs, array())); } public function setProjectPHIDs(array $phids) { $this->projectPHIDs = array_values($phids); $this->projectsNeedUpdate = true; return $this; } public function getProjectPHIDs() { return array_values(nonempty($this->projectPHIDs, array())); } public function setCCPHIDs(array $phids) { $this->ccPHIDs = array_values($phids); $this->subscribersNeedUpdate = true; return $this; } public function setOwnerPHID($phid) { $this->ownerPHID = nonempty($phid, null); $this->subscribersNeedUpdate = true; return $this; } public function setTitle($title) { $this->title = $title; if (!$this->getID()) { $this->originalTitle = $title; } return $this; } public function attachGroupByProjectPHID($phid) { $this->groupByProjectPHID = $phid; return $this; } public function getGroupByProjectPHID() { return $this->assertAttached($this->groupByProjectPHID); } public function save() { if (!$this->mailKey) { $this->mailKey = Filesystem::readRandomCharacters(20); } $result = parent::save(); if ($this->projectsNeedUpdate) { // If we've changed the project PHIDs for this task, update the link // table. ManiphestTaskProject::updateTaskProjects($this); $this->projectsNeedUpdate = false; } if ($this->subscribersNeedUpdate) { // If we've changed the subscriber PHIDs for this task, update the link // table. ManiphestTaskSubscriber::updateTaskSubscribers($this); $this->subscribersNeedUpdate = false; } return $result; } /* -( Markup Interface )--------------------------------------------------- */ /** * @task markup */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digest($this->getMarkupText($field)); $id = $this->getID(); return "maniphest:T{$id}:{$field}:{$hash}"; } /** * @task markup */ public function getMarkupText($field) { return $this->getDescription(); } /** * @task markup */ public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newManiphestMarkupEngine(); } /** * @task markup */ public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return $output; } /** * @task markup */ public function shouldUseMarkupCache($field) { return (bool)$this->getID(); } /* -( Policy Interface )--------------------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { // The owner of a task can always view and edit it. $owner_phid = $this->getOwnerPHID(); if ($owner_phid) { $user_phid = $user->getPHID(); if ($user_phid == $owner_phid) { return true; } } return false; } public function describeAutomaticCapability($capability) { return pht( 'The owner of a task can always view and edit it.'); } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { // Sort of ambiguous who this was intended for; just let them both know. return array_filter( array_unique( array( $this->getAuthorPHID(), $this->getOwnerPHID(), ))); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return PhabricatorEnv::getEnvConfig('maniphest.fields'); } public function getCustomFieldBaseClass() { return 'ManiphestCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php index 1544138ec9..81aaff88db 100644 --- a/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php +++ b/src/applications/policy/__tests__/PhabricatorPolicyDataTestCase.php @@ -1,34 +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, + '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); + } + } diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php index 2ed1dec241..ef6108ceaa 100644 --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -1,319 +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) { + return true; + } + + return false; + } + } diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php index a58c085f54..f810c345f7 100644 --- a/src/applications/policy/query/PhabricatorPolicyQuery.php +++ b/src/applications/policy/query/PhabricatorPolicyQuery.php @@ -1,200 +1,219 @@ viewer = $viewer; return $this; } public function setObject(PhabricatorPolicyInterface $object) { $this->object = $object; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public static function loadPolicies( PhabricatorUser $viewer, 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; } public static function renderPolicyDescriptions( PhabricatorUser $viewer, PhabricatorPolicyInterface $object, $icon = false) { $policies = self::loadPolicies($viewer, $object); foreach ($policies as $capability => $policy) { $policies[$capability] = $policy->renderDescription($icon); } return $policies; } public function execute() { if (!$this->viewer) { throw new Exception('Call setViewer() before execute()!'); } - $results = $this->getGlobalPolicies(); - - 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()); - } - } + 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; } - $results = mpull($results, null, 'getPHID'); + $phids = array_fuse($phids); - $other_policies = array(); - if ($this->object) { - $capabilities = $this->object->getCapabilities(); - foreach ($capabilities as $capability) { - $policy = $this->object->getPolicy($capability); - if (!$policy) { - continue; - } - $other_policies[$policy] = $policy; + $results = array(); + + // First, load global policies. + foreach ($this->getGlobalPolicies() as $phid => $policy) { + if (isset($phids[$phid])) { + $results[$phid] = $policy; + unset($phids[$phid]); } } - // 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 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 = array_diff_key($other_policies, $results); + 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]); + } + } - 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; } public static function isGlobalPolicy($policy) { $globalPolicies = self::getGlobalPolicies(); if (isset($globalPolicies[$policy])) { return true; } return false; } public static function getGlobalPolicy($policy) { if (!self::isGlobalPolicy($policy)) { throw new Exception("Policy '{$policy}' is not a global policy!"); } return idx(self::getGlobalPolicies(), $policy); } private static function getGlobalPolicies() { static $constants = array( PhabricatorPolicies::POLICY_PUBLIC, PhabricatorPolicies::POLICY_USER, PhabricatorPolicies::POLICY_ADMIN, PhabricatorPolicies::POLICY_NOONE, ); $results = array(); foreach ($constants as $constant) { $results[$constant] = id(new PhabricatorPolicy()) ->setType(PhabricatorPolicyType::TYPE_GLOBAL) ->setPHID($constant) ->setName(self::getGlobalPolicyName($constant)) ->makeEphemeral(); } return $results; } private static function getGlobalPolicyName($policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return pht('Public (No Login Required)'); case PhabricatorPolicies::POLICY_USER: return pht('All Users'); case PhabricatorPolicies::POLICY_ADMIN: return pht('Administrators'); case PhabricatorPolicies::POLICY_NOONE: return pht('No One'); default: return pht('Unknown Policy'); } } + 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; + } + } diff --git a/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php b/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php index a4f28aa5dc..7ac490ed28 100644 --- a/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php +++ b/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php @@ -1,44 +1,49 @@ getPHID()]); + foreach ($value as $phid) { + if ($phid == $viewer->getPHID()) { + return true; + } + } + return false; } public function getValueControlType() { return self::CONTROL_TYPE_TOKENIZER; } public function getValueControlTemplate() { return array( 'markup' => new AphrontTokenizerTemplateView(), 'uri' => '/typeahead/common/accounts/', 'placeholder' => pht('Type a user name...'), ); } public function getRuleOrder() { return 100; } public function getValueForStorage($value) { PhutilTypeSpec::newFromString('list')->check($value); return array_values($value); } public function getValueForDisplay(PhabricatorUser $viewer, $value) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($value) ->execute(); return mpull($handles, 'getFullName', 'getPHID'); } } diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php index b1f60f1268..e14f49c6f9 100644 --- a/src/applications/policy/storage/PhabricatorPolicy.php +++ b/src/applications/policy/storage/PhabricatorPolicy.php @@ -1,231 +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); + } + }