Sunday, 20 January 2008

Workflow: tasks which can only be actioned by task owner

When coming to Visual Studio workflows, something which surprises many SharePoint developers is that tasks assigned in the workflow can actually be actioned by any SharePoint user with basic permissions. In the worst scenario, 'basic permissions' means any user with contribute permissions to the SharePoint web which contains the workflow tasks list, and clearly this could be a whole lot of users who have nothing to do with your workflows. In the best case, you might have tied down permissions so that only users involved in workflows can use the list. Even so, this still means that any actor in the workflow can respond to any task, not just tasks which have actually been assigned to them. To my mind, this is bad - all it takes is a confused user to accidentally respond to someone else's task and your workflow is in a whole world of chaos.

So what can we do about this?

Well, there doesn't seem to be much written about this, but fortunately the best solution (AFAIK) is also the simplest one. Before we dive in, I notice other people needing to solve this problem have taken the approach that since a workflow task is just a list item, we can execute some code to set permissions on the list item using the API. A logical tactic, but happily there is special provision for doing this in the workflow framework - we still need to write a little code, but it's much simpler than that approach. The key is the 'SpecialPermissions' property of the CreateTask activity:



Pitfall - confusingly, clicking the ellipses button (...) for the property presents a generic VS collection editor (shown below), which as far as I can tell just flat cannot be used with this property - all the controls are disabled!




I'm assuming this is a bug in the Visual Studio 2005 Extensions for Workflow Foundation, so we'll ignore that! However, clicking the tiny blue 'bind property' button presents the more familiar 'bind the property to an instance variable' dialog - assuming you haven't already created a variable to store the permissions for this CreateTask, we should select 'Bind to a new member', and create either a field or property to store the permissions:



This creates a collection object, specifically a HybridDictionary, to which we can add items for each permission we need for this task. And we only need a handful of code lines to do it! Since we're likely to use it for many (i.e. all) tasks in our workflow, let's have a separate method we can call each time:

private void setTaskPermissions(HybridDictionary boundDictionary, string sTaskOwner)

{

     boundDictionary.Clear();

     boundDictionary.Add(sTaskOwner, SPRoleType.Contributor);

     boundDictionary.Add("NT AUTHORITY\\authenticated users", SPRoleType.Reader);

}


So, we pass in the collection specific to each task, and also the string username for the task owner. We then add an entry for the task owner to the dictionary with the 'contributor' permission, and one for all other users with just read permissions. Note we also clear out the dictionary before adding in case this task has already been issued (i.e. something got rejected in the workflow and we came back to this task a second time) - this avoids any errors due to the key already existing in the dictionary.


The calling code then, looks like this:

setTaskPermissions(approveExpenseClaim_SpecialPermissions, taskProps.AssignedTo);

This would be added to the code for each CreateTask activity in your workflow. The first parameter is the variable we bound earlier to the SpecialPermissions property (of the particular task we are dealing with), and taskProps is the SPWorkflowTaskProperties object which holds data for the task.

And that's it - much less code than you'd need to modify permissions for the list item with general API usage. The effect of this is that the task owner is the only standard user (administrators with full control excepted) who can respond to the task, but all others can read it. Needless to say, you could customize the code to your specific permission requirements if they are different to mine.

The user experience

One final thing worth pointing out is that the user experience might not be quite as slick as you'd like. Since we've restricted permissions on the item, any user who clicks on the task but doesn't have permissions will see the standard access denied message:



Personally I think an improvement would be to show a more friendly message, but this would require substantially more effort and complexity. My view is that for a few lines of code, this approach is a great trade off between effort required and benefit of protecting the integrity of the workflow - I'm definitely not a fan of sleepless nights wondering just what would happen in the workflow if users unintentionally responded to tasks which didn't belong to them, so it works for me. As always, if you've implemented a different way of dealing with this problem or have other comments, it would be great to hear.

17 comments:

Matt Morse said...

Nice post, Chris. I've had this exact question come up a number of times from clients, and while item-level permissions is the logical answer, I wasn't aware how straightforward it was to accomplish within the workflow. Thanks for the example!

Anonymous said...

Hi Chis, like your example.
But now I have the requirement that the user I'd like to add as Contributer should not be able to delete the task assigned to him. Therefore I need to add some other permissionset which does not allow the user to delete the item. Any suggestions how I could manage that? THX and best regards.

Chris O'Brien said...

Hi,

I think what you should seek to do is not to give the contributor permission set to the user, but apply individual permission instead. This will give you the flexibility to give 'edit item' but not 'delete item' permissions. You could probably create a custom role to do this, since this would group together the set of permissions you wish to apply.

You'd have to do some research on exactly how to write the code, but I think that's the approach you could take.

HTH,

Chris.

Brad said...

This works very well with individuals. However when assigning an item to a group, if someone claims the task and then tries to edit the task it causes an error in workflow. Presumably because the individual's name is not included with contribute access. Any ideas on getting around this. I'd like to get rid of the Claim Task function but don't know how.

Chris O'Brien said...

Hi Brad,

Hmm, interesting - but can you not simply add the group to the dictionary with SPRoleType.Contributor access instead of an individual?

Cheers,

Chris.

Giabba said...

The example is great and works but how can i be sure that only one user edit the task ? I haven't found a lock/unlock mechanism in SP Task handling used in workflow.

Thanks again.

Chris O'Brien said...

Giabba,

It's been a while since I've thought about this, but I'm pretty sure there is some protection from this scenario somewhere. Could be because the task is a list item which requires check-in/check-out. Failing that, it could be that the workflow task screens provide conflict resolution (e.g. messages such as 'Task has been modifed, do you want to overwrite or discard your changes?' etc.).

Suggest doing some testing to find out for sure.

HTH,

Chris.

Anonymous said...

Thanks for the tip. This was very helpful

Anas Boushie said...

can I set permission for group of users, That I have created group and call it HR Group, i added users(usera , userb) to it.
when both usera and userb tried to open task the got Error" Value doesnt fall withen expected range. but when i assing task to one user , every thing is OK

Jyothish Nair said...

You can add a group to the dictionary and that works just fine... (at least it works for me :) )

Anonymous said...

I've got a new Sharepoint dev box I'm working on, and I noticed that I no longer have the little blue link for "Bind Selected Property" under the Properties window. Does anyone know how I can get it to show up again. I can't set the SpecialPermissions property without it. Thanks.

Chris O'Brien said...

@Anonymous,

Could it be that the property is already bound to something? Suggest checking by doing a search in the code..

HTH,

Chris.

ginni said...

Hi Chris,

This is a nice post. I must appreciate it. I also faced this problem and figured out a solution which is little diff from this. I would like to share it.

I used SharePoint Designer to acheieve this task. Over there i downloaded an activity known as Grant permission (Which assign permissions to any user). Then i gave all the users of that task list just one single read permission and created a small workflow that whenever any item is created, grant permission activity will give contribute permission to "Assigned To" person. In this way, a person who has been assigned a task can only have Contribute permission.

Regards
Ginni

Anonymous said...

This is a nice post but it doesn't work for groups. It is possible to add a group name to the dictionary but doing so doesn't assign rights to the group. Does anyone know of a way to do this for a group?

Mina Samy said...

Hi Chris
thanks for this post

Macrel said...

If you want to restrict access to one or more groups, you can use the built in Roles functionality. Ref. Ch 6. Professional Microsoft SharePoint 2007 Workflow Programming by Khosravi.
1) For each task, set the Roles property to a unique field.
2) In Workflow Activated set which group can execute a task:
WorkflowRole burnPermitApproversWorkflowRole = SPWorkflowWorkflowRoleCreator.GetWorkflowRoleForGroups(
site, Config.GetAppSetting("BurnPermitApproversGroupName"));
// Add role to the event's roles collection to allow users with this role to
// execute the onTaskChanged_InitialReview event
onTaskChanged_InitialReview_Roles.Add(burnPermitApproversWorkflowRole);

--Marcel B

Anonymous said...

Very nice ;-)

It works fine for me, thanks a lot