Roles/Entitlements that are removed by Policy Violation Revocations are re-added by Identity Refresh Task

Greetings, everyone.

We have an issue regarding re-assignment of roles in IIQ v8.3.

We have a Role SOD that gets triggered when two conflicting roles are found in an identity cube, or when a requester requests a role that would result in a violation.

When an identity is found to be violating of the SOD policy, the manager gets a Policy Violation work item to complete, in which they can allow or revoke access.

When the manager revokes one of the roles from the identity, it is removed normally from the identity cube and the corresponding entitlements from the native application. However, when an Identity Refresh task runs after the revocation, the role and the corresponding entitlements get re-assigned to the identity on both IIQ and the native application.

If the role removal is done by normal LCM means (i.e., Manage User Access quicklink), the role gets removed normally and it doesn’t get re-assigned when an Identity Refresh takes place.

We tried adding assignment attribute and set it to true to each AttributeRequest in all the Provisioning Plans by making use of the Before Provisioning rule, didn’t work.

We tried removing Sticky Entitlements, attributeAssignments, and IdentityEntitlements using the following Task and Rule before and after the revocation, but still didn’t work.

Task:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE TaskDefinition PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<TaskDefinition name="Task-StickyEntRemoval-NEW" resultAction="Rename"
                subType="task_item_type_generic" type="Generic">

  <Attributes>
    <Map>
      <entry key="TaskDefinition.runLengthAverage"/>
      <entry key="TaskDefinition.runLengthTotal"/>
      <entry key="TaskDefinition.runs"/>
      <entry key="TaskSchedule.host"/>

      <entry key="deleteStickEnt" value="false"/>
      <!-- NEW -->
      <entry key="deleteIdentityEntitlements" value="false"/>

      <entry key="ruleName" value="Rule-StickyEntRemoval-NEW"/>
      <entry key="taskCompletionEmailNotify" value="Disabled"/>
      <entry key="taskCompletionEmailRecipients"/>
      <entry key="taskCompletionEmailTemplate"/>
    </Map>
  </Attributes>

  <Description>
    Removes Sticky Entitlements, AttributeAssignments, and IdentityEntitlements.
  </Description>

  <Owner>
    <Reference class="sailpoint.object.Identity" name="spadmin"/>
  </Owner>

  <Parent>
    <Reference class="sailpoint.object.TaskDefinition" name="Run Rule"/>
  </Parent>

  <Signature>
    <Inputs>
      <Argument name="deleteStickEnt" type="boolean">
        <Prompt>Delete the Sticky Entitlements</Prompt>
      </Argument>

      <!-- NEW -->
      <Argument name="deleteIdentityEntitlements" type="boolean">
        <Prompt>Delete IdentityEntitlements</Prompt>
      </Argument>

      <Argument name="identityValue" type="string">
        <Prompt>Identity Distinguished Name</Prompt>
      </Argument>
    </Inputs>

    <Returns>
      <Argument name="attrAssignRemovalCount" type="string"/>
      <Argument name="attrAssignIdList" type="string"/>
      <Argument name="idenEntRemovalCount" type="string"/>

      <!-- NEW -->
      <Argument name="identityEntRemovalCount" type="int"/>

      <Argument name="totalIE" type="int"/>
      <Argument name="idenEntList" type="string"/>
    </Returns>
  </Signature>
</TaskDefinition>

Rule:

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE Rule PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Rule language="beanshell" name="Rule-StickyEntRemoval-NEW">
  <Source><![CDATA[

import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;

import sailpoint.api.ObjectUtil;
import sailpoint.api.Terminator;
import sailpoint.object.AttributeAssignment;
import sailpoint.object.Filter;
import sailpoint.object.Identity;
import sailpoint.object.IdentityEntitlement;
import sailpoint.object.QueryOptions;
import sailpoint.object.TaskResult;
import sailpoint.tools.GeneralException;
import sailpoint.tools.Message;
import sailpoint.tools.Util;

if (void == taskResult || taskResult == null || config == null) {
  return;
}

int attrAssignRemovalCount = 0;
int idenEntRemovalCount = 0;
int totalIE = 0;

/* NEW */
int identityEntRemovalCount = 0;

boolean isDelete = Util.otob(config.get("deleteStickEnt"));
/* NEW */
boolean deleteIdentityEntitlements =
    Util.otob(config.get("deleteIdentityEntitlements"));

String identityValue = Util.otoa(config.get("identityValue"));

List attrAssignIdList = new ArrayList();
List idenEntList = new ArrayList();

Iterator itr = null;
Iterator aaIt = null;

String RESULT = "Success";

/* ================= EXISTING METHODS (UNCHANGED) ================= */

private void getAttrAssignmentsId(IdentityEntitlement idenEnt)
    throws GeneralException {

  Identity id = null;
  try {
    id = idenEnt.getIdentity();
    if (id != null) {
      List attributeAssignmentList = id.getAttributeAssignments();
      if (!Util.isEmpty(attributeAssignmentList)) {
        aaIt = attributeAssignmentList.iterator();
        while (aaIt.hasNext()) {
          AttributeAssignment attAssign = (AttributeAssignment) aaIt.next();
          if (Util.isNotNullOrEmpty(idenEnt.getAssignmentId())
              && Util.isNotNullOrEmpty(attAssign.getAssignmentId())
              && attAssign.getAssignmentId()
                   .equalsIgnoreCase(idenEnt.getAssignmentId())) {
            attrAssignIdList.add(id.getName());
          }
        }
      }
    }
  } finally {
    if (aaIt != null) Util.flushIterator(aaIt);
  }
}

private void removeAttrAssignment(IdentityEntitlement idenEnt)
    throws GeneralException {

  Identity id = null;
  try {
    id = ObjectUtil.lockIfNecessary(context,
        idenEnt.getIdentity().getName());

    if (id != null) {
      List attributeAssignmentList = id.getAttributeAssignments();
      if (!Util.isEmpty(attributeAssignmentList)) {
        aaIt = attributeAssignmentList.iterator();
        while (aaIt.hasNext()) {
          AttributeAssignment attAssign = (AttributeAssignment) aaIt.next();
          if (Util.isNotNullOrEmpty(idenEnt.getAssignmentId())
              && Util.isNotNullOrEmpty(attAssign.getAssignmentId())
              && attAssign.getAssignmentId()
                   .equalsIgnoreCase(idenEnt.getAssignmentId())) {

            aaIt.remove();
            attrAssignIdList.add(id.getName());
            attrAssignRemovalCount++;
          }
        }
        context.saveObject(id);
      }
    }
  } finally {
    if (id != null) ObjectUtil.unlockIdentity(context, id);
    if (aaIt != null) Util.flushIterator(aaIt);
  }
}

/* ================= RULE EXECUTION ================= */

try {
  QueryOptions qo = new QueryOptions();
  qo.setCloneResults(true);
  qo.setDistinct(true);

  if (Util.isNotNullOrEmpty(identityValue)) {
    qo.addFilter(Filter.ignoreCase(
        Filter.eq("nativeIdentity", identityValue)));
  }

  qo.addFilter(Filter.ignoreCase(
      Filter.eq("aggregationState", "disconnected")));

  itr = context.search(IdentityEntitlement.class, qo);

  while (itr.hasNext()) {
    IdentityEntitlement idenEnt = (IdentityEntitlement) itr.next();

    if (isDelete || deleteIdentityEntitlements) {
      context.startTransaction();

      /* EXISTING */
      removeAttrAssignment(idenEnt);

      /* NEW – delete IdentityEntitlement */
      if (deleteIdentityEntitlements) {
        new Terminator(context).deleteObject(idenEnt);
        identityEntRemovalCount++;
      }

      /* EXISTING */
      if (isDelete) {
        idenEntRemovalCount++;
      }

      context.commitTransaction();
    } else {
      getAttrAssignmentsId(idenEnt);
    }

    if (totalIE < 100) {
      idenEntList.add(" [ " + idenEnt.getValue() + " ] ");
    }
    totalIE++;
  }

  if (!isDelete && !deleteIdentityEntitlements) {
    taskResult.addMessage(
      new Message(Message.Type.Info,
        "No records were deleted", null));
  }

} catch (GeneralException e) {
  context.rollbackTransaction();
  taskResult.addMessage(
    new Message(Message.Type.Error, e.getMessage(), null));
  RESULT = "Fail";
} finally {
  if (itr != null) Util.flushIterator(itr);
  context.decache();
}

/* ================= TASK RESULT ================= */

taskResult.put("attrAssignRemovalCount", attrAssignRemovalCount);
taskResult.put("attrAssignIdList", attrAssignIdList);
taskResult.put("idenEntRemovalCount", idenEntRemovalCount);

/* NEW */
taskResult.put("identityEntRemovalCount", identityEntRemovalCount);

taskResult.put("totalIE", totalIE);
taskResult.put("idenEntList", idenEntList);

return RESULT;

  ]]></Source>
</Rule>

Note: This also happens in IIQ v8.5, and the same approach did NOT work as well.

Thank you in advance.

@kareem_ramzi After revoking the role from policy voilations, do you see “negative=true” flag against the role name in identity xml?

Is this an assigned role or detected role?

You can try to compare the identity XML (before and after) when going through LCM process where the process is working fine.
This way you get to know exactly what artifact is getting removed from identity in the happy scenario. Once you know what artifact is getting removed you can compare identity xml with the non-working scenario then you will have more knowledge about what artifact/attribute is causing the re-adding of entitlement

I couldn’t find the negative flag you’re talking about in the identity XML.

I tried comparing before-and-after XMLs of the same identity: first XML is before they even have the access, and the other XML is when the access is revoked via Policy Violation.

I noticed something weird - the second XML somehow still contains the <RoleAssignment> element of the REVOKED role, which is weird.

NB:- The revoked access is AD-TempElevatedAccess

The before XML’s roleAssignments element:

      <entry key="roleAssignments">
        <value>
          <List>
            <RoleAssignment assignmentId="88dc5fc1ee4a4823a07c4ff2f31604b8" date="1768471079266" roleId="7f0001019bb215b0819bb6b63b740325" roleName="IT Administrator" source="Rule">
              <PermittedRoleAssignments>
                <RoleAssignment assigner="spadmin" date="1769427397369" roleId="7f0001019bda11d5819bfa05a4a8136e" roleName="AD-FileShareAccess"/>
              </PermittedRoleAssignments>
              <RoleTarget applicationId="7f0001019b9d12b2819bb2216a3b0a82" applicationName="Web Service Application" displayName="<REDACTED>" nativeIdentity="<REDACTED>"/>
              <RoleTarget applicationId="7f0001019b9d12b2819b9dea69fa0359" applicationName="Active Directory" displayName="<REDACTED>" nativeIdentity="<REDACTED>"/>
            </RoleAssignment>
          </List>
        </value>
      </entry>

The after XML’s roleAssignmentselement:

      <entry key="roleAssignments">
        <value>
          <List>
            <RoleAssignment assignmentId="88dc5fc1ee4a4823a07c4ff2f31604b8" date="1768471079266" roleId="7f0001019bb215b0819bb6b63b740325" roleName="IT Administrator" source="Rule">
              <PermittedRoleAssignments>
                <RoleAssignment assigner="spadmin" date="1769427397369" roleId="7f0001019bda11d5819bfa05a4a8136e" roleName="AD-FileShareAccess"/>
                <RoleAssignment assigner="spadmin" date="1769427982456" roleId="7f0001019bda11d5819bfa061b591374" roleName="AD-TempElevatedAccess"/>
              </PermittedRoleAssignments>
              <RoleTarget applicationId="7f0001019b9d12b2819bb2216a3b0a82" applicationName="Web Service Application" displayName="<REDACTED>" nativeIdentity="<REDACTED>"/>
              <RoleTarget applicationId="7f0001019b9d12b2819b9dea69fa0359" applicationName="Active Directory" displayName="<REDACTED>" nativeIdentity="<REDACTED>"/>
            </RoleAssignment>
          </List>
        </value>
      </entry>

But BOTH XMLs have the same bundleSummary value in the <Identity> element as follows (Notice how there is no AD-TempElevatedAccess):

Before XML:

<Identity assignedRoleSummary="IT Administrator" bundleSummary="ITAdmin-Web-SuperAdmin, ITAdmin-AD, AD-FileShareAccess" correlated="true" created="1767871095111" id="7f0001019b9d12b2819b9d54214600c2" lastLogin="1768744377686" lastRefresh="1769427787244" managerStatus="true" modified="1769427787245" name="<REDACTED>" password="<REDACTED>" significantModified="1769427787220">

After XML:

<Identity assignedRoleSummary="IT Administrator" bundleSummary="ITAdmin-Web-SuperAdmin, ITAdmin-AD, AD-FileShareAccess" correlated="true" created="1767871095111" id="7f0001019b9d12b2819b9d54214600c2" lastLogin="1768744377686" lastRefresh="1769428019220" managerStatus="true" modified="1769428117928" name="<REDACTED>" password="<REDACTED>" significantModified="1769428117907">

When the manager revokes one of the roles from the identity, it is removed normally from the identity cube and the corresponding entitlements from the native application ==>

In this step remove <RoleAssignment> element of the REVOKED role as well.
As per your findings it may be getting re-added during refresh because of this <RoleAssignment>

Alright that would work, got any idea how to remove them? Do you have any sample rule/task that I could use?

you can try using below APIs on Identity object to get RA and remove also -

getRoleAssignments()

removeRoleAssignment​(RoleAssignment ra)

After that save object and commit transaction.

@kareem_ramzi any progress on this?

I will try your approach as soon as I can and will inform you, much appreciated!

I’m trying to apply your approach by reading the raw ProvisioningPlan which includes information about addition/removal of roles in IIQ itself as an application, a sample would be as follows:

<!DOCTYPE ProvisioningPlan PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<ProvisioningPlan>
  <AccountRequest application="IIQ" op="Modify">
    <Attributes>
      <Map>
        <entry key="attachmentConfigList"/>
        <entry key="attachments"/>
        <entry key="flow" value="AccessRequest"/>
        <entry key="id" value="7f0001019bb71524819bbbb370b711a9"/>
        <entry key="interface" value="LCM"/>
        <entry key="operation" value="RoleRemove"/>
      </Map>
    </Attributes>
    <AttributeRequest assignmentId="b2f427e00ef544e087b0592c1fea13ff" name="detectedRoles" op="Remove" value="AD-TempElevatedAccess">
      <Attributes>
        <Map>
          <entry key="deassignEntitlements">
            <value>
              <Boolean>true</Boolean>
            </value>
          </entry>
        </Map>
      </Attributes>
    </AttributeRequest>
  </AccountRequest>
  <Attributes>
    <Map>
      <entry key="identityRequestId" value="0000000403"/>
      <entry key="requester" value="spadmin"/>
      <entry key="source" value="LCM"/>
    </Map>
  </Attributes>
  <Requesters>
    <Reference class="sailpoint.object.Identity" id="7f0001019b9c1ce6819b9cfd330d00ea" name="spadmin"/>
  </Requesters>
</ProvisioningPlan>

The XML above is ONLY generated when the role is being removed via LCM (i.e., Manage User Access QuickLink). However, when the role is being removed part of a Policy Violation revocation decision, the above XML is not even generated. It never generates a plan for IIQ application to remove the role. Despite that, it removes the entitlement from the native application.

In other words, the entitlement is the only thing getting removed, NOT the role (even though it appears like it was removed as well in the identity cube UI). So, that explains why the entitlement gets re-added, as IIQ detects that the role applied to the identity has some missing entitlements for that specific identity, so it re-provisions the entitlement.

I don’t know how to apply your approach since the role name itself is not being mentioned in the ProvisioningPlan, which means I can’t find a way to retrieve the role name in order to remove it via API.

How do I fix this issue? It doesn’t even generate a plan for role removal. Does Policy Violation revocation decision invoke the LCM Provisioning business process, or does it invoke another business process?

NB:- The SOD is of type Role SOD, not Entitlement SOD, so that’s not the issue.

@kareem_ramzi - How are you removing the role? Are you removing it through the policy violation UI? If so, this is out-of-the-box behavior—the role will not be reassigned dynamically. Please raise a ticket with SailPoint support if this is not happening.

Alternatively, are you removing the role only from the access request to satisfy the policy? In that case, SailPoint has no way of knowing that the role should not be reassigned, and it will be added back during the next refresh.

Yes, the role is being revoked from the policy violation UI when IIQ detects an identity violating a specific policy and leaves the decision to the manager/owner to accept/revoke the role. When they revoke the role via the policy violation UI, the entitlement gets revoked normally, but not the role itself in the identity XML. Kindly view my reply above for extended details.