diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php index b2475cdee5..9e7a9d11a9 100644 --- a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php +++ b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php @@ -1,435 +1,466 @@ <?php final class PhabricatorPolicyTestCase extends PhabricatorTestCase { /** * Verify that any user can view an object with POLICY_PUBLIC. */ public function testPublicPolicyEnabled() { $env = PhabricatorEnv::beginScopedEnv(); $env->overrideEnvConfig('policy.allow-public', true); $this->expectVisibility( $this->buildObject(PhabricatorPolicies::POLICY_PUBLIC), array( 'public' => true, 'user' => true, 'admin' => true, ), pht('Public Policy (Enabled in Config)')); } /** * Verify that POLICY_PUBLIC is interpreted as POLICY_USER when public * policies are disallowed. */ public function testPublicPolicyDisabled() { $env = PhabricatorEnv::beginScopedEnv(); $env->overrideEnvConfig('policy.allow-public', false); $this->expectVisibility( $this->buildObject(PhabricatorPolicies::POLICY_PUBLIC), array( 'public' => false, 'user' => true, 'admin' => true, ), pht('Public Policy (Disabled in Config)')); } /** * Verify that any logged-in user can view an object with POLICY_USER, but * logged-out users can not. */ public function testUsersPolicy() { $this->expectVisibility( $this->buildObject(PhabricatorPolicies::POLICY_USER), array( 'public' => false, 'user' => true, 'admin' => true, ), pht('User Policy')); } /** * Verify that only administrators can view an object with POLICY_ADMIN. */ public function testAdminPolicy() { $this->expectVisibility( $this->buildObject(PhabricatorPolicies::POLICY_ADMIN), array( 'public' => false, 'user' => false, 'admin' => true, ), pht('Admin Policy')); } /** * Verify that no one can view an object with POLICY_NOONE. */ public function testNoOnePolicy() { $this->expectVisibility( $this->buildObject(PhabricatorPolicies::POLICY_NOONE), array( 'public' => false, 'user' => false, 'admin' => false, ), pht('No One Policy')); } /** * Test offset-based filtering. */ public function testOffsets() { $results = array( $this->buildObject(PhabricatorPolicies::POLICY_NOONE), $this->buildObject(PhabricatorPolicies::POLICY_NOONE), $this->buildObject(PhabricatorPolicies::POLICY_NOONE), $this->buildObject(PhabricatorPolicies::POLICY_USER), $this->buildObject(PhabricatorPolicies::POLICY_USER), $this->buildObject(PhabricatorPolicies::POLICY_USER), ); $query = new PhabricatorPolicyAwareTestQuery(); $query->setResults($results); $query->setViewer($this->buildUser('user')); $this->assertEqual( 3, count($query->setLimit(3)->setOffset(0)->execute()), pht('Invisible objects are ignored.')); $this->assertEqual( 0, count($query->setLimit(3)->setOffset(3)->execute()), pht('Offset pages through visible objects only.')); $this->assertEqual( 2, count($query->setLimit(3)->setOffset(1)->execute()), pht('Offsets work correctly.')); $this->assertEqual( 2, count($query->setLimit(0)->setOffset(1)->execute()), pht('Offset with no limit works.')); } /** * Test limits. */ public function testLimits() { $results = array( $this->buildObject(PhabricatorPolicies::POLICY_USER), $this->buildObject(PhabricatorPolicies::POLICY_USER), $this->buildObject(PhabricatorPolicies::POLICY_USER), $this->buildObject(PhabricatorPolicies::POLICY_USER), $this->buildObject(PhabricatorPolicies::POLICY_USER), $this->buildObject(PhabricatorPolicies::POLICY_USER), ); $query = new PhabricatorPolicyAwareTestQuery(); $query->setResults($results); $query->setViewer($this->buildUser('user')); $this->assertEqual( 3, count($query->setLimit(3)->setOffset(0)->execute()), pht('Limits work.')); $this->assertEqual( 2, count($query->setLimit(3)->setOffset(4)->execute()), pht('Limit + offset work.')); } /** * Test that omnipotent users bypass policies. */ public function testOmnipotence() { $results = array( $this->buildObject(PhabricatorPolicies::POLICY_NOONE), ); $query = new PhabricatorPolicyAwareTestQuery(); $query->setResults($results); $query->setViewer(PhabricatorUser::getOmnipotentUser()); $this->assertEqual( 1, count($query->execute())); } /** * Test that invalid policies reject viewers of all types. */ public function testRejectInvalidPolicy() { $invalid_policy = 'the duck goes quack'; $object = $this->buildObject($invalid_policy); $this->expectVisibility( $object = $this->buildObject($invalid_policy), array( 'public' => false, 'user' => false, 'admin' => false, ), pht('Invalid Policy')); } /** * Test that extended policies work. */ public function testExtendedPolicies() { $object = $this->buildObject(PhabricatorPolicies::POLICY_USER) ->setPHID('PHID-TEST-1'); $this->expectVisibility( $object, array( 'public' => false, 'user' => true, 'admin' => true, ), pht('No Extended Policy')); // Add a restrictive extended policy. $extended = $this->buildObject(PhabricatorPolicies::POLICY_ADMIN) ->setPHID('PHID-TEST-2'); $object->setExtendedPolicies( array( PhabricatorPolicyCapability::CAN_VIEW => array( array($extended, PhabricatorPolicyCapability::CAN_VIEW), ), )); $this->expectVisibility( $object, array( 'public' => false, 'user' => false, 'admin' => true, ), pht('With Extended Policy')); // Depend on a different capability. $object->setExtendedPolicies( array( PhabricatorPolicyCapability::CAN_VIEW => array( array($extended, PhabricatorPolicyCapability::CAN_EDIT), ), )); $extended->setCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)); $extended->setPolicies( array( PhabricatorPolicyCapability::CAN_EDIT => PhabricatorPolicies::POLICY_NOONE, )); $this->expectVisibility( $object, array( 'public' => false, 'user' => false, 'admin' => false, ), pht('With Extended Policy + Edit')); } /** * Test that cyclic extended policies are arrested properly. */ public function testExtendedPolicyCycles() { $object = $this->buildObject(PhabricatorPolicies::POLICY_USER) ->setPHID('PHID-TEST-1'); $this->expectVisibility( $object, array( 'public' => false, 'user' => true, 'admin' => true, ), pht('No Extended Policy')); // Set a self-referential extended policy on the object. This should // make it fail all policy checks. $object->setExtendedPolicies( array( PhabricatorPolicyCapability::CAN_VIEW => array( array($object, PhabricatorPolicyCapability::CAN_VIEW), ), )); $this->expectVisibility( $object, array( 'public' => false, 'user' => false, 'admin' => false, ), pht('Extended Policy with Cycle')); } /** * An omnipotent user should be able to see even objects with invalid * policies. */ public function testInvalidPolicyVisibleByOmnipotentUser() { $invalid_policy = 'the cow goes moo'; $object = $this->buildObject($invalid_policy); $results = array( $object, ); $query = new PhabricatorPolicyAwareTestQuery(); $query->setResults($results); $query->setViewer(PhabricatorUser::getOmnipotentUser()); $this->assertEqual( 1, count($query->execute())); } public function testAllQueriesBelongToActualApplications() { $queries = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorPolicyAwareQuery') ->loadObjects(); foreach ($queries as $qclass => $query) { $class = $query->getQueryApplicationClass(); if (!$class) { continue; } $this->assertTrue( (bool)PhabricatorApplication::getByClass($class), pht( "Application class '%s' for query '%s'.", $class, $qclass)); } } public function testMultipleCapabilities() { $object = new PhabricatorPolicyTestObject(); $object->setCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )); $object->setPolicies( array( PhabricatorPolicyCapability::CAN_VIEW => PhabricatorPolicies::POLICY_USER, PhabricatorPolicyCapability::CAN_EDIT => PhabricatorPolicies::POLICY_NOONE, )); $filter = new PhabricatorPolicyFilter(); $filter->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )); $filter->setViewer($this->buildUser('user')); $result = $filter->apply(array($object)); $this->assertEqual(array(), $result); } + public function testPolicyStrength() { + $public = PhabricatorPolicyQuery::getGlobalPolicy( + PhabricatorPolicies::POLICY_PUBLIC); + $user = PhabricatorPolicyQuery::getGlobalPolicy( + PhabricatorPolicies::POLICY_USER); + $admin = PhabricatorPolicyQuery::getGlobalPolicy( + PhabricatorPolicies::POLICY_ADMIN); + $noone = PhabricatorPolicyQuery::getGlobalPolicy( + PhabricatorPolicies::POLICY_NOONE); + + $this->assertFalse($public->isStrongerThan($public)); + $this->assertFalse($public->isStrongerThan($user)); + $this->assertFalse($public->isStrongerThan($admin)); + $this->assertFalse($public->isStrongerThan($noone)); + + $this->assertTrue($user->isStrongerThan($public)); + $this->assertFalse($user->isStrongerThan($user)); + $this->assertFalse($user->isStrongerThan($admin)); + $this->assertFalse($user->isStrongerThan($noone)); + + $this->assertTrue($admin->isStrongerThan($public)); + $this->assertTrue($admin->isStrongerThan($user)); + $this->assertFalse($admin->isStrongerThan($admin)); + $this->assertFalse($admin->isStrongerThan($noone)); + + $this->assertTrue($noone->isStrongerThan($public)); + $this->assertTrue($noone->isStrongerThan($user)); + $this->assertTrue($noone->isStrongerThan($admin)); + $this->assertFalse($admin->isStrongerThan($noone)); + } + /** * Test an object for visibility across multiple user specifications. */ private function expectVisibility( PhabricatorPolicyTestObject $object, array $map, $description) { foreach ($map as $spec => $expect) { $viewer = $this->buildUser($spec); $query = new PhabricatorPolicyAwareTestQuery(); $query->setResults(array($object)); $query->setViewer($viewer); $caught = null; $result = null; try { $result = $query->executeOne(); } catch (PhabricatorPolicyException $ex) { $caught = $ex; } if ($expect) { $this->assertEqual( $object, $result, pht('%s with user %s should succeed.', $description, $spec)); } else { $this->assertTrue( $caught instanceof PhabricatorPolicyException, pht('%s with user %s should fail.', $description, $spec)); } } } /** * Build a test object to spec. */ private function buildObject($policy) { $object = new PhabricatorPolicyTestObject(); $object->setCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, )); $object->setPolicies( array( PhabricatorPolicyCapability::CAN_VIEW => $policy, )); return $object; } /** * Build a test user to spec. */ private function buildUser($spec) { $user = new PhabricatorUser(); switch ($spec) { case 'public': break; case 'user': $user->setPHID(1); break; case 'admin': $user->setPHID(1); $user->setIsAdmin(true); break; default: throw new Exception(pht("Unknown user spec '%s'.", $spec)); } return $user; } } diff --git a/src/applications/policy/controller/PhabricatorPolicyExplainController.php b/src/applications/policy/controller/PhabricatorPolicyExplainController.php index 4224319d9c..46dfad3f57 100644 --- a/src/applications/policy/controller/PhabricatorPolicyExplainController.php +++ b/src/applications/policy/controller/PhabricatorPolicyExplainController.php @@ -1,112 +1,183 @@ <?php final class PhabricatorPolicyExplainController extends PhabricatorPolicyController { private $phid; private $capability; public function shouldAllowPublic() { return true; } public function willProcessRequest(array $data) { $this->phid = $data['phid']; $this->capability = $data['capability']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $phid = $this->phid; $capability = $this->capability; $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); if (!$object) { return new Aphront404Response(); } $policies = PhabricatorPolicyQuery::loadPolicies( $viewer, $object); $policy = idx($policies, $capability); if (!$policy) { return new Aphront404Response(); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->executeOne(); $object_uri = nonempty($handle->getURI(), '/'); $explanation = PhabricatorPolicy::getPolicyExplanation( $viewer, $policy->getPHID()); $auto_info = (array)$object->describeAutomaticCapability($capability); $auto_info = array_merge( array($explanation), $auto_info); $auto_info = array_filter($auto_info); foreach ($auto_info as $key => $info) { $auto_info[$key] = phutil_tag('li', array(), $info); } if ($auto_info) { $auto_info = phutil_tag('ul', array(), $auto_info); } $capability_name = $capability; $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if ($capobj) { $capability_name = $capobj->getCapabilityName(); } - $space_info = null; - if ($object instanceof PhabricatorSpacesInterface) { - if (PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) { - $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( - $object); - - $handles = $viewer->loadHandles(array($space_phid)); + $dialog = id(new AphrontDialogView()) + ->setUser($viewer) + ->setClass('aphront-access-dialog'); - $space_info = array( - pht( - 'This object is in %s, and can only be seen by users with '. - 'access to that space.', - $handles[$space_phid]->renderLink()), - phutil_tag('br'), - phutil_tag('br'), - ); - } - } + $this->appendSpaceInformation($dialog, $object, $policy, $capability); - $content = array( - $space_info, - pht('Users with the "%s" capability:', $capability_name), - $auto_info, - ); + $intro = pht( + 'Users with the "%s" capability for this object:', + $capability_name); $object_name = pht( '%s %s', $handle->getTypeName(), $handle->getObjectName()); - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setClass('aphront-access-dialog') + return $dialog ->setTitle(pht('Policy Details: %s', $object_name)) - ->appendChild($content) + ->appendParagraph($intro) + ->appendChild($auto_info) ->addCancelButton($object_uri, pht('Done')); + } + + private function appendSpaceInformation( + AphrontDialogView $dialog, + PhabricatorPolicyInterface $object, + PhabricatorPolicy $policy, + $capability) { + $viewer = $this->getViewer(); + + if (!($object instanceof PhabricatorSpacesInterface)) { + return; + } + + if (!PhabricatorSpacesNamespaceQuery::getSpacesExist($viewer)) { + return; + } + + // NOTE: We're intentionally letting users through here, even if they only + // have access to one space. The intent is to help users in "space jail" + // understand who objects they create are visible to: + + $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( + $object); + + $handles = $viewer->loadHandles(array($space_phid)); + $doc_href = PhabricatorEnv::getDoclink('Spaces User Guide'); + + $dialog->appendParagraph( + array( + pht( + 'This object is in %s, and can only be seen or edited by users with '. + 'access to view objects in the space.', + $handles[$space_phid]->renderLink()), + ' ', + phutil_tag( + 'strong', + array(), + phutil_tag( + 'a', + array( + 'href' => $doc_href, + 'target' => '_blank', + ), + pht('Learn More'))), + )); + + $spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($viewer); + $space = idx($spaces, $space_phid); + if (!$space) { + return; + } + + $space_policies = PhabricatorPolicyQuery::loadPolicies($viewer, $space); + $space_policy = idx($space_policies, PhabricatorPolicyCapability::CAN_VIEW); + if (!$space_policy) { + return; + } + + $space_explanation = PhabricatorPolicy::getPolicyExplanation( + $viewer, + $space_policy->getPHID()); + $items = array(); + $items[] = $space_explanation; + + foreach ($items as $key => $item) { + $items[$key] = phutil_tag('li', array(), $item); + } + + $dialog->appendParagraph(pht('Users who can see objects in this space:')); + $dialog->appendChild(phutil_tag('ul', array(), $items)); + + $view_capability = PhabricatorPolicyCapability::CAN_VIEW; + if ($capability == $view_capability) { + $stronger = $space_policy->isStrongerThan($policy); + if ($stronger) { + $dialog->appendParagraph( + pht( + 'The space this object is in has a more restrictive view '. + 'policy ("%s") than the object does ("%s"), so the space\'s '. + 'view policy is shown as a hint instead of the object policy.', + $space_policy->getShortName(), + $policy->getShortName())); + } + } - return id(new AphrontDialogResponse())->setDialog($dialog); + $dialog->appendParagraph( + pht( + 'After a user passes space policy checks, they must still pass '. + 'object policy checks.')); } } diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php index e01e0dd416..c23858b0a8 100644 --- a/src/applications/policy/storage/PhabricatorPolicy.php +++ b/src/applications/policy/storage/PhabricatorPolicy.php @@ -1,400 +1,454 @@ <?php final class PhabricatorPolicy extends PhabricatorPolicyDAO implements PhabricatorPolicyInterface, PhabricatorDestructibleInterface { const ACTION_ALLOW = 'allow'; const ACTION_DENY = 'deny'; private $name; private $shortName; private $type; private $href; private $workflow; private $icon; protected $rules = array(); protected $defaultAction = self::ACTION_DENY; private $ruleObjects = self::ATTACHABLE; protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'rules' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'defaultAction' => 'text32', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), ), ) + 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); } $policy = PhabricatorPolicyQuery::getObjectPolicy($policy_identifier); if ($policy) { return $policy; } if (!$handle) { throw new Exception( pht( "Policy identifier is an object PHID ('%s'), but no object handle ". "was provided. A handle must be provided for object policies.", $policy_identifier)); } $handle_phid = $handle->getPHID(); if ($policy_identifier != $handle_phid) { throw new Exception( pht( "Policy identifier is an object PHID ('%s'), but the provided ". "handle has a different PHID ('%s'). The handle must correspond ". "to the policy identifier.", $policy_identifier, $handle_phid)); } $policy = id(new PhabricatorPolicy()) ->setPHID($policy_identifier) ->setHref($handle->getURI()); $phid_type = phid_get_type($policy_identifier); switch ($phid_type) { case PhabricatorProjectProjectPHIDType::TYPECONST: $policy->setType(PhabricatorPolicyType::TYPE_PROJECT); $policy->setName($handle->getName()); break; case PhabricatorPeopleUserPHIDType::TYPECONST: $policy->setType(PhabricatorPolicyType::TYPE_USER); $policy->setName($handle->getFullName()); break; case PhabricatorPolicyPHIDTypePolicy::TYPECONST: // TODO: This creates a weird handle-based version of a rule policy. // It behaves correctly, but can't be applied since it doesn't have // any rules. It is used to render transactions, and might need some // cleanup. 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() { if (!$this->type) { return PhabricatorPolicyType::TYPE_CUSTOM; } return $this->type; } public function setName($name) { $this->name = $name; return $this; } public function getName() { if (!$this->name) { return pht('Custom Policy'); } return $this->name; } public function setShortName($short_name) { $this->shortName = $short_name; return $this; } public function getShortName() { if ($this->shortName) { return $this->shortName; } return $this->getName(); } public function setHref($href) { $this->href = $href; return $this; } public function getHref() { return $this->href; } public function setWorkflow($workflow) { $this->workflow = $workflow; return $this; } public function getWorkflow() { 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( PhabricatorPolicies::POLICY_PUBLIC => 'fa-globe', PhabricatorPolicies::POLICY_USER => 'fa-users', PhabricatorPolicies::POLICY_ADMIN => 'fa-eye', PhabricatorPolicies::POLICY_NOONE => 'fa-ban', ); return idx($map, $this->getPHID(), 'fa-question-circle'); case PhabricatorPolicyType::TYPE_USER: return 'fa-user'; case PhabricatorPolicyType::TYPE_PROJECT: return 'fa-briefcase'; case PhabricatorPolicyType::TYPE_CUSTOM: case PhabricatorPolicyType::TYPE_MASKED: return 'fa-certificate'; default: return 'fa-question-circle'; } } 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) { $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy); if ($rule) { return $rule->getPolicyExplanation(); } 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 == PhabricatorProjectProjectPHIDType::TYPECONST) { return pht( 'Members of the project "%s" can take this action.', $handle->getFullName()); } else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) { return pht( '%s can take this action.', $handle->getFullName()); } else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { return pht( 'This object has a custom policy controlling who can take this '. 'action.'); } 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()) ->setIconFont($this->getIcon()); } if ($this->getHref()) { $desc = javelin_tag( 'a', array( 'href' => $this->getHref(), 'class' => 'policy-link', 'sigil' => $this->getWorkflow() ? 'workflow' : null, ), 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_CUSTOM: return $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<string> 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<wild> 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); } + /** + * Return `true` if this policy is stronger (more restrictive) than some + * other policy. + * + * Because policies are complicated, determining which policies are + * "stronger" is not trivial. This method uses a very coarse working + * definition of policy strength which is cheap to compute, unambiguous, + * and intuitive in the common cases. + * + * This method returns `true` if the //class// of this policy is stronger + * than the other policy, even if the policies are (or might be) the same in + * practice. For example, "Members of Project X" is considered a stronger + * policy than "All Users", even though "Project X" might (in some rare + * cases) contain every user. + * + * Generally, the ordering here is: + * + * - Public + * - All Users + * - (Everything Else) + * - No One + * + * In the "everything else" bucket, we can't make any broad claims about + * which policy is stronger (and we especially can't make those claims + * cheaply). + * + * Even if we fully evaluated each policy, the two policies might be + * "Members of X" and "Members of Y", each of which permits access to some + * set of unique users. In this case, neither is strictly stronger than + * the other. + * + * @param PhabricatorPolicy Other policy. + * @return bool `true` if this policy is more restrictive than the other + * policy. + */ + public function isStrongerThan(PhabricatorPolicy $other) { + $this_policy = $this->getPHID(); + $other_policy = $other->getPHID(); + + $strengths = array( + PhabricatorPolicies::POLICY_PUBLIC => -2, + PhabricatorPolicies::POLICY_USER => -1, + // (Default policies have strength 0.) + PhabricatorPolicies::POLICY_NOONE => 1, + ); + + $this_strength = idx($strengths, $this->getPHID(), 0); + $other_strength = idx($strengths, $other->getPHID(), 0); + + return ($this_strength > $other_strength); + } + + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // NOTE: We implement policies only so we can comply with the interface. // The actual query skips them, as enforcing policies on policies seems // perilous and isn't currently required by the application. return PhabricatorPolicies::POLICY_PUBLIC; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->delete(); } } diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php index 29aa2d07df..100ee93d4b 100644 --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -1,305 +1,331 @@ <?php final class PHUIHeaderView extends AphrontTagView { const PROPERTY_STATUS = 1; private $objectName; private $header; private $tags = array(); private $image; private $imageURL = null; private $subheader; private $headerColor; private $noBackground; private $bleedHeader; private $properties = array(); private $actionLinks = array(); private $buttonBar = null; private $policyObject; private $epoch; public function setHeader($header) { $this->header = $header; return $this; } public function setObjectName($object_name) { $this->objectName = $object_name; return $this; } public function setNoBackground($nada) { $this->noBackground = $nada; return $this; } public function addTag(PHUITagView $tag) { $this->tags[] = $tag; return $this; } public function setImage($uri) { $this->image = $uri; return $this; } public function setImageURL($url) { $this->imageURL = $url; return $this; } public function setSubheader($subheader) { $this->subheader = $subheader; return $this; } public function setBleedHeader($bleed) { $this->bleedHeader = $bleed; return $this; } public function setHeaderColor($color) { $this->headerColor = $color; return $this; } public function setPolicyObject(PhabricatorPolicyInterface $object) { $this->policyObject = $object; return $this; } public function addProperty($property, $value) { $this->properties[$property] = $value; return $this; } public function addActionLink(PHUIButtonView $button) { $this->actionLinks[] = $button; return $this; } public function setButtonBar(PHUIButtonBarView $bb) { $this->buttonBar = $bb; return $this; } public function setStatus($icon, $color, $name) { $header_class = 'phui-header-status'; if ($color) { $icon = $icon.' '.$color; $header_class = $header_class.'-'.$color; } $img = id(new PHUIIconView()) ->setIconFont($icon); $tag = phutil_tag( 'span', array( 'class' => "{$header_class} plr", ), array( $img, $name, )); return $this->addProperty(self::PROPERTY_STATUS, $tag); } public function setEpoch($epoch) { $age = time() - $epoch; $age = floor($age / (60 * 60 * 24)); if ($age < 1) { $when = pht('Today'); } else if ($age == 1) { $when = pht('Yesterday'); } else { $when = pht('%d Days Ago', $age); } $this->setStatus('fa-clock-o bluegrey', null, pht('Updated %s', $when)); return $this; } protected function getTagName() { return 'div'; } protected function getTagAttributes() { require_celerity_resource('phui-header-view-css'); $classes = array(); $classes[] = 'phui-header-shell'; if ($this->noBackground) { $classes[] = 'phui-header-no-backgound'; } if ($this->bleedHeader) { $classes[] = 'phui-bleed-header'; } if ($this->headerColor) { $classes[] = 'sprite-gradient'; $classes[] = 'gradient-'.$this->headerColor.'-header'; } if ($this->properties || $this->policyObject || $this->subheader) { $classes[] = 'phui-header-tall'; } if ($this->image) { $classes[] = 'phui-header-has-image'; } return array( 'class' => $classes, ); } protected function getTagContent() { $image = null; if ($this->image) { $image = phutil_tag( ($this->imageURL ? 'a' : 'span'), array( 'href' => $this->imageURL, 'class' => 'phui-header-image', 'style' => 'background-image: url('.$this->image.')', ), ' '); } $viewer = $this->getUser(); $header = array(); if ($viewer) { $header[] = id(new PHUISpacesNamespaceContextView()) ->setUser($viewer) ->setObject($this->policyObject); } if ($this->objectName) { $header[] = array( phutil_tag( 'a', array( 'href' => '/'.$this->objectName, ), $this->objectName), ' ', ); } if ($this->actionLinks) { $actions = array(); foreach ($this->actionLinks as $button) { $button->setColor(PHUIButtonView::SIMPLE); $button->addClass(PHUI::MARGIN_SMALL_LEFT); $button->addClass('phui-header-action-link'); $actions[] = $button; } $header[] = phutil_tag( 'div', array( 'class' => 'phui-header-action-links', ), $actions); } if ($this->buttonBar) { $header[] = phutil_tag( 'div', array( 'class' => 'phui-header-action-links', ), $this->buttonBar); } $header[] = $this->header; if ($this->tags) { $header[] = ' '; $header[] = phutil_tag( 'span', array( 'class' => 'phui-header-tags', ), array_interleave(' ', $this->tags)); } if ($this->subheader) { $header[] = phutil_tag( 'div', array( 'class' => 'phui-header-subheader', ), $this->subheader); } if ($this->properties || $this->policyObject) { $property_list = array(); foreach ($this->properties as $type => $property) { switch ($type) { case self::PROPERTY_STATUS: $property_list[] = $property; break; default: throw new Exception(pht('Incorrect Property Passed')); break; } } if ($this->policyObject) { $property_list[] = $this->renderPolicyProperty($this->policyObject); } $header[] = phutil_tag( 'div', array( 'class' => 'phui-header-subheader', ), $property_list); } return array( $image, phutil_tag( 'h1', array( 'class' => 'phui-header-view grouped', ), $header), ); } private function renderPolicyProperty(PhabricatorPolicyInterface $object) { $viewer = $this->getUser(); $policies = PhabricatorPolicyQuery::loadPolicies($viewer, $object); $view_capability = PhabricatorPolicyCapability::CAN_VIEW; $policy = idx($policies, $view_capability); if (!$policy) { return null; } + // If an object is in a Space with a strictly stronger (more restrictive) + // policy, we show the more restrictive policy. This better aligns the + // UI hint with the actual behavior. + + // NOTE: We'll do this even if the viewer has access to only one space, and + // show them information about the existence of spaces if they click + // through. + if ($object instanceof PhabricatorSpacesInterface) { + $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( + $object); + + $spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces($viewer); + $space = idx($spaces, $space_phid); + if ($space) { + $space_policies = PhabricatorPolicyQuery::loadPolicies( + $viewer, + $space); + $space_policy = idx($space_policies, $view_capability); + if ($space_policy) { + if ($space_policy->isStrongerThan($policy)) { + $policy = $space_policy; + } + } + } + } + $phid = $object->getPHID(); $icon = id(new PHUIIconView()) ->setIconFont($policy->getIcon().' bluegrey'); $link = javelin_tag( 'a', array( 'class' => 'policy-link', 'href' => '/policy/explain/'.$phid.'/'.$view_capability.'/', 'sigil' => 'workflow', ), $policy->getShortName()); return array($icon, $link); } }