Customizers
You can customize the Web Services SaaS connector using cloud-based connector customizers. By using a customizer, you can change a connector's input before the connector ingests the data, or you can change the connector's output before the output is sent to SailPoint.
Prerequisites
- To use the customizers, SailPoint CLI is required. To set it up, refer to CLI in the SailPoint Developer Community.
- To build and upload the SaaS connector customizer, refer to Building and Uploading to Identity Security Cloud in the SailPoint Developer Community.
- To find the commands available to implement in a SaaS connector customizer, refer to Customizer Commands in the SailPoint Developer Community.
Example 1
If you want to change the input attributes before account creation, you can use the beforeStdAccountCreate method. Consider a use case where an attribute that is not present in the schema (userType) needs to be added before the account creation to determine which account type you want to create. Refer to the following template:
// Get connector source config
const config = await readConfig()
return createConnectorCustomizer()
.beforeStdAccountCreate(async (context: Context, input: StdAccountCreateInput) => {
logger.info(`Running before account, for account ${input.identity}`)
input.attributes.userType = 'member';
return input
})
Example 2
If you want to change the output attributes after account read, you can use the afterStdAccountRead method. Let's consider a use case where an attribute that is not present in the schema (active), needs to be added after the account aggregation has been performed for determining whether the user is active or not. Refer to the following template:
// Get connector source config
const config = await readConfig()
return createConnectorCustomizer()
.afterStdAccountRead(async (context: Context, output: StdAccountReadOutput) => {
logger.info(`Running after account read for account ${output.identity}`)
output.attributes.isActive = true;
return output
})
Example 3
If you want to change the input attributes before account aggregation, you can use the beforeStdAccountAggregation method. Consider a use case where you want to determine the number of accounts that were aggregated the last time aggregation was performed. Refer to the following template:
Example 4
If you want to make changes in one or more operations as mentioned in the above templates, you can club them together. Refer to the following template:
// Get connector source config
const config = await readConfig()
return createConnectorCustomizer()
.beforeStdAccountList(async (context: Context, input: StdAccountListInput) => {
logger.info(`Running before account list for account. State: ${input.state.last_aggregation_count}`)
return input
})
.afterStdAccountRead(async (context: Context, output: StdAccountReadOutput) => {
logger.info(`Running after account read for account ${output.identity}`)
output.attributes.isActive = true;
return output
})
.beforeStdAccountCreate(async (context: Context, input: StdAccountCreateInput) => {
logger.info(`Running before account, for account ${input.identity}`)
input.attributes.userType = 'member';
return input
})
Endpoint Customization
You can customize endpoints used by the Web Services SaaS connector. The endpoint customizations can take place before or after HTTP operations that affect a given endpoint, such as Test Connection or Aggregation. Customizations that the connector can make to an endpoint include, but aren't limited to, changes to its physical address or its behavior.
- Configure Endpoint Customization Before Operations
- Configure Endpoint Customization After Operations
Objects Used in Endpoint Customization
| Object Name | Object Reference |
|---|---|
| Request Body | input._requestConfig.body |
| Request URL | input._requestConfig.url |
| Request Headers | input._requestConfig.headers |
| Request method | input._requestConfig.httpMethodType |
| Page Offset | input._pagingProcessor._pageInitialOffset |
| Page Size | input._pagingProcessor._pageSize |
| Application Attributes | input.applicationAttributes |
| Response | input.rawResponse.response |
| Response Code | input.rawResponse.status |
Configure Endpoint Customization Before Operations
Configure the connector to make a customization to an existing endpoint before operations. The connector looks for the isBeforeCustomizer parameter in the endpoint. The presence of this parameter in the endpoint signals to the connector that there is a customization process that needs to take place before the operation.
To add an endpoint customization before the operation, ensure that the following prerequisites have been completed prior to running the operation:
-
Add a new parameter to the endpoint using the SailPoint REST API to indicate a customization requirement:
Note
Refer to Best Practices: REST API Authentication and REST API - Update Source (Partial) in the SailPoint Developer Community for more information on SailPoint's REST APIs.
-
Create a customizer project that actually executes the required endpoint customization. Refer to Customizers - Get started in the SailPoint Developer documentation for more information about creating a customizer project. Add the
customizedOperationmethod with two parameters:- The
uniqueNameForEndPointfollowed by a:beforesuffix (for example,Test Connection:before) to execute the customizer project before the endpoint - The handler to run
- The
Objects Used in Before Operation Customizations
| Object | Description |
|---|---|
application |
The application whose data file is being processed. |
requestEndPoint |
Current request information. It contains the header, body, context URL, method type, response attribute map, and successful response code. |
Examples
The following are possible example configurations for before operation configurations.
Before Operation Customization Example #1
In the following example, SailPoint is executing an additional API to add an access token to the headers of the Test Connection endpoint.
export const connectorCustomizer = async () => {
// Get connector source config
const config = await readConfig()
return createConnectorCustomizer()
.customizedOperation('Test Connection:before', async (context: Context, input: any) => {
// The private key is a sensitive credential. Store it securely, not in code.
const privateKeyPem = `-----BEGIN RSA PRIVATE KEY-----
[YOUR_ENCRYPTED_PRIVATE_KEY_CONTENT_HERE]
-----END RSA PRIVATE KEY-----`
const decryptPrivateKey = (encryptedPem: string, password: string): string => {
const decryptPrivateKeyWithPassword = (encryptedPem: string, password: string): string => {
// Remove header/footer and line breaks
const pem = encryptedPem
.replace('-----BEGIN RSA PRIVATE KEY-----', '')
.replace('-----END RSA PRIVATE KEY-----', '')
.replace(/\r?\n|\r/g, '')
.trim()
try {
const decryptedKey = crypto.createPrivateKey({
key: encryptedPem,
format: 'pem',
passphrase: password,
})
return decryptedKey.export({ format: 'pem', type: 'pkcs1' }).toString()
} catch (err) {
throw new Error('Failed to decrypt private key: ' + err)
}
}
return decryptPrivateKeyWithPassword(encryptedPem, password)
}
const generateAssertion = (decryptedKey: string): string => {
const now = Math.floor(Date.now() / 1000)
// Payload values are specific to your application configuration in Okta.
const payload = {
aud: 'https://your-okta-domain.com/oauth2/v1/token',
sub: 'your-client-id',
iss: 'your-client-id',
jti: Math.random().toString(36).substring(2),
iat: now,
exp: now + 600, // 10 minutes
}
return jwt.sign(payload, decryptedKey, {
algorithm: 'RS256',
header: {
typ: 'JWT',
alg: 'RS256',
},
})
}
// The passphrase for the private key is also a secret.
const decryptedKey = decryptPrivateKey(privateKeyPem, 'your-private-key-passphrase')
const assertion = generateAssertion(decryptedKey)
const authenticate = async (jwtAssertion: string): Promise<string | null> => {
const url = 'https://your-okta-domain.com/oauth2/v1/token'
const data = new URLSearchParams({
grant_type: 'client_credentials',
// Scopes define the permissions your application is requesting.
scope: 'okta.users.read okta.groups.read okta.schemas.manage',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: jwtAssertion,
})
try {
const response = await axios.post(url, data.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
const accessToken = response.data.access_token
return accessToken
} catch (error: any) {
console.error('Authentication failed:', error.message)
return null
}
}
const accessToken = await authenticate(assertion)
const headers = {
Authorization: `Bearer ${accessToken}`,
}
input._requestConfig.headers = headers
return input
})
}
Before Operation Customization Example #2
In this example, SailPoint is iterating through the user role received from the input, and then assigning them to the request object based on their type, array, or string.
export const connectorCustomizer = async () => {
// Get connector source config
const config = await readConfig()
return createConnectorCustomizer()
.customizedOperation('Add Entitlement:before', async (context: Context, input: any) => {
// Custom logic for the new operation
logger.info('Update Account:before - running ' + JSON.stringify(input._requestConfig))
const body: Map<string, any> = input._requestConfig.body
logger.info('bodyyyyy ' + JSON.stringify(input._requestConfig.body))
const jsonBody: string = body.get('jsonBody') as string
logger.info('JSON body received ' + JSON.stringify(jsonBody))
try {
let jsonMap: any = null
try {
jsonMap = JSON.parse(jsonBody)
logger.info('Parsed jsonBody successfully ' + JSON.stringify(jsonMap))
} catch (e) {
logger.error('Failed to parse jsonBody:', e)
jsonMap = {}
}
if (jsonMap !== null) {
let roleEntry: any = jsonMap['USER_ROLE']
let role: string = ''
if (roleEntry !== null && Array.isArray(roleEntry)) {
const rolesArray: Array<any> = roleEntry
if (rolesArray.length > 0) {
role = rolesArray[0] as string
}
} else if (roleEntry !== null) {
role = roleEntry as string
}
jsonMap['USER_ROLE'] = role
const finalBody: string = JSON.stringify(jsonMap)
input._requestConfig.body = finalBody
logger.info('input body ' + JSON.stringify(input._requestConfig))
}
} catch (ex) {
console.error('Rule - MULE Modify Body: ' + ex)
}
return input
})
}
Configure Endpoint Customization After Operations
Configure the connector to make a customization to an existing endpoint after operations. The connector looks for the isAfterCustomizer parameter in the endpoint. The presence of this parameter in the endpoint signals to the connector that there is a customization process that needs to take place after the operation is completed.
To add an endpoint customization after operation completion, ensure that the following prerequisites have been completed prior to running the operation:
-
Add the
isAfterCustomizerparameter to the endpoint via the SailPoint REST API to indicate that there is a customization requirement.Note
Refer to Best Practices: REST API Authentication and REST API - Update Source (Partial) in the SailPoint Developer Community for more information on SailPoint's REST APIs.
-
Create a customizer project that actually executes the required endpoint customization. Refer to Customizers - Get started in the SailPoint Developer documentation for more information about creating a customizer project. Add the
customizedOperationmethod with two parameters:- An endpoint identifier followed by an
:aftersuffix to execute the customizer project before the endpoint - The handler to run
- An endpoint identifier followed by an
Objects Used in After Operation Customizations
| Object | Description |
|---|---|
application |
The application whose data file is being processed. |
requestEndPoint |
Current request information. It contains the header, body, context URL, method type, response attribute map, and successful response code. |
processedResponseObject |
This object is a list of map objects for accounts and groups. The map objects contain keys as identity attributes for the application schema, and the associated values are all the account and group attributes passed by the connector after parsing the respective API response. |
rawResponseObject |
The raw response string returned from the target system. This can be in JSON or XML format. |
Example
The following is a possible example for an after operation configuration.
After Operation Customization Example
In this example, SailPoint is converting the XML object received as a raw response from the API to JSON, and then mapping the attributes to the schema.
export const connectorCustomizer = async () => {
// Get connector source config
const config = await readConfig()
return createConnectorCustomizer()
.customizedOperation('Account Aggregation:after', async (context: Context, input: any) => {
const xml2js = require('xml2js') // Require inside an async function
const parseStringPromise = xml2js.parseStringPromise
let jsonPrettyPrintString
let xmlJSONObj = await parseStringPromise(input.rawResponse.response, {
explicitArray: false, // Avoid unnecessary arrays
trim: true, // Remove whitespace
})
logger.info(`JSON parsed string ` + JSON.stringify(xmlJSONObj))
jsonPrettyPrintString = JSON.stringify(xmlJSONObj)
let response = JSON.parse(jsonPrettyPrintString)
logger.info('Response obj : ' + JSON.stringify(response))
let userListOutput = response.USER_LIST_OUTPUT || {}
if (!userListOutput.USER_LIST) throw new Error('USER_LIST_OUTPUT is undefined')
let userList = userListOutput.USER_LIST || {}
if (!userList?.USER) throw new Error('USER_LIST is undefined')
let finalList: {}[] = []
if (userList.USER) {
userList.USER.forEach(
(user: {
CONTACT_INFO: any
USER_STATUS: string
BUSINESS_UNIT: string
USER_ROLE: string
USER_LOGIN: string
USER_ID: string
}) => {
let newmap = new Map<any, any>()
let contactInfo = user.CONTACT_INFO
newmap.set('FIRSTNAME', contactInfo?.FIRSTNAME || 'Unknown')
newmap.set('LASTNAME', contactInfo?.LASTNAME || 'Unknown')
newmap.set('EMAIL', contactInfo?.EMAIL || 'Unknown')
newmap.set('USER_STATUS', user.USER_STATUS || 'Unknown')
newmap.set('BUSINESS_UNIT', user.BUSINESS_UNIT || 'Unknown')
newmap.set('USER_ROLE', user.USER_ROLE || 'Unknown')
newmap.set('USER_LOGIN', user.USER_LOGIN || 'Unknown')
newmap.set('USER_ID', user.USER_ID || 'Unknown')
finalList.push(newmap)
}
)
}
logger.info('final list: ' + JSON.stringify(finalList))
const returnData = { data: finalList }
return returnData
})
}
Documentation Feedback
Feedback is provided as an informational resource only and does not form part of SailPoint’s official product documentation. SailPoint does not warrant or make any guarantees about the feedback (including without limitation as to its accuracy, relevance, or reliability). All feedback is subject to the terms set forth at https://developer.sailpoint.com/discuss/tos.