Access Request shows as completed upon expiry

Hello Community,

I am using IdentityIQ version 8.3 and working on expiring work items after a defined period of time as part of the access request approval process.

Normally, when a work item expires and there is only one approver, the access request correctly ends with the status DENIED.

However, in the case of Access Requests with two approval levels:

  • Approval scheme: serial (approver 1 → then approver 2)

Expected behavior

If any approver’s work item expires, the access request should be DENIED and no provisioning should occur.

Observed behavior
When there are two serial approvers:

  • If the first or second approver’s work item expires,

  • The access request still ends with the status COMPLETED,

  • Provisioning is executed and the role/account is added

Has anyone encountered the same issue please?

Thank you,

You might need to customize approval workflow to convert expired access reviews to Denied.

Can you check your provisioning approval subprocess workflow and check for the variable “lastApprovalState equals to expired”??

You have to consider expired time as a rejected status in the workflow.

I faced the same issue while implementing a custom workflow. Therefore, I considered expired requests as rejected, and then the transaction was completed by marking the request as rejected.

Hello,

Thank you all for your answers,

Could you please advise where the best place in the workflow would be to customize the logic so that, if the work item expires at the first approval level, the access request is automatically denied (DENIED) and no provisioning is executed and so it would not impact the lcm provisioning workflow ?

Thank you so much in advance,

@DivyaL_7

Please share your customized workflow.

Hi Sumit,

It’s just the regular LCM Provisioning workflow we are using

did you check your provisioning approval subprocess workflow and the variable??

I dont see the lastApprovalState variable in the workflows i think it’s a variable that is inside the workitem

Here is the code of the Provisioning Approval Subprocess workflow :

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Workflow PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Workflow explicitTransitions="true"  libraries="Identity" name="Provisioning Approval Subprocess" type="Subprocess">
  <Variable initializer="serial" input="true" name="approvalMode">
    <Description>
        A string that specifies how we should handle the approvals.

        By default this is serial since most of these request with
        the exception of manager transfers will have only one approver.

        parallel
        Approvals are processed concurrently and there must be consensus,
        we wait for all approvers to approve.  The first approver that
        rejects terminates the entire approval.

        parallelPoll
        Approvals are processed concurrently but consensus is not required.
        All approvals will be processed, we don't stop if there are any
        rejections.

        serial
        Approvals are processed one at a time and there must be consensus.
        The first approver that rejects terminates the entire approval.

        serialPoll
        Approvals are processed in order but consensus is not required.
        All approvals will be processed, we don't stop if there are any
        rejections.  In effect we are "taking a poll" of the approvers.

        any
        Approvals are processed concurrently, the first approver to
        respond makes the decision for the group.
    </Description>
  </Variable>
  <Variable input="true" name="approvalScheme">
    <Description>
      A csv string that specifies how approval items should be generated 
      for the incoming request.

      The value can be "none", in which case approvals are disabled.

      The value can also be a combination of any of the values below 
      in any order, separated by commas. The order in which they are 
      specified is the order in which they are processed:

      owner
        The object owner gets the approval item. 
        For Role approvals this is the Role object owner. 
        For Entitlement approvals this is the Entitlement object owner.

      manager
        The manager gets the approval item.

      securityOfficer
        The identity in the variable securityOfficerName gets the approval item.

      identity
        The identities/workgroups in the variable approvingIdentities get the approval item.

    </Description>
  </Variable>
  <Variable input="true" name="approvingIdentities">
    <Description>
       List of identities and/or workgroups names/ids that should be involved in the approval 
       process.    
    </Description>
  </Variable>
  <Variable initializer="spadmin" input="true" name="fallbackApprover">
    <Description>
      A String that specifies the name of the Identity that will 
      be assigned any approvals where the owner of the approver 
      can't be resolved. Example if the scheme is "owner" and the 
      application doesn't specify and owner.
    </Description>
  </Variable>
  <Variable input="true" name="flow">
    <Description>
      The name of the LCM flow that launched this workflow.

      This is one of these values:

      AccountsRequest
      EntitlementsRequest
      RolesRequest
      IdentityCreateRequest
      IdentityEditRequest
      ForgotPassword
      ExpiredPassword
      PasswordRequest
      joiner
      mover
      leaver
      AttributeSync

    </Description>
  </Variable>
  <Variable input="true" name="identityName">
    <Description>The name of the identity being updated.</Description>
  </Variable>
  <Variable input="true" name="identityDisplayName">
    <Description>
      The displayName of the identity being updated.
      Query for this using a projection query and fall back to the name.      
    </Description>
  </Variable>
  <Variable input="true" name="identityRequestId">
    <Description>
       The ID of the IdentityRequest for this request.
    </Description>
  </Variable>
  <Variable input="true" name="noTriggers">
    <Description>
      If true, then do no attempt to trigger lifecycle events.
    </Description>
  </Variable>
  <Variable input="true" name="policyViolations">
    <Description>
       List of policy violations that were found during our initial policy scan.
       This list is passed into each work item so the approvers can see
       pending violations.
    </Description>
  </Variable>
  <Variable input="true" name="managerElectronicSignature">
    <Description>
       The name of the electronic signature object that should be used when workitems
       are completed by a manager.
    </Description>
  </Variable>
  <Variable input="true" name="ownerElectronicSignature">
    <Description>
       The name of the electronic signature object that should be used when workitems
       are completed by object owners.
    </Description>
  </Variable>
  <Variable input="true" name="securityOfficerElectronicSignature">
    <Description>
       The name of the electronic signature object that should be used when workitems
       are completed by the security officer.
    </Description>
  </Variable>
  <Variable input="true" name="identityElectronicSignature">
    <Description>
       The name of the electronic signature object that should be used when workitems
       are completed by object owners.
    </Description>
  </Variable>
  <Variable initializer="LCM Identity Update Approval" input="true" name="approvalEmailTemplate">
    <Description>
       Name of the email template to use when notifying the approver of pending approvals.
    </Description>
  </Variable>
  <Variable initializer="LCM Identity Update Approval" input="true" name="managerEmailTemplate">
    <Description>
       Name of the email template to use when notifying the manager of pending approvals.
    </Description>
  </Variable>
  <Variable initializer="LCM Identity Update Approval" input="true" name="ownerEmailTemplate">
    <Description>
       Name of the email template to use when notifying the owner of pending approvals.
    </Description>
  </Variable>
  <Variable initializer="LCM Identity Update Approval" input="true" name="securityOfficerEmailTemplate">
    <Description>
       Name of the email template to use when notifying the security officer of pending approvals.
    </Description>
  </Variable>
  <Variable initializer="LCM Identity Update Approval" input="true" name="identityEmailTemplate">
    <Description>
       Name of the email template to use when notifying the security officer of pending approvals.
    </Description>
  </Variable>
  <Variable initializer="true" name="trace">
    <Description>
      Used for debugging this workflow and when set to true trace
      will be sent to stdout.
    </Description>
  </Variable>
  <Variable input="true" name="workItemDescription">
    <Description>String "template" that will be used as the workitem description.</Description>
  </Variable>
  <Variable initializer="SomeValue" input="true" name="workItemEscalationTemplate">
    <Description>The email template that should be used when sending out reminders.</Description>
  </Variable>
  <Variable initializer="SomeValue" input="true" name="workItemHoursBetweenReminders">
    <Description>The number of hours to wait between sending reminders to approvers.</Description>
  </Variable>
  <Variable initializer="SomeValue" input="true" name="workItemHoursTillEscalation">
    <Description>The number of hourse to wait until an approval should be escalated.</Description>
  </Variable>
  <Variable initializer="Normal" input="true" name="workItemPriority">
    <Description>
       The String version of a WorkItem.Priority. This variable is
       used to set the priority on all of the workitems generated
       as part of this workflow and also set on the IdentityRequest
       object.
    </Description>
  </Variable>
  <Variable initializer="LCM Identity Approval Reminder" input="true" name="workItemReminderTemplate">
    <Description>The email template that should be used when sending out reminders.</Description>
  </Variable>
  <Variable input="true" name="workItemEscalationRule">
    <Description>The rule used to assign a new ownder during escalation.</Description>
  </Variable>
  <Variable initializer="SomeValue" input="true" name="workItemMaxReminders">
    <Description>The maximum number of reminder emails that will be sent before we begin the escalation process.</Description>
  </Variable>
  <Variable initializer="true" input="true" name="filterRejects">
    <Description>True to filter rejected items when running in Serial/SerialPoll mode.</Description>
  </Variable>
  <Variable initializer="false" input="true" name="setPreviousApprovalDecisions">
    <Description>True to pre-populate approval decisions from previous approvals.</Description>
  </Variable>
  <Variable initializer="false" input="true" name="clearApprovalDecisions">
    <Description>True to clear all decisions when generating approvals</Description>
  </Variable>
  <Variable initializer="true" input="true" name="dontUpdatePlan">
    <Description>True to skip updating the ProvisioningProject masterPlan when processing approval decisions.</Description>
  </Variable>
  <Variable input="true" name="approvalSet" output="true">
    <Description>
      ApprovalSet representing the things that were requested and come from
      the master provisioning plan. 
    </Description>
  </Variable>
  <Variable input="true" name="project" output="true">
    <Description>
      ProvisioningProject which is just a compiled version of the ProvisioningPlan, the 
      master plan will be adjusted when approval decisions are applied and a new 
      revised provisiobion project will be returned.
    </Description>
  </Variable>
  <Variable input="true" name="plan" output="true">
    <Description>The provisioning plan.</Description>
  </Variable>
  <Variable initializer="false" input="true" name="requireCommentsForApproval">
    <Description>When true we will require comments when an approval item is approved.</Description>
  </Variable>
  <Variable initializer="false" input="true" name="requireCommentsForDenial">
    <Description>When true we will require comments when an approval item is denied.</Description>
  </Variable>
  <Description>
    Simple approval subrocess introduced into release 6.2 to make
    adding custom approvals easier.  This approval subprocess
    has a configuration form to make it easier to configure
    from the Business Process Editor.
  </Description>
  <RuleLibraries>
    <Reference class="sailpoint.object.Rule" name="LCM Workflow Library"/>
  </RuleLibraries>
  <Step icon="Start" name="Start" >
    <Transition to="Approval"/>
  </Step>
  <Step icon="Approval" name="Approval">
    <Approval mode="ref:approvalMode" owner="call:buildCommonApprovals" renderer="lcmWorkItemRenderer.xhtml" send="identityDisplayName,identityName,approvalSet,flow,policyViolations,identityRequestId">
      <AfterScript>
        <Source>
          import sailpoint.workflow.IdentityRequestLibrary;
                    
          if ( item == null ) 
              return;

          assimilateWorkItemApprovalSet(wfcontext, item, approvalSet);          
          auditDecisions(item);  
          IdentityRequestLibrary.assimilateWorkItemApprovalSetToIdentityRequest(wfcontext, approvalSet);
        </Source>
      </AfterScript>
      <Arg name="launcher" value="ref:launcher"/>
      <Arg name="workItemDescription" value="ref:workItemDescription"/>
      <Arg name="workItemEscalationRule" value="ref:workItemEscalationRule"/>
      <Arg name="workItemEscalationTemplate" value="ref:workItemEscalationTemplate"/>
      <Arg name="workItemHoursTillEscalation" value="ref:workItemHoursTillEscalation"/>
      <Arg name="workItemMaxReminders" value="ref:workItemMaxReminders"/>
      <Arg name="workItemEscalationFrequency" value="ref:workItemEscalationFrequency"/>
      <Arg name="workItemHoursBetweenReminders" value="ref:workItemHoursBetweenReminders"/>
      <Arg name="workItemIdentityRequestId" value="ref:identityRequestId"/>
      <Arg name="workItemNotificationTemplate" value="ref:approvalEmailTemplate"/>
      <Arg name="workItemPriority" value="ref:workItemPriority"/>
      <Arg name="approvalMode" value="ref:approvalMode"/>
      <Arg name="approvalScheme" value="ref:approvalScheme"/>
      <Arg name="approvalSplitPoint" value="ref:approvalSplitPoint"/>
      <Arg name="clearApprovalDecisions" value="ref:clearApprovalDecisions"/>
      <Arg name="workItemReminderTemplate" value="ref:workItemReminderTemplate"/>
      <Arg name="workItemRequester" value="$(launcher)"/>
      <InterceptorScript>
        <Source>
            
            import sailpoint.object.Workflow.Approval;
            import sailpoint.object.ApprovalItem;
            import sailpoint.object.ApprovalSet;
            import sailpoint.object.Workflow;
            import sailpoint.object.WorkItem;
            import sailpoint.tools.Util;
            import java.util.Iterator;
            import java.util.ArrayList;
            import java.lang.Boolean;

            if (Workflow.INTERCEPTOR_PRE_ASSIMILATION.equals(method)) {
                // promote completion state to Rejected if all items are rejected
                ApprovalSet aset = item.getApprovalSet();
                if (aset != null) {
                    List items = aset.getItems();
                    if (items != null) {
                        int rejectCount = 0;
                        for (ApprovalItem item : items) {
                            // note that isRejected can't be used since that
                            // assumes no answer means rejected
                            if (item.getState() == WorkItem.State.Rejected) {
                                rejectCount++;
                            }
                        }
                        if (rejectCount == items.size()) {
                            item.setState(WorkItem.State.Rejected);
                        }
                    }
                }
            } else if (Workflow.INTERCEPTOR_START_APPROVAL.equals(method)) {

                ApprovalSet currentSet = approval.getApprovalSet();

                if (currentSet != null &amp;&amp; !Util.isEmpty(currentSet.getItems())) {
                    //If filterRejects is true, filter any rejected items in the master ApprovalSet from the currentSet
                    if (Boolean.valueOf(filterRejects)) {
                        filterRejectsFromApprovalSet(approvalSet, currentSet);
                    }

                    //We've filtered all items from the approval set
                    if (Util.isEmpty(currentSet.getItems())) {
                        //Complete the approval if it contains no ApprovalItems
                        approval.setComplete(true);
                    } else {

                        if (Boolean.valueOf(setPreviousApprovalDecisions)) {
                            // If setPreviousApprovalDecisions is enabled, set the decision
                            // on the items to that of the global item.
                            setPreviousDecisionsOnApprovalSet(approvalSet, currentSet);
                        }
                    }
                }

            } else if (Workflow.INTERCEPTOR_END_APPROVAL.equals(method)) {
                // Owner children approvals need to propagate the rejection state if all children are rejected
                Approval parentApp = approval.getParent();
                // Never relay the state to the root approval
                // If we don't have an approvalSet, must be a container approval
                if (parentApp != null &amp;&amp; approval.getApprovalSet() == null) {
                    boolean completeAndRejected = false;
                    for (Approval child : Util.safeIterable(approval.getChildren())) {
                        if (child.isComplete() &amp;&amp; child.getState() == WorkItem.State.Rejected) {
                            completeAndRejected = true;
                        } else {
                            completeAndRejected = false;
                            break;
                        }
                    }
                    //If all children complete and rejected, set the status on the parent approval
                    if (completeAndRejected) {
                        approval.setState(WorkItem.State.Rejected);
                    }
                }
            } else if (Workflow.INTERCEPTOR_OPEN_WORK_ITEM.equals(method)) {
               import sailpoint.workflow.IdentityRequestLibrary;

               //Sync IdentityRequestItems with the WorkItem
               IdentityRequestLibrary.assimilateWorkItemApprovalSetToIdentityRequest(wfcontext, item.getApprovalSet(), false);
            }
            
        </Source>
      </InterceptorScript>
    </Approval>
    <Transition to="Process Approval Decisions" when="script:(step.getApproval() != null &amp;&amp; step.getApproval().containsApprovalItems())"/>
    <Transition to="end"/>
  </Step>
  <Step action="call:processApprovalDecisions" icon="Task" name="Process Approval Decisions"  resultVariable="project">
    <Arg name="approvalSet" value="ref:approvalSet"/>
    <Arg name="disableAudit" value="true"/>
    <Arg name="project" value="ref:project"/>
    <Arg name="plan" value="ref:plan"/>
    <Arg name="recompilePlan" value="true"/>
    <Arg name="dontUpdatePlan" value="ref:dontUpdatePlan"/>
    <Transition to="end"/>
  </Step>
  <Step icon="Stop" name="end"/>
</Workflow>

in workflowcase you will see lastApprovalState = Expired in case of expired workitem, try printing it in lcm provisioning

in workflow you will get directly in "lastApprovalState" variable. 

can you update your workflow; declare the variable.

The state of the last approval step.

Just above Transition to process approval decision and try it

<Transition to="end" when="script:(step.getApproval() != null &amp;&amp; (WorkItem.State.Expired.equals(step.getApproval().getState()) || WorkItem.State.Rejected.equals(step.getApproval().getState())))"/>

Hello @naveenkumar3 ,

Thank you so much for your reply,

I added logs to print the lastApprovalState variable it is indeed set to Expired

I added the transition you mentioned but it was not working

<Transition to="end" when="script:(step.getApproval() != null &amp;&amp; (WorkItem.State.Expired.equals(step.getApproval().getState()) || WorkItem.State.Rejected.equals(step.getApproval().getState())))"/>

So i tried to change the condition it works it transitions to end but the access request is completed and provisioning is occuring..

    <Transition to="Expiring WorkItem" when="script:(workflow.get(&quot;lastApprovalState&quot;) != null  &amp;&amp; (&quot;Expired&quot;.equals(workflow.get(&quot;lastApprovalState&quot;)) || &quot;Rejected&quot;.equals(workflow.get(&quot;lastApprovalState&quot;))))"/>

So i tried to add another step to stop the provisioning like so:





<Step name="Expiring WorkItem">
    <Return name="approvalSet" to="approvalSet"/>
    <Return name="approved" to="approved"/>
    <Return name="provision" to="provision"/>
    <Return name="doProvision" to="doProvision"/>
    <Script>
      <Source>
        import org.apache.log4j.Logger;
        Logger log = Logger.getLogger("sailpoint.workflow");
String s = (String) workflow.get("lastApprovalState");
    log.error("EXPIRY CHECK: lastApprovalState=" + s);
    if ("Expired".equalsIgnoreCase(s)) {
    workflow.put("approved", false);         
    workflow.put("provision",false);        
    workflow.put("doProvision", false);
    workflow.put("foregroundProvisioning", false);
    workflow.put("plan", null);
    workflow.put("provisioningPlan", null);
    log.error("EXPIRY CHECK : provisioning flags forced to FALSE");
    }
  </Source>
</Script>
<Transition to="end"/>
</Step>

But it’s still not working i see an approvalSet and a provisioning..

You need to mark all items in the approvalSet as rejected** before returning to the parent workflow. This is what the parent workflow checks to determine if provisioning should occur. try it and let me know

<Step name="Expiring WorkItem">
  <Script>
    <Source>
      import sailpoint.object.ApprovalSet;
      import sailpoint.object.ApprovalItem;
      import sailpoint.object.WorkItem;
      import org.apache.log4j.Logger;
      
      Logger log = Logger.getLogger("sailpoint.workflow.expiry");
      
      log.error("EXPIRY CHECK: Marking all approval items as Rejected due to expiration");
      
      if (approvalSet != null) {
          List items = approvalSet.getItems();
          if (items != null) {
              for (ApprovalItem item : items) {
                  item.setState(WorkItem.State.Rejected);
                  if (item.getApproverComments() == null) {
                      item.setApproverComments("Auto-rejected: Work item expired");
                  }
                  log.error("EXPIRY CHECK: Rejected item - " + item.getValue());
              }
          }
      }
      
      // Also clear the project/plan to prevent provisioning
      project = null;
      plan = null;
    </Source>
  </Script>
  <Transition to="end"/>
</Step>




And update your transitions in the Approval step to be in this order:

<Transition to="Expiring WorkItem" when="script:(&quot;Expired&quot;.equals(workflow.get(&quot;lastApprovalState&quot;)))"/>
<Transition to="Process Approval Decisions" when="script:(step.getApproval() != null &amp;&amp; step.getApproval().containsApprovalItems())"/>
<Transition to="end"/>

Hi @naveenkumar3 , thank you

Omg it totally worked, i just removed item.setRejected(true); because apparently the method doesn’t exist for class ApprovalItem,

Thank you so much you’re the best !!

Yeah, my bad “item.setState(WorkItem.State.Rejected);” is correct, may be while copying in my eclipse , it came item.setRejected(true); , because i was just checking.. Glad your issue is resolved.

We took a slightly different approach, by using the “workItemMaxAge” variable in LCM Provisioning.

<Variable initializer="56" name="workItemMaxAge">
    <Description>
      Number of days before a workitem is expired / canceled
    </Description>
  </Variable>
<Step icon="Stop" name="end" posX="710" posY="175">
    <Script>
      <Source>
        <![CDATA[
          import progressive.core.WorkflowApproval;
          import sailpoint.object.ApprovalItem;
          import sailpoint.object.IdentityRequest;
          import sailpoint.object.IdentityRequestItem;
          import sailpoint.object.IdentityRequest.ExecutionStatus;
          import sailpoint.workflow.IdentityRequestLibrary;
          import java.util.Date;
          IdentityRequest ir = IdentityRequestLibrary.getIdentityRequest(wfcontext);
          if (ir != null) {
            if (WorkflowApproval.hasExpiredApprovals(ir.getApprovalSummaries())) {
              Date terminationDate = new Date();
              for (IdentityRequestItem item : ir.getItems()) {
                item.setApprovalState(WorkItem.State.Canceled);
                item.setProvisioningState(ApprovalItem.ProvisioningState.Failed);
                item.setEndDate(terminationDate);
              }
              ir.setExecutionStatus(ExecutionStatus.Terminated);
              ir.setState("End");
              ir.computeCompletionStatus();
              ir.setEndDate(terminationDate);
              context.saveObject(ir);
              context.commitTransaction();
            }
          }
        ]]>
      </Source>
    </Script>
  </Step>
// Custom Java code - can be moved to the workflow step as inline code.
public static boolean hasExpiredApprovals(List<ApprovalSummary> summaries) {
	for (ApprovalSummary as : Util.safeIterable(summaries)) {
		if (Util.nullSafeEq(as.getState(), State.Expired) == true) {
			return true;
		}
	}
	return false;
}

The variable is then added to the “Identity Request Approve Identity Changes” workflow:

<Variable input="true" name="workItemMaxAge">
    <Description>
      Number of days before a workitem is expired / canceled
    </Description>
  </Variable>

And then passed as an argument to the “Approval” step:

<Step icon="Approval" name="Approval" posX="255" posY="434">

  <Arg name="workItemRequester" value="$(launcher)"/>
  <Arg name="workItemType" value="Approval"/>
  <Arg name="workItemHoursTillEscalation" value="ref:workItemHoursTillEscalation"/>
  <Arg name="workItemHoursBetweenReminders" value="ref:workItemHoursBetweenReminders"/>
  <Arg name="workItemReminderTemplate" value="ref:workItemReminderTemplate"/>
  <Arg name="workItemMaxAge" value="ref:workItemMaxAge"/>
  <Arg name="workItemMaxReminders" value="ref:workItemMaxReminders"/>
  <Arg name="workItemEscalationStyle" value="ref:workItemEscalationStyle"/>
  <Arg name="workItemEscalationRule" value="ref:workItemEscalationRule"/>

We also have an additional customization to the Do Provisioning Forms workflow, to handle expiration of provisioning form work items for requests that don’t even make it to the approval sub-workflow. I can share that approach with you as well.

what is it?? I did not get it?? if you have any queries, can you please create a different topic and there we can suggest it. This topic is already closed.