Page MenuHomePhorge

No OneTemporary

diff --git a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
index 96ad88d2a4..2cea894cfc 100644
--- a/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
+++ b/src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
@@ -1,466 +1,538 @@
<?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'));
}
+
+ /**
+ * Test bulk checks of extended policies.
+ *
+ * This is testing an issue with extended policy filtering which allowed
+ * unusual inputs to slip objects through the filter. See D14993.
+ */
+ public function testBulkExtendedPolicies() {
+ $object1 = $this->buildObject(PhabricatorPolicies::POLICY_USER)
+ ->setPHID('PHID-TEST-1');
+ $object2 = $this->buildObject(PhabricatorPolicies::POLICY_USER)
+ ->setPHID('PHID-TEST-2');
+ $object3 = $this->buildObject(PhabricatorPolicies::POLICY_USER)
+ ->setPHID('PHID-TEST-3');
+
+ $extended = $this->buildObject(PhabricatorPolicies::POLICY_ADMIN)
+ ->setPHID('PHID-TEST-999');
+
+ $object1->setExtendedPolicies(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW => array(
+ array(
+ $extended,
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ),
+ ),
+ ),
+ ));
+
+ $object2->setExtendedPolicies(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW => array(
+ array($extended, PhabricatorPolicyCapability::CAN_VIEW),
+ ),
+ ));
+
+ $object3->setExtendedPolicies(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW => array(
+ array(
+ $extended,
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ),
+ ),
+ ),
+ ));
+
+ $user = $this->buildUser('user');
+
+ $visible = id(new PhabricatorPolicyFilter())
+ ->setViewer($user)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ ))
+ ->apply(
+ array(
+ $object1,
+ $object2,
+ $object3,
+ ));
+
+ $this->assertEqual(array(), $visible);
+ }
+
+
/**
* 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 PhutilClassMapQuery())
->setAncestorClass('PhabricatorPolicyAwareQuery')
->execute();
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,
+ PhabricatorPolicyCapability::CAN_EDIT,
));
$object->setPolicies(
array(
PhabricatorPolicyCapability::CAN_VIEW => $policy,
+ PhabricatorPolicyCapability::CAN_EDIT => $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/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php
index ded757f305..f70b77f99a 100644
--- a/src/applications/policy/filter/PhabricatorPolicyFilter.php
+++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php
@@ -1,857 +1,857 @@
<?php
final class PhabricatorPolicyFilter extends Phobject {
private $viewer;
private $objects;
private $capabilities;
private $raisePolicyExceptions;
private $userProjects;
private $customPolicies = array();
private $objectPolicies = array();
private $forcedPolicy;
public static function mustRetainCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
if (!self::hasCapability($user, $object, $capability)) {
throw new Exception(
pht(
"You can not make that edit, because it would remove your ability ".
"to '%s' the object.",
$capability));
}
}
public static function requireCapability(
PhabricatorUser $user,
PhabricatorPolicyInterface $object,
$capability) {
$filter = id(new PhabricatorPolicyFilter())
->setViewer($user)
->requireCapabilities(array($capability))
->raisePolicyExceptions(true)
->apply(array($object));
}
/**
* Perform a capability check, acting as though an object had a specific
* policy. This is primarily used to check if a policy is valid (for example,
* to prevent users from editing away their ability to edit an object).
*
* Specifically, a check like this:
*
* PhabricatorPolicyFilter::requireCapabilityWithForcedPolicy(
* $viewer,
* $object,
* PhabricatorPolicyCapability::CAN_EDIT,
* $potential_new_policy);
*
* ...will throw a @{class:PhabricatorPolicyException} if the new policy would
* remove the user's ability to edit the object.
*
* @param PhabricatorUser The viewer to perform a policy check for.
* @param PhabricatorPolicyInterface The object to perform a policy check on.
* @param string Capability to test.
* @param string Perform the test as though the object has this
* policy instead of the policy it actually has.
* @return void
*/
public static function requireCapabilityWithForcedPolicy(
PhabricatorUser $viewer,
PhabricatorPolicyInterface $object,
$capability,
$forced_policy) {
id(new PhabricatorPolicyFilter())
->setViewer($viewer)
->requireCapabilities(array($capability))
->raisePolicyExceptions(true)
->forcePolicy($forced_policy)
->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 forcePolicy($forced_policy) {
$this->forcedPolicy = $forced_policy;
return $this;
}
public function apply(array $objects) {
assert_instances_of($objects, 'PhabricatorPolicyInterface');
$viewer = $this->viewer;
$capabilities = $this->capabilities;
if (!$viewer || !$capabilities) {
throw new PhutilInvalidStateException('setViewer', 'requireCapabilities');
}
// 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();
$need_objpolicies = array();
foreach ($objects as $key => $object) {
$object_capabilities = $object->getCapabilities();
foreach ($capabilities as $capability) {
if (!in_array($capability, $object_capabilities)) {
throw new Exception(
pht(
"Testing for capability '%s' on an object which does ".
"not have that capability!",
$capability));
}
$policy = $this->getObjectPolicy($object, $capability);
if (PhabricatorPolicyQuery::isObjectPolicy($policy)) {
$need_objpolicies[$policy][] = $object;
continue;
}
$type = phid_get_type($policy);
if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
$need_projects[$policy] = $policy;
continue;
}
if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
$need_policies[$policy][] = $object;
continue;
}
}
}
if ($need_objpolicies) {
$this->loadObjectPolicies($need_objpolicies);
}
if ($need_policies) {
$this->loadCustomPolicies($need_policies);
}
// 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) {
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;
}
// If we survied the primary checks, apply extended checks to objects
// with extended policies.
$results = array();
$extended = array();
foreach ($filtered as $key => $object) {
if ($object instanceof PhabricatorExtendedPolicyInterface) {
$extended[$key] = $object;
} else {
$results[$key] = $object;
}
}
if ($extended) {
$results += $this->applyExtendedPolicyChecks($extended);
// Put results back in the original order.
$results = array_select_keys($results, array_keys($filtered));
}
return $results;
}
private function applyExtendedPolicyChecks(array $extended_objects) {
$viewer = $this->viewer;
$filter_capabilities = $this->capabilities;
// Iterate over the objects we need to filter and pull all the nonempty
// policies into a flat, structured list.
$all_structs = array();
foreach ($extended_objects as $key => $extended_object) {
foreach ($filter_capabilities as $extended_capability) {
$extended_policies = $extended_object->getExtendedPolicy(
$extended_capability,
$viewer);
if (!$extended_policies) {
continue;
}
foreach ($extended_policies as $extended_policy) {
list($object, $capabilities) = $extended_policy;
// Build a description of the capabilities we need to check. This
// will be something like `"view"`, or `"edit view"`, or possibly
// a longer string with custom capabilities. Later, group the objects
// up into groups which need the same capabilities tested.
$capabilities = (array)$capabilities;
$capabilities = array_fuse($capabilities);
ksort($capabilities);
$group = implode(' ', $capabilities);
$struct = array(
'key' => $key,
'for' => $extended_capability,
'object' => $object,
'capabilities' => $capabilities,
'group' => $group,
);
$all_structs[] = $struct;
}
}
}
// Extract any bare PHIDs from the structs; we need to load these objects.
// These are objects which are required in order to perform an extended
// policy check but which the original viewer did not have permission to
// see (they presumably had other permissions which let them load the
// object in the first place).
$all_phids = array();
foreach ($all_structs as $idx => $struct) {
$object = $struct['object'];
if (is_string($object)) {
$all_phids[$object] = $object;
}
}
// If we have some bare PHIDs, we need to load the corresponding objects.
if ($all_phids) {
// We can pull these with the omnipotent user because we're immediately
// filtering them.
$ref_objects = id(new PhabricatorObjectQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs($all_phids)
->execute();
$ref_objects = mpull($ref_objects, null, 'getPHID');
} else {
$ref_objects = array();
}
// Group the list of checks by the capabilities we need to check.
$groups = igroup($all_structs, 'group');
foreach ($groups as $structs) {
$head = head($structs);
// All of the items in each group are checking for the same capabilities.
$capabilities = $head['capabilities'];
$key_map = array();
$objects_in = array();
foreach ($structs as $struct) {
$extended_key = $struct['key'];
- if (empty($extended_objects[$key])) {
+ if (empty($extended_objects[$extended_key])) {
// If this object has already been rejected by an earlier filtering
// pass, we don't need to do any tests on it.
continue;
}
$object = $struct['object'];
if (is_string($object)) {
// This is really a PHID, so look it up.
$object_phid = $object;
if (empty($ref_objects[$object_phid])) {
// We weren't able to load the corresponding object, so just
// reject this result outright.
- $reject = $extended_objects[$key];
- unset($extended_objects[$key]);
+ $reject = $extended_objects[$extended_key];
+ unset($extended_objects[$extended_key]);
// TODO: This could be friendlier.
$this->rejectObject($reject, false, '<bad-ref>');
continue;
}
$object = $ref_objects[$object_phid];
}
$phid = $object->getPHID();
$key_map[$phid][] = $extended_key;
$objects_in[$phid] = $object;
}
if ($objects_in) {
$objects_out = $this->executeExtendedPolicyChecks(
$viewer,
$capabilities,
$objects_in,
$key_map);
$objects_out = mpull($objects_out, null, 'getPHID');
} else {
$objects_out = array();
}
// If any objects were removed by filtering, we're going to reject all
// of the original objects which needed them.
foreach ($objects_in as $phid => $object_in) {
if (isset($objects_out[$phid])) {
// This object survived filtering, so we don't need to throw any
// results away.
continue;
}
foreach ($key_map[$phid] as $extended_key) {
if (empty($extended_objects[$extended_key])) {
// We've already rejected this object, so we don't need to reject
// it again.
continue;
}
$reject = $extended_objects[$extended_key];
unset($extended_objects[$extended_key]);
// TODO: This isn't as user-friendly as it could be. It's possible
// that we're rejecting this object for multiple capability/policy
// failures, though.
$this->rejectObject($reject, false, '<extended>');
}
}
}
return $extended_objects;
}
private function executeExtendedPolicyChecks(
PhabricatorUser $viewer,
array $capabilities,
array $objects,
array $key_map) {
// Do crude cycle detection by seeing if we have a huge stack depth.
// Although more sophisticated cycle detection is possible in theory,
// it is difficult with hierarchical objects like subprojects. Many other
// checks make it difficult to create cycles normally, so just do a
// simple check here to limit damage.
static $depth;
$depth++;
if ($depth > 32) {
foreach ($objects as $key => $object) {
$this->rejectObject($objects[$key], false, '<cycle>');
unset($objects[$key]);
continue;
}
}
if (!$objects) {
return array();
}
$caught = null;
try {
$result = id(new PhabricatorPolicyFilter())
->setViewer($viewer)
->requireCapabilities($capabilities)
->apply($objects);
} catch (Exception $ex) {
$caught = $ex;
}
$depth--;
if ($caught) {
throw $caught;
}
return $result;
}
private function checkCapability(
PhabricatorPolicyInterface $object,
$capability) {
$policy = $this->getObjectPolicy($object, $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 instanceof PhabricatorSpacesInterface) {
$space_phid = $object->getSpacePHID();
if (!$this->canViewerSeeObjectsInSpace($viewer, $space_phid)) {
$this->rejectObjectFromSpace($object, $space_phid);
return false;
}
}
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:
if (PhabricatorPolicyQuery::isObjectPolicy($policy)) {
if ($this->checkObjectPolicy($policy, $object)) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
break;
}
}
$type = phid_get_type($policy);
if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) {
if (!empty($this->userProjects[$viewer->getPHID()][$policy])) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) {
if ($viewer->getPHID() == $policy) {
return true;
} else {
$this->rejectObject($object, $policy, $capability);
}
} else if ($type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) {
if ($this->checkCustomPolicy($policy, $object)) {
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;
}
if ($this->viewer->isOmnipotent()) {
// Never raise policy exceptions for the omnipotent viewer. Although we
// will never normally issue a policy rejection for the omnipotent
// viewer, we can end up here when queries blanket reject objects that
// have failed to load, without distinguishing between nonexistent and
// nonvisible objects.
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));
$access_denied = $this->renderAccessDenied($object);
$full_message = pht(
'[%s] (%s) %s // %s',
$access_denied,
$capability_name,
$rejection,
implode(' ', $details));
$exception = id(new PhabricatorPolicyException($full_message))
->setTitle($access_denied)
->setObjectPHID($object->getPHID())
->setRejection($rejection)
->setCapability($capability)
->setCapabilityName($capability_name)
->setMoreInfo($details);
throw $exception;
}
private function loadObjectPolicies(array $map) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$rules = PhabricatorPolicyQuery::getObjectPolicyRules(null);
// Make sure we have clean, empty policy rule objects.
foreach ($rules as $key => $rule) {
$rules[$key] = clone $rule;
}
$results = array();
foreach ($map as $key => $object_list) {
$rule = idx($rules, $key);
if (!$rule) {
continue;
}
foreach ($object_list as $object_key => $object) {
if (!$rule->canApplyToObject($object)) {
unset($object_list[$object_key]);
}
}
$rule->willApplyRules($viewer, array(), $object_list);
$results[$key] = $rule;
}
$this->objectPolicies[$viewer_phid] = $results;
}
private function loadCustomPolicies(array $map) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$custom_policies = id(new PhabricatorPolicyQuery())
->setViewer($viewer)
->withPHIDs(array_keys($map))
->execute();
$custom_policies = mpull($custom_policies, null, 'getPHID');
$classes = array();
$values = array();
$objects = array();
foreach ($custom_policies as $policy_phid => $policy) {
foreach ($policy->getCustomRuleClasses() as $class) {
$classes[$class] = $class;
$values[$class][] = $policy->getCustomRuleValues($class);
foreach (idx($map, $policy_phid, array()) as $object) {
$objects[$class][] = $object;
}
}
}
foreach ($classes as $class => $ignored) {
$rule_object = newv($class, array());
// Filter out any objects which the rule can't apply to.
$target_objects = idx($objects, $class, array());
foreach ($target_objects as $key => $target_object) {
if (!$rule_object->canApplyToObject($target_object)) {
unset($target_objects[$key]);
}
}
$rule_object->willApplyRules(
$viewer,
array_mergev($values[$class]),
$target_objects);
$classes[$class] = $rule_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 checkObjectPolicy(
$policy_phid,
PhabricatorPolicyInterface $object) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$rule = idx($this->objectPolicies[$viewer_phid], $policy_phid);
if (!$rule) {
return false;
}
if (!$rule->canApplyToObject($object)) {
return false;
}
return $rule->applyRule($viewer, null, $object);
}
private function checkCustomPolicy(
$policy_phid,
PhabricatorPolicyInterface $object) {
$viewer = $this->viewer;
$viewer_phid = $viewer->getPHID();
$policy = idx($this->customPolicies[$viewer_phid], $policy_phid);
if (!$policy) {
// Reject, this policy is bogus.
return false;
}
$objects = $policy->getRuleObjects();
$action = null;
foreach ($policy->getRules() as $rule) {
if (!is_array($rule)) {
// Reject, this policy rule is invalid.
return false;
}
$rule_object = idx($objects, idx($rule, 'rule'));
if (!$rule_object) {
// Reject, this policy has a bogus rule.
return false;
}
if (!$rule_object->canApplyToObject($object)) {
// Reject, this policy rule can't be applied to the given object.
return false;
}
// If the user matches this rule, use this action.
if ($rule_object->applyRule($viewer, idx($rule, 'value'), $object)) {
$action = idx($rule, 'action');
break;
}
}
if ($action === null) {
$action = $policy->getDefaultAction();
}
if ($action === PhabricatorPolicy::ACTION_ALLOW) {
return true;
}
return false;
}
private function getObjectPolicy(
PhabricatorPolicyInterface $object,
$capability) {
if ($this->forcedPolicy) {
return $this->forcedPolicy;
} else {
return $object->getPolicy($capability);
}
}
private function renderAccessDenied(PhabricatorPolicyInterface $object) {
// NOTE: Not every type of policy object has a real PHID; just load an
// empty handle if a real PHID isn't available.
$phid = nonempty($object->getPHID(), PhabricatorPHIDConstants::PHID_VOID);
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->viewer)
->withPHIDs(array($phid))
->executeOne();
$object_name = $handle->getObjectName();
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
$access_denied = pht(
'Access Denied: %s',
$object_name);
} else {
$access_denied = pht(
'You Shall Not Pass: %s',
$object_name);
}
return $access_denied;
}
private function canViewerSeeObjectsInSpace(
PhabricatorUser $viewer,
$space_phid) {
$spaces = PhabricatorSpacesNamespaceQuery::getAllSpaces();
// If there are no spaces, everything exists in an implicit default space
// with no policy controls. This is the default state.
if (!$spaces) {
if ($space_phid !== null) {
return false;
} else {
return true;
}
}
if ($space_phid === null) {
$space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
} else {
$space = idx($spaces, $space_phid);
}
if (!$space) {
return false;
}
// This may be more involved later, but for now being able to see the
// space is equivalent to being able to see everything in it.
return self::hasCapability(
$viewer,
$space,
PhabricatorPolicyCapability::CAN_VIEW);
}
private function rejectObjectFromSpace(
PhabricatorPolicyInterface $object,
$space_phid) {
if (!$this->raisePolicyExceptions) {
return;
}
if ($this->viewer->isOmnipotent()) {
return;
}
$access_denied = $this->renderAccessDenied($object);
$rejection = pht(
'This object is in a space you do not have permission to access.');
$full_message = pht('[%s] %s', $access_denied, $rejection);
$exception = id(new PhabricatorPolicyException($full_message))
->setTitle($access_denied)
->setObjectPHID($object->getPHID())
->setRejection($rejection)
->setCapability(PhabricatorPolicyCapability::CAN_VIEW);
throw $exception;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Jan 19 2025, 22:20 (6 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129328
Default Alt Text
(41 KB)

Event Timeline