Overview
This guide shows how to configure ServiceNow to create tickets or tasks to handle manual provisioning requests.
ServiceNow is a powerful solution and there are many ways to organize work items in it. This guide shows how to use the catalog to create work items and how to add tasks to them.
Configuration
To configure manual provisioning for ServiceNow, follow the general manual provisioning configuration steps outlined in the Manual Provisioning guide.
API
All manual provisioning scripts have access to the following API: API Reference
The API provides access to the requests that need to be processed and also to utility methods.
Handling requests based on ordering an item in the catalog and adding tasks to it
This sample script shows how to handle requests based on ordering an item in the catalog and adding tasks to it.
Prerequisites
- A ServiceNow catalog item that will trigger the creation of a request item
- The script assumes some fields, but they should be customized to fit the specific requirements of the organization.
Customization
The script needs to be customized to fit the specific requirements of the organization. Specifically these elements need to be customized:
- The fields in the catalog item
- The fields in the user table, if users need to be created dynamically
- The fields in the task table
The script declares several variables and hook functions that must be provided in order to make it work.
Sample script
import com.okiok.pheonix.api.manualProvisioning.*
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import okhttp3.*
import org.slf4j.Logger
api = api as ManualProvisioningAPI
utils = api.utils as ManualProvisioningAPIUtils
logger = api.logger as Logger
httpClient = api.httpClient as OkHttpClient
provisioningRequests = api.provisioningRequests
/****** Variables that change depending on the environment *******/
serviceNowURL = "https://example.service-now.com"
serviceNowUser = "[YOUR_SERVICE_NOW_USER]"
serviceNowPassword = "[YOUR_SERVICE_NOW_PASSWORD]"
racmURL = "https://racm.example.com"
/****** ServiceNow Configuration *******/
// ServiceNow catalog item ID for ordering requests
serviceNowCatalogItemId = "[YOUR_SERVICE_NOW_CATALOG_ITEM_ID]"
// Default assignment groups
defaultAssignmentGroup = "[YOUR_DEFAULT_ASSIGNMENT_GROUP]"
defaultWrittenRequestAssignmentGroup = "[YOUR_DEFAULT_WRITTEN_REQUEST_ASSIGNMENT_GROUP]"
// Default email for system-generated requests (when requester.id == 0)
defaultSystemRequesterEmail = "[YOUR_SYSTEM_REQUESTER_EMAIL]"
/****** Hook functions to customize the script *******/
// Override these functions to customize application grouping logic
// By default, each application is grouped separately
Closure getApplicationGroupName = { String applicationName ->
return applicationName
}
// Override this function to customize assignment group determination for request items
// By default, returns the defaultAssignmentGroup
Closure getRequestItemAssignmentGroup = { ProvisioningRequest provisioningRequest ->
if(provisioningRequest.accountRequests.isEmpty() && provisioningRequest.action != Action.IDENTITY_TERMINATION) {
return defaultWrittenRequestAssignmentGroup
}
return defaultAssignmentGroup
}
// Override this function to customize assignment group determination for tasks
// By default, returns the defaultAssignmentGroup
Closure getTaskAssignmentGroup = { String applicationName ->
return defaultAssignmentGroup
}
/****** Main script *******/
logger.info("Entering Provisioning Script")
def nbOfSuccessfulRequest = 0
//For each provisioning request, create a request item, a task for each application and a task for the all written requests
provisioningRequests.each { def provisioningRequest ->
logger.info("Processing Provisioning Request ID ${provisioningRequest.getRequestIdIfPresent()}")
try {
// Create a request item via the Order Item API
if(provisioningRequest.action == Action.IDENTITY_CREATION && provisioningRequest.hasTarget()) {
// External users that were just created might not exist yet in ServiceNow, attempt to create them here
// If they already exist, it will return an error and we will ignore it
createServiceNowIdentity(provisioningRequest.target)
}
def orderItemResponse = callOrderItem(provisioningRequest)
def requestId = processOrderItemResponse(orderItemResponse)
// Fetch the RITM SysId required to build the link to the request in ServiceNow from RACM
def getRITMResponse = callGetRITM(requestId)
def (ritmSysID,ritmNumber) = processGetRITMResponse(getRITMResponse)
def ticketUrl = buildTicketURL(ritmSysID)
// Group account requests by application name (customize via getApplicationGroupName hook)
def accountRequestsByApplication = provisioningRequest.accountRequests.groupBy({accReq ->
return getApplicationGroupName(accReq.applicationName)
})
// For each account request, create a task associated to the request item
accountRequestsByApplication.each { appName,accountRequests ->
try {
def createTaskResponse = callCreateTask(ritmSysID, appName, accountRequests, provisioningRequest)
processCreateTaskResponse(createTaskResponse)
// No error occurred, complete the account request with request number & URL
accountRequests.each {accountRequest ->
try {
api.completeAccountRequest(accountRequest, ritmNumber, ticketUrl)
nbOfSuccessfulRequest++
} catch (Exception e) {
logger.error("An error occurred completing the account request with ID: ${accountRequest.id}. The account management will be put in error status with the error message", e)
api.putAccountRequestsInError(accountRequest, e)
}
}
} catch (Exception e) {
logger.error("An error occurred creating the task for application with ID: ${appName}. The account managements will be put in error status with the error message", e)
accountRequests.each { accountRequest -> api.putAccountRequestsInError(accountRequest, e) }
}
}
if (provisioningRequest.writtenRequests.size() > 0) {
// Create a task for the written requests
try {
def createTaskResponse = callCreateTask(ritmSysID, provisioningRequest.writtenRequests, provisioningRequest)
processCreateTaskResponse(createTaskResponse)
// No error occurred, complete the written requests with the request number & URL
api.completeWrittenRequests(provisioningRequest.writtenRequests, ritmNumber, ticketUrl)
nbOfSuccessfulRequest += provisioningRequest.writtenRequests.size()
} catch (Exception e) {
logger.error("An error occurred creating task for written requests in provisioning request: ${provisioningRequest.getRequestIdIfPresent()}. The written request managements will be put in error status with the error message", e)
// Put the written requests in error status so that it can be retried later on
api.putWrittenRequestsInError(provisioningRequest.writtenRequests, e)
}
}
logger.info("Provisioning Request ID: ${provisioningRequest.getRequestIdIfPresent()} processed")
} catch (Exception e) {
logger.error("An error occurred processing Provisioning Request ID: ${provisioningRequest.getRequestIdIfPresent()}. All the account managements associated with this provisioning request will be put in error status with the error message", e)
// Put all the account requests in error status so that they can be retried later on
api.putProvisioningRequestInError(provisioningRequest, e)
}
}
logger.info("Exiting Provisioning Script")
// Return the number of successfully provisioned requests for monitoring purpose
return new ManualProvisioningOutcome()
.setNbOfEntriesProcessed(nbOfSuccessfulRequest)
def createServiceNowIdentity(Identity target) {
def url = "${serviceNowURL}/api/now/table/sys_user"
def jsonBuilder = new JsonBuilder()
jsonBuilder {
user_name target.email
email target.email
first_name target.firstname
last_name target.lastname
employee_number "N/A"
// Customize these fields based on your ServiceNow user table schema
// u_division_code target.organization.name[-4..-1] // Example: last 4 chars of org name
// title target.job.name
source "RACM"
}
def body = jsonBuilder.toPrettyString()
def requestBody = RequestBody.create(MediaType.parse("application/json"), body)
def request = new Request.Builder()
.header("Authorization", buildAuthHeader())
.url(url)
.post(requestBody)
.build()
logger.info("Calling ${request} with body: ${body}...")
def response = httpClient.newCall(request).execute()
if(response.code != 201 && response.code != 403) {
// 201 created means success
// 403 means the user already exists
throw new Exception("Unsuccessful Response received while creating user. Code: ${response.code}, Body: ${response.body}")
}
}
Response callOrderItem(ProvisioningRequest provisioningRequest) {
def url = "${serviceNowURL}/api/sn_sc/servicecatalog/items/${serviceNowCatalogItemId}/order_now"
def jsonBuilder = new JsonBuilder()
def (cat,subcat) = getCategoryAndSubCategory(provisioningRequest)
def shortDescription = buildShortDescription(provisioningRequest,null)
def assignmentGroup = getRequestItemAssignmentGroup(provisioningRequest)
def openedBy = determineOpenedBy(provisioningRequest)
def requestedFor = provisioningRequest.target.email
if(provisioningRequest.action == Action.IDENTITY_TERMINATION) {
// For terminations, use openedBy instead of target to avoid sending notifications to departing user
requestedFor = openedBy
}
jsonBuilder {
sysparm_quantity "1"
variables {
requested_for_id requestedFor
opened_by_id openedBy
racm_number provisioningRequest.requestId
racm_url racmURL
short_description shortDescription
assignment_group assignmentGroup
category cat
subcategory subcat
}
}
def body = jsonBuilder.toPrettyString()
def requestBody = RequestBody.create(MediaType.parse("application/json"), body)
def request = new Request.Builder()
.header("Authorization", buildAuthHeader())
.url(url)
.post(requestBody)
.build()
logger.info("Calling ${request} with body: ${body}...")
return httpClient.newCall(request).execute()
}
def processOrderItemResponse(Response response) {
def statusCode = response.code()
def body = response.body().string()
if (response.isSuccessful()) {
logger.info("Successful Response Received. Code: ${statusCode}, Body: ${body}")
return extractRequestIdAndNumber(body)
}
else {
throw new Exception("Unsuccessful Response received. Code: ${statusCode}, Body: ${body}")
}
}
Response callGetRITM(String requestId) {
def requestIdURl = "${serviceNowURL}/api/now/table/sc_req_item?sysparm_query=request.sys_id=${requestId}&sysparm_fields=number,sys_id,sys_class_name,request&sysparm_limit=1"
def request = new Request.Builder()
.header("Authorization", buildAuthHeader())
.url(requestIdURl)
.build()
logger.info("Calling ${request}...")
return httpClient.newCall(request).execute()
}
def processGetRITMResponse(Response response) {
def statusCode = response.code()
def body = response.body().string()
if (response.isSuccessful()) {
logger.info("Successful Response Received. Code: ${statusCode}, Body: ${body}")
//Extract RITM sys_id & number
return extractRITM(body)
}
else {
throw new Exception("Unsuccessful Response received. Code: ${statusCode}, Body: ${body}")
}
}
/**
* Call Create Task endpoint for a Account Request
*
* @param ritmSysId
* @param accountRequest
* @param provisioningRequest
* @return
*/
Response callCreateTask(String ritmSysId, String applicationName, List<AccountRequest> accountRequests, ProvisioningRequest provisioningRequest) {
def url = "${serviceNowURL}/api/now/table/sc_task"
def jsonBuilder = new JsonBuilder()
jsonBuilder {
request_item ritmSysId
short_description buildShortDescription(provisioningRequest, applicationName)
description buildCreateTaskDescription(accountRequests, provisioningRequest)
assignment_group getTaskAssignmentGroup(applicationName)
}
def body = jsonBuilder.toPrettyString()
def requestBody = RequestBody.create(MediaType.parse("application/json"), body)
def request = new Request.Builder()
.header("Authorization", buildAuthHeader())
.url(url)
.post(requestBody)
.build()
logger.info("Calling ${request} with body: ${body}...")
return httpClient.newCall(request).execute()
}
/**
* Call the create task endpoint for a list of written requests
*
* @param ritmSysId
* @param accountRequest
* @param provisioningRequest
* @return
*/
Response callCreateTask(String ritmSysId, List<WrittenRequest> writtenRequests, ProvisioningRequest provisioningRequest) {
def url = "${serviceNowURL}/api/now/table/sc_task"
def jsonBuilder = new JsonBuilder()
def assignmentGroup = getRequestItemAssignmentGroup(provisioningRequest)
jsonBuilder {
request_item ritmSysId
short_description buildShortDescription(provisioningRequest,null)
description buildWrittenTaskDescription(writtenRequests, provisioningRequest)
assignment_group assignmentGroup
}
def body = jsonBuilder.toPrettyString()
def requestBody = RequestBody.create(MediaType.parse("application/json"), body)
def request = new Request.Builder()
.header("Authorization", buildAuthHeader())
.url(url)
.post(requestBody)
.build()
logger.info("Calling ${request} with body: ${body}...")
return httpClient.newCall(request).execute()
}
void processCreateTaskResponse(Response response) {
def statusCode = response.code()
def body = response.body().string()
if (response.isSuccessful()) {
logger.info("Successful Response Received. Code: ${statusCode}, Body: ${body}")
}
else {
throw new Exception("Unsuccessful Response received. Code: ${statusCode}, Body: ${body}")
}
}
String extractRequestIdAndNumber(String orderItemResponseBody) {
def json = new JsonSlurper().parseText(orderItemResponseBody)
return json.result.request_id
}
def extractRITM(String responseBody) {
def json = new JsonSlurper().parseText(responseBody)
return [json.result[0].sys_id, json.result[0].number]
}
def buildTicketURL(String ritmSysID) {
return "${serviceNowURL}/sp?id=ticket&table=sc_req_item&sys_id=${ritmSysID}"
}
def buildAuthHeader() {
def auth = "${serviceNowUser}:${serviceNowPassword}"
def encodedString = Base64.getEncoder().encodeToString(auth.getBytes())
return "Basic " + encodedString
}
/**
* Build short description for ServiceNow tasks
*
* @param provisioningRequest
* @param applicationName
*/
def buildShortDescription(ProvisioningRequest provisioningRequest, String applicationName) {
def shortDescription = ""
def origin = provisioningRequest.origin
def action = provisioningRequest.action
def targetName = provisioningRequest.hasTarget() ? provisioningRequest.target?.fullName : "Generic"
def appSuffix = applicationName != null ? " - ${applicationName}" : ""
def requestIdSuffix = provisioningRequest.hasARequestId() ? " - ${provisioningRequest.requestId}" : ""
switch (action) {
case Action.ADD_ACCESSES_REQUEST:
shortDescription = "Update - ${targetName}${appSuffix}${requestIdSuffix}"
break
case Action.REMOVE_ACCESSES_REQUEST:
if (origin == Origin.CAMPAIGN) {
shortDescription = "Campaign Revoke - ${targetName}${appSuffix}${requestIdSuffix}"
} else {
shortDescription = "Update - ${targetName}${appSuffix}${requestIdSuffix}"
}
break
case Action.IDENTITY_CREATION:
shortDescription = "New - ${targetName}${appSuffix}${requestIdSuffix}"
break
case Action.IDENTITY_TRANSFER:
shortDescription = "Transfer - ${targetName}${appSuffix}${requestIdSuffix}"
break
case Action.IDENTITY_TERMINATION:
if (origin == Origin.CAMPAIGN) {
shortDescription = "Campaign Departure - ${targetName}${appSuffix}${requestIdSuffix}"
} else {
shortDescription = "Departure - ${targetName}${appSuffix}${requestIdSuffix}"
}
break
case Action.UNDEFINED:
shortDescription = "Undefined"
break
}
logger.info("Task Short Description is ${shortDescription}")
return shortDescription
}
/**
* Build Task description for an Account Request
* Should match the actual ticketing email template (manualProvisioning_ticketingSystem.html)
*
* @param accountRequest
* @param provisioningRequest
* @return
*/
def buildCreateTaskDescription(List<AccountRequest> accountRequests, ProvisioningRequest provisioningRequest) {
def target = provisioningRequest.target
def requester = provisioningRequest.requester
def taskDescription = """\
Request ID: ${provisioningRequest.id}
${provisioningRequest.hasARequestId() ? "Request Number: ${provisioningRequest.requestId}" : ""}
${provisioningRequest.hasRequester() ? "Requester: ${requester.fullName} (${requester.email})" : "Requester: [Auto-generated per policies]"}
Target User:
${target.fullName} ${utils.isNotEmpty(target.email) ? "(${target.email})" : ""}${utils.isEmpty(target.identifier4) ? "" : """
${target.identifier4}"""}${utils.isUndefined(target.organization) ? "" : """
Organization: ${target.organization.name}"""}
Access to Grant:
""".stripIndent()
accountRequests.each { accountRequest -> taskDescription += """\
- ${getAccountRequestTypeLabel(accountRequest.type)} - ${accountRequest.applicationName}\
${utils.isEmpty(accountRequest.description) ? "" : """
- ${accountRequest.description}"""}
${(accountRequest.groupRequests.collect { groupRequest -> buildGroupRequest(groupRequest) }).join(" \n")}
----------------------------------------------------------------------
""".stripIndent()
}
logger.info("Task Description is\n${taskDescription}")
return taskDescription
}
/**
* Extracted group building in a separate method to allow group splitting if serviceNow description size limit is reached
*
* @param groupRequest
* @return
*/
String buildGroupRequest(GroupRequest groupRequest) {
def groupRequestInfo = " - ${getGroupRequestTypeLabel(groupRequest.type)} - ${groupRequest.group.name}"
if (groupRequest.type == GroupRequest.Type.ASSIGNMENT) {
if (groupRequest.endDate != null) {
groupRequestInfo += "\n End Date: " + utils.formatDate(groupRequest.endDate)
}
if (groupRequest.group.hasProvisioningProcedureWhenAdding) {
groupRequestInfo += "\n Special instructions for assignment: ${groupRequest.group.provisioningProcedureWhenAdding}"
}
}
else if (groupRequest.type == GroupRequest.Type.UNASSIGNMENT){
if (groupRequest.group.hasProvisioningProcedureWhenRemoving) {
groupRequestInfo += "\n Special instructions for removal: ${groupRequest.group.provisioningProcedureWhenRemoving}"
}
} else if (groupRequest.type == GroupRequest.Type.PROVISIONING_PROCEDURE_FOR_ASSIGNMENT) {
if (groupRequest.endDate != null) {
groupRequestInfo += "\n End Date: " + utils.formatDate(groupRequest.endDate)
}
groupRequestInfo += "\n ${groupRequest.group.provisioningProcedureWhenAdding}"
}
else if (groupRequest.type == GroupRequest.Type.PROVISIONING_PROCEDURE_FOR_UNASSIGNMENT){
groupRequestInfo += "\n ${groupRequest.group.provisioningProcedureWhenRemoving}"
}
return groupRequestInfo
}
def buildWrittenTaskDescription(List<WrittenRequest> writtenRequests, ProvisioningRequest provisioningRequest) {
def target = provisioningRequest.target
def requester = provisioningRequest.requester
def taskDescription = """${provisioningRequest.hasRequester() ? """Requested by: ${requester.fullName} (${requester.email})""" : "Requested by: [Auto-generated request]"}
${provisioningRequest.hasTarget() ? """
Target User:
${target.fullName} ${utils.isNotEmpty(target.email) ? "(${target.email})" : ""}${utils.isEmpty(target.identifier4) ? "" : """
${target.identifier4}"""} ${utils.isUndefined(target.organization) ? "" : """
Organization: ${target.organization.name}"""}""" : """
This request is for a technical account"""}
Written Requests to Grant:
${(writtenRequests.collect { writtenRequest -> buildWrittenRequest(writtenRequest) }).join("\n")}
"""
logger.info("Task Description is\n ${taskDescription}")
return taskDescription
}
def buildWrittenRequest(WrittenRequest writtenRequest) {
return " - Request: ${writtenRequest.written}${writtenRequest.startDate != null ? ", to be executed on: ${utils.formatDate(writtenRequest.startDate)}" : ""}${writtenRequest.endDate != null ? ", expiration: ${utils.formatDate(writtenRequest.endDate)}" : ""}"
}
def getCategoryAndSubCategory(ProvisioningRequest provisioningRequest) {
if(provisioningRequest.origin == Origin.CAMPAIGN) {
if(provisioningRequest.action == Action.IDENTITY_TERMINATION) {
return ["campaign","remove_identity"]
} else {
return ["campaign","remove_access"]
}
} else {
switch (provisioningRequest.action) {
case Action.IDENTITY_CREATION:
return ["add", "identity"]
case Action.IDENTITY_TERMINATION:
return ["terminaison", "remove_identity"]
case Action.IDENTITY_TRANSFER:
return ["transfer", "NA"]
case Action.ADD_ACCESSES_REQUEST:
return ["add", "access"]
case Action.REMOVE_ACCESSES_REQUEST:
return ["terminaison", "access"]
}
}
logger.error("Unable to determine category for request ${provisioningRequest.requestIdIfPresent}")
return ["NA","NA"]
}
def getAccountRequestTypeLabel(AccountRequest.Type type) {
switch (type) {
case AccountRequest.Type.CREATION:
return "Creation"
break
case AccountRequest.Type.MODIFICATION:
return "Modification"
break
case AccountRequest.Type.TERMINATION:
return "Termination"
break
case AccountRequest.Type.DELETION:
return "Deletion"
break
}
}
def getGroupRequestTypeLabel(GroupRequest.Type type) {
switch (type) {
case GroupRequest.Type.ASSIGNMENT:
return "Assignment"
break
case GroupRequest.Type.UNASSIGNMENT:
return "Unassignment"
break
case GroupRequest.Type.PROVISIONING_PROCEDURE_FOR_ASSIGNMENT:
return "Additional instructions for assignment"
break
case GroupRequest.Type.PROVISIONING_PROCEDURE_FOR_UNASSIGNMENT:
return "Additional instructions for unassignment"
break
}
}
def determineOpenedBy(ProvisioningRequest provisioningRequest)
{
if(provisioningRequest.requester.id == 0) {
return defaultSystemRequesterEmail
}
else {
return provisioningRequest?.requester?.email ?: "[No requester]"
}
}