Access request shows as completed and successful even with expired status

Hello Community,

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

When the Check Workitem Expired task runs (return null), provisioning does not occur, but the workitem status shows completed and success (green), even though the status=“expired”.

IdentityRequest:
CompletionStatus=“Sucess” , ExecutionStatus= “Completed” , State=“end”
ApprovalSumary Status=“Expired”

<Step icon="Start" name="Start" posX="20" posY="22">
<Transition to="Approval"/>
</Step>
<Step icon="Approval" name="Approval" posX="158" posY="22">
<Approval mode="ref:approvalMode" owner="call:buildCommonApprovals" renderer="lcmWorkItemRenderer.xhtml" send="identityDisplayName,identityName,approvalSet,flow,policyViolations,identityRequestId">
<AfterScript>
<Source>
   import sailpoint.workflow.IdentityRequestLibrary;
import sailpoint.object.WorkItem;
import sailpoint.object.ApprovalSet;
import sailpoint.object.ApprovalItem;
if (item == null) return;

if (WorkItem.State.Expired.equals(item.getState())) {
    if (approvalSet != null) {
        approvalSet.reject(); 
        
  List items = approvalSet.getItems();
        if (items != null) {
            for (ApprovalItem aItem : items) {
                aItem.setState(WorkItem.State.Rejected);
            }
        }
        item.setCompletionComments("Expirado automaticamente após timeout sem resposta.");
    }
    
    workflow.put("lastApprovalState", "Expired");
}
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="workItemEscalationTemplate" value="ref:workItemEscalationTemplate"/>
<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)"/>
<Arg name="workItemEscalationStyle" value="both"/>
<Arg name="workItemHoursTillEscalation" value="1"/>
<Arg name="workItemEscalationRule" value="Rule-Expire-WorkItem-On-Timeout"/>
<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="Expiring WorkItem" when="script:(&quot;Expired&quot;.equals(lastApprovalState))"/>
<Transition to="Process Approval Decisions" when="script:(!&quot;Expired&quot;.equals(workflow.get(&quot;lastApprovalState&quot;)) &amp;&amp; step.getApproval() != null &amp;&amp; step.getApproval().containsApprovalItems())"/>
<Transition to="end"/>
</Step>
<Step name="Expiring WorkItem" posX="240" posY="171">
<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>
<Step action="call:processApprovalDecisions" icon="Task" name="Process Approval Decisions" posX="420" resultVariable="project">
<Arg name="disableAudit" value="true"/>
<Arg name="dontUpdatePlan" value="ref:dontUpdatePlan"/>
<Arg name="approvalSet" value="ref:approvalSet"/>
<Arg name="recompilePlan" value="true"/>
<Arg name="project" value="ref:project"/>
<Arg name="plan" value="ref:plan"/>
<Transition to="end"/>
</Step>
<Step icon="Stop" name="end" posX="420" posY="225"/>

Hello,

I’ve worked on this use-case recently, configured the escalation rule in the Provisioning Approval Subprocess workflow like below:


<Variable initializer="Test-Rule-Escalation" input="true" name="workItemEscalationRule">
    <Description>The rule used to assign a new ownder during escalation.</Description>
  </Variable>

The rule used to assign a new ownder during escalation.

Below is my escalation rule:
import java.util.List;

import sailpoint.api.SailPointContext;
import sailpoint.object.ApprovalItem;
import sailpoint.object.ApprovalSet;
import sailpoint.object.Comment;
import sailpoint.object.Identity;
import sailpoint.object.WorkItem;
import sailpoint.object.WorkItem.State;
import sailpoint.tools.GeneralException;

if (item instanceof WorkItem) {
WorkItem workItem = (WorkItem) item;
ApprovalSet appSet = workItem.getApprovalSet();
String ownerName = workItem.getOwner() != null ? workItem.getOwner().getName() : “unknown”;
if (null != appSet) {
List approvalItems = appSet.getItems();

  Identity requestee;
  try {
    requestee = context.getObjectByName(Identity.class, workItem.getString("identityName"));
  } catch (GeneralException e) {
    e.printStackTrace();
  }
  for (ApprovalItem approvalItem : approvalItems) {

    String itemValue = approvalItem.getDisplayValue();
    String operation = approvalItem.getOperation();
	
      approvalItem.setState(State.Expired);
      approvalItem.add(new Comment(
          "Request to [" + operation + "] item [" + itemValue + "] was auto-expired as the owner [" + ownerName + "] failed to take action within the escalation period.",
          "system"));

  }
}
}
return null;

In my case, there will be no escalation of the item, only expiration within a set period.

<Source>
   import org.apache.log4j.Logger;
   Logger log = Logger.getLogger("org.example.rules.escalation");

   if (item != null) {
       log.info("Expired WorkItem timeout: " + item.getName());
   }

   return null; 
</Source>

@helanprates1- what is your requirement when the workitem expired? Aceess request should not show successfull?

If in understand your requirement correctly, you should explicitly update the IdentityRequest status inside the Expiring WorkItem step so the request is marked as Rejected or Expired .

just add few line of the code to mark it rejected or expired.

I see that you are using escalation rule

<Arg name="workItemEscalationRule" value="Rule-Expire-WorkItem-On-Timeout"/>

In our configuration, we have set up two reminder notifications to be sent every three days. This means the first reminder is issued on the 3rd day (72 hours) after the work item is created, and the second reminder is sent on the 6th day (144 hours). Then, on the 9th day, our escalation rule is triggered, which expires the work item. We are not reassigning ownership at any point, as reflected in the code sample I shared earlier.

If your requirement is different, please share the exact requirement details

Hi @helanprates1 you have to add this transition in approval step

<Transition to="end" when="script:(&quot;Expired&quot;.equals(workflow.get(&quot;lastApprovalState&quot;)))"/>

or

<Transition to="end" when="script:lastApprovalState.equals(&quot;Expired&quot;)"/>

and in finalize step, you have to update the IR with state Expired when lastApprovalState = Expired. lastApprovalState you will get from wfcontext. let me know if you have any issue.

Exactly that! The work item is expiring as scheduled, but in the interface it appears as completed and successful (green). The user who requested the item might think that access was granted. Therefore, I would like the work item to expire and appear in the interface as rejected or canceled - Expired after 10 days without approval (red).

Can you help me with this code within the Expiring WorkItem step?


@helanprates1

if ("Expired".equalsIgnoreCase(lastApprovalState)) {
IdentityRequest ir = context.getObjectById(IdentityRequest.class, irId); 
if (ir == null) { 
log.error("Finalize: IR not found for id=" + irId); return; 
} 
// Set the custom state 
ir.setState("end - Expired"); 
context.saveObject(ir); 
context.commitTransaction();
}

the code snippet should be like above.

a. send your transition to end or expired step like this thread : Solved: Re: Workitem expiry - Compass

<Transition to="Expired" when="script:lastApprovalState.equals(&quot;Expired&quot;)"/>

b. in finalize step or expire step you have to add code as above to change IR state.

Hi @helanprates1 ,

Looking like your use case requires one more approval. Instead of writing the access request status explicitly, add one more approval for the manual work item team queue, since they are not taking any action, or they can accept or reject at the approval level. Once they approve it, it will generate the manual work item, and they can close the ticket as soon as it is approved. If rejected or expired approvals , it will show at the access request level.

Note: Manual work item is provisioning level.

Thanks,

PVR.

kumar ranjan has given you the code sinipped, can you please try and see if it works for you??

Hi @helanprates1 try this one for your use case : Manual Task Work Item - Closed Inccomplete/Rejected - #5 by Peddapolu

Thanks,

PVR.

@helanprates1 You can add above snippet in Identity Request Finalize. This is last second step in LCM and by this time, LCM already processed the identity request or other audits. Adding it in subprocess, may get overridden in subsequent steps.

Hello everyone, I was involved in other tasks and was away for a few days. Nothing has been changed in the proposed code. Please check if I did the right thing.

</Approval>
<Transition to="Expiring WorkItem" when="script:(&quot;Expired&quot;.equals(lastApprovalState))"/>
<Transition to="Process Approval Decisions" when="script:(!&quot;Expired&quot;.equals(workflow.get(&quot;lastApprovalState&quot;)) &amp;&amp; step.getApproval() != null &amp;&amp; step.getApproval().containsApprovalItems())"/>
<Transition to="end"/>
</Step>
<Step name="Expiring WorkItem" posX="240" posY="171">
<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());
              }
          }
      }
          if ("Expired".equalsIgnoreCase(lastApprovalState)) {
          IdentityRequest ir = context.getObjectById(IdentityRequest.class, irId); 
          if (ir == null) { 
          log.error("Finalize: IR not found for id=" + irId); return; 
} 
         // Set the custom state 
        ir.setState("end - Expired"); 
        context.saveObject(ir); 
        context.commitTransaction();
}


      // Also clear the project/plan to prevent provisioning
      project = null;
      plan = null;
</Source>
</Script>
<Transition to="end"/>
</Step>

check some import is missing. try to run and provide the logs. i will check at my end today with your code.

import sailpoint.object.IdentityRequest
added, but nothing changed.

try this way,

IdentityRequest ir = context.getObjectById(IdentityRequest.class, irId); 
List items = ir.getItems();
for(IdentityRequestItem item : items) {
  item.setApprovalState(WorkItem.state.Expired);
  item.setProvisioningState(ApprovalItem.ProvisioningState.Failed);
}
ir.setItems(items);
}
ir.setExecutionStatus(IdentityRequest.ExecutionStatus.Terminated);
ir.setCompletionStatus(IdentityRequest.CompletionStattus.Failure);
ir.setState("End - Expired");
ir.setDate(date); // Date date - defined before use
context.saveObject(ir);
context.commitTransation();

you might need to fix the syntax and add some imports . ref this page : Access request state/status - Compass

let me know the outcome. will be happy to troubleshoot more.

Hi @helanprates1 Check the transition to see if the work item is expired. if it is expried It ends with the error message in the access request, which is why you don’t need to set the access request status programmatically. Similar implementation. Here is the link: please follow it and let me know if you have any questions. Manual Task Work Item - Closed Inccomplete/Rejected - #5 by Peddapolu

Thanks,

PVR.

Good afternoon! I noticed that I deleted the Expiring step and the behavior was the same, so the flow is probably not entering the expiring step, staying only in the afterScript.

<Step icon="Approval" name="Approval" posX="158" posY="22">
<Approval mode="ref:approvalMode" owner="call:buildCommonApprovals" renderer="lcmWorkItemRenderer.xhtml" send="identityDisplayName,identityName,approvalSet,flow,policyViolations,identityRequestId">
<AfterScript>
<Source>
   import sailpoint.workflow.IdentityRequestLibrary;
import sailpoint.object.WorkItem;
import sailpoint.object.ApprovalSet;
import sailpoint.object.ApprovalItem;
if (item == null) return;

if (WorkItem.State.Expired.equals(item.getState())) {
    if (approvalSet != null) {
        approvalSet.reject(); 
        
  List items = approvalSet.getItems();
        if (items != null) {
            for (ApprovalItem aItem : items) {
                aItem.setState(WorkItem.State.Rejected);
            }
        }
        item.setCompletionComments("Expirado automaticamente após timeout sem resposta.");
    }
    
    workflow.put("lastApprovalState", "Expired");
}
assimilateWorkItemApprovalSet(wfcontext, item, approvalSet);
auditDecisions(item);
IdentityRequestLibrary.assimilateWorkItemApprovalSetToIdentityRequest(wfcontext, approvalSet);
</Source>
</AfterScript>
</Approval>
<Transition to="Expiring WorkItem" when="script:(&quot;Expired&quot;.equals(lastApprovalState))"/>
<Transition to="Process Approval Decisions" when="script:(!&quot;Expired&quot;.equals(workflow.get(&quot;lastApprovalState&quot;)) &amp;&amp; step.getApproval() != null &amp;&amp; step.getApproval().containsApprovalItems())"/>
<Transition to="end"/>
</Step>

Can u try to add this logic in finalalize step ? Or you mean the expiration of workItem not working ?

if("Expired".equals(wfcontext.getWorkflowCase().getString("lastApprovalState")) {
IdentityRequest ir = context.getObjectById(IdentityRequest.class, irId); 
List items = ir.getItems();
for(IdentityRequestItem item : items) {
  item.setApprovalState(WorkItem.state.Expired);
  item.setProvisioningState(ApprovalItem.ProvisioningState.Failed);
}
ir.setItems(items);
}
ir.setExecutionStatus(IdentityRequest.ExecutionStatus.Terminated);
ir.setCompletionStatus(IdentityRequest.CompletionStattus.Failure);
ir.setState("End - Expired");
ir.setDate(date); // Date date - defined before use
context.saveObject(ir);
context.commitTransation();
}