We are using IdentityIQ 8.3p3. We have a requirement to bypass owner approval on unlock account operation from Manage Account quicklink. We have added a custum assignment rule to bypass approval in the OOTB LCM Provisioning workflow.
Following is the assignment Rule:
import sailpoint.object.Workflow;
import sailpoint.object.*;
import java.util.ArrayList;
import sailpoint.object.Workflow.Approval;
import sailpoint.object.ApprovalItem;
import sailpoint.object.ApprovalSet;
import sailpoint.object.Identity;
import org.apache.log4j.Logger;
import org.apache.log4j.Level;
Logger log = Logger.getLogger("rule.LCMManageAccountAssign");
log.setLevel(Level.DEBUG);
log.debug("The approvals is: "+approvals);
log.debug("The approvalset is: "+approvalSet.toXml());
List newApprovals = new ArrayList();
if(!sailpoint.tools.Util.isEmpty(approvals))
{
log.debug(" Approvals present");
for (Iterator iterator = approvals.iterator(); iterator.hasNext();)
{
Approval approval = (Approval) iterator.next();
if(approval != null)
{
log.debug(" Approval Object: "+approval.toXml());
List childApprovals = approval.getChildren();
if(null != childApprovals && !childApprovals.isEmpty())
{
log.debug(" Approval has child approval object");
for (Object object2 : childApprovals)
{
Approval childApproval = (Approval) object2;
if(null != childApproval)
{
log.debug(" Approval Object: "+ childApproval.toXml());
ApprovalSet childset = childApproval.getApprovalSet();
log.debug("-------------------------childset-----------------------------------"+ childset.toXml());
List items = childset.getItems();
log.debug("-items size-----------------------------------:"+items.size()) ;
for (ApprovalItem item : items)
{
if(null != item)
{
log.debug(" Approval Item from ApprovalSet: "+ item.toXml() );
if(item.getOperation().equalsIgnoreCase("Unlock"))
{
log.debug(" Unlock operation - Adding Approval Object to remove list");
childset.remove(item);
log.debug("--------new object3 size-----------------------------------:"+items.size()) ;
}
}
}
childApproval.setApprovalSet(childset);
newApprovals.add(childApproval);
for ( Approval approval : newApprovals )
{
log.debug("Approval expand: "+approval.toXml());
}
}
}
} //child approval if
}//approval if
}//for approvals
}//approvals if
if ( newApprovals != null && newApprovals.size() > 0 )
{
log.debug("size of newApprovals is "+ newApprovals.size());
for ( Approval approval : newApprovals ) {
log.debug("Expanded new approval : "+approval.toXml());
}
return newApprovals;
}
When same request has more than 2 unlock operation for different applications having same application owner, the for-loop iteration on items is not getting executed more than once.
This is the approvals we have on 2 unlock operation:
From what I can tell, the issue arises because you’re modifying the list (items) while iterating over it:
for (ApprovalItem item : items)
{
if (item.getOperation().equalsIgnoreCase("Unlock")) {
childset.remove(item); // <-- modifying the list being iterated
}
}
To resolve this, you should avoid modifying the list while iterating over it. Instead, collect the items to remove in a temporary list and then remove them in a second step.
List itemsToRemove = new ArrayList<ApprovalItem>();
for (ApprovalItem item : items) {
if (item != null && "Unlock".equalsIgnoreCase(item.getOperation())) {
log.debug("Unlock operation - Queuing Approval Item for removal: " + item.toXml());
itemsToRemove.add(item);
}
}
// Remove after iteration
for (ApprovalItem itemToRemove : itemsToRemove) {
childset.remove(itemToRemove);
}
You can drop this into your existing logic to ensure all Unlock operations are processed, regardless of how many exist.
Hi @dheerajk27 , Thanks for your suggestion. Based on your input I updated the rule as below:
List newApprovals = new ArrayList();
List itemsToRemove = new ArrayList();
ApprovalSet newSet = new ApprovalSet();
if(!sailpoint.tools.Util.isEmpty(approvals))
{
for (Iterator iterator = approvals.iterator(); iterator.hasNext();)
{
Approval approval = (Approval) iterator.next();
if(approval != null)
{
List childApprovals = approval.getChildren();
if(null != childApprovals && !childApprovals.isEmpty())
{
for (Object object2 : childApprovals)
{
log.debug("praj1 ");
Approval childApproval = (Approval) object2;
if(null != childApproval)
{
ApprovalSet childset = childApproval.getApprovalSet();
List items = childset.getItems();
for (ApprovalItem item : items)
{
if(null != item)
{
log.debug(" Approval Item from ApprovalSet: "+ item.toXml() );
// DEMO: Check if the approval item is for a unlock operation, if so remove the approval from the list.
if (item != null && "Unlock".equalsIgnoreCase(item.getOperation()))
{
log.debug(" Unlock operation - Adding Approval Object to remove list" + item.toXml());
itemsToRemove.add(item);
log.debug("--------new object3 size-----------------------------------:"+items.size()) ;
}
else
{
newSet.add(item);
childApproval.setApprovalSet(newSet);
childApproval.setMode("parallelPoll");
newApprovals.add(childApproval);
}
}
}
/***************************************************/
if(itemsToRemove!=null && !itemsToRemove.isEmpty())
{
for (ApprovalItem itemToRemove : itemsToRemove)
{
childset.remove(itemToRemove);
}
if (childset != null && !Util.isEmpty(childset.getItems()))
{
childApproval.setApprovalSet(childset);
childApproval.setMode("parallelPoll");
newApprovals.add(childApproval);
}
}
for ( Approval approval : newApprovals )
{
log.debug("Approval expand: "+approval.toXml());
}
}
}
} //child approval if
}//approval if
}//for approvals
}//approvals if
if(null != approvalSet)
{
log.debug("******* Approval Set********** : "+ approvalSet.toXml());
log.debug(" Updating Approval Set to reflect approval status on removed items");
for (Iterator iterator = approvalSet.getItems().iterator(); iterator.hasNext();)
{
ApprovalItem approvalItem = (ApprovalItem) iterator.next();
if(null != approvalItem)
{
log.debug(" Checking approval Item");
log.debug(" Approval Item: "+ approvalItem.toXml());
if(approvalItem.getOperation().equalsIgnoreCase("Unlock"))
{
approvalItem.setState(WorkItem.State.Finished);
approvalItem.approve();
approvalItem.add(new Comment("Auto Approving Unlock", "spadmin"));
}
log.debug(" Updated approval Item: "+approvalItem.toXml());
}
}
}
if ( newApprovals != null && newApprovals.size() > 0 )
{
log.debug("size of newApprovals is "+ newApprovals.size());
for ( Approval approval : newApprovals )
{
log.debug("Expanded new approval : "+approval.toXml());
}
return newApprovals;
}
log.debug("Expanded final blank approval : "+newApprovals);
return newApprovals;
The newApprovals are getting generated as expected. But when the same request has 2 items like Unlock and Enable or Enable and Delete account operation, after approving the owner workitem, the request is stuck in step “Approve and Provision” on the access request without provisioning. The error in the log is: “WorkItem deleted out from under case”
childApproval.setApprovalSet(newSet) is called inside the loop
You’re reusing newSet, which is shared across all child approvals. This means you’re mutating a shared set across multiple approvals, causing overlapping and unexpected behavior.
This can result in certain ApprovalItems being incorrectly removed or processed twice.
You’re modifying the childApproval while looping over its items and also modifying the same childset later
This can lead to inconsistent or empty states, particularly when approvals are processed in parallel (parallelPoll).
You add()childApproval to newApprovals in both else and if(itemsToRemove...)
This means the same approval object can be added multiple times under different conditions, potentially with an inconsistent approval set.
You are calling approve() on ApprovalItem, but the parent Approval may still be waiting for work item completion
If the system expects an associated WorkItem and it was removed or altered improperly, you’ll get WorkItem deleted out from under case.
Try below code:
List newApprovals = new ArrayList();
List itemsToRemove = new ArrayList();
ApprovalSet newSet = new ApprovalSet();
if(!sailpoint.tools.Util.isEmpty(approvals))
{
for (Iterator iterator = approvals.iterator(); iterator.hasNext();)
{
Approval approval = (Approval) iterator.next();
if(approval != null)
{
List childApprovals = approval.getChildren();
if(null != childApprovals && !childApprovals.isEmpty()){
for (Object object2 : childApprovals) {
Approval childApproval = (Approval) object2;
if (childApproval != null) {
ApprovalSet originalSet = childApproval.getApprovalSet();
ApprovalSet filteredSet = new ApprovalSet(); // Unique set per childApproval
List<ApprovalItem> toRemove = new ArrayList<>();
for (ApprovalItem item : originalSet.getItems()) {
if ("Unlock".equalsIgnoreCase(item.getOperation())) {
// Auto-approve Unlocks
item.setState(WorkItem.State.Finished);
item.approve();
item.add(new Comment("Auto Approving Unlock", "spadmin"));
toRemove.add(item);
} else {
filteredSet.add(item); // Keep other ops
}
}
// Remove Unlock items
for (ApprovalItem item : toRemove) {
originalSet.remove(item);
}
if (!filteredSet.getItems().isEmpty()){
childApproval.setApprovalSet(filteredSet);
childApproval.setMode("parallelPoll");
newApprovals.add(childApproval);
}
// Clear list for next loop
toRemove.clear();
}
}
} //child approval if
}//approval if
}//for approvals
}//approvals if
if(null != approvalSet)
{
log.debug("******* Approval Set********** : "+ approvalSet.toXml());
log.debug(" Updating Approval Set to reflect approval status on removed items");
for (Iterator iterator = approvalSet.getItems().iterator(); iterator.hasNext();)
{
ApprovalItem approvalItem = (ApprovalItem) iterator.next();
if(null != approvalItem)
{
log.debug(" Checking approval Item");
log.debug(" Approval Item: "+ approvalItem.toXml());
if(approvalItem.getOperation().equalsIgnoreCase("Unlock"))
{
approvalItem.setState(WorkItem.State.Finished);
approvalItem.approve();
approvalItem.add(new Comment("Auto Approving Unlock", "spadmin"));
}
log.debug(" Updated approval Item: "+approvalItem.toXml());
}
}
}
if ( newApprovals != null && newApprovals.size() > 0 )
{
log.debug("size of newApprovals is "+ newApprovals.size());
for ( Approval approval : newApprovals )
{
log.debug("Expanded new approval : "+approval.toXml());
}
return newApprovals;
}
log.debug("Expanded final blank approval : "+newApprovals);
return newApprovals;
Thanks @dheerajk27 for the detailed explanation. It made sense. I updated my code to reflect these changes, and I am no longer getting the error.
All the use cases seem to be working now.
The only weird part I see, if I remove the second half of the code where I am setting the workitem to finished on unlock as it is being set now in the first part, the usecase having only unlock requests get auto-approved without generating a provisioning transaction.
So, I had to keep that code to satisfy this use-case and approvalItem.setState(WorkItem.State.Finished); is being set twice in the code on unlock.