-
Notifications
You must be signed in to change notification settings - Fork 36
Complex Approvals
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...
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.
- Extend the role/group object and add
ApprovalType
string attribute to define an approval workflow:matrix, regular or none
. - Create a custom object type
ApprovalMatrix
to hold approval definitions from the table above - 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 regularjoin a group
request, the group requested will be kept intarget\target
as thetarget
itself points toAddAccessRequest
, and the user to be added to the group will be kept intarget\applicant
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:
Create new object type: ApprovalMatrix
as depicted below, where *Department
attributes are references to SAP Division objects (some standard attributes are not shown):
###Create authorization workflow Now the interesting part. The workflow that does all the magic. First, see the workflow logic:
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]
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]
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.
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
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]
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")))
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).
- MIMWAL Site - http://aka.ms/MIMWAL
- MIMWAL Releases - http://aka.ms/MIMWAL/Releases
- MIMWAL Documentation Wiki - http://aka.ms/MIMWAL/Wiki
- MIMWAL FAQ - http://aka.ms/mimwal/faq
- MIMWAL GitHub Code Repo - http://aka.ms/MIMWAL/Repo
- MIMWAL TechNet Q&A Forum (now read-only) - http://aka.ms/MIMWAL/Forum