SailPoint IIQ and ServiceNow custom integration using ServiceNow REST APIs (ServiceNow Table API) – PART 1

[vc_row][vc_column][vc_column_text]SailPoint IIQ provides many out-of-the-box (OOTB) options to integrate with ServiceNow.

Though OOTB integrations are great for most of the common use cases, it lacks the flexibility and provides less control over the entire process. Many times we come across a requirement where we need total control, and that’s where this custom integration can be very useful.

The Problem

SailPoint IIQ provides multiple ways to integrate with ServiceNow (SN) e.g. direct connector, through SIM integration module, and using SN service catalogs. Details on OOTB integrations can be found here here.

My requirement was to create ServiceNow incident and Service Catalog requests using a custom workflow with the expectation of getting information using custom forms in IIQ and then adding the gathered fields in SN incident/service request for audit information. Additionally, there needed to be a way to close the incidents created earlier. The fields in custom forms were not related to any application and had to be entered manually.

The Solution

This wasn’t the typical de-provisioning to the disconnected application. Though this requirement could have been fulfilled using some workarounds to the OOTB integration, the solution would not look clean, as this would involve creating a fake disconnected application.

In order to address the requirement, I decided to go with SN REST table APIs. SN exposes the table APIs to integrate using JSON over HTTP/S. You can find more details on table APIs here. (This is a series of two blogs and this blog only covers the Incident Management. In the second blog, we will cover the Service Catalog management.)

ServiceNow REST API can be called using the table API endpoints/URIs. URI has table names which represent the resource name.

As our goal was to create incidents, the respective SN table is incident. The complete URI for incident management should look like the table below.

Refer below table for SN URIs, operations, and HTTP Verbs.

No. Operation URI HTTP Verb
1 Create new Incident in ServiceNow https://xxx.service-now.com/api/now/v1/table/incident POST
2 Get the existing incident from ServiceNow https://xxx.service-now.com/api/now/v1/table/incident/sys_id

Note: sys_id is internal system identifier of the incident object created earlier. Incident Number won’t work.

Examples:
Sys_id: 1a451fad4ff3130081630fbf9310c7b0
Incident Number: INC0010119

GET
3 Update the incident created earlier https://xxx.service-now.com/api/now/v1/table/incident/sys_id

Note: sys_id is internal system identifier of the incident object created earlier. Incident Number won’t work.

Examples:
Sys_id: 1a451fad4ff3130081630fbf9310c7b0
Incident Number: INC0010119

PUT

ServiceNow exposes multiple ways to authenticate. For this blog, we will use “basic authentication”. You can find more about basic authentication here.

The Prerequisite

Following variable values must be gathered before you start the implementation.

No. Variable E.g. Value Comments
1 EndpointURL https://xxx.service-now.com/api/now/v1/table Common portion of the SN URI. The suffix can be added to base URI to generate table specific URI.
2 Username admin Username is used in basic authentication. This user must have the appropriate rights to manage incidents in SN.
3 Password P#ss&rd134 Password is used in basic authentication.
4 CallerID 1a451fad4ff3130081630fbf933454 Sys_id of the caller. This is the sys_id of the SN user who will be set as a requester of an incident.

This example expects the custom IIQ object which holds the ServiceNow connection details.

The Implementation

The details of the custom workflow are left to the implementer.  My workflow looks something like this:

The “Create ServiceNow ticket” step is responsible for creating the ServiceNow incident.

The following ServiceNow target variables are being passed to the workflow using the workflow variable.

  <Variable name="customObject">
    <Script>
      <Source>
        import java.util.*;
        import sailpoint.object.*;
        Custom mappingObj = context.getObjectByName(Custom.class, "Custom-Object");
        return mappingObj;
      </Source>
    </Script>
  </Variable>
<Arg name="ServiceNowCredsMap">
      <Script>
        <Source>
          Map ouMap = customObject.get("SNDetails");
          return ouMap;
        </Source>
      </Script>
    </Arg>
    <Arg name="UserAttributeToDisplayNameMap">
      <Script>
        <Source>
          Map ouMap = customObject.get("UserAttributeToDisplayNameMapping");
          return ouMap;
        </Source>
      </Script>
    </Arg>

The following script is responsible for creating the Incident in SN. Code comments should help in understanding the overall flow of the request.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import java.util.*;
import sailpoint.object.*;

log.info("Creating serviceNow Incident");
try
{
	//Get Credentials and endpoint from IIQ custom object 
	//The custom object is passed as workflow variable to Workflow
	String endPoint = ServiceNowCredsMap.get("EndpointURL");
	String username = ServiceNowCredsMap.get("Username");
	String encryptedPassword = ServiceNowCredsMap.get("Password");
	String callerId = ServiceNowCredsMap.get("CallerID");
	
	//check if password is not set
	//Its recommended to store SN credentials in encrypted form. We are using OOB SailPoint
	//IIQ encryption for same.
	if(encryptedPassword == null || encryptedPassword.isEmpty())
	{
		log.error("ServiceNow Error:Password can not be null or empty");
		return;
	}
	String decryptedPassword ="";
	try
	{
		decryptedPassword = context.decrypt(encryptedPassword);
	}
	catch(Exception e)
	{
		log.error("Error while decrypting password" + e);
	}

	//Create HTTP Connection
	URL myURL = new URL(endPoint);
	HttpURLConnection c = (HttpURLConnection) myURL.openConnection();
	
	//Set Authentication information
	String authStr = Base64.getEncoder().encodeToString((username +":"+ decryptedPassword).getBytes());
	
	//setting Authorization header
	c.setDoOutput(true);
	c.setRequestMethod("POST");
	c.setRequestProperty("Authorization", "Basic " + authStr);
	c.setRequestProperty("Content-Type", "application/json");

	//Request JSON Input 
	String input = "{'short_description':'IAM request for disabling network access for #UserName#','assignment_group':'Service Desk','urgency':'3','impact':'3','category':'request','incident_state':'1','contact_type':'email','caller_id':'#CallerID#','opened_by':'admin','description':'#Description#'}";


	//Add request details
	String desc = "This is notification for disabling user " + requestMap.get("username") + "rn" + "User Information";
	
	//Create Description
	//Add additional details to incident, this can be anything coming
	//from provisioning plan or something gathered using custom form fields 
	//I have added all those attributes in map and passed it as workflow varible
	for (String key : UserAttributeToDisplayNameMap.keySet())
	{
		if(key != null)
		{
			if(!desc.isEmpty())
			desc = desc + "rn";
		
			desc = desc + UserAttributeToDisplayNameMap.get(key) + ": " + requestMap.get(key);
		}
	}

	//Replace tokens
	input = input.replace("#UserName#",requestMap.get("username")).replace("#Description#",desc).replace("#CallerID#",callerId).replace("rn","n");

	//Write to output stream
	OutputStream os = c.getOutputStream();
	os.write(input.getBytes());
	os.flush();

	//Send Request and get response code
	if (c.getResponseCode() != HttpURLConnection.HTTP_CREATED) {
	throw new RuntimeException("Failed : HTTP error code : " +
	c.getResponseCode());

	}
	else 
	{
		System.out.println("-- Response body --");

		BufferedReader bf = new BufferedReader(new InputStreamReader(c.getInputStream()));
		StringBuilder stringBuilder = new StringBuilder();
		String line;
		while ((line = bf.readLine()) != null) {
			stringBuilder.append(line);
		}
		
		String response = stringBuilder.toString();

		//return response;
		System.out.println("response : " + response);

		//Parse the JSON data present in the string format
		JSONParser parse = new JSONParser();

		//typecast the parsed json data in json object and get the result back 
		JSONObject jobj = (JSONObject) parse.parse(response);
		JSONObject result = (JSONObject) jobj.get("result");

		//Retrieve Incident Number and sys_id 
		//Sys_id is required only if you are planning to modify this ticket later
		String incNumber = result.get("number");
		String sys_id = result.get("sys_id");
		
		//Store incidentId in Workflow variable to used in subsequent steps
		workflow.put("incidentid",incNumber);
        
		//Store incidentid on cube : Optional 
		//Only store if you need to modify the same incident in future
		//e.g. Close the incident if event is canceled 
		Identity idObj = context.getObjectByName(Identity.class,identityName);
		idObj.setAttribute("IncidentInternalID",sys_id);
		context.saveObject(idObj);
		
		log.info("ServiceNow incident created..");
	}
}
catch(Exception ex)
{
	log.error("Unable to create serviceNow ticket:" + ex);
}

ServiceNow incident should be visible once workflow completes its execution.

Highlighted fields in the above screenshot are set using the REST API.

Closing The Incident

If you want to close the incident created earlier, follow the code below:

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import java.util.*;
import sailpoint.object.*;

log.info("Updating serviceNow Incident");
try
{
	//Get Credentials
	String endPoint = ServiceNowCredsMap.get("EndpointURL");
	String username = ServiceNowCredsMap.get("Username");
	String encryptedPassword = ServiceNowCredsMap.get("Password");
	String caller_id = ServiceNowCredsMap.get("CallerID");
			
	if(encryptedPassword == null || encryptedPassword.isEmpty())
	{
		log.error("ServiceNow Error:Password can not be null or empty");
		return;
	}

	String decryptedPassword ="";
	try
	{
		decryptedPassword = context.decrypt(encryptedPassword);
	}
	catch(Exception e)
	{
		log.error("Error while decrypting password" + e);
	}
	
	Identity idObj = context.getObjectByName(Identity.class,identityName);
	
	//Get the sys_id of the incident stored earlier
	String sNowTicketID = idObj.getAttribute("IncidentInternalID");
			
	if(sNowTicketID != null &amp;&amp; !sNowTicketID.isEmpty())
	{
		URL myURL = new URL(endPoint + "/" + sNowTicketID);
		HttpURLConnection c = (HttpURLConnection) myURL.openConnection();
		
		//Set Auth information
		String authStr = Base64.getEncoder().encodeToString((username +":"+ decryptedPassword).getBytes());
						
		//setting Authorization header
		c.setDoOutput(true);
		c.setRequestMethod("PUT");
		c.setRequestProperty("Authorization", "Basic " + authStr);
		c.setRequestProperty("Content-Type", "application/json");

		String input = "{'close_code':'Closed/Resolved By Caller','state':'7','caller_id':'#CallerSysId#','close_notes':'Process canceled by:#Launcher#'}";
				
		//Replace tokens
		input = input.replace("#CallerSysId#",caller_id).replace("#Launcher#", launcher).replace("rn","n");

		//Write to output stream
		OutputStream os = c.getOutputStream();
		os.write(input.getBytes());
		os.flush();

		//Send Request and get response code
		if (c.getResponseCode() != HttpURLConnection.HTTP_OK) 
		{
			throw new RuntimeException("Failed : HTTP error code : " +
			c.getResponseCode());
		} 
		else 
		{
			System.out.println("-- Response body --");

			BufferedReader bf = new BufferedReader(new InputStreamReader(c.getInputStream()));
			StringBuilder stringBuilder = new StringBuilder();
			String line;
			while ((line = bf.readLine()) != null) {
				stringBuilder.append(line);
			}
			String response = stringBuilder.toString();

			//return response;
			System.out.println("response : " + response);

			//Parse the JSON data present in the string format
			JSONParser parse = new JSONParser();

			//Type caste the parsed json data in json object
			JSONObject jobj = (JSONObject) parse.parse(response);
			JSONObject result = (JSONObject) jobj.get("result");

			//Retrieve Incident Number
			String incNumber = result.get("number");
			incidentid = incNumber;
			workflow.put("incidentid",incNumber);
			
			log.info("ServiceNow incident updated");
		}
	}
	else
	{
		log.error("ServiceNow ticket not found for this user, Skipping ticket update process");
	}
}
catch(Exception ex)
{
	log.error("Unable to update serviceNow ticket:" + ex);
}

This should close the incident by setting its close_code, close_notes and state.

Setting Additional Fields In SN:

If you want to set different or additional fields than shown in the above example, please go through the ServiceNow Table API documentation to get the details on all the available fields. I have included the list of available fields for Incident table below.

        "parent": "",
        "made_sla": "true",
        "caused_by": "",
        "watch_list": "",
        "upon_reject": "cancel",
        "sys_updated_on": "2018-08-16 02:45:00",
        "child_incidents": "0",
        "hold_reason": "",
        "approval_history": "",
        "number": "INC0010119",
        "resolved_by": "",
        "sys_updated_by": "admin",
        "opened_by": "",
        "user_input": "",
        "sys_created_on": "2018-08-16 02:45:00",
        "sys_domain": ""
        "state": "1",
        "sys_created_by": "admin",
        "knowledge": "false",
        "order": "",
        "calendar_stc": "",
        "closed_at": "",
        "cmdb_ci": "",
        "delivery_plan": "",
        "impact": "3",
        "active": "true",
        "work_notes_list": "",
        "business_service": "",
        "priority": "5",
        "sys_domain_path": "/",
        "rfc": "",
        "time_worked": "",
        "expected_start": "",
        "opened_at": "2018-08-16 02:44:59",
        "business_duration": "",
        "group_list": "",
        "work_end": "",
        "caller_id": "",
        "reopened_time": "",
        "resolved_at": "",
        "approval_set": "",
        "subcategory": "",
        "work_notes": "",
        "short_description": "IAM request for disabling network access for IAM, PilotTest1",
        "close_code": "",
        "correlation_display": "",
        "delivery_task": "",
        "work_start": "",
        "additional_assignee_list": "",
        "business_stc": "",
        "description": "",
        "calendar_duration": "",
        "close_notes": "",
        "notify": "1",
        "sys_class_name": "incident",
        "closed_by": "",
        "follow_up": "",
        "parent_incident": "",
        "sys_id": "c0ef214c4f4023zsfsdf1630fbf9310c7ad",
        "contact_type": "email",
        "reopened_by": "",
        "incident_state": "1",
        "urgency": "3",
        "problem_id": "",
        "company": "",
        "reassignment_count": "0",
        "activity_due": "",
        "assigned_to": "",
        "severity": "3",
        "comments": "",
        "approval": "not requested",
        "sla_due": "",
        "comments_and_work_notes": "",
        "due_date": "",
        "sys_mod_count": "0",
        "reopen_count": "0",
        "sys_tags": "",
        "escalation": "0",
        "upon_approval": "proceed",
        "correlation_id": "",
        "location": "",
        "category": "request"

[/vc_column_text][/vc_column][/vc_row]