Skip to main content

Rules, Actors and integration with your security system

How to Manage User Roles in WorkflowEngine?
How to Manage User Roles in WorkflowServer?

Rule - Actor - Restriction

Process

Workflow Engine does not have its own security system. That's why you won't have to adjust the existing one if you are going to integrate Workflow Engine into your application. Rules serve as a starting point for security integration. They allow you to call the available authorization methods, or write the new ones. Rules represent the functions that are called at a certain moment in time, and they are quite similar to Actions.

Workflow Engine identifies a Rule by its unique name. Each Rule has two functions:

  • Check - this function checks whether IdentityId conveyed to it satisfies this Rule. Check-function is called upon receiving of the list of the available commands after when the WorkflowInit.Runtime.GetAvailableCommands method is called. It is mandatory for the proper functioning of the system;
  • Get-function (GetIdentities) - returns the list of all system users who satisfy the Rule. It is required when the Pre-execution mode is on. However, it is not mandatory for the proper functioning of the methods which receive the list of users who can execute a command of the process in a given state. In other words, you may use this function if you want to fill the Inbox or notify users who should do something with your business entity. This feature is optional.

Each function always receives the ProcessInstance object, for which the Rule is checked, WorkflowRuntime, and a string parameter, which is specified in Designer (in the Actor object) and may contain JSON. IdentityId, a string that identifies a user, is conveyed to the Check-function. We opt for this string because different systems utilize different user identifiers, including line logins, GUIDs, int, etc., and it is possible to convey any of these types in the string.

Actor represents another object of the access control system. Actors are created in the respective section of Designer, and may be used in Transitions in the Restrictions section. Actor is a combination of Rule and Value: a string of the parameter conveyed to this Rule. Why do we need it? For example, a common task is to check whether a user satisfies a certain role. In this case, we simply create a Rule with the CheckRole name, and convey the role name through a string parameter. Then we create an Actor for each role, choose Rule=CheckRole, and specify the name of the role to be checked in Value. You may refer to the example in the Rules section for more details.

After all the required Actors are created, they can be used in Transitions in the Restrictions section. Restriction with the Allow type enables a user who satisfies the Rule to execute a command. This is the most common case. We will discuss Restrictions combinations further on. All Rules chosen in Restrictions through the Actor object will be automatically executed upon receiving the list of available commands.

There are two ways of creating Rules in your application: to inherit the IWorkflowRuleProvider interface and create Rules in this class, or to create Rules right in the scheme in the CodeActions section. These two approaches may be combined.

IWorkflowRuleProvider

This is a RuleProvider template. It is quite similar to ActionProvider:

public class RuleProvider : OptimaJet.Workflow.Core.Runtime.IWorkflowRuleProvider
{
private class RuleFunction
{
public Func<ProcessInstance, WorkflowRuntime, string, IEnumerable<string>> GetFunction { get; set; }

public Func<ProcessInstance, WorkflowRuntime, string, string, bool> CheckFunction { get; set; }
}

private readonly Dictionary<string, RuleFunction> _rules = new Dictionary<string, RuleFunction>();

public RuleProvider()
{
// Register your rules in the _rules Dictionary
_rules.Add("MyRule", new RuleFunction { CheckFunction = MyRuleCheck, GetFunction = MyRuleGet });
}

public IEnumerable<string> MyRuleGet(ProcessInstance processInstance, WorkflowRuntime runtime,
string parameter)
{
return new List<string>();
}

public bool MyRuleCheck(ProcessInstance processInstance, WorkflowRuntime runtime, string identityId,
string parameter)
{
return false;
}

#region Implementation of IWorkflowRuleProvider

public List<string> GetRules()
{
return _rules.Keys.ToList();
}

public bool Check(ProcessInstance processInstance, WorkflowRuntime runtime, string identityId,
string ruleName, string parameter)
{
if (_rules.ContainsKey(ruleName))
return _rules[ruleName].CheckFunction(processInstance, runtime, identityId, parameter);
throw new NotImplementedException();
}

public IEnumerable<string> GetIdentities(ProcessInstance processInstance, WorkflowRuntime runtime,
string ruleName, string parameter)
{
if (_rules.ContainsKey(ruleName))
return _rules[ruleName].GetFunction(processInstance, runtime, parameter);
throw new NotImplementedException();
}

#endregion
}

As it was mentioned earlier, WorkflowEngine identifies a Rule by its unique name. In order for Rules' names to appear in Designer in the Actor entity, they should be returned by the GetRules method from RuleProvider. When WorkflowEngine checks whether a user has access to a command, it calls the Check method and conveys ruleName (the name of Rule), processInstance object, workflowRuntime object and identityId (user identifier) to it. In case a user satisfies the Rule, Check method should return true. When WorkflowEngine wants to receive the list of identifiers of those users who satisfy the Rule, it calls the GetIdentities method; the conveyed parameters are the same as with the Check method.

In the abovementioned example, the IWorkflowRuleProvider implementation code won't change upon adding new Rules. To add a Rule, you simply need to add two functions, Get and Check, to the class, and include the delegate links to them to the _rules dictionary with the key corresponding to the rule name.

In order for WorkflowRuntime to learn about the provider you created, the provider's instance should be conveyed to WorkflowRuntime upon configuring:

runtime.WithRuleProvider(new RuleProvider())

Rule in the scheme

Rules can also be created in the scheme in the CodeAction section, just like Actions. The only difference is that RuleGet and RuleCheck types. For more details on how to create and debug CodeAction, refer to the respective section. However, there's an important difference. For a Rule to appear, you simply need to create one of the functions, RuleGet or RuleCheck. Here, three cases may occur:

  • Only RuleCheck is created. In this case, RuleGet will be created automatically, however, it will return an empty list of user identifiers. In other words, this is not what you need if you are going to use Pre-Execution, and receive the lists of users to create notifications and Inbox;

  • Only RuleGet is created. In this case, RuleCheck will be generated automatically on the basis of RuleGet. This is a much more preferable option. Here's what the generated code looks like:

    public bool MyRule_RuleCheck(ProcessInstance processInstance, WorkflowRuntime runtime,
    string identityId, string parameter)
    {
    return MyRule_RuleGet(processInstance, runtime, parameter)
    .Any(id => id.Equals(identityId, StringComparison.InvariantCultureIgnoreCase));
    }
  • In case both RuleGet and RuleCheck are created, each of them is used according to its purpose. You may use this option to enhance the performance of your solution.

Restrictions, their concatenation and checks

Each Transition can have more than one Restriction with Allow and Restrict types. It is also possible to change the way Restrictions are concatenated separately for each type. Remember that in Transition you may specify Concat allow as and Concat restrict as attributes with values And or Or. Concat allow as and Concat restrict as have an And value by default. Let's see how it works in all cases:

  • No Restrictions are attributed to a transition. In this case, a command corresponding to this transition will be available to all users. However, if you try to obtain the list of all users whom this command is available to, the system will return an empty list;

  • The most common case is when at least one Restriction with the Allow type and Concat allow as = And is attributed to a transition and there are no Restrictions with Restrict type. In this case, a command will be available to a user who satisfies all Restrictions. The final list of the users whom the command is available to will be formed as an intersection of lists returned by all Rules.

Below are rare but sometimes useful cases:

  • At least one Restriction with the Allow type, Concat allow as = And and any number (including 0) of Restrictions with type Restrict, Concat restrict as = And are attributed to a transition. A command will be available to a user who satisfies all Allow Restrictions and does not satisfy all Restrict Restrictions (if there are any). The list of users will be formed in the following way: first, all Allow Restrictions are identified, and the intersection of the received lists is built. In other words, only those user identifiers which are contained in all lists are selected. Then, Restrict Restrictions are identified, and all user identifiers which are contained in all Restrict lists are removed from the final list;

  • At least one Restriction with the Allow type, Concat allow as = And and any number (including 0) of Restrictions with type Restrict, Concat restrict as = Or are attributed to a transition. A command will be available to the user who satisfies all Allow Restrictions and does not satisfy at least one Restrict Restriction (if there are any). The list of users will be formed in the following way: first, all Allow restrictions are identified, and the intersection of the received lists is built. In other words, all user identifiers which are contained at least in one Allow list are included in the final list. Then, Restrict Restrictions are identified, and all user identifiers which are contained at least in one Restrict list are removed from the final list;

  • At least one Restriction with the Allow type, Concat allow as = Or and any number (including 0) of Restrictions with type Restrict, Concat restrict as = And are attributed to a transition. A command will be available to the user who satisfies at least one Allow Restriction and does not satisfy all Restrict Restrictions (if there are any). The list of users will be formed in the following way: first, all Allow Restrictions are identified, and these lists are combined. Then, Restrict Restrictions are identified, and all user identifiers which are contained in all Restrict lists are removed from the final list.

  • At least one Restriction with the Allow type, Concat allow as = Or and any number (including 0) of Restrictions with type Restrict, Concat restrict as = Or are attributed to a transition. A command will be available to the user who satisfies at least one Allow Restriction and does not satisfy all Restrict Restrictions (if there are any). The list of users will be formed in the following way: first, all Allow Restrictions are identified, and these lists are combined. Then, Restrict Restrictions are identified, and all user identifiers which are contained at least in one Restrict list are removed from the final list.

    Let's look at an example. Imagine that we have four Rules which are set in a transition through the Actor entity:

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

Four cases may occur:

  • Concat allow as = And and Concat restrict as = And. The command will be available to users 2 and 3, and forbidden to user 2. As a result, the command will only be available to user 3.
  • Concat allow as = And and Concat restrict as = Or. The command will be available to users 2 and 3, and forbidden to users 1, 2, 3. As a result, the command will not be available to users.
  • Concat allow as = Or and Concat restrict as = And. The command will be available to users 1, 2, 3, 4 and forbidden to user 2. As a result, the command will be available to users 1, 3, 4.
  • Concat allow as = Or and Concat restrict as = Or. The command will be available to users 1, 2, 3, 4 and forbidden to user 1, 2, 3. As a result, the command will be available to user 4.

There is one more unique case, when no Restrictions of the Allow type are specified but Restrictions of the Restrict type are. In this case, the command will be available to all users who do not fall under Restrictions of the Restrict type. However, if you try to obtain the list of all users whom the command is available to, the system will return an empty list.

Impersonation / Deputy

Impersonation is when a user can execute commands on behalf of another user. For example, in everyday life, an employee may catch a cold, and all responsibilities are fulfilled by a deputy. The commands are executed by the deputy, however, the system should contain information on whose behalf a command is executed. Thus, the goal of your system is to obtain a list of represented users' ids and provide for impersonation. If there is impersonation in your system, you may use the following methods.

To get a list of commands:

var identityIds = new List<string> {identityId, impersonatedIdentityId1, impersonatedIdentityId2 ... };
var commands = WorkflowInit.Runtime.GetAvailableCommands(processId, identityIds);

You will receive a list of commands available at least to one user. In order to identify whom exactly a command is available to, you need to use the Identities property of the WorkflowCommand object.

var availableFor = command.Identities;

It depends on your system's logic how a user is on whose behalf the command is executed is selected. This is not for Workflow Engine to determine. Then, upon the execution of a command, you convey two user identifiers to a corresponding method: identityId - a real, logged-in user and impersonatedIdentityId - an impersonated user.

WorkflowInit.Runtime.ExecuteCommand(command, identityId, impersonatedIdentityId);
WorkflowInit.Runtime.SetState(processId, identityId, impersonatedIdentityId, stateName);

Value impersonatedIdentityId is always written to the WorkflowProcessTransitionHistory table containing the history of transitions. You can access both identifiers at any time, using an instance of the ProcessInstance object.

var identityId = processInstance.IdentityId;
var impersonatedIdentityId = processInstance.ImpersonatedIdentityId;

Other methods (obtaining a list of all approvers for notifications and Inbox)

Rules created by you are called automatically when getting a list of available commands, and you won't have to refer to them directly most of the time. However, there is a common exception to this scenario that occurs when you need to obtain a list of users who can execute a command at the current stage. This case is useful when you need to notify users that they need to perform an action with a document. To get a list use the following method:

var identityIds = GetAllActorsForCommandTransitions(processInstance);

Another option of the method contains a filter on transition's Classifier. For example, the following code will return a list of users who can execute direct transitions within a process:

var filter = new List<TransitionClassifier> {TransitionClassifier.Direct};
var identityIds = GetAllActorsForCommandTransitions(processInstance, filter);

Please consider that these methods will work properly if all Rules related to the transition have a clearly implemented Get function.

Also, you can always access the current RuleProvider:

var ruleProvider = runtime.RuleProvider;

Connection with workflow designer

The list of Rules that you see in the workflow designer (in the Actors window) is formed as a combination of string lists returned by the IWorkflowActionProvider.GetRules method and received directly from the scheme. Elements with type RuleGet or RuleCheck are selected from the CodeActions section and a list of unique names is returned (Rule is always represented by a couple of methods: Get and Check).

Priority of search for Rule

Situations where Rule with the same name is simultaneously present in Global CodeActions, scheme CodeActions and in IWorkflowRuleProvider occur rarely. However, sometimes a need may arise to make an override and replace the Global CodeAction of a scheme with a CodeAction from a scheme (or from IWorkflowRuleProvider). The behaviour of WorkflowRuntime in this case is set with the ExecutionSearchOrder parameter. This parameter sets the Rule search priority. By default, the following search algorithm is executed. Rule with the specified name is searched for in CodeActions of the scheme, and, if found, it will be executed. If it is not found, then the search in Global CodeActions is performed. If it is not found there, then the search in IWorkflowRuleProvider is performed. If it is not found in any of the three sources, then the NotImplementedException is thrown. Such a behaviour corresponds to the following setting:

runtime.SetExecutionSearchOrder(ExecutionSearchOrder.LocalGlobalProvider);

This is a default behaviour, and you should not specify it explicitly. You can change it by specifying one of the 6 possible priorities (LocalGlobalProvider, GlobalLocalProvider, LocalProviderGlobal, GlobalProviderLocal, ProviderLocalGlobal or ProviderGlobalLocal) which are set in the ExecutionSearchOrder enum.

runtime.SetExecutionSearchOrder(ExecutionSearchOrder order);