SailPoint IDN Web Service After Operation Rule

Hello everyone!

I am currently working on developing an after operation rule for account aggregation. This is the code I have prepared for it.

import org.json.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import connector.common.JsonUtil;
import connector.common.JsonUtil.*;
import sailpoint.object.*;
import connector.common.Util;

/*
 * CREATED BY: Zeel Sinojia
 * Date: 01/21/2024
 * Purpose: Code to fetch the last login information from the custom url
 */

 
//setting a log prefix allows us to easily filter ccg.log for relevant items
String logPrefix = "WSAO_OracleERP";

//retrieve the transient values, if they exits, from application storage. will be Null if not set.
Map transientValues = (Map) application.getAttributeValue("transientValues");

//build our return object structure
List returnObj = new ArrayList();
Map processedData = new HashMap();
log.info(logPrefix + " starting to process");

//only call additional APIs if we do not already have them in memory
if(transientValues == null) {
    ///////// START LOOKUPS ///////////
    Map transientValues = new HashMap();
    
    //set up the requirements for making API calls
    List<String> allowedStatuses = new ArrayList();
    allowedStatuses.add( "2**" );
    Map header = requestEndPoint.getHeader();

    //call the last login API, build a key/value Map of login_id : last_access_in_days
    Map LastLoginLookupMap = new HashMap();
    try{
        String lastLoginUrl = "";
        //executeGet operates in the context of the source restClient.  
        //will follow Authentication on the source/endpoint.
        String lastLogins = restClient.executeGet(lastLoginUrl, header, allowedStatuses );
		JSONObject responseJsonObject = new JSONObject(lastLogins);
		JSONArray lastLoginJsonArray = responseJsonObject.get( "items" );
        for (int i = 0; i < lastLoginJsonArray.length(); i++) {
            JSONObject lastLoginJsonObject = lastLoginJsonArray.getJSONObject(i);
            String emailId = lastLoginJsonObject.getString("login_id");
            String lastAccessInDays = lastLoginJsonObject.getString("last_access_in_days");
            LastLoginLookupMap.put(emailId,lastAccessInDays);          
        }  
        //store the resultant map into transient values
        transientValues.put("LastLoginLookupMap", LastLoginLookupMap);
    } catch (Exception e) {
        log.error(logPrefix + "********ERROR AFTER RULE: " + e.getMessage(), e );
    }

    //store "our" transient values into the application (until the aggregation completes, wherein it will erase)
    application.setAttribute("transientValues", transientValues);
    log.error(logPrefix + "setting transients");
}
    
/////// END LOOKUPS //////////

// ensure that we have data to actuall operate on
if(rawResponseObject != null && processedResponseObject != null){
	
    //retrieve the transientValues from application storage
    transientValues = (Map) application.getAttributeValue("transientValues");

	Map resultMap = JsonUtil.parseJson( rawResponseObject );

    //iterate over every Record in the response
    // processedResponseObject will have the JSONPath values defined on the source endpoint.
    for (Map userEntry : processedResponseObject ) {
		
        log.error(logPrefix + "updating processed data");

        // grab key values from the record, from the JSONPath'd output of the endpoint itself
        String emailId = null;
		
		if ( userEntry.containsKey("Email") )
			emailId = userEntry.get( "Email" );
		
		userEntry.put( "displayName", "after operation test" );

        //null safety. dont look it up if it doesnt exist.
        if (transientValues.containsKey("LastLoginLookupMap") && Util.isNotNullOrEmpty( emailId )){
            HashMap LastLoginLookupMap = (HashMap) transientValues.get("LastLoginLookupMap");
            //only try to add the value if it actually exists.
            if (LastLoginLookupMap.containsKey(emailId)){
                userEntry.put("lastLogin",LastLoginLookupMap.get(emailId));
            } else{
                log.error(logPrefix + "lookup failed for jobcode: " + emailId);
            }
        }

        //add the modified record to our new return result
		returnObj.add(userEntry);
    }
	
	//add the data to the reply, and send it
	processedData.put("data", returnObj);
	log.info(logPrefix + "finished running");
	return processedData;
    
} else{
    log.error(logPrefix + "Response Object was empty, standard endpoint data will be passed.");
}


I have also updated the response mapping accordingly for it.

I am getting the following error while running the aggregation.

I believe there should something wrong with my rule itself but I am not sure. Could anyone please help?

Thanks in advance!

I have also tried it with rawResponseObject, where I would create the response map and add all the attributes. Still the same issue.

Hi,

Is your webservice using GraphQL?

Nope, it is using REST APIs.

hI @ZeelSinojia01,

are you sure to have a “data” in you response? or data is array for example
Can you share the response?

1 Like

Okay, can you check what is the common path present in your JSON response. “data” doesn’t seem to be present in your JSON response.

You may refer to this - Aggregation (sailpoint.com)

I see the rule, try to change the response with $.data or $.data[*]

I don’t have a “data” in the response.

However I am creating a new response map, so I should I be giving the new response mapping. Is that not correct?

Yes tried that as well but still the same error message.

Can you try just $ for Root Path

ok, in your root path, you must declare where the connector start to read the the data.

image
for example in this case, the root data is employees[*], because is an array of object.

See your responde anche change the root path

I have written the same kind rules in the IIQ as well when I used to work on it. I used to create new object and return that and update the response mapping accordingly.

I tried both with $ as root path and empty root path, in both the cases the aggregation was successful but no accounts were aggregated.

{
    "itemsPerPage": 100,
    "startIndex": 1,
    "Resources": [
        {
               "id": "loremipsum",
               "displayName":"Test Display Name",
               .........
        },
        {
               "id": "loremipsum2",
               "displayName":"Test Display Name2",
               .........
        }
       ...........
    ]
}

This is how my response from the API looks like. If I give $.Resources as my root path then the aggregation will be successful and it fetch the accounts. However the login I have written in rule to fetch the last login information would not be fetched.

1 Like

@ZeelSinojia01 I have written similar afterOperation rule it works perfectly fine for me.

Looking at your JSON response you should use $.Resources[*] in your root path mapping.

Rule looks fine to me. Make sure you map the response mapping as per the JOSN path.

As per your account schema map the response mapping for e.g. if your schema attribute for id is accountId then map it as accountId = $.id , just do not put id.

try this out.

Regards,
Shekhar Das

Hey, I tried with the changes you mentioned.

I am facing the same issue.

One thing I noticed is the rule is getting executed as I had some errors while developing it earlier so SailPoint UI was showing those issues. However I don’t feel like the object I am updating and returning is getting picked up for the response mapping.

you do not have to use $ Infront of attribute what you are setting in the rule. Use it like below:

Also do not forget to add this as your account schema.

Also print the returnObj in your rule to validate if the value is set as you are expecting.

Regards,
Shekhar Das

Thank you @shaileeM @enistri_devo @shekhardas1825 for your time and responses.

For anyone who comes here in future. I am still not sure why it was not working. But I made following changes and my aggregation is working.

Response Information

Response Mapping

Even the “Last Login” attribute which is not part of response and I am calling custom URL to fetch that information.

Web Service After Operation Rule

  • I resorted to using rawResponseObject instead of processedResponseObject
  • One more thing I am unaware of is in the custom response object the keys are named as “Display Name”, “First Name”, “Last Name”, etc. however if you see in the response mapping then the attribute paths are like “$.displayName”, “$.firstName”, “$.lastName” etc.
  • Still the data is getting aggregated successfully and all the values I in target system are correctly fetched into the SailPoint. :exploding_head:
import org.json.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import connector.common.JsonUtil;
import connector.common.JsonUtil.*;
import sailpoint.object.*;
import connector.common.Util;

/*
 * CREATED BY: Zeel Sinojia
 * Date: 01/21/2024
 * Purpose: Code to fetch the last login information from the custom url
 */

 
//setting a log prefix allows us to easily filter ccg.log for relevant items
String logPrefix = "WSAO_OracleERP";

//retrieve the transient values, if they exits, from application storage. will be Null if not set.
Map transientValues = (Map) application.getAttributeValue("transientValues");

//build our return object structure
List returnObj = new ArrayList();
Map processedData = new HashMap();
log.info(logPrefix + " starting to process");

//only call additional APIs if we do not already have them in memory
if(transientValues == null) {
    ///////// START LOOKUPS ///////////
    Map transientValues = new HashMap();
    
    //set up the requirements for making API calls
    List<String> allowedStatuses = new ArrayList();
    allowedStatuses.add( "200" );

    //call the last login API, build a key/value Map of login_id : last_access_in_days
    Map LastLoginLookupMap = new HashMap();
    try{
        String lastLoginUrl = "";
        //executeGet operates in the context of the source restClient.  
        //will follow Authentication on the source/endpoint.
        String lastLogins = restClient.executeGet(lastLoginUrl, new HashMap(), allowedStatuses );
		JSONObject responseJsonObject = new JSONObject(lastLogins);
		JSONArray lastLoginJsonArray = responseJsonObject.get( "items" );
        for (int i = 0; i < lastLoginJsonArray.length(); i++) {
            JSONObject lastLoginJsonObject = lastLoginJsonArray.getJSONObject(i);
            String emailId = lastLoginJsonObject.getString("login_id");
            String lastAccessInDays = String.valueOf( lastLoginJsonObject.getInt("last_access_in_days") );
            LastLoginLookupMap.put(emailId,lastAccessInDays);          
        }  
        //store the resultant map into transient values
        transientValues.put("LastLoginLookupMap", LastLoginLookupMap);
    } catch (Exception e) {
        log.error(logPrefix + "********ERROR AFTER RULE: " + e.getMessage(), e );
    }

    //store "our" transient values into the application (until the aggregation completes, wherein it will erase)
    application.setAttribute("transientValues", transientValues);
    log.error(logPrefix + "setting transients");
}
    
/////// END LOOKUPS //////////

// ensure that we have data to actuall operate on
if(rawResponseObject != null && processedResponseObject != null){
	
    //retrieve the transientValues from application storage
    transientValues = (Map) application.getAttributeValue("transientValues");

	Map resultMap = JsonUtil.parseJson( rawResponseObject );

    //iterate over every Record in the response
    // processedResponseObject will have the JSONPath values defined on the source endpoint.
    for (Map userEntry : resultMap.get( "Resources" ) ) {
		
		Map newEntry = new HashMap();
		
        log.error(logPrefix + "updating processed data");

        // grab key values from the record, from the JSONPath'd output of the endpoint itself
        String emailId = null;
		
		if ( userEntry.containsKey("emails") ) {
			emailId = userEntry.get( "emails" ).get(0).get( "value" );
			
			// put the email id is present
			newEntry.put( "Email", emailId );
		}
		
		Map faUserEntry = userEntry.get( "urn:scim:schemas:extension:fa:2.0:faUser" );
		
		if ( userEntry.containsKey("displayName") )
			newEntry.put( "Display Name", userEntry.get( "displayName" ) );
		if ( userEntry.containsKey("active") )
			newEntry.put( "Status", userEntry.get( "active" ) );
		if ( userEntry.containsKey("meta") ) {
			newEntry.put( "Last Modified Date", userEntry.get( "meta" ).get( "lastModified" ) );
			newEntry.put( "Created Date", userEntry.get( "meta" ).get( "created" ) );
		}
		if ( faUserEntry.containsKey("workerInformation") ) {
			newEntry.put( "Business Unit", faUserEntry.get( "workerInformation" ).get( "businessUnit" ) );
			newEntry.put( "Person Number", faUserEntry.get( "workerInformation" ).get( "personNumber" ) );
			newEntry.put( "Manager", faUserEntry.get( "workerInformation" ).get( "manager" ) );
			newEntry.put( "Department", faUserEntry.get( "workerInformation" ).get( "department" ) );
		}	
		if ( userEntry.containsKey("userName") )
			newEntry.put( "User Name", userEntry.get( "userName" ) );
		if ( userEntry.containsKey("name") ){
			newEntry.put( "First Name", userEntry.get( "name" ).get( "givenName" ) );
			newEntry.put( "Last Name", userEntry.get( "name" ).get( "familyName" ) );
		}
		
		newEntry.put( "ID", userEntry.get( "id" ) );
		if ( faUserEntry.containsKey("userCategory") )
			newEntry.put( "User Category", faUserEntry.get( "userCategory" ) );
		if ( faUserEntry.containsKey("accountType") )
			newEntry.put( "Account Type", faUserEntry.get( "accountType" ) );

        //null safety. dont look it up if it doesnt exist.
        if (transientValues.containsKey("LastLoginLookupMap") && Util.isNotNullOrEmpty( emailId )){
            HashMap LastLoginLookupMap = (HashMap) transientValues.get("LastLoginLookupMap");
            //only try to add the value if it actually exists.
            if (LastLoginLookupMap.containsKey(emailId)){
                newEntry.put("Last Login",LastLoginLookupMap.get(emailId));
            } else{
                log.error(logPrefix + "lookup failed for jobcode: " + emailId);
            }
        }

        //add the modified record to our new return result
		if ( !newEntry.isEmpty() )
			returnObj.add(newEntry);
    }
    
} else{
    log.error(logPrefix + "Response Object was empty, standard endpoint data will be passed.");
}

//add the data to the reply, and send it
processedData.put("data", returnObj);
log.info(logPrefix + "finished running");
return processedData;

Hope this helps!

4 Likes

Great post on webservice after operation rule!

1 Like

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.