Security in Workflow Engine .NET

Security in Workflow Engine .NET

Security in Workflow Engine .NET

photo by Sylwia Bartyzel

Introduction

The security system of the workflow engine can be described in just two words. The engine doesn't have embedded security. In fact, this is great news. This means that you can easily integrate the engine with any of your security system. In this article, we examine the following:

  •  The transfer of the user ID into the rules.
  •  Integration with your role system.
  •  Combining rules.

The transfer of the user ID into the rules

As we can see in the demo application was added the command - "Set current User".  The code which implements this  command works extremely simple. You enter the user ID and it is written to the static field, after that it transferred to the runtime functions such as: GetAvailableCommands, ExecuteCommand and SetState. When you call the "GetAvailableCommands" method user's ID is transferred to the methods of the Rule Provider or rules described in code actions. In these methods, you can implement the access logic to  execution of the transitions. The user ID is passed to all methods as a string, so you can use any data type for your ID: GUID, integer or just the username/login.

private static string _currentUser = string.Empty;
...
private static void SetUser()
{
    Console.Write("Enter user's id:");
    var readLine = Console.ReadLine();
    if (readLine != null)
    {
    	_currentUser = readLine.Trim();
    }
}
...
WorkflowInit.Runtime.GetAvailableCommands(_processId.Value, _currentUser);
...
WorkflowInit.Runtime.ExecuteCommand(command,_currentUser,_currentUser);
...
WorkflowInit.Runtime.SetState(_processId.Value, _currentUser, _currentUser, state.Name ...);

Integration with role system

The best way to inform the engine about your role model is the implementation of IWorkflowRuleProvider.  In it, we are implementing a method that returns a list of user IDs that belong to the role of the name (or ID) that is passed via string parameter. In the sample, this method returns a list of hardcoded user logins, but there you can implement any logic of integration with your role system.

It should be noted that the format of a user ID, which is passed to the function _runtime.GetAvailableCommands will be same as the one that is passed to a function WorkflowRule.Check (parameter identityId) and therefore must be the same format as the return value of the WorkflowRule.CheckRole function. In other words, if we passed  a user's login to the GetAvailableCommands function  (user1, user2, etc.), the WorkflowRule.CheckRole function must return a list of a user's logins.

public class WorkflowRule : IWorkflowRuleProvider
{
 	...
    private IEnumerable CheckRole(ProcessInstance pi, string parameter)
        {
            List result = new List();
            switch (parameter.ToLower())
            {
                case "manager":
                    result.Add("user1");
                    result.Add("user3");
                    break;
                case "ceo":
                    result.Add("user2");
                    break;
                case "accountant":
                     result.Add("user4");
                    break;
                default:
                    throw new Exception("UnknownRole");

            }
            return result;
        }	
 	...
}

In order to use the roles in the designer, we need to create the actors. As a rule choose "CheckRole", as well as the Value to specify the name (or code or identifier) of the role, which will correspond to the actor.

After that we can use roles as the restrictions for transitions.

Сombining of rules

Sometimes it is necessary to combine the rules,such as: an user could execute a transition if the user belong to the role "ceo" or "accountant" or an user could execute a transition if the user belong to the role "manager" and working in same division as the author of the document. There are two ways to do this using the workflow engine.

The first way, is an concatenation of the rules in the designer. In the window "Transition" in the "Restrictions" section, you can click on the button "Additional params". After that will be available two options "Concat allow as" and "Concat restrict as". By default, both options are And. 

They work as follows. First, for each rule where "Type" == Allow identifies all users who are allowed to make the transition. If "Concat allow as" == And , the calculated intersection of the lists obtained. If "Concat allow as" == Or, the lists are merged. The output is a list of user IDs, which allowed operation. In a similar way the engine obtains a list users who are not allowed operation. Only in this way using the rules in which "Type" == Allow and using the value of "Concat restrict as" option. The output is a list of user IDs, which not allowed the operation. Then removed from the list of permits user IDs that are present in the list of prohibitions. At the output, we have a list of user IDs that are allowed to execute the transition. We explain the algorithm, Let we have the four rules which returning the following user IDs.

  • Rule1 (Type==Allow) returns Ids: 1,2,3
  • Rule2 (Type==Allow) returns Ids: 2,3,4
  • Rule3 (Type==Restrict) returns Ids: 1,2
  • Rule4 (Type==Restrict) returns Ids: 2,3

If "Concat allow as" == And and "Concat restrict as" == And (it is default values). We get the following lists:
Allowed, user's IDs : 2,3
Not allowed, user's IDs : 2
Result: 3

If "Concat allow as" == Or and "Concat restrict as" == Or. We get the following lists:
Allowed, user's IDs : 1,2,3,4
Not allowed, user's IDs : 1,2,3
Result: 4

If "Concat allow as" == And and "Concat restrict as" == Or. We get the following lists:
Allowed, user's IDs : 2,3
Not allowed, user's IDs : 1,2,3
Result: empty

If "Concat allow as" == Or and "Concat restrict as" == And . We get the following lists:
Allowed, user's IDs : 1,2,3,4
Not allowed, user's IDs : 2
Result: 1,3,4

The second way is to write a new rule, which can execute existing rules. And combine them in any way. Such a rule will be discussed in the next section. For calling the rule from the code, you can use the following instructions:

var managers = runtime.RuleProvider.GetIdentities(processInstance, "CheckRole","manager");

The Scheme

The process shown in the scheme is trivial. The document goes the stage of approval manager, who must work in the same unit as the author of the document. Delee document must approve the accountant, CEO also has the ability to approve the document.

The code below is executed when the the document comes to the state of "Manager Approval". In it the user's ID whic executed the command "start" is stored in the process variable.

void SaveAuthor (ProcessInstance processInstance, WorkflowRuntime runtime, string parameter) {
	processInstance.SetParameter ("Author",processInstance.IdentityId,ParameterPurpose.Persistence);
}

Access to the transition from a state of "Manager Approval" to "Accountant Approval" is defined by the rule "ManagerInDivision". It uses the call of the "CheckRule" rule for the role of "manager", and then returned the intersection of the list of users in the role of "manager" with a list of members in the division of the author.

IEnumerable<string> ManagerInDivision (ProcessInstance processInstance, WorkflowRuntime runtime, string parameter) {
	var managers = runtime.RuleProvider.GetIdentities(processInstance, runtime, "CheckRole","manager");

	var author = processInstance.GetParameter<string>("Author");

	var division1 = new List<string>{"user1","user2"};
	var division2 = new List<string>{"user3","user4"};

	if (division1.Contains(author))
	{
	    return division1.Intersect(managers);
	}
	else if (division2.Contains(author))
	{
	    return division2.Intersect(managers);
	}
	else
	{
	    return new List<string>();
	}
}

Access to the transition from a state of "Accountant Approval" to "Final" is determined by checking the user's role as "ceo" or "accountant". Execution of the rule "CheckRule" with the parameter "ceo" or "manager" are combined using "Concat allow as" = Or in the settings of the transition.

The Demo

First, we specify the user to execute "start" command. Let it be "user2". Execute the "start" command. User ID is stored in the variable of the process named "Author".

...
The process is not created.
CreateInstance - OK.
ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: Draft, CurrentActivity: Draft
Current user is undefined.
Enter code of operation:0
Enter user id:user2

ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: Draft, CurrentActivity: Draft
Current user = user2.
Enter code of operation:3
Available commands:
- start (LocalizedName:start, Classifier:Direct)
Enter command:start
ExecuteCommand - OK.
ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: ManagerApprove, CurrentActivity: ManagerApprove

State of the process changed to "MangerApprove". In this state, the document can be approved by an user with the role of "manager" working in same division as the author of the document. The role of "manager" assigned to users "user1" and "user3". Let us specify "user3" as the user to execute the commands. We see that the the commands are not available for the user, because according to the rule "ManagerInDivision" "user3" works in another division.

ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: ManagerApprove, CurrentActivity: ManagerApprove
Current user = user2.
Enter code of operation:0
Enter user id:user3

ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: ManagerApprove, CurrentActivity: ManagerApprove
Current user = user3.
Enter code of operation:3
Available commands:
Not found!
Enter command:

Set the current user in the "user 1", the user has the role of "manager" and works in the same division as the "user2". Now we can execute the command "approve".

ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: ManagerApprove, CurrentActivity: ManagerApprove
Current user = user3.
Enter code of operation:0
Enter user id:user1

ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: ManagerApprove, CurrentActivity: ManagerApprove
Current user = user1.
Enter code of operation:3
Available commands:
- approve (LocalizedName:approve, Classifier:Direct)
Enter command:approve
ExecuteCommand - OK.
ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: AccountantApprove, CurrentActivity: AccountantApprove

State of the process changed to "AccountantApprove". In this state, the document can be approved by an user with the role of "ceo" or "accountant". You can specify "user2" or "user4" as currnet user and execute the command "approve".

ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: AccountantApprove, CurrentActivity: AccountantApprove
Current user = user1.
Enter code of operation:0
Enter user id:user4

ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: AccountantApprove, CurrentActivity: AccountantApprove
Current user = user4.
Enter code of operation:3
Available commands:
- approve (LocalizedName:approve, Classifier:Direct)
Enter command:approve
ExecuteCommand - OK.

ProcessId = '0cda22b0-6cb6-496f-a99c-a3cb311c32d8'. CurrentState: Final, CurrentActivity: Final

Thank you for the attention!

  • By Dmitry Melnikov
  • 12/28/2015
  • security, role, rule, integration