@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.

Remove requireCapabilities() from ApplicationTransactionEditor and require CAN_EDIT by default

Summary:
Depends on D19585. Ref T13164.

Almost all transactions require CAN_EDIT on the object, but they generally do not enforce this directly today. Instead, this is effectively enforced by Controllers, API methods, and EditEngine doing a `CAN_EDIT` check when loading the object to be edited.

A small number of transactions do not require CAN_EDIT, and instead require only a weaker/lesser permission. These are:

- Joining a project which you have CAN_JOIN on.
- Leaving a project which isn't locked.
- Joining a Conpherence thread you can see (today, no separate CAN_JOIN permission for Conpherence).
- Leaving a Conpherence thread.
- Unsubscribing.
- Using the special `!history` command from email.

Additionally, these require CAN_INTERACT, which is weaker than CAN_EDIT:

- Adding comments.
- Subscribing.
- Awarding tokens.

Soon, I want to add "disabling users" to this list, so that you can disable users if you have "Can Disable User" permission, even if you can not otherwise edit users.

It's possible this list isn't exhaustive, so this change might break something by adding a policy check to a place where we previously didn't have one. If so, we can go weaken that policy check to the appropriate level.

Enforcement of these special cases is currently weird:

- We mostly don't actually enforce CAN_EDIT in the Editor; instead, it's enforced before you get to the editor (in EditEngine/Controllers).
- To apply a weaker requirement (like leaving comments or leaving a project), we let you get through the Controller without CAN_EDIT, then apply the weaker policy check in the Editor.
- Some transactions apply a confusing/redundant explicit CAN_EDIT policy check. These mostly got cleaned up in previous changes.

Instead, the new world order is:

- Every transaction has capability/policy requirements.
- The default is CAN_EDIT, but transactions can weaken this explicitly they want.
- So now we'll get requirements right in the Editor, even if Controllers or API endpoints make a mistake.
- And you don't have to copy/paste a bunch of code to say "yes, every transaction should require CAN_EDIT".

Test Plan:
- Tried to add members to a Conpherence thread I could not edit (permissions error).
- Left a Conpherence thread I could not edit (worked properly).
- Joined a thread I could see but could not edit (worked properly).
- Tried to join a thread I could not see (permissions error).
- Implemented `requireCapabilites()` on ManiphestTransactionEditor and tried to edit a task (upgrade guidance error).
- Mentioned an object I can not edit on another object (works).
- Mentioned another object on an object I can not edit (works).
- Added a `{F...}` reference to an object I can not edit (works).
- Awarded tokens to an object I can not edit (works).
- Subscribed/unsubscribed from an object I can not edit (works).
- Muted/unmuted an object I can not edit (works).
- Tried to do other types of edits to an object I can not edit (correctly results in a permissions error).
- Joined and left a project I can not edit (works).
- Tried to edit and add members to a project I can not edit (permissions error).

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13164

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

+213 -122
-43
src/applications/conpherence/editor/ConpherenceEditor.php
··· 146 146 return $xactions; 147 147 } 148 148 149 - protected function requireCapabilities( 150 - PhabricatorLiskDAO $object, 151 - PhabricatorApplicationTransaction $xaction) { 152 - 153 - parent::requireCapabilities($object, $xaction); 154 - 155 - switch ($xaction->getTransactionType()) { 156 - case ConpherenceThreadParticipantsTransaction::TRANSACTIONTYPE: 157 - $old_map = array_fuse($xaction->getOldValue()); 158 - $new_map = array_fuse($xaction->getNewValue()); 159 - 160 - $add = array_keys(array_diff_key($new_map, $old_map)); 161 - $rem = array_keys(array_diff_key($old_map, $new_map)); 162 - 163 - $actor_phid = $this->getActingAsPHID(); 164 - 165 - $is_join = (($add === array($actor_phid)) && !$rem); 166 - $is_leave = (($rem === array($actor_phid)) && !$add); 167 - 168 - if ($is_join) { 169 - // Anyone can join a thread they can see. 170 - } else if ($is_leave) { 171 - // Anyone can leave a thread. 172 - } else { 173 - // You need CAN_EDIT to add or remove participants. For additional 174 - // discussion, see D17696 and T4411. 175 - PhabricatorPolicyFilter::requireCapability( 176 - $this->requireActor(), 177 - $object, 178 - PhabricatorPolicyCapability::CAN_EDIT); 179 - } 180 - break; 181 - 182 - case ConpherenceThreadTitleTransaction::TRANSACTIONTYPE: 183 - case ConpherenceThreadTopicTransaction::TRANSACTIONTYPE: 184 - PhabricatorPolicyFilter::requireCapability( 185 - $this->requireActor(), 186 - $object, 187 - PhabricatorPolicyCapability::CAN_EDIT); 188 - break; 189 - } 190 - } 191 - 192 149 protected function shouldSendMail( 193 150 PhabricatorLiskDAO $object, 194 151 array $xactions) {
+30
src/applications/conpherence/xaction/ConpherenceThreadParticipantsTransaction.php
··· 114 114 return $errors; 115 115 } 116 116 117 + public function getRequiredCapabilities( 118 + $object, 119 + PhabricatorApplicationTransaction $xaction) { 120 + 121 + $old_map = array_fuse($xaction->getOldValue()); 122 + $new_map = array_fuse($xaction->getNewValue()); 123 + 124 + $add = array_keys(array_diff_key($new_map, $old_map)); 125 + $rem = array_keys(array_diff_key($old_map, $new_map)); 126 + 127 + $actor_phid = $this->getActingAsPHID(); 128 + 129 + $is_join = (($add === array($actor_phid)) && !$rem); 130 + $is_leave = (($rem === array($actor_phid)) && !$add); 131 + 132 + if ($is_join) { 133 + // Anyone can join a thread they can see. 134 + return null; 135 + } 136 + 137 + if ($is_leave) { 138 + // Anyone can leave a thread. 139 + return null; 140 + } 141 + 142 + // You need CAN_EDIT to add or remove participants. For additional 143 + // discussion, see D17696 and T4411. 144 + return PhabricatorPolicyCapability::CAN_EDIT; 145 + } 146 + 117 147 }
-52
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 115 115 return $errors; 116 116 } 117 117 118 - protected function requireCapabilities( 119 - PhabricatorLiskDAO $object, 120 - PhabricatorApplicationTransaction $xaction) { 121 - 122 - switch ($xaction->getTransactionType()) { 123 - case PhabricatorTransactions::TYPE_EDGE: 124 - switch ($xaction->getMetadataValue('edge:type')) { 125 - case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: 126 - $old = $xaction->getOldValue(); 127 - $new = $xaction->getNewValue(); 128 - 129 - $add = array_keys(array_diff_key($new, $old)); 130 - $rem = array_keys(array_diff_key($old, $new)); 131 - 132 - $actor_phid = $this->requireActor()->getPHID(); 133 - 134 - $is_join = (($add === array($actor_phid)) && !$rem); 135 - $is_leave = (($rem === array($actor_phid)) && !$add); 136 - 137 - if ($is_join) { 138 - // You need CAN_JOIN to join a project. 139 - PhabricatorPolicyFilter::requireCapability( 140 - $this->requireActor(), 141 - $object, 142 - PhabricatorPolicyCapability::CAN_JOIN); 143 - } else if ($is_leave) { 144 - // You usually don't need any capabilities to leave a project. 145 - if ($object->getIsMembershipLocked()) { 146 - // you must be able to edit though to leave locked projects 147 - PhabricatorPolicyFilter::requireCapability( 148 - $this->requireActor(), 149 - $object, 150 - PhabricatorPolicyCapability::CAN_EDIT); 151 - } 152 - } else { 153 - if (!$this->getIsNewObject()) { 154 - // You need CAN_EDIT to change members other than yourself. 155 - // (PHI193) Just skip this check if we're creating a project. 156 - PhabricatorPolicyFilter::requireCapability( 157 - $this->requireActor(), 158 - $object, 159 - PhabricatorPolicyCapability::CAN_EDIT); 160 - } 161 - } 162 - return; 163 - } 164 - break; 165 - } 166 - 167 - return parent::requireCapabilities($object, $xaction); 168 - } 169 - 170 118 protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { 171 119 // NOTE: We're using the omnipotent user here because the original actor 172 120 // may no longer have permission to view the object.
-2
src/applications/subscriptions/controller/PhabricatorSubscriptionsMuteController.php
··· 44 44 ); 45 45 } 46 46 47 - $muted_type = PhabricatorMutedByEdgeType::EDGECONST; 48 - 49 47 $xaction = id($object->getApplicationTransactionTemplate()) 50 48 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 51 49 ->setMetadataValue('edge:type', $muted_type)
+157 -25
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 976 976 $this->adjustTransactionValues($object, $xaction); 977 977 } 978 978 979 + // Now that we've merged and combined transactions, check for required 980 + // capabilities. Note that we're doing this before filtering 981 + // transactions: if you try to apply an edit which you do not have 982 + // permission to apply, we want to give you a permissions error even 983 + // if the edit would have no effect. 984 + $this->applyCapabilityChecks($object, $xactions); 985 + 986 + // See T13186. Fatal hard if this object has an older 987 + // "requireCapabilities()" method. The code may rely on this method being 988 + // called to apply policy checks, so err on the side of safety and fatal. 989 + // TODO: Remove this check after some time has passed. 990 + if (method_exists($this, 'requireCapabilities')) { 991 + throw new Exception( 992 + pht( 993 + 'Editor (of class "%s") implements obsolete policy method '. 994 + 'requireCapabilities(). The implementation for this Editor '. 995 + 'MUST be updated. See <%s> for discussion.', 996 + get_class($this), 997 + 'https://secure.phabricator.com/T13186')); 998 + } 999 + 979 1000 $xactions = $this->filterTransactions($object, $xactions); 980 1001 981 1002 // TODO: Once everything is on EditEngine, just use getIsNewObject() to ··· 992 1013 foreach ($xactions as $xaction) { 993 1014 $xaction->setIsCreateTransaction(true); 994 1015 } 995 - } 996 - 997 - // Now that we've merged, filtered, and combined transactions, check for 998 - // required capabilities. 999 - foreach ($xactions as $xaction) { 1000 - $this->requireCapabilities($object, $xaction); 1001 1016 } 1002 1017 1003 1018 $xactions = $this->sortTransactions($xactions); ··· 1459 1474 } 1460 1475 } 1461 1476 1462 - protected function requireCapabilities( 1477 + private function applyCapabilityChecks( 1463 1478 PhabricatorLiskDAO $object, 1464 - PhabricatorApplicationTransaction $xaction) { 1479 + array $xactions) { 1480 + assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); 1481 + 1482 + $can_edit = PhabricatorPolicyCapability::CAN_EDIT; 1465 1483 1466 1484 if ($this->getIsNewObject()) { 1467 - return; 1485 + // If we're creating a new object, we don't need any special capabilities 1486 + // on the object. The actor has already made it through creation checks, 1487 + // and objects which haven't been created yet often can not be 1488 + // meaningfully tested for capabilities anyway. 1489 + $required_capabilities = array(); 1490 + } else { 1491 + if (!$xactions && !$this->xactions) { 1492 + // If we aren't doing anything, require CAN_EDIT to improve consistency. 1493 + $required_capabilities = array($can_edit); 1494 + } else { 1495 + $required_capabilities = array(); 1496 + 1497 + foreach ($xactions as $xaction) { 1498 + $type = $xaction->getTransactionType(); 1499 + 1500 + $xtype = $this->getModularTransactionType($type); 1501 + if (!$xtype) { 1502 + $capabilities = $this->getLegacyRequiredCapabilities($xaction); 1503 + } else { 1504 + $capabilities = $xtype->getRequiredCapabilities($object, $xaction); 1505 + } 1506 + 1507 + // For convenience, we allow flexibility in the return types because 1508 + // it's very unusual that a transaction actually requires multiple 1509 + // capability checks. 1510 + if ($capabilities === null) { 1511 + $capabilities = array(); 1512 + } else { 1513 + $capabilities = (array)$capabilities; 1514 + } 1515 + 1516 + foreach ($capabilities as $capability) { 1517 + $required_capabilities[$capability] = $capability; 1518 + } 1519 + } 1520 + } 1468 1521 } 1469 1522 1470 - $actor = $this->requireActor(); 1471 - switch ($xaction->getTransactionType()) { 1523 + $required_capabilities = array_fuse($required_capabilities); 1524 + $actor = $this->getActor(); 1525 + 1526 + if ($required_capabilities) { 1527 + id(new PhabricatorPolicyFilter()) 1528 + ->setViewer($actor) 1529 + ->requireCapabilities($required_capabilities) 1530 + ->raisePolicyExceptions(true) 1531 + ->apply(array($object)); 1532 + } 1533 + } 1534 + 1535 + private function getLegacyRequiredCapabilities( 1536 + PhabricatorApplicationTransaction $xaction) { 1537 + 1538 + $type = $xaction->getTransactionType(); 1539 + switch ($type) { 1472 1540 case PhabricatorTransactions::TYPE_COMMENT: 1473 - PhabricatorPolicyFilter::requireCapability( 1474 - $actor, 1475 - $object, 1476 - PhabricatorPolicyCapability::CAN_VIEW); 1477 - break; 1478 - case PhabricatorTransactions::TYPE_VIEW_POLICY: 1479 - case PhabricatorTransactions::TYPE_EDIT_POLICY: 1480 - case PhabricatorTransactions::TYPE_JOIN_POLICY: 1481 - case PhabricatorTransactions::TYPE_SPACE: 1482 - PhabricatorPolicyFilter::requireCapability( 1483 - $actor, 1484 - $object, 1485 - PhabricatorPolicyCapability::CAN_EDIT); 1486 - break; 1541 + // TODO: Comments technically require CAN_INTERACT, but this is 1542 + // currently somewhat special and handled through EditEngine. For now, 1543 + // don't enforce it here. 1544 + return null; 1545 + case PhabricatorTransactions::TYPE_SUBSCRIBERS: 1546 + // TODO: Removing subscribers other than yourself should probably 1547 + // require CAN_EDIT permission. You can do this via the API but 1548 + // generally can not via the web interface. 1549 + return null; 1550 + case PhabricatorTransactions::TYPE_TOKEN: 1551 + // TODO: This technically requires CAN_INTERACT, like comments. 1552 + return null; 1553 + case PhabricatorTransactions::TYPE_HISTORY: 1554 + // This is a special magic transaction which sends you history via 1555 + // email and is only partially supported in the upstream. You don't 1556 + // need any capabilities to apply it. 1557 + return null; 1558 + case PhabricatorTransactions::TYPE_EDGE: 1559 + return $this->getLegacyRequiredEdgeCapabilities($xaction); 1560 + default: 1561 + // For other older (non-modular) transactions, always require exactly 1562 + // CAN_EDIT. Transactions which do not need CAN_EDIT or need additional 1563 + // capabilities must move to ModularTransactions. 1564 + return PhabricatorPolicyCapability::CAN_EDIT; 1565 + } 1566 + } 1567 + 1568 + private function getLegacyRequiredEdgeCapabilities( 1569 + PhabricatorApplicationTransaction $xaction) { 1570 + 1571 + // You don't need to have edit permission on an object to mention it or 1572 + // otherwise add a relationship pointing toward it. 1573 + if ($this->getIsInverseEdgeEditor()) { 1574 + return null; 1575 + } 1576 + 1577 + $edge_type = $xaction->getMetadataValue('edge:type'); 1578 + switch ($edge_type) { 1579 + case PhabricatorMutedByEdgeType::EDGECONST: 1580 + // At time of writing, you can only write this edge for yourself, so 1581 + // you don't need permissions. If you can eventually mute an object 1582 + // for other users, this would need to be revisited. 1583 + return null; 1584 + case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: 1585 + return null; 1586 + case PhabricatorProjectProjectHasMemberEdgeType::EDGECONST: 1587 + $old = $xaction->getOldValue(); 1588 + $new = $xaction->getNewValue(); 1589 + 1590 + $add = array_keys(array_diff_key($new, $old)); 1591 + $rem = array_keys(array_diff_key($old, $new)); 1592 + 1593 + $actor_phid = $this->requireActor()->getPHID(); 1594 + 1595 + $is_join = (($add === array($actor_phid)) && !$rem); 1596 + $is_leave = (($rem === array($actor_phid)) && !$add); 1597 + 1598 + if ($is_join) { 1599 + // You need CAN_JOIN to join a project. 1600 + return PhabricatorPolicyCapability::CAN_JOIN; 1601 + } 1602 + 1603 + if ($is_leave) { 1604 + $object = $this->object; 1605 + // You usually don't need any capabilities to leave a project... 1606 + if ($object->getIsMembershipLocked()) { 1607 + // ...you must be able to edit to leave locked projects, though. 1608 + return PhabricatorPolicyCapability::CAN_EDIT; 1609 + } else { 1610 + return null; 1611 + } 1612 + } 1613 + 1614 + // You need CAN_EDIT to change members other than yourself. 1615 + return PhabricatorPolicyCapability::CAN_EDIT; 1616 + default: 1617 + return PhabricatorPolicyCapability::CAN_EDIT; 1487 1618 } 1488 1619 } 1620 + 1489 1621 1490 1622 private function buildSubscribeTransaction( 1491 1623 PhabricatorLiskDAO $object,
+26
src/applications/transactions/storage/PhabricatorModularTransactionType.php
··· 366 366 $capability); 367 367 } 368 368 369 + /** 370 + * Get a list of capabilities the actor must have on the object to apply 371 + * a transaction to it. 372 + * 373 + * Usually, you should use this to reduce capability requirements when a 374 + * transaction (like leaving a Conpherence thread) can be applied without 375 + * having edit permission on the object. You can override this method to 376 + * remove the CAN_EDIT requirement, or to replace it with a different 377 + * requirement. 378 + * 379 + * If you are increasing capability requirements and need to add an 380 + * additional capability or policy requirement above and beyond CAN_EDIT, it 381 + * is usually better implemented as a validation check. 382 + * 383 + * @param object Object being edited. 384 + * @param PhabricatorApplicationTransaction Transaction being applied. 385 + * @return null|const|list<const> A capability constant (or list of 386 + * capability constants) which the actor must have on the object. You can 387 + * return `null` as a shorthand for "no capabilities are required". 388 + */ 389 + public function getRequiredCapabilities( 390 + $object, 391 + PhabricatorApplicationTransaction $xaction) { 392 + return PhabricatorPolicyCapability::CAN_EDIT; 393 + } 394 + 369 395 }