diff --git a/src/applications/differential/capability/DifferentialCapabilityDefaultView.php b/src/applications/differential/capability/DifferentialCapabilityDefaultView.php index c33fdc7a03..a0dbd99e5f 100644 --- a/src/applications/differential/capability/DifferentialCapabilityDefaultView.php +++ b/src/applications/differential/capability/DifferentialCapabilityDefaultView.php @@ -1,16 +1,20 @@ application = $data['application']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $application = id(new PhabricatorApplicationQuery()) ->setViewer($user) ->withClasses(array($this->application)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$application) { return new Aphront404Response(); } $title = $application->getName(); $view_uri = $this->getApplicationURI('view/'.get_class($application).'/'); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($application) ->execute(); if ($request->isFormPost()) { $result = array(); foreach ($application->getCapabilities() as $capability) { $old = $application->getPolicy($capability); $new = $request->getStr('policy:'.$capability); if ($old == $new) { // No change to the setting. continue; } if (empty($policies[$new])) { // Can't set the policy to something invalid. continue; } - if ($new == PhabricatorPolicies::POLICY_PUBLIC && - $capability != PhabricatorPolicyCapability::CAN_VIEW) { - // Can't set policies other than "view" to public. + $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); + if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { + // Can't set non-public policies to public. continue; } $result[$capability] = $new; } if ($result) { $key = 'phabricator.application-settings'; $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); $value = $config_entry->getValue(); $phid = $application->getPHID(); if (empty($value[$phid])) { $value[$application->getPHID()] = array(); } if (empty($value[$phid]['policy'])) { $value[$phid]['policy'] = array(); } $value[$phid]['policy'] = $result + $value[$phid]['policy']; // Don't allow users to make policy edits which would lock them out of // applications, since they would be unable to undo those actions. PhabricatorEnv::overrideConfig($key, $value); PhabricatorPolicyFilter::mustRetainCapability( $user, $application, PhabricatorPolicyCapability::CAN_VIEW); PhabricatorPolicyFilter::mustRetainCapability( $user, $application, PhabricatorPolicyCapability::CAN_EDIT); PhabricatorConfigEditor::storeNewValue( $config_entry, $value, $this->getRequest()); } return id(new AphrontRedirectResponse())->setURI($view_uri); } $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $user, $application); $form = id(new AphrontFormView()) ->setUser($user); foreach ($application->getCapabilities() as $capability) { $label = $application->getCapabilityLabel($capability); $can_edit = $application->isCapabilityEditable($capability); $caption = $application->getCapabilityCaption($capability); if (!$can_edit) { $form->appendChild( id(new AphrontFormStaticControl()) ->setLabel($label) ->setValue(idx($descriptions, $capability)) ->setCaption($caption)); } else { $form->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setCapability($capability) ->setPolicyObject($application) ->setPolicies($policies) ->setLabel($label) ->setName('policy:'.$capability) ->setCaption($caption)); } } $form->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Policies')) ->addCancelButton($view_uri)); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($application->getName()) ->setHref($view_uri)); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName(pht('Edit Policies'))); $header = id(new PHUIHeaderView()) ->setHeader(pht('Edit Policies: %s', $application->getName())); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => $title, 'device' => true, )); } } diff --git a/src/applications/policy/capability/PhabricatorPolicyCapability.php b/src/applications/policy/capability/PhabricatorPolicyCapability.php index ea925f2541..a34e0f76bd 100644 --- a/src/applications/policy/capability/PhabricatorPolicyCapability.php +++ b/src/applications/policy/capability/PhabricatorPolicyCapability.php @@ -1,63 +1,72 @@ setAncestorClass(__CLASS__) ->loadObjects(); $map = mpull($capabilities, null, 'getCapabilityKey'); } return $map; } } diff --git a/src/applications/policy/capability/PhabricatorPolicyCapabilityCanView.php b/src/applications/policy/capability/PhabricatorPolicyCapabilityCanView.php index b9d2df47b9..f56b1d7407 100644 --- a/src/applications/policy/capability/PhabricatorPolicyCapabilityCanView.php +++ b/src/applications/policy/capability/PhabricatorPolicyCapabilityCanView.php @@ -1,18 +1,22 @@ 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(); 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 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 anything other - // than "view", restrict the policy to "user". - if ($capability != PhabricatorPolicyCapability::CAN_VIEW) { + // 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 { // 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; } } diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php index 3db5be8d18..c145a9707b 100644 --- a/src/view/form/control/AphrontFormPolicyControl.php +++ b/src/view/form/control/AphrontFormPolicyControl.php @@ -1,79 +1,83 @@ object = $object; return $this; } public function setPolicies(array $policies) { assert_instances_of($policies, 'PhabricatorPolicy'); $this->policies = $policies; return $this; } public function setCapability($capability) { $this->capability = $capability; $labels = array( PhabricatorPolicyCapability::CAN_VIEW => pht('Visible To'), PhabricatorPolicyCapability::CAN_EDIT => pht('Editable By'), PhabricatorPolicyCapability::CAN_JOIN => pht('Joinable By'), ); $this->setLabel(idx($labels, $this->capability, pht('Unknown Policy'))); return $this; } protected function getCustomControlClass() { return 'aphront-form-control-policy'; } protected function getOptions() { + $capability = $this->capability; + $options = array(); foreach ($this->policies as $policy) { - if (($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) && - ($this->capability != PhabricatorPolicyCapability::CAN_VIEW)) { - // Never expose "Public" for anything except "Can View". - continue; + if ($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) { + // Never expose "Public" for capabilities which don't support it. + $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); + if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { + continue; + } } $type_name = PhabricatorPolicyType::getPolicyTypeName($policy->getType()); $options[$type_name][$policy->getPHID()] = $policy->getFullName(); } return $options; } protected function renderInput() { if (!$this->object) { throw new Exception(pht("Call setPolicyObject() before rendering!")); } if (!$this->capability) { throw new Exception(pht("Call setCapability() before rendering!")); } $policy = $this->object->getPolicy($this->capability); if (!$policy) { // TODO: Make this configurable. $policy = PhabricatorPolicies::POLICY_USER; } $this->setValue($policy); return AphrontFormSelectControl::renderSelectTag( $this->getValue(), $this->getOptions(), array( 'name' => $this->getName(), 'disabled' => $this->getDisabled() ? 'disabled' : null, 'id' => $this->getID(), )); } }