1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-23 07:12:41 +01:00

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
This commit is contained in:
epriestley 2013-10-21 16:59:06 -07:00
parent 00bf47f973
commit 7fedfacbca
14 changed files with 333 additions and 76 deletions

View file

@ -705,8 +705,14 @@ phutil_register_library_map(array(
'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php', 'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php',
'LiskRawMigrationIterator' => 'infrastructure/storage/lisk/LiskRawMigrationIterator.php', 'LiskRawMigrationIterator' => 'infrastructure/storage/lisk/LiskRawMigrationIterator.php',
'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php', 'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php',
'ManiphestCapabilityBulkEdit' => 'applications/maniphest/capability/ManiphestCapabilityBulkEdit.php',
'ManiphestCapabilityDefaultEdit' => 'applications/maniphest/capability/ManiphestCapabilityDefaultEdit.php', 'ManiphestCapabilityDefaultEdit' => 'applications/maniphest/capability/ManiphestCapabilityDefaultEdit.php',
'ManiphestCapabilityDefaultView' => 'applications/maniphest/capability/ManiphestCapabilityDefaultView.php', 'ManiphestCapabilityDefaultView' => 'applications/maniphest/capability/ManiphestCapabilityDefaultView.php',
'ManiphestCapabilityEditAssign' => 'applications/maniphest/capability/ManiphestCapabilityEditAssign.php',
'ManiphestCapabilityEditPolicies' => 'applications/maniphest/capability/ManiphestCapabilityEditPolicies.php',
'ManiphestCapabilityEditPriority' => 'applications/maniphest/capability/ManiphestCapabilityEditPriority.php',
'ManiphestCapabilityEditProjects' => 'applications/maniphest/capability/ManiphestCapabilityEditProjects.php',
'ManiphestCapabilityEditStatus' => 'applications/maniphest/capability/ManiphestCapabilityEditStatus.php',
'ManiphestConfiguredCustomField' => 'applications/maniphest/field/ManiphestConfiguredCustomField.php', 'ManiphestConfiguredCustomField' => 'applications/maniphest/field/ManiphestConfiguredCustomField.php',
'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php', 'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php',
'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php', 'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php',
@ -2837,8 +2843,14 @@ phutil_register_library_map(array(
'LiskMigrationIterator' => 'PhutilBufferedIterator', 'LiskMigrationIterator' => 'PhutilBufferedIterator',
'LiskRawMigrationIterator' => 'PhutilBufferedIterator', 'LiskRawMigrationIterator' => 'PhutilBufferedIterator',
'ManiphestBatchEditController' => 'ManiphestController', 'ManiphestBatchEditController' => 'ManiphestController',
'ManiphestCapabilityBulkEdit' => 'PhabricatorPolicyCapability',
'ManiphestCapabilityDefaultEdit' => 'PhabricatorPolicyCapability', 'ManiphestCapabilityDefaultEdit' => 'PhabricatorPolicyCapability',
'ManiphestCapabilityDefaultView' => 'PhabricatorPolicyCapability', 'ManiphestCapabilityDefaultView' => 'PhabricatorPolicyCapability',
'ManiphestCapabilityEditAssign' => 'PhabricatorPolicyCapability',
'ManiphestCapabilityEditPolicies' => 'PhabricatorPolicyCapability',
'ManiphestCapabilityEditPriority' => 'PhabricatorPolicyCapability',
'ManiphestCapabilityEditProjects' => 'PhabricatorPolicyCapability',
'ManiphestCapabilityEditStatus' => 'PhabricatorPolicyCapability',
'ManiphestConfiguredCustomField' => 'ManiphestConfiguredCustomField' =>
array( array(
0 => 'ManiphestCustomField', 0 => 'ManiphestCustomField',

View file

@ -67,7 +67,7 @@ final class PhabricatorApplicationManiphest extends PhabricatorApplication {
'export/(?P<key>[^/]+)/' => 'ManiphestExportController', 'export/(?P<key>[^/]+)/' => 'ManiphestExportController',
'subpriority/' => 'ManiphestSubpriorityController', 'subpriority/' => 'ManiphestSubpriorityController',
'subscribe/(?P<action>add|rem)/(?P<id>[1-9]\d*)/' 'subscribe/(?P<action>add|rem)/(?P<id>[1-9]\d*)/'
=> 'ManiphestSubscribeController', => 'ManiphestSubscribeController',
), ),
); );
} }
@ -100,6 +100,18 @@ final class PhabricatorApplicationManiphest extends PhabricatorApplication {
'caption' => pht( 'caption' => pht(
'Default edit policy for newly created tasks.'), 'Default edit policy for newly created tasks.'),
), ),
ManiphestCapabilityEditStatus::CAPABILITY => array(
),
ManiphestCapabilityEditAssign::CAPABILITY => array(
),
ManiphestCapabilityEditPolicies::CAPABILITY => array(
),
ManiphestCapabilityEditPriority::CAPABILITY => array(
),
ManiphestCapabilityEditProjects::CAPABILITY => array(
),
ManiphestCapabilityBulkEdit::CAPABILITY => array(
),
); );
} }

View file

@ -0,0 +1,20 @@
<?php
final class ManiphestCapabilityBulkEdit
extends PhabricatorPolicyCapability {
const CAPABILITY = 'maniphest.edit.bulk';
public function getCapabilityKey() {
return self::CAPABILITY;
}
public function getCapabilityName() {
return pht('Can Bulk Edit Tasks');
}
public function describeCapabilityRejection() {
return pht('You do not have permission to bulk edit tasks.');
}
}

View file

@ -0,0 +1,20 @@
<?php
final class ManiphestCapabilityEditAssign
extends PhabricatorPolicyCapability {
const CAPABILITY = 'maniphest.edit.assign';
public function getCapabilityKey() {
return self::CAPABILITY;
}
public function getCapabilityName() {
return pht('Can Assign Tasks');
}
public function describeCapabilityRejection() {
return pht('You do not have permission to assign tasks.');
}
}

View file

@ -0,0 +1,20 @@
<?php
final class ManiphestCapabilityEditPolicies
extends PhabricatorPolicyCapability {
const CAPABILITY = 'maniphest.edit.policies';
public function getCapabilityKey() {
return self::CAPABILITY;
}
public function getCapabilityName() {
return pht('Can Edit Task Policies');
}
public function describeCapabilityRejection() {
return pht('You do not have permission to edit task policies.');
}
}

View file

@ -0,0 +1,20 @@
<?php
final class ManiphestCapabilityEditPriority
extends PhabricatorPolicyCapability {
const CAPABILITY = 'maniphest.edit.priority';
public function getCapabilityKey() {
return self::CAPABILITY;
}
public function getCapabilityName() {
return pht('Can Prioritize Tasks');
}
public function describeCapabilityRejection() {
return pht('You do not have permission to prioritize tasks.');
}
}

View file

@ -0,0 +1,20 @@
<?php
final class ManiphestCapabilityEditProjects
extends PhabricatorPolicyCapability {
const CAPABILITY = 'maniphest.edit.projects';
public function getCapabilityKey() {
return self::CAPABILITY;
}
public function getCapabilityName() {
return pht('Can Edit Task Projects');
}
public function describeCapabilityRejection() {
return pht('You do not have permission to edit task projects.');
}
}

View file

@ -0,0 +1,20 @@
<?php
final class ManiphestCapabilityEditStatus
extends PhabricatorPolicyCapability {
const CAPABILITY = 'maniphest.edit.status';
public function getCapabilityKey() {
return self::CAPABILITY;
}
public function getCapabilityName() {
return pht('Can Edit Task Status');
}
public function describeCapabilityRejection() {
return pht('You do not have permission to edit task status.');
}
}

View file

@ -7,6 +7,9 @@ final class ManiphestBatchEditController extends ManiphestController {
public function processRequest() { public function processRequest() {
$this->requireApplicationCapability(
ManiphestCapabilityBulkEdit::CAPABILITY);
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();

View file

@ -165,6 +165,27 @@ final class ManiphestTaskDetailController extends ManiphestController {
ManiphestTransaction::TYPE_PROJECTS => pht('Associate Projects'), ManiphestTransaction::TYPE_PROJECTS => pht('Associate Projects'),
); );
// Remove actions the user doesn't have permission to take.
$requires = array(
ManiphestTransaction::TYPE_OWNER =>
ManiphestCapabilityEditAssign::CAPABILITY,
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestCapabilityEditPriority::CAPABILITY,
ManiphestTransaction::TYPE_PROJECTS =>
ManiphestCapabilityEditProjects::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestCapabilityEditStatus::CAPABILITY,
);
foreach ($transaction_types as $type => $name) {
if (isset($requires[$type])) {
if (!$this->hasApplicationCapability($requires[$type])) {
unset($transaction_types[$type]);
}
}
}
if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) { if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) {
$resolution_types = array_select_keys( $resolution_types = array_select_keys(
$resolution_types, $resolution_types,

View file

@ -1,8 +1,5 @@
<?php <?php
/**
* @group maniphest
*/
final class ManiphestTaskEditController extends ManiphestController { final class ManiphestTaskEditController extends ManiphestController {
private $id; private $id;
@ -16,6 +13,17 @@ final class ManiphestTaskEditController extends ManiphestController {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$can_edit_assign = $this->hasApplicationCapability(
ManiphestCapabilityEditAssign::CAPABILITY);
$can_edit_policies = $this->hasApplicationCapability(
ManiphestCapabilityEditPolicies::CAPABILITY);
$can_edit_priority = $this->hasApplicationCapability(
ManiphestCapabilityEditPriority::CAPABILITY);
$can_edit_projects = $this->hasApplicationCapability(
ManiphestCapabilityEditProjects::CAPABILITY);
$can_edit_status = $this->hasApplicationCapability(
ManiphestCapabilityEditStatus::CAPABILITY);
$files = array(); $files = array();
$parent_task = null; $parent_task = null;
$template_id = null; $template_id = null;
@ -40,20 +48,24 @@ final class ManiphestTaskEditController extends ManiphestController {
if (!$request->isFormPost()) { if (!$request->isFormPost()) {
$task->setTitle($request->getStr('title')); $task->setTitle($request->getStr('title'));
$default_projects = $request->getStr('projects'); if ($can_edit_projects) {
if ($default_projects) { $default_projects = $request->getStr('projects');
$task->setProjectPHIDs(explode(';', $default_projects)); if ($default_projects) {
$task->setProjectPHIDs(explode(';', $default_projects));
}
} }
$task->setDescription($request->getStr('description')); $task->setDescription($request->getStr('description'));
$assign = $request->getStr('assign'); if ($can_edit_assign) {
if (strlen($assign)) { $assign = $request->getStr('assign');
$assign_user = id(new PhabricatorUser())->loadOneWhere( if (strlen($assign)) {
'username = %s', $assign_user = id(new PhabricatorUser())->loadOneWhere(
$assign); 'username = %s',
if ($assign_user) { $assign);
$task->setOwnerPHID($assign_user->getPHID()); if ($assign_user) {
$task->setOwnerPHID($assign_user->getPHID());
}
} }
} }
} }
@ -122,7 +134,9 @@ final class ManiphestTaskEditController extends ManiphestController {
$changes[ManiphestTransaction::TYPE_TITLE] = $new_title; $changes[ManiphestTransaction::TYPE_TITLE] = $new_title;
$changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc; $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc;
$changes[ManiphestTransaction::TYPE_STATUS] = $new_status; if ($can_edit_status) {
$changes[ManiphestTransaction::TYPE_STATUS] = $new_status;
}
$owner_tokenizer = $request->getArr('assigned_to'); $owner_tokenizer = $request->getArr('assigned_to');
$owner_phid = reset($owner_tokenizer); $owner_phid = reset($owner_tokenizer);
@ -173,17 +187,27 @@ final class ManiphestTaskEditController extends ManiphestController {
$task->setProjectPHIDs($request->getArr('projects')); $task->setProjectPHIDs($request->getArr('projects'));
} else { } else {
$changes[ManiphestTransaction::TYPE_PRIORITY] = if ($can_edit_priority) {
$request->getInt('priority'); $changes[ManiphestTransaction::TYPE_PRIORITY] =
$changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid; $request->getInt('priority');
$changes[ManiphestTransaction::TYPE_CCS] = $request->getArr('cc'); }
$changes[ManiphestTransaction::TYPE_PROJECTS] = if ($can_edit_assign) {
$request->getArr('projects'); $changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid;
}
$changes[PhabricatorTransactions::TYPE_VIEW_POLICY] = $changes[ManiphestTransaction::TYPE_CCS] = $request->getArr('cc');
$request->getStr('viewPolicy');
$changes[PhabricatorTransactions::TYPE_EDIT_POLICY] = if ($can_edit_projects) {
$request->getStr('editPolicy'); $changes[ManiphestTransaction::TYPE_PROJECTS] =
$request->getArr('projects');
}
if ($can_edit_policies) {
$changes[PhabricatorTransactions::TYPE_VIEW_POLICY] =
$request->getStr('viewPolicy');
$changes[PhabricatorTransactions::TYPE_EDIT_POLICY] =
$request->getStr('editPolicy');
}
if ($files) { if ($files) {
$file_map = mpull($files, 'getPHID'); $file_map = mpull($files, 'getPHID');
@ -418,7 +442,7 @@ final class ManiphestTaskEditController extends ManiphestController {
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($task->getTitle())); ->setValue($task->getTitle()));
if ($task->getID()) { if ($task->getID() && $can_edit_status) {
// Only show this in "edit" mode, not "create" mode, since creating a // Only show this in "edit" mode, not "create" mode, since creating a
// non-open task is kind of silly and it would just clutter up the // non-open task is kind of silly and it would just clutter up the
// "create" interface. // "create" interface.
@ -436,58 +460,73 @@ final class ManiphestTaskEditController extends ManiphestController {
->setObject($task) ->setObject($task)
->execute(); ->execute();
$form if ($can_edit_assign) {
->appendChild( $form->appendChild(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setLabel(pht('Assigned To')) ->setLabel(pht('Assigned To'))
->setName('assigned_to') ->setName('assigned_to')
->setValue($assigned_value) ->setValue($assigned_value)
->setUser($user) ->setUser($user)
->setDatasource('/typeahead/common/users/') ->setDatasource('/typeahead/common/users/')
->setLimit(1)) ->setLimit(1));
}
$form
->appendChild( ->appendChild(
id(new AphrontFormTokenizerControl()) id(new AphrontFormTokenizerControl())
->setLabel(pht('CC')) ->setLabel(pht('CC'))
->setName('cc') ->setName('cc')
->setValue($cc_value) ->setValue($cc_value)
->setUser($user) ->setUser($user)
->setDatasource('/typeahead/common/mailable/')) ->setDatasource('/typeahead/common/mailable/'));
->appendChild(
id(new AphrontFormSelectControl()) if ($can_edit_priority) {
->setLabel(pht('Priority')) $form
->setName('priority') ->appendChild(
->setOptions($priority_map) id(new AphrontFormSelectControl())
->setValue($task->getPriority())) ->setLabel(pht('Priority'))
->appendChild( ->setName('priority')
id(new AphrontFormPolicyControl()) ->setOptions($priority_map)
->setUser($user) ->setValue($task->getPriority()));
->setCapability(PhabricatorPolicyCapability::CAN_VIEW) }
->setPolicyObject($task)
->setPolicies($policies) if ($can_edit_policies) {
->setName('viewPolicy')) $form
->appendChild( ->appendChild(
id(new AphrontFormPolicyControl()) id(new AphrontFormPolicyControl())
->setUser($user) ->setUser($user)
->setCapability(PhabricatorPolicyCapability::CAN_EDIT) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)
->setPolicyObject($task) ->setPolicyObject($task)
->setPolicies($policies) ->setPolicies($policies)
->setName('editPolicy')) ->setName('viewPolicy'))
->appendChild( ->appendChild(
id(new AphrontFormTokenizerControl()) id(new AphrontFormPolicyControl())
->setLabel(pht('Projects')) ->setUser($user)
->setName('projects') ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
->setValue($projects_value) ->setPolicyObject($task)
->setID($project_tokenizer_id) ->setPolicies($policies)
->setCaption( ->setName('editPolicy'));
javelin_tag( }
'a',
array( if ($can_edit_projects) {
'href' => '/project/create/', $form
'mustcapture' => true, ->appendChild(
'sigil' => 'project-create', id(new AphrontFormTokenizerControl())
), ->setLabel(pht('Projects'))
pht('Create New Project'))) ->setName('projects')
->setDatasource('/typeahead/common/projects/')); ->setValue($projects_value)
->setID($project_tokenizer_id)
->setCaption(
javelin_tag(
'a',
array(
'href' => '/project/create/',
'mustcapture' => true,
'sigil' => 'project-create',
),
pht('Create New Project')))
->setDatasource('/typeahead/common/projects/'));
}
foreach ($aux_fields as $aux_field) { foreach ($aux_fields as $aux_field) {
$aux_control = $aux_field->renderEditControl(); $aux_control = $aux_field->renderEditControl();

View file

@ -46,7 +46,11 @@ final class ManiphestTaskListController
$group_parameter, $group_parameter,
$handles); $handles);
$can_edit_priority = $this->hasApplicationCapability(
ManiphestCapabilityEditPriority::CAPABILITY);
$can_drag = ($order_parameter == 'priority') && $can_drag = ($order_parameter == 'priority') &&
($can_edit_priority) &&
($group_parameter == 'none' || $group_parameter == 'priority'); ($group_parameter == 'none' || $group_parameter == 'priority');
if (!$viewer->isLoggedIn()) { if (!$viewer->isLoggedIn()) {
@ -91,11 +95,13 @@ final class ManiphestTaskListController
)); ));
} }
Javelin::initBehavior( if ($can_drag) {
'maniphest-subpriority-editor', Javelin::initBehavior(
array( 'maniphest-subpriority-editor',
'uri' => '/maniphest/subpriority/', array(
)); 'uri' => '/maniphest/subpriority/',
));
}
return phutil_tag( return phutil_tag(
'div', 'div',
@ -191,6 +197,11 @@ final class ManiphestTaskListController
private function renderBatchEditor(PhabricatorSavedQuery $saved_query) { private function renderBatchEditor(PhabricatorSavedQuery $saved_query) {
$user = $this->getRequest()->getUser(); $user = $this->getRequest()->getUser();
$batch_capability = ManiphestCapabilityBulkEdit::CAPABILITY;
if (!$this->hasApplicationCapability($batch_capability)) {
return null;
}
if (!$user->isLoggedIn()) { if (!$user->isLoggedIn()) {
// Don't show the batch editor or excel export for logged-out users. // Don't show the batch editor or excel export for logged-out users.
// Technically we //could// let them export, but ehh. // Technically we //could// let them export, but ehh.

View file

@ -81,9 +81,9 @@ final class ManiphestTransactionEditor
case ManiphestTransaction::TYPE_EDGE: case ManiphestTransaction::TYPE_EDGE:
return $xaction->getNewValue(); return $xaction->getNewValue();
} }
} }
protected function transactionHasEffect( protected function transactionHasEffect(
PhabricatorLiskDAO $object, PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
@ -249,6 +249,43 @@ final class ManiphestTransactionEditor
} }
} }
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
parent::requireCapabilities($object, $xaction);
$app_capability_map = array(
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestCapabilityEditPriority::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestCapabilityEditStatus::CAPABILITY,
ManiphestTransaction::TYPE_PROJECTS =>
ManiphestCapabilityEditProjects::CAPABILITY,
ManiphestTransaction::TYPE_OWNER =>
ManiphestCapabilityEditAssign::CAPABILITY,
PhabricatorTransactions::TYPE_EDIT_POLICY =>
ManiphestCapabilityEditPolicies::CAPABILITY,
PhabricatorTransactions::TYPE_VIEW_POLICY =>
ManiphestCapabilityEditPolicies::CAPABILITY,
);
$transaction_type = $xaction->getTransactionType();
$app_capability = idx($app_capability_map, $transaction_type);
if ($app_capability) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($this->getActor())
->withClasses(array('PhabricatorApplicationManiphest'))
->executeOne();
PhabricatorPolicyFilter::requireCapability(
$this->getActor(),
$app,
$app_capability);
}
}
public static function getNextSubpriority($pri, $sub) { public static function getNextSubpriority($pri, $sub) {
// TODO: T603 Figure out what the policies here should be once this gets // TODO: T603 Figure out what the policies here should be once this gets

View file

@ -460,6 +460,12 @@ abstract class PhabricatorApplicationTransactionEditor
return array(); return array();
} }
// Now that we've merged, filtered, and combined transactions, check for
// required capabilities.
foreach ($xactions as $xaction) {
$this->requireCapabilities($object, $xaction);
}
$xactions = $this->sortTransactions($xactions); $xactions = $this->sortTransactions($xactions);
if ($is_preview) { if ($is_preview) {
@ -696,10 +702,6 @@ abstract class PhabricatorApplicationTransactionEditor
$actor, $actor,
$object, $object,
PhabricatorPolicyCapability::CAN_VIEW); PhabricatorPolicyCapability::CAN_VIEW);
foreach ($xactions as $xaction) {
$this->requireCapabilities($object, $xaction);
}
} }
protected function requireCapabilities( protected function requireCapabilities(