For contractors, only Identity Certification is required; there’s no need to decide on access items in SailPoint ISC

We need to certify the contractor, which means the certification should be routed to their manager. The manager would then decide whether the contractor is required. If not, LCS needs to be updated as terminate. Is this possible in ISC?

Hello Oviya. Yes, this is possible, but I would suggest using a Workflow with Generic Approval Policy rather than a Certification Campaign, since certifications are intended to review access items.

If Adaptive Approvals is available in your tenant, the flow could be:

Contractor review → Manager approval → Approved = no change → Denied = Terminate

You can use Get Identity to retrieve the contractor’s managerRef.id and dynamically use that manager as the Identity (Other) reviewer.

On denial, the workflow can use an HTTP Request to call the Set Lifecycle State API and move the contractor to the configured Terminated state.

If lifecycle state is driven from the authoritative source, I would update the contractor status there instead so it remains consistent.

You can run a review campaign on a group and you will also need a workflow or another utility which will read access review status and post an update to Non Employee profile to trigger termination.

Here is some code you use for reference. Note this code is untested, as we decided not to use ISC Certification module all together (Our internal tools had better feature set :))

We have a Identity attribute called as dataReplacementID, this will contain the account id of Non Employee HR Source (which is a CSV source). In Non Employee Source we have another attribute certificationTeminationDate, this will trigger change to Identity state to terminated.

import org.apache.logging.log4j.LogManager;

import org.apache.logging.log4j.Logger;

import org.json.JSONObject;



import java.time.Instant;

import java.util.List;

import java.net.http.HttpResponse;  // Added import for HTTP response




import java.util.Arrays;



public class CertClosureUtil {

    private static final Logger logger = LogManager.getLogger(CertClosureUtil.class);

    private final ISCAPIService iscApiService;



    public CertClosureUtil(ISCAPIService iscApiService) {

        // Added parameter validation

        if (iscApiService == null) {

            String errorMsg = "ISCAPIService cannot be null";

            logger.error(errorMsg);

            throw new IllegalArgumentException(errorMsg);

        }

        this.iscApiService = iscApiService;

        logger.debug("CertClosureUtil initialized with ISCAPIService");

    }



    // Added throws declaration

    public void processCertifications() throws Exception {

        logger.info("Starting certification processing");

        try {

            List<JSONObject> certifications = iscApiService.getCertifications();

            // Added null check

            if (certifications == null) {

                throw new RuntimeException("Failed to retrieve certifications");

            }

            

            logger.info("Retrieved {} certifications for processing", certifications.size());

            Instant today = Instant.now();



            for (JSONObject certification : certifications) {

                try {

                    processSingleCertification(certification);

                } catch (Exception e) {

                    String certId = certification.optString("id", "unknown");

                    logger.error("Error processing certification {}: {}", certId, e.getMessage(), e);

                    // Continue with other certifications

                }

            }

            logger.info("Certification processing completed");

        } catch (Exception e) {

            logger.error("Critical error processing certifications: {}", e.getMessage(), e);

            throw new Exception("Failed to process certifications", e);

        }

    }



    // Added throws declaration and parameter validation

    private void processSingleCertification(JSONObject certification) throws Exception {

        if (certification == null) {

            throw new IllegalArgumentException("Certification cannot be null");

        }

        

        try {

            String certificationId = certification.getString("id");

            logger.debug("Processing certification: {}", certificationId);

            

            String dueDate = certification.getString("due");

            Instant dueDateInstant = Instant.parse(dueDate);

            Instant currentTime = Instant.now();



            // Check if due date is past current date/time

            if (dueDateInstant.isBefore(currentTime)) {

                logger.info("Certification {} is past due date ({}), processing access review items", 

                    certificationId, dueDate);

                processAccessReviewItems(certificationId);

            } else {

                logger.debug("Certification {} is not past due date yet", certificationId);

            }

        } catch (Exception e) {

            logger.error("Error processing certification: {}", certification, e);

            throw new Exception("Failed to process certification", e);

        }

    }



    // Added throws declaration and parameter validation

    private void processAccessReviewItems(String certificationId) throws Exception {

        if (certificationId == null || certificationId.trim().isEmpty()) {

            throw new IllegalArgumentException("Certification ID cannot be null or empty");

        }

        

        try {

            List<JSONObject> reviewItems = iscApiService.getAccessReviewItems(certificationId);

            // Added null check

            if (reviewItems == null) {

                throw new RuntimeException("Failed to retrieve access review items for certification: " + certificationId);

            }

            

            logger.info("Retrieved {} access review items for certification {}", 

                reviewItems.size(), certificationId);



            int completedCount = 0;

            int terminationCount = 0;

            int errorCount = 0;  // Added counter for errors

            

            for (JSONObject reviewItem : reviewItems) {

                try {

                    if (!reviewItem.getBoolean("completed")) {

                        invokeTermination(reviewItem);

                        terminationCount++;

                    } else {

                        completedCount++;

                    }

                } catch (Exception e) {

                    String reviewItemId = reviewItem.optString("id", "unknown");

                    logger.error("Error processing review item {}: {}", reviewItemId, e.getMessage(), e);

                    errorCount++;  // Increment error counter

                    // Continue with other review items

                }

            }

            

            // Added detailed summary logging

            logger.info("Processed {} review items: {} completed, {} termination requests, {} errors", 

                reviewItems.size(), completedCount, terminationCount, errorCount);

                

            if (errorCount > 0) {

                logger.warn("Encountered {} errors while processing review items", errorCount);

            }

        } catch (Exception e) {

            logger.error("Critical error processing access review items for certification {}: {}", 

                certificationId, e.getMessage(), e);

            throw new Exception("Failed to process access review items for certification: " + certificationId, e);

        }

    }



    // Added throws declaration and parameter validation

    private void invokeTermination(JSONObject reviewItem) throws Exception {

        if (reviewItem == null) {

            throw new IllegalArgumentException("Review item cannot be null");

        }

        

        JSONObject identitySummary = reviewItem.getJSONObject("identitySummary");

        if (identitySummary == null) {

            throw new IllegalArgumentException("Identity summary is missing in review item");

        }

        

        String identityId = identitySummary.getString("identityId");

        String reviewItemId = reviewItem.optString("id", "unknown");

        

        logger.debug("Invoking termination for identity {} (review item {})", identityId, reviewItemId);

        

        try {

            String todayString = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.BASIC_ISO_DATE);

            

            JSONObject identity = iscApiService.getIdentity(identityId);

            // Added null check

            if (identity == null) {

                throw new RuntimeException("Failed to retrieve identity: " + identityId);

            }

            

            String dataReplacementID = getdataReplacementAccountId(identity);



            if (dataReplacementID == null || dataReplacementID.isEmpty()) {

                logger.warn("No dataReplacementID found for identity: {}", identityId);

                return;

            }



            List<IdentityPatchAttribute> patchAttributes = Arrays.asList(

                new IdentityPatchAttribute(

                    "add",

                    "snReqReason",

                    "Auto Terminated ID as certification was not review by due date"),

                new IdentityPatchAttribute(

                    "add",

                    "updated",

                    "ISC CertClosure Util"),

                new IdentityPatchAttribute(

                    "add",

                    "/attributes/certificationTeminationDate",

                    todayString));



            // Changed to use HttpResponse for better error handling

            HttpResponse<String> response = iscApiService.patchAccount(dataReplacementID, patchAttributes);

            

            // Added HTTP response validation

            if (response.statusCode() >= 200 && response.statusCode() < 300) {

                logger.info("Successfully submitted termination request for identity: {}", identityId);

            } else {

                String errorMsg = "Failed to submit termination request for identity: " + identityId + 

                                 ". Status: " + response.statusCode() + ", Response: " + response.body();

                logger.error(errorMsg);

                throw new RuntimeException(errorMsg);

            }

        } catch (Exception e) {

            logger.error("Failed to submit termination request for identity: {}", identityId, e);

            throw new Exception("Failed to submit termination request for identity: " + identityId, e);

        }

    }



    // Added null checks and improved error handling

    public String getdataReplacementAccountId(JSONObject identity) {

        if (identity == null) {

            logger.warn("Identity is null");

            return null;

        }

        

        try {

            if (!identity.has("attributes")) {

                logger.warn("Identity does not have attributes");

                return null;

            }

            

            JSONObject attributes = identity.getJSONObject("attributes");

            if (!attributes.has("secondaryManagerIdentityId")) {

                logger.warn("Identity does not have secondaryManagerIdentityId attribute");

                return null;

            }

            

            return attributes.getString("secondaryManagerIdentityId");

        } catch (Exception e) {

            logger.warn("Error getting dataReplacementAccountId: {}", e.getMessage());

            return null;

        }

    }

}

Another option could also be by using an entitlements to identity all active contractors.

  1. Use a role and map this entitlement to the role.
  2. Use a workflow to assign this role to all the active contractors and remove from the terminated ones.
  3. Once the entitlement is assigned to user set the cloud lifecyle state of the user as Active.
  4. Then you can launch the certification on this role.
  5. If manager declines the role for the user, role will be removed.
  6. Once role is removed, the transform for the LCS should make the user TERMINATED.

Thank you.

Thanks For the response !!!

Here we have another doubt, if we have 50 contractor ,under all the 50 managers takes decision workflow will be keep running?

Hello Oviya. Yes, each contractor’s approval can remain pending until that manager responds or the configured timeout is reached. Other managers can still complete their approvals independently.

If you are processing all 50 contractors together in one workflow, I would suggest using a Loop so the contractor reviews run in parallel. The workflow will continue once all loop iterations are completed.

I would also suggest configuring reminders and a timeout for pending approvals.