Skip to content

Complex Approvals

Eugene Sergeev edited this page Jan 7, 2016 · 2 revisions

Complex approval scenario using an approval matrix and approval delegation

The original intention of this page was to show a real-world usage of WAL's activities, how to use update resource and verify request activities, how to use variables and workflow data, how to do 'for-each' calculations and so on... While the solution described below works in production environment, it could not be easily copy-pasted to your environment due to other schema differences, policies, RCDC objects and millions of other things...

Problem definition

Let's say, you get a request from your customer to implement an approval workflow using a matrix as no one wants to populate primary and secondary approver fields for 50k role objects on MIM portal. Even using a script it is not something one wants to do.

Later, you receive a matrix like this:

Organization System (mandant) Role name. Prefix Role name. Suffix Role description Owner Co-owner
Contoso BI2(200) ZCO 7700 Control Head of the finance division Head of the finance department, finance division
Litware RT2(700) ZSD * Sales Head of the services sales division Deputy head of the service sales department, services sales division
Litware HR2(700), HA3(300) ZHR * Human capital management Head of the HCM division Head of the motivation department, HCM division
All All systems SM3 * SAP Solution Manager Head of the application support department

… and another 5 pages of this table.

Question: how would you do this? Find a person on a portal and assign primary and secondary approvers for each 50k roles? Extend a group object and store an approver department and job title for each 50k roles?

Answer: you use MIM WAL with its Update Resource, Verify Request and Request Approval activities. You want to use its conditional execution, iteration, query and all other feature it provides.

Proposed approach

  1. Extend the role/group object and add ApprovalType string attribute to define an approval workflow: matrix, regular or none.
  2. Create a custom object type ApprovalMatrix to hold approval definitions from the table above
  3. Create your own authorization workflow

this scenario leverages a custom object type AddAccessRequest which contains 2 references to an applicant and to a target, therefore unlike in a regular join a group request, the group requested will be kept in target\target as the target itself points to AddAccessRequest, and the user to be added to the group will be kept in target\applicant

Implementation steps

Custom attributes on group object

Add 2 attributes to the group object

Application – a reference for a parent application

Approval Type – to specify an approval workflow

RegExp: ^(ApprovalMatrix|ApplicationOwner|PermissionOwner|RoleOwner|None)?$

As a result, your group object will look like this:

Custom object type

Create new object type: ApprovalMatrix as depicted below, where *Department attributes are references to SAP Division objects (some standard attributes are not shown):

Fill approval matrix

###Create authorization workflow Now the interesting part. The workflow that does all the magic. First, see the workflow logic:

Find duplicated requests

Checks whether a similar request has been made in the past, if it is in active and approved state with overlapping access request start and end dates. We want to deny a request if user was already given a requested group/role.

So, the first part of this step is to query MIM Service for duplicated requests:

The XPath filter is /AddAccessRequest[(Applicant='[//Target/Applicant]') and (Target='[//Target/Target]') and ((RequestStatus='Completed') or (RequestStatus='Approved'))]

This query tries to find AddAccessRequest object (custom type) holding our user and group.

The second part of this step is to make an iteration on each found object ([//Queries/dups]) and run some checks on it.

What it tries to do is to check all possible cases (5 cases) when an old access request overlaps with submitted access requests. Then store the ID of conflicting request in the workflow data.

Example of such exception is depicted below (made up picture, original text was not in English).

A couple of explanations here: Check if an old request had non-empty start and end dates and store this as a Boolean value IsPresent([//Value/AccessRequestStartDate]) -> $nonEmptyOldStart

If an old request had both start and end dates empty, we consider this as a conflict of type 1: And(not($nonEmptyOldStart),not($nonEmptyOldEnd)) -> $conflict1

After we completed all checks, store the result, if there’s a conflict or not: IIF(Or(Or(Or(Or($conflict1,$conflict2),$conflict3),$conflict4),$conflict5),true,false) -> [//WorkflowData/conflict]

Read approval matrix dictionary

Reads all approval matrix objects and tries to find if any of them contains an application, a prefix, a suffix that match requested group.

XPath filter queries MIM Services for those objects that reference either the same application as requested group is linked to or ‘All Systems’: /ApprovalMatrix[(Application='[//Target/Target/Application]') or (Application=/Application[DisplayName='All systems']/ObjectID)]

Then we iterate through each and every object found. Unfortunately, there’s no ‘break’ command, so the result of the workflow step will be ‘the last matching condition found’, not the first one.

by the time this was implemented there was no 'break' command, as for now you can stop iterations by setting $_BREAK_ITERATION_ variable to true, regarding to the situation described below we simply need to add one extra expression: $found = $_BREAK_ITERATION_

Since the XPath dialect in MIM supports only starts-with(attributename,value) but not the other way round (as we need), we cannot run the XPath query to find matching approval matrix objects. We have to use RegexMatch function of MIMWAL.

Check if the approval matrix role name prefix matches the group display name. We do not want to run Regex query on empty value, so we use IsPresent First. If the prefix contains an asterisk, the match is found. Otherwise, check if the role name contains ‘: ’ in its name IIF(IsPresent([//Value/ApprovalMatrixRolePrefix]),IIF(Eq([//Value/ApprovalMatrixRolePrefix],"*"),true,IIF(RegexMatch([//Target/Target/DisplayName],": "+ReplaceString([//Value/ApprovalMatrixRolePrefix],"*","")),true,false)),true) -> $prefixMatch

Then check if the suffix matches the group name (mind the $ sign indicating match in the end of the string). IIF(IsPresent([//Value/ApprovalMatrixRoleSuffix]),IIF(Eq([//Value/ApprovalMatrixRoleSuffix],"*"),true,IIF(RegexMatch([//Target/Target/DisplayName],ReplaceString([//Value/ApprovalMatrixRoleSuffix],"*","")+"$"),true,false)),true) -> $suffixMatch

If the match is found, we need a reference to approvers’ department and his job title, and we want to keep it in WorkflowData: IIF($found,IIF(IsPresent([//Value/ApprovalMatrixPrimaryApproverDepartment/ObjectID]),InsertValues([//Value/ApprovalMatrixPrimaryApproverDepartment/ObjectID]),Null()),Null()) -> [//WorkflowData/MatrixPrimaryDepartment]

IIF($found,IIF(IsPresent([//Value/ApprovalMatrixPrimaryApproverJobTitle]),InsertValues([//Value/ApprovalMatrixPrimaryApproverJobTitle]),Null()),Null()) -> [//WorkflowData/MatrixPrimaryJob]

Check approval matrix ids

As a result of the previous step it could happen that we find nothing. None of the approval matrix objects will match requested group name. We do not want subsequent queries to fail, so we fall back to default values if nothing was found.

IIF(IsPresent([//WorkflowData/MatrixPrimaryDepartment]),First([//WorkflowData/MatrixPrimaryDepartment]),"00000000-FF00-0000-0000-000000000000") -> [//WorkflowData/MatrixPrimaryDepartment] This statement resets Primary Approver’s department to the fake ObjectID which doesn’t exist in MIM Service and guaranties that further queries will find nothing instead of failing because of empty value.

####Find matrix approvers Finally, we are ready to start searching for any person who has SAP division reference and Job Title matching our approval matrix query results.

The XPath query for the primary approver: /Person[_DivisionRef='[//WorkflowData/MatrixPrimaryDepartment]' and JobTitle='[//WorkflowData/MatrixPrimaryJob]']

IIF(IsPresent([//Queries/prime]),First([//Queries/prime/ObjectID]),Null()) -> [//WorkflowData/PrimaryApprover] If nothing is found, use Null() and Allow Null option, so the [//WorkflowData/PrimaryApprover] will not be created at all.

Get approvers and update request

This step checks the approval type, whether its matrix (99% of requests), a regular Role or Application owner approval (approver is specified in the group properties or application properties), whether approval is required at all, combines primary and secondary approver into one list and converts their GUIDs to UniqueIdentifier types to be used in approval activity later.

As a bonus, let’s do approval delegation. Like this

IIF(Eq([//Target/Target/ApprovalType],"Application Owner"),IIF(IsPresent([//Target/Target/Application/DisplayedOwner/DelegateApprovalsTo]),IIF(IsPresent([//Target/Target/Application/DisplayedOwner/DelegateApprovalsStartDate]),IIF(Before([//Target/Target/Application/DisplayedOwner/DelegateApprovalsStartDate],DateTimeNow()),IIF(IsPresent([//Target/Target/Application/DisplayedOwner/DelegateApprovalsEndDate]),IIF(After([//Target/Target/Application/DisplayedOwner/DelegateApprovalsEndDate],DateTimeNow()),[//Target/Target/Application/DisplayedOwner/DelegateApprovalsTo],[//Target/Target/Application/DisplayedOwner]),[//Target/Target/Application/DisplayedOwner/DelegateApprovalsTo]),[//Target/Target/Application/DisplayedOwner]),IIF(IsPresent([//Target/Target/Application/DisplayedOwner/DelegateApprovalsEndDate]),IIF(After([//Target/Target/Application/DisplayedOwner/DelegateApprovalsEndDate],DateTimeNow()),[//Target/Target/Application/DisplayedOwner/DelegateApprovalsTo],[//Target/Target/Application/DisplayedOwner]),[//Target/Target/Application/DisplayedOwner/DelegateApprovalsTo])),IIF(IsPresent([//Target/Target/Application/DisplayedOwner]),[//Target/Target/Application/DisplayedOwner],Null())),Null()) -> $AppApprover

As approval delegation is a subject for another long session, I’ll just leave that monstrous expression ‘as-is’. It also updates request name to be DateTimeFormat(DateTimeNow(),"yyyy-MM-dd HH:mm")+" Grant "+ IIF(Eq([//Target/Target/ObjectType],"Role"),"role '"+[//Target/Target/DisplayName] + "'",[//Target/Target/Application/DisplayName]+":'"+[//Target/Target/DisplayName]) + "' to '" + [//Target/Applicant/DisplayName] + "'" -> [//Request/DisplayName]

Approver will see not a ‘Update to group’ in the approval request list, but a ‘Grant Application:GroupName for John Doe’ as a request display name

Get owner details

When we done all calculations regarding who’ll be an approver we need to check his status, AD account present and so on… We can actually do this (getting his status) in 2 ways: a) Specify an expression [//WorkflowData/PrimaryApprover/EmployeeStatus] b) Run a query I would prefer the latter for no real reason. However, let’s use condition for execution and will run such a query if approval is required and if approver is found.

Execution condition: And(And([//WorkflowData/ApprovalRequired],true),IsPresent([//WorkflowData/PrimaryApprover]))

If approver has an account the use it, otherwise put nothing into workflow data IIF(IsPresent([//Queries/appowner/AccountName]),[//Queries/appowner/AccountName],Null()) -> [//WorkflowData/ApproverAccount]

Verify request

Now, we can check if there’s a duplicated access request, if the approver is not found while approval type is not ‘None’, if the approver and applicant are working and so on…

Approver status validation for cases when approval is required: Or(not([//WorkflowData/ApprovalRequired]),not(Eq([//WorkflowData/ApproverStatus],"Dismissed")))

Fill variables for approval

Name is not that good. What it does, it stores all the values like approver’s name in the WorkflowData for later use in approval activity templates. Sure, it had execution condition: And([//WorkflowData/ApprovalRequired],true)

####Request approval

Finally, we raise approval request (only if needed, if approval type is None, then it will be skipped).

The listing

download workflow definition as a web-page

Clone this wiki locally