@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add support for "Extended Policies"

Summary:
Ref T7703. See that task and inline for a bunch of discussion.

Briefly, when we run implicit policy rules ("to see a revision, you must also be able to see its repository") at query time, they don't apply to other viewers we might check later.

We do this very rarely, but when we do we're often doing it for a bunch of different viewers (for example, in Herald) so I don't want to just reload the object a million times.

Test Plan:
- Added and executed unit tests.
- Wrote a "flag everything" Herald rule, as in the original report in T7703, and no longer got "Unknown Object" flags on revisions.
- Rigged up a lot of cases in the web UI and couldn't find any inconsistencies, although this case is normally very hard to hit.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T7703

Differential Revision: https://secure.phabricator.com/D13104

+470 -8
+6 -1
src/__phutil_library_map__.php
··· 1794 1794 'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php', 1795 1795 'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php', 1796 1796 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', 1797 + 'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php', 1797 1798 'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php', 1798 1799 'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php', 1799 1800 'PhabricatorExternalAccount' => 'applications/people/storage/PhabricatorExternalAccount.php', ··· 3668 3669 'DifferentialDAO', 3669 3670 'PhabricatorTokenReceiverInterface', 3670 3671 'PhabricatorPolicyInterface', 3672 + 'PhabricatorExtendedPolicyInterface', 3671 3673 'PhabricatorFlaggableInterface', 3672 3674 'PhrequentTrackableInterface', 3673 3675 'HarbormasterBuildableInterface', ··· 5698 5700 'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType', 5699 5701 'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 5700 5702 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', 5701 - 'PhabricatorPolicyTestObject' => 'PhabricatorPolicyInterface', 5703 + 'PhabricatorPolicyTestObject' => array( 5704 + 'PhabricatorPolicyInterface', 5705 + 'PhabricatorExtendedPolicyInterface', 5706 + ), 5702 5707 'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', 5703 5708 'PhabricatorPonderApplication' => 'PhabricatorApplication', 5704 5709 'PhabricatorProject' => array(
+44
src/applications/differential/storage/DifferentialRevision.php
··· 4 4 implements 5 5 PhabricatorTokenReceiverInterface, 6 6 PhabricatorPolicyInterface, 7 + PhabricatorExtendedPolicyInterface, 7 8 PhabricatorFlaggableInterface, 8 9 PhrequentTrackableInterface, 9 10 HarbormasterBuildableInterface, ··· 280 281 return $this; 281 282 } 282 283 284 + 285 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 286 + 287 + 283 288 public function getCapabilities() { 284 289 return array( 285 290 PhabricatorPolicyCapability::CAN_VIEW, ··· 325 330 326 331 return $description; 327 332 } 333 + 334 + 335 + /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ 336 + 337 + 338 + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { 339 + $extended = array(); 340 + 341 + switch ($capability) { 342 + case PhabricatorPolicyCapability::CAN_VIEW: 343 + // NOTE: In Differential, an automatic capability on a revision (being 344 + // an author) is sufficient to view it, even if you can not see the 345 + // repository the revision belongs to. We can bail out early in this 346 + // case. 347 + if ($this->hasAutomaticCapability($capability, $viewer)) { 348 + break; 349 + } 350 + 351 + $repository_phid = $this->getRepositoryPHID(); 352 + $repository = $this->getRepository(); 353 + 354 + // Try to use the object if we have it, since it will save us some 355 + // data fetching later on. In some cases, we might not have it. 356 + $repository_ref = nonempty($repository, $repository_phid); 357 + if ($repository_ref) { 358 + $extended[] = array( 359 + $repository_ref, 360 + PhabricatorPolicyCapability::CAN_VIEW, 361 + ); 362 + } 363 + break; 364 + } 365 + 366 + return $extended; 367 + } 368 + 369 + 370 + /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ 371 + 328 372 329 373 public function getUsersToNotifyOfTokenGiven() { 330 374 return array(
+97
src/applications/policy/__tests__/PhabricatorPolicyTestCase.php
··· 190 190 191 191 192 192 /** 193 + * Test that extended policies work. 194 + */ 195 + public function testExtendedPolicies() { 196 + $object = $this->buildObject(PhabricatorPolicies::POLICY_USER) 197 + ->setPHID('PHID-TEST-1'); 198 + 199 + $this->expectVisibility( 200 + $object, 201 + array( 202 + 'public' => false, 203 + 'user' => true, 204 + 'admin' => true, 205 + ), 206 + pht('No Extended Policy')); 207 + 208 + // Add a restrictive extended policy. 209 + $extended = $this->buildObject(PhabricatorPolicies::POLICY_ADMIN) 210 + ->setPHID('PHID-TEST-2'); 211 + $object->setExtendedPolicies( 212 + array( 213 + PhabricatorPolicyCapability::CAN_VIEW => array( 214 + array($extended, PhabricatorPolicyCapability::CAN_VIEW), 215 + ), 216 + )); 217 + 218 + $this->expectVisibility( 219 + $object, 220 + array( 221 + 'public' => false, 222 + 'user' => false, 223 + 'admin' => true, 224 + ), 225 + pht('With Extended Policy')); 226 + 227 + // Depend on a different capability. 228 + $object->setExtendedPolicies( 229 + array( 230 + PhabricatorPolicyCapability::CAN_VIEW => array( 231 + array($extended, PhabricatorPolicyCapability::CAN_EDIT), 232 + ), 233 + )); 234 + 235 + $extended->setCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)); 236 + $extended->setPolicies( 237 + array( 238 + PhabricatorPolicyCapability::CAN_EDIT => 239 + PhabricatorPolicies::POLICY_NOONE, 240 + )); 241 + 242 + $this->expectVisibility( 243 + $object, 244 + array( 245 + 'public' => false, 246 + 'user' => false, 247 + 'admin' => false, 248 + ), 249 + pht('With Extended Policy + Edit')); 250 + } 251 + 252 + 253 + /** 254 + * Test that cyclic extended policies are arrested properly. 255 + */ 256 + public function testExtendedPolicyCycles() { 257 + $object = $this->buildObject(PhabricatorPolicies::POLICY_USER) 258 + ->setPHID('PHID-TEST-1'); 259 + 260 + $this->expectVisibility( 261 + $object, 262 + array( 263 + 'public' => false, 264 + 'user' => true, 265 + 'admin' => true, 266 + ), 267 + pht('No Extended Policy')); 268 + 269 + // Set a self-referential extended policy on the object. This should 270 + // make it fail all policy checks. 271 + $object->setExtendedPolicies( 272 + array( 273 + PhabricatorPolicyCapability::CAN_VIEW => array( 274 + array($object, PhabricatorPolicyCapability::CAN_VIEW), 275 + ), 276 + )); 277 + 278 + $this->expectVisibility( 279 + $object, 280 + array( 281 + 'public' => false, 282 + 'user' => false, 283 + 'admin' => false, 284 + ), 285 + pht('Extended Policy with Cycle')); 286 + } 287 + 288 + /** 193 289 * An omnipotent user should be able to see even objects with invalid 194 290 * policies. 195 291 */ ··· 274 370 $query->setViewer($viewer); 275 371 276 372 $caught = null; 373 + $result = null; 277 374 try { 278 375 $result = $query->executeOne(); 279 376 } catch (PhabricatorPolicyException $ex) {
+23 -5
src/applications/policy/__tests__/PhabricatorPolicyTestObject.php
··· 4 4 * Configurable test object for implementing Policy unit tests. 5 5 */ 6 6 final class PhabricatorPolicyTestObject 7 - implements PhabricatorPolicyInterface { 7 + implements 8 + PhabricatorPolicyInterface, 9 + PhabricatorExtendedPolicyInterface { 10 + 11 + private $phid; 12 + private $capabilities = array(); 13 + private $policies = array(); 14 + private $automaticCapabilities = array(); 15 + private $extendedPolicies = array(); 8 16 9 - private $capabilities = array(); 10 - private $policies = array(); 11 - private $automaticCapabilities = array(); 17 + public function setPHID($phid) { 18 + $this->phid = $phid; 19 + return $this; 20 + } 12 21 13 22 public function getPHID() { 14 - return null; 23 + return $this->phid; 15 24 } 16 25 17 26 public function getCapabilities() { ··· 44 53 45 54 public function describeAutomaticCapability($capability) { 46 55 return null; 56 + } 57 + 58 + public function setExtendedPolicies(array $extended_policies) { 59 + $this->extendedPolicies = $extended_policies; 60 + return $this; 61 + } 62 + 63 + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { 64 + return idx($this->extendedPolicies, $capability, array()); 47 65 } 48 66 49 67 }
+212 -2
src/applications/policy/filter/PhabricatorPolicyFilter.php
··· 196 196 } 197 197 198 198 foreach ($objects as $key => $object) { 199 - $object_capabilities = $object->getCapabilities(); 200 199 foreach ($capabilities as $capability) { 201 200 if (!$this->checkCapability($object, $capability)) { 202 201 // If we're missing any capability, move on to the next object. ··· 208 207 $filtered[$key] = $object; 209 208 } 210 209 211 - return $filtered; 210 + // If we survied the primary checks, apply extended checks to objects 211 + // with extended policies. 212 + $results = array(); 213 + $extended = array(); 214 + foreach ($filtered as $key => $object) { 215 + if ($object instanceof PhabricatorExtendedPolicyInterface) { 216 + $extended[$key] = $object; 217 + } else { 218 + $results[$key] = $object; 219 + } 220 + } 221 + 222 + if ($extended) { 223 + $results += $this->applyExtendedPolicyChecks($extended); 224 + // Put results back in the original order. 225 + $results = array_select_keys($results, array_keys($filtered)); 226 + } 227 + 228 + return $results; 229 + } 230 + 231 + private function applyExtendedPolicyChecks(array $extended_objects) { 232 + // First, we're going to detect cycles and reject any objects which are 233 + // part of a cycle. We don't want to loop forever if an object has a 234 + // self-referential or nonsense policy. 235 + 236 + static $in_flight = array(); 237 + 238 + $all_phids = array(); 239 + foreach ($extended_objects as $key => $object) { 240 + $phid = $object->getPHID(); 241 + if (isset($in_flight[$phid])) { 242 + // TODO: This could be more user-friendly. 243 + $this->rejectObject($extended_objects[$key], false, '<cycle>'); 244 + unset($extended_objects[$key]); 245 + continue; 246 + } 247 + 248 + // We might throw from rejectObject(), so we don't want to actually mark 249 + // anything as in-flight until we survive this entire step. 250 + $all_phids[$phid] = $phid; 251 + } 252 + 253 + foreach ($all_phids as $phid) { 254 + $in_flight[$phid] = true; 255 + } 256 + 257 + $caught = null; 258 + try { 259 + $extended_objects = $this->executeExtendedPolicyChecks($extended_objects); 260 + } catch (Exception $ex) { 261 + $caught = $ex; 262 + } 263 + 264 + foreach ($all_phids as $phid) { 265 + unset($in_flight[$phid]); 266 + } 267 + 268 + if ($caught) { 269 + throw $caught; 270 + } 271 + 272 + return $extended_objects; 273 + } 274 + 275 + private function executeExtendedPolicyChecks(array $extended_objects) { 276 + $viewer = $this->viewer; 277 + $filter_capabilities = $this->capabilities; 278 + 279 + // Iterate over the objects we need to filter and pull all the nonempty 280 + // policies into a flat, structured list. 281 + $all_structs = array(); 282 + foreach ($extended_objects as $key => $extended_object) { 283 + foreach ($filter_capabilities as $extended_capability) { 284 + $extended_policies = $extended_object->getExtendedPolicy( 285 + $extended_capability, 286 + $viewer); 287 + if (!$extended_policies) { 288 + continue; 289 + } 290 + 291 + foreach ($extended_policies as $extended_policy) { 292 + list($object, $capabilities) = $extended_policy; 293 + 294 + // Build a description of the capabilities we need to check. This 295 + // will be something like `"view"`, or `"edit view"`, or possibly 296 + // a longer string with custom capabilities. Later, group the objects 297 + // up into groups which need the same capabilities tested. 298 + $capabilities = (array)$capabilities; 299 + $capabilities = array_fuse($capabilities); 300 + ksort($capabilities); 301 + $group = implode(' ', $capabilities); 302 + 303 + $struct = array( 304 + 'key' => $key, 305 + 'for' => $extended_capability, 306 + 'object' => $object, 307 + 'capabilities' => $capabilities, 308 + 'group' => $group, 309 + ); 310 + 311 + $all_structs[] = $struct; 312 + } 313 + } 314 + } 315 + 316 + // Extract any bare PHIDs from the structs; we need to load these objects. 317 + // These are objects which are required in order to perform an extended 318 + // policy check but which the original viewer did not have permission to 319 + // see (they presumably had other permissions which let them load the 320 + // object in the first place). 321 + $all_phids = array(); 322 + foreach ($all_structs as $idx => $struct) { 323 + $object = $struct['object']; 324 + if (is_string($object)) { 325 + $all_phids[$object] = $object; 326 + } 327 + } 328 + 329 + // If we have some bare PHIDs, we need to load the corresponding objects. 330 + if ($all_phids) { 331 + // We can pull these with the omnipotent user because we're immediately 332 + // filtering them. 333 + $ref_objects = id(new PhabricatorObjectQuery()) 334 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 335 + ->withPHIDs($all_phids) 336 + ->execute(); 337 + $ref_objects = mpull($ref_objects, null, 'getPHID'); 338 + } else { 339 + $ref_objects = array(); 340 + } 341 + 342 + // Group the list of checks by the capabilities we need to check. 343 + $groups = igroup($all_structs, 'group'); 344 + foreach ($groups as $structs) { 345 + $head = head($structs); 346 + 347 + // All of the items in each group are checking for the same capabilities. 348 + $capabilities = $head['capabilities']; 349 + 350 + $key_map = array(); 351 + $objects_in = array(); 352 + foreach ($structs as $struct) { 353 + $extended_key = $struct['key']; 354 + if (empty($extended_objects[$key])) { 355 + // If this object has already been rejected by an earlier filtering 356 + // pass, we don't need to do any tests on it. 357 + continue; 358 + } 359 + 360 + $object = $struct['object']; 361 + if (is_string($object)) { 362 + // This is really a PHID, so look it up. 363 + $object_phid = $object; 364 + if (empty($ref_objects[$object_phid])) { 365 + // We weren't able to load the corresponding object, so just 366 + // reject this result outright. 367 + 368 + $reject = $extended_objects[$key]; 369 + unset($extended_objects[$key]); 370 + 371 + // TODO: This could be friendlier. 372 + $this->rejectObject($reject, false, '<bad-ref>'); 373 + continue; 374 + } 375 + $object = $ref_objects[$object_phid]; 376 + } 377 + 378 + $phid = $object->getPHID(); 379 + 380 + $key_map[$phid][] = $extended_key; 381 + $objects_in[$phid] = $object; 382 + } 383 + 384 + if ($objects_in) { 385 + $objects_out = id(new PhabricatorPolicyFilter()) 386 + ->setViewer($viewer) 387 + ->requireCapabilities($capabilities) 388 + ->apply($objects_in); 389 + $objects_out = mpull($objects_out, null, 'getPHID'); 390 + } else { 391 + $objects_out = array(); 392 + } 393 + 394 + // If any objects were removed by filtering, we're going to reject all 395 + // of the original objects which needed them. 396 + foreach ($objects_in as $phid => $object_in) { 397 + if (isset($objects_out[$phid])) { 398 + // This object survived filtering, so we don't need to throw any 399 + // results away. 400 + continue; 401 + } 402 + 403 + foreach ($key_map[$phid] as $extended_key) { 404 + if (empty($extended_objects[$extended_key])) { 405 + // We've already rejected this object, so we don't need to reject 406 + // it again. 407 + continue; 408 + } 409 + 410 + $reject = $extended_objects[$extended_key]; 411 + unset($extended_objects[$extended_key]); 412 + 413 + // TODO: This isn't as user-friendly as it could be. It's possible 414 + // that we're rejecting this object for multiple capability/policy 415 + // failures, though. 416 + $this->rejectObject($reject, false, '<extended>'); 417 + } 418 + } 419 + } 420 + 421 + return $extended_objects; 212 422 } 213 423 214 424 private function checkCapability(
+88
src/applications/policy/interface/PhabricatorExtendedPolicyInterface.php
··· 1 + <?php 2 + 3 + /** 4 + * Allows an object to define a more complex policy than it can with 5 + * @{interface:PhabricatorPolicyInterface} alone. 6 + * 7 + * Some objects have complex policies which depend on the policies of other 8 + * objects. For example, users can generally only see a Revision in 9 + * Differential if they can also see the Repository it belongs to. 10 + * 11 + * These policies are normally enforced implicitly in the Query layer, by 12 + * discarding objects which have related objects that can not be loaded. In 13 + * most cases this has the same effect as really applying these policy checks 14 + * would. 15 + * 16 + * However, in some cases an object's policies are later checked by a different 17 + * viewer. For example, before we execute Herald rules, we check that the rule 18 + * owners can see the object we are about to evaluate. 19 + * 20 + * In these cases, we need to account for these complex policies. We could do 21 + * this by reloading the object over and over again for each viewer, but this 22 + * implies a large performance cost. Instead, extended policies make it 23 + * efficient to check policies against an object for multiple viewers. 24 + */ 25 + interface PhabricatorExtendedPolicyInterface { 26 + 27 + /** 28 + * Get the extended policy for an object. 29 + * 30 + * Return a list of additional policy checks that the viewer must satisfy 31 + * in order to have the specified capability. This allows you to encode rules 32 + * like "to see a revision, the viewer must also be able to see the repository 33 + * it belongs to". 34 + * 35 + * For example, to specify that the viewer must be able to see some other 36 + * object in order to see this one, you could return: 37 + * 38 + * return array( 39 + * array($other_object, PhabricatorPolicyCapability::CAN_VIEW), 40 + * // ... 41 + * ); 42 + * 43 + * If you don't have the actual object you want to check, you can return its 44 + * PHID instead: 45 + * 46 + * return array( 47 + * array($other_phid, PhabricatorPolicyCapability::CAN_VIEW), 48 + * // ... 49 + * ); 50 + * 51 + * You can return a list of capabilities instead of a single capability if 52 + * you want to require multiple capabilities on a single object: 53 + * 54 + * return array( 55 + * array( 56 + * $other_object, 57 + * array( 58 + * PhabricatorPolicyCapability::CAN_VIEW, 59 + * PhabricatorPolicyCapability::CAN_EDIT, 60 + * ), 61 + * ), 62 + * // ... 63 + * ); 64 + * 65 + * @param const Capability being tested. 66 + * @param PhabricatorUser Viewer whose capabilities are being tested. 67 + * @return list<pair<wild, wild>> List of extended policies. 68 + */ 69 + public function getExtendedPolicy($capability, PhabricatorUser $viewer); 70 + 71 + } 72 + 73 + // TEMPLATE IMPLEMENTATION ///////////////////////////////////////////////////// 74 + 75 + /* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ 76 + /* 77 + 78 + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { 79 + $extended = array(); 80 + switch ($capability) { 81 + case PhabricatorPolicyCapability::CAN_VIEW: 82 + // ... 83 + break; 84 + } 85 + return $extended; 86 + } 87 + 88 + */