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
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);