@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 capabilities for editing task triage details (priority, assignee, etc)

Summary:
This is primarily a client request, and a little bit use-case specific, but policies seem to be holding up well and I'm getting more comfortable about maintaining this. Much if it can run through ApplicationTransactions.

Allow the ability to edit status, policies, priorities, assignees and projects of a task to be restricted to some subset of users. Also allow bulk edit to be locked. This affects the editor itself and the edit, view and list interfaces.

Test Plan: As a restricted user, created, edited and commented on tasks. Tried to drag them around.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

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

+332 -75
+12
src/__phutil_library_map__.php
··· 705 705 'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php', 706 706 'LiskRawMigrationIterator' => 'infrastructure/storage/lisk/LiskRawMigrationIterator.php', 707 707 'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php', 708 + 'ManiphestCapabilityBulkEdit' => 'applications/maniphest/capability/ManiphestCapabilityBulkEdit.php', 708 709 'ManiphestCapabilityDefaultEdit' => 'applications/maniphest/capability/ManiphestCapabilityDefaultEdit.php', 709 710 'ManiphestCapabilityDefaultView' => 'applications/maniphest/capability/ManiphestCapabilityDefaultView.php', 711 + 'ManiphestCapabilityEditAssign' => 'applications/maniphest/capability/ManiphestCapabilityEditAssign.php', 712 + 'ManiphestCapabilityEditPolicies' => 'applications/maniphest/capability/ManiphestCapabilityEditPolicies.php', 713 + 'ManiphestCapabilityEditPriority' => 'applications/maniphest/capability/ManiphestCapabilityEditPriority.php', 714 + 'ManiphestCapabilityEditProjects' => 'applications/maniphest/capability/ManiphestCapabilityEditProjects.php', 715 + 'ManiphestCapabilityEditStatus' => 'applications/maniphest/capability/ManiphestCapabilityEditStatus.php', 710 716 'ManiphestConfiguredCustomField' => 'applications/maniphest/field/ManiphestConfiguredCustomField.php', 711 717 'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php', 712 718 'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php', ··· 2837 2843 'LiskMigrationIterator' => 'PhutilBufferedIterator', 2838 2844 'LiskRawMigrationIterator' => 'PhutilBufferedIterator', 2839 2845 'ManiphestBatchEditController' => 'ManiphestController', 2846 + 'ManiphestCapabilityBulkEdit' => 'PhabricatorPolicyCapability', 2840 2847 'ManiphestCapabilityDefaultEdit' => 'PhabricatorPolicyCapability', 2841 2848 'ManiphestCapabilityDefaultView' => 'PhabricatorPolicyCapability', 2849 + 'ManiphestCapabilityEditAssign' => 'PhabricatorPolicyCapability', 2850 + 'ManiphestCapabilityEditPolicies' => 'PhabricatorPolicyCapability', 2851 + 'ManiphestCapabilityEditPriority' => 'PhabricatorPolicyCapability', 2852 + 'ManiphestCapabilityEditProjects' => 'PhabricatorPolicyCapability', 2853 + 'ManiphestCapabilityEditStatus' => 'PhabricatorPolicyCapability', 2842 2854 'ManiphestConfiguredCustomField' => 2843 2855 array( 2844 2856 0 => 'ManiphestCustomField',
+13 -1
src/applications/maniphest/application/PhabricatorApplicationManiphest.php
··· 67 67 'export/(?P<key>[^/]+)/' => 'ManiphestExportController', 68 68 'subpriority/' => 'ManiphestSubpriorityController', 69 69 'subscribe/(?P<action>add|rem)/(?P<id>[1-9]\d*)/' 70 - => 'ManiphestSubscribeController', 70 + => 'ManiphestSubscribeController', 71 71 ), 72 72 ); 73 73 } ··· 99 99 ManiphestCapabilityDefaultEdit::CAPABILITY => array( 100 100 'caption' => pht( 101 101 'Default edit policy for newly created tasks.'), 102 + ), 103 + ManiphestCapabilityEditStatus::CAPABILITY => array( 104 + ), 105 + ManiphestCapabilityEditAssign::CAPABILITY => array( 106 + ), 107 + ManiphestCapabilityEditPolicies::CAPABILITY => array( 108 + ), 109 + ManiphestCapabilityEditPriority::CAPABILITY => array( 110 + ), 111 + ManiphestCapabilityEditProjects::CAPABILITY => array( 112 + ), 113 + ManiphestCapabilityBulkEdit::CAPABILITY => array( 102 114 ), 103 115 ); 104 116 }
+20
src/applications/maniphest/capability/ManiphestCapabilityBulkEdit.php
··· 1 + <?php 2 + 3 + final class ManiphestCapabilityBulkEdit 4 + extends PhabricatorPolicyCapability { 5 + 6 + const CAPABILITY = 'maniphest.edit.bulk'; 7 + 8 + public function getCapabilityKey() { 9 + return self::CAPABILITY; 10 + } 11 + 12 + public function getCapabilityName() { 13 + return pht('Can Bulk Edit Tasks'); 14 + } 15 + 16 + public function describeCapabilityRejection() { 17 + return pht('You do not have permission to bulk edit tasks.'); 18 + } 19 + 20 + }
+20
src/applications/maniphest/capability/ManiphestCapabilityEditAssign.php
··· 1 + <?php 2 + 3 + final class ManiphestCapabilityEditAssign 4 + extends PhabricatorPolicyCapability { 5 + 6 + const CAPABILITY = 'maniphest.edit.assign'; 7 + 8 + public function getCapabilityKey() { 9 + return self::CAPABILITY; 10 + } 11 + 12 + public function getCapabilityName() { 13 + return pht('Can Assign Tasks'); 14 + } 15 + 16 + public function describeCapabilityRejection() { 17 + return pht('You do not have permission to assign tasks.'); 18 + } 19 + 20 + }
+20
src/applications/maniphest/capability/ManiphestCapabilityEditPolicies.php
··· 1 + <?php 2 + 3 + final class ManiphestCapabilityEditPolicies 4 + extends PhabricatorPolicyCapability { 5 + 6 + const CAPABILITY = 'maniphest.edit.policies'; 7 + 8 + public function getCapabilityKey() { 9 + return self::CAPABILITY; 10 + } 11 + 12 + public function getCapabilityName() { 13 + return pht('Can Edit Task Policies'); 14 + } 15 + 16 + public function describeCapabilityRejection() { 17 + return pht('You do not have permission to edit task policies.'); 18 + } 19 + 20 + }
+20
src/applications/maniphest/capability/ManiphestCapabilityEditPriority.php
··· 1 + <?php 2 + 3 + final class ManiphestCapabilityEditPriority 4 + extends PhabricatorPolicyCapability { 5 + 6 + const CAPABILITY = 'maniphest.edit.priority'; 7 + 8 + public function getCapabilityKey() { 9 + return self::CAPABILITY; 10 + } 11 + 12 + public function getCapabilityName() { 13 + return pht('Can Prioritize Tasks'); 14 + } 15 + 16 + public function describeCapabilityRejection() { 17 + return pht('You do not have permission to prioritize tasks.'); 18 + } 19 + 20 + }
+20
src/applications/maniphest/capability/ManiphestCapabilityEditProjects.php
··· 1 + <?php 2 + 3 + final class ManiphestCapabilityEditProjects 4 + extends PhabricatorPolicyCapability { 5 + 6 + const CAPABILITY = 'maniphest.edit.projects'; 7 + 8 + public function getCapabilityKey() { 9 + return self::CAPABILITY; 10 + } 11 + 12 + public function getCapabilityName() { 13 + return pht('Can Edit Task Projects'); 14 + } 15 + 16 + public function describeCapabilityRejection() { 17 + return pht('You do not have permission to edit task projects.'); 18 + } 19 + 20 + }
+20
src/applications/maniphest/capability/ManiphestCapabilityEditStatus.php
··· 1 + <?php 2 + 3 + final class ManiphestCapabilityEditStatus 4 + extends PhabricatorPolicyCapability { 5 + 6 + const CAPABILITY = 'maniphest.edit.status'; 7 + 8 + public function getCapabilityKey() { 9 + return self::CAPABILITY; 10 + } 11 + 12 + public function getCapabilityName() { 13 + return pht('Can Edit Task Status'); 14 + } 15 + 16 + public function describeCapabilityRejection() { 17 + return pht('You do not have permission to edit task status.'); 18 + } 19 + 20 + }
+3
src/applications/maniphest/controller/ManiphestBatchEditController.php
··· 7 7 8 8 public function processRequest() { 9 9 10 + $this->requireApplicationCapability( 11 + ManiphestCapabilityBulkEdit::CAPABILITY); 12 + 10 13 $request = $this->getRequest(); 11 14 $user = $request->getUser(); 12 15
+21
src/applications/maniphest/controller/ManiphestTaskDetailController.php
··· 165 165 ManiphestTransaction::TYPE_PROJECTS => pht('Associate Projects'), 166 166 ); 167 167 168 + // Remove actions the user doesn't have permission to take. 169 + 170 + $requires = array( 171 + ManiphestTransaction::TYPE_OWNER => 172 + ManiphestCapabilityEditAssign::CAPABILITY, 173 + ManiphestTransaction::TYPE_PRIORITY => 174 + ManiphestCapabilityEditPriority::CAPABILITY, 175 + ManiphestTransaction::TYPE_PROJECTS => 176 + ManiphestCapabilityEditProjects::CAPABILITY, 177 + ManiphestTransaction::TYPE_STATUS => 178 + ManiphestCapabilityEditStatus::CAPABILITY, 179 + ); 180 + 181 + foreach ($transaction_types as $type => $name) { 182 + if (isset($requires[$type])) { 183 + if (!$this->hasApplicationCapability($requires[$type])) { 184 + unset($transaction_types[$type]); 185 + } 186 + } 187 + } 188 + 168 189 if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) { 169 190 $resolution_types = array_select_keys( 170 191 $resolution_types,
+103 -64
src/applications/maniphest/controller/ManiphestTaskEditController.php
··· 1 1 <?php 2 2 3 - /** 4 - * @group maniphest 5 - */ 6 3 final class ManiphestTaskEditController extends ManiphestController { 7 4 8 5 private $id; ··· 16 13 $request = $this->getRequest(); 17 14 $user = $request->getUser(); 18 15 16 + $can_edit_assign = $this->hasApplicationCapability( 17 + ManiphestCapabilityEditAssign::CAPABILITY); 18 + $can_edit_policies = $this->hasApplicationCapability( 19 + ManiphestCapabilityEditPolicies::CAPABILITY); 20 + $can_edit_priority = $this->hasApplicationCapability( 21 + ManiphestCapabilityEditPriority::CAPABILITY); 22 + $can_edit_projects = $this->hasApplicationCapability( 23 + ManiphestCapabilityEditProjects::CAPABILITY); 24 + $can_edit_status = $this->hasApplicationCapability( 25 + ManiphestCapabilityEditStatus::CAPABILITY); 26 + 19 27 $files = array(); 20 28 $parent_task = null; 21 29 $template_id = null; ··· 40 48 if (!$request->isFormPost()) { 41 49 $task->setTitle($request->getStr('title')); 42 50 43 - $default_projects = $request->getStr('projects'); 44 - if ($default_projects) { 45 - $task->setProjectPHIDs(explode(';', $default_projects)); 51 + if ($can_edit_projects) { 52 + $default_projects = $request->getStr('projects'); 53 + if ($default_projects) { 54 + $task->setProjectPHIDs(explode(';', $default_projects)); 55 + } 46 56 } 47 57 48 58 $task->setDescription($request->getStr('description')); 49 59 50 - $assign = $request->getStr('assign'); 51 - if (strlen($assign)) { 52 - $assign_user = id(new PhabricatorUser())->loadOneWhere( 53 - 'username = %s', 54 - $assign); 55 - if ($assign_user) { 56 - $task->setOwnerPHID($assign_user->getPHID()); 60 + if ($can_edit_assign) { 61 + $assign = $request->getStr('assign'); 62 + if (strlen($assign)) { 63 + $assign_user = id(new PhabricatorUser())->loadOneWhere( 64 + 'username = %s', 65 + $assign); 66 + if ($assign_user) { 67 + $task->setOwnerPHID($assign_user->getPHID()); 68 + } 57 69 } 58 70 } 59 71 } ··· 122 134 123 135 $changes[ManiphestTransaction::TYPE_TITLE] = $new_title; 124 136 $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc; 125 - $changes[ManiphestTransaction::TYPE_STATUS] = $new_status; 137 + if ($can_edit_status) { 138 + $changes[ManiphestTransaction::TYPE_STATUS] = $new_status; 139 + } 126 140 127 141 $owner_tokenizer = $request->getArr('assigned_to'); 128 142 $owner_phid = reset($owner_tokenizer); ··· 173 187 $task->setProjectPHIDs($request->getArr('projects')); 174 188 } else { 175 189 176 - $changes[ManiphestTransaction::TYPE_PRIORITY] = 177 - $request->getInt('priority'); 178 - $changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid; 190 + if ($can_edit_priority) { 191 + $changes[ManiphestTransaction::TYPE_PRIORITY] = 192 + $request->getInt('priority'); 193 + } 194 + if ($can_edit_assign) { 195 + $changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid; 196 + } 197 + 179 198 $changes[ManiphestTransaction::TYPE_CCS] = $request->getArr('cc'); 180 - $changes[ManiphestTransaction::TYPE_PROJECTS] = 181 - $request->getArr('projects'); 182 199 183 - $changes[PhabricatorTransactions::TYPE_VIEW_POLICY] = 184 - $request->getStr('viewPolicy'); 185 - $changes[PhabricatorTransactions::TYPE_EDIT_POLICY] = 186 - $request->getStr('editPolicy'); 200 + if ($can_edit_projects) { 201 + $changes[ManiphestTransaction::TYPE_PROJECTS] = 202 + $request->getArr('projects'); 203 + } 204 + 205 + if ($can_edit_policies) { 206 + $changes[PhabricatorTransactions::TYPE_VIEW_POLICY] = 207 + $request->getStr('viewPolicy'); 208 + $changes[PhabricatorTransactions::TYPE_EDIT_POLICY] = 209 + $request->getStr('editPolicy'); 210 + } 187 211 188 212 if ($files) { 189 213 $file_map = mpull($files, 'getPHID'); ··· 418 442 ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) 419 443 ->setValue($task->getTitle())); 420 444 421 - if ($task->getID()) { 445 + if ($task->getID() && $can_edit_status) { 422 446 // Only show this in "edit" mode, not "create" mode, since creating a 423 447 // non-open task is kind of silly and it would just clutter up the 424 448 // "create" interface. ··· 436 460 ->setObject($task) 437 461 ->execute(); 438 462 439 - $form 440 - ->appendChild( 463 + if ($can_edit_assign) { 464 + $form->appendChild( 441 465 id(new AphrontFormTokenizerControl()) 442 466 ->setLabel(pht('Assigned To')) 443 467 ->setName('assigned_to') 444 468 ->setValue($assigned_value) 445 469 ->setUser($user) 446 470 ->setDatasource('/typeahead/common/users/') 447 - ->setLimit(1)) 471 + ->setLimit(1)); 472 + } 473 + 474 + $form 448 475 ->appendChild( 449 476 id(new AphrontFormTokenizerControl()) 450 477 ->setLabel(pht('CC')) 451 478 ->setName('cc') 452 479 ->setValue($cc_value) 453 480 ->setUser($user) 454 - ->setDatasource('/typeahead/common/mailable/')) 455 - ->appendChild( 456 - id(new AphrontFormSelectControl()) 457 - ->setLabel(pht('Priority')) 458 - ->setName('priority') 459 - ->setOptions($priority_map) 460 - ->setValue($task->getPriority())) 461 - ->appendChild( 462 - id(new AphrontFormPolicyControl()) 463 - ->setUser($user) 464 - ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) 465 - ->setPolicyObject($task) 466 - ->setPolicies($policies) 467 - ->setName('viewPolicy')) 468 - ->appendChild( 469 - id(new AphrontFormPolicyControl()) 470 - ->setUser($user) 471 - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) 472 - ->setPolicyObject($task) 473 - ->setPolicies($policies) 474 - ->setName('editPolicy')) 475 - ->appendChild( 476 - id(new AphrontFormTokenizerControl()) 477 - ->setLabel(pht('Projects')) 478 - ->setName('projects') 479 - ->setValue($projects_value) 480 - ->setID($project_tokenizer_id) 481 - ->setCaption( 482 - javelin_tag( 483 - 'a', 484 - array( 485 - 'href' => '/project/create/', 486 - 'mustcapture' => true, 487 - 'sigil' => 'project-create', 488 - ), 489 - pht('Create New Project'))) 490 - ->setDatasource('/typeahead/common/projects/')); 481 + ->setDatasource('/typeahead/common/mailable/')); 482 + 483 + if ($can_edit_priority) { 484 + $form 485 + ->appendChild( 486 + id(new AphrontFormSelectControl()) 487 + ->setLabel(pht('Priority')) 488 + ->setName('priority') 489 + ->setOptions($priority_map) 490 + ->setValue($task->getPriority())); 491 + } 492 + 493 + if ($can_edit_policies) { 494 + $form 495 + ->appendChild( 496 + id(new AphrontFormPolicyControl()) 497 + ->setUser($user) 498 + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) 499 + ->setPolicyObject($task) 500 + ->setPolicies($policies) 501 + ->setName('viewPolicy')) 502 + ->appendChild( 503 + id(new AphrontFormPolicyControl()) 504 + ->setUser($user) 505 + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) 506 + ->setPolicyObject($task) 507 + ->setPolicies($policies) 508 + ->setName('editPolicy')); 509 + } 510 + 511 + if ($can_edit_projects) { 512 + $form 513 + ->appendChild( 514 + id(new AphrontFormTokenizerControl()) 515 + ->setLabel(pht('Projects')) 516 + ->setName('projects') 517 + ->setValue($projects_value) 518 + ->setID($project_tokenizer_id) 519 + ->setCaption( 520 + javelin_tag( 521 + 'a', 522 + array( 523 + 'href' => '/project/create/', 524 + 'mustcapture' => true, 525 + 'sigil' => 'project-create', 526 + ), 527 + pht('Create New Project'))) 528 + ->setDatasource('/typeahead/common/projects/')); 529 + } 491 530 492 531 foreach ($aux_fields as $aux_field) { 493 532 $aux_control = $aux_field->renderEditControl();
+16 -5
src/applications/maniphest/controller/ManiphestTaskListController.php
··· 46 46 $group_parameter, 47 47 $handles); 48 48 49 + $can_edit_priority = $this->hasApplicationCapability( 50 + ManiphestCapabilityEditPriority::CAPABILITY); 51 + 49 52 $can_drag = ($order_parameter == 'priority') && 53 + ($can_edit_priority) && 50 54 ($group_parameter == 'none' || $group_parameter == 'priority'); 51 55 52 56 if (!$viewer->isLoggedIn()) { ··· 91 95 )); 92 96 } 93 97 94 - Javelin::initBehavior( 95 - 'maniphest-subpriority-editor', 96 - array( 97 - 'uri' => '/maniphest/subpriority/', 98 - )); 98 + if ($can_drag) { 99 + Javelin::initBehavior( 100 + 'maniphest-subpriority-editor', 101 + array( 102 + 'uri' => '/maniphest/subpriority/', 103 + )); 104 + } 99 105 100 106 return phutil_tag( 101 107 'div', ··· 190 196 191 197 private function renderBatchEditor(PhabricatorSavedQuery $saved_query) { 192 198 $user = $this->getRequest()->getUser(); 199 + 200 + $batch_capability = ManiphestCapabilityBulkEdit::CAPABILITY; 201 + if (!$this->hasApplicationCapability($batch_capability)) { 202 + return null; 203 + } 193 204 194 205 if (!$user->isLoggedIn()) { 195 206 // Don't show the batch editor or excel export for logged-out users.
+38 -1
src/applications/maniphest/editor/ManiphestTransactionEditor.php
··· 81 81 case ManiphestTransaction::TYPE_EDGE: 82 82 return $xaction->getNewValue(); 83 83 } 84 - 85 84 } 85 + 86 86 87 87 protected function transactionHasEffect( 88 88 PhabricatorLiskDAO $object, ··· 248 248 $object->save(); 249 249 } 250 250 } 251 + 252 + protected function requireCapabilities( 253 + PhabricatorLiskDAO $object, 254 + PhabricatorApplicationTransaction $xaction) { 255 + 256 + parent::requireCapabilities($object, $xaction); 257 + 258 + $app_capability_map = array( 259 + ManiphestTransaction::TYPE_PRIORITY => 260 + ManiphestCapabilityEditPriority::CAPABILITY, 261 + ManiphestTransaction::TYPE_STATUS => 262 + ManiphestCapabilityEditStatus::CAPABILITY, 263 + ManiphestTransaction::TYPE_PROJECTS => 264 + ManiphestCapabilityEditProjects::CAPABILITY, 265 + ManiphestTransaction::TYPE_OWNER => 266 + ManiphestCapabilityEditAssign::CAPABILITY, 267 + PhabricatorTransactions::TYPE_EDIT_POLICY => 268 + ManiphestCapabilityEditPolicies::CAPABILITY, 269 + PhabricatorTransactions::TYPE_VIEW_POLICY => 270 + ManiphestCapabilityEditPolicies::CAPABILITY, 271 + ); 272 + 273 + $transaction_type = $xaction->getTransactionType(); 274 + $app_capability = idx($app_capability_map, $transaction_type); 275 + 276 + if ($app_capability) { 277 + $app = id(new PhabricatorApplicationQuery()) 278 + ->setViewer($this->getActor()) 279 + ->withClasses(array('PhabricatorApplicationManiphest')) 280 + ->executeOne(); 281 + PhabricatorPolicyFilter::requireCapability( 282 + $this->getActor(), 283 + $app, 284 + $app_capability); 285 + } 286 + } 287 + 251 288 252 289 public static function getNextSubpriority($pri, $sub) { 253 290
+6 -4
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
··· 460 460 return array(); 461 461 } 462 462 463 + // Now that we've merged, filtered, and combined transactions, check for 464 + // required capabilities. 465 + foreach ($xactions as $xaction) { 466 + $this->requireCapabilities($object, $xaction); 467 + } 468 + 463 469 $xactions = $this->sortTransactions($xactions); 464 470 465 471 if ($is_preview) { ··· 696 702 $actor, 697 703 $object, 698 704 PhabricatorPolicyCapability::CAN_VIEW); 699 - 700 - foreach ($xactions as $xaction) { 701 - $this->requireCapabilities($object, $xaction); 702 - } 703 705 } 704 706 705 707 protected function requireCapabilities(