Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2895953
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
41 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment