Skip to content

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:

// 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
    })

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.

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:

    "isBeforeCustomizer":true
    

    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 customizedOperation method with two parameters:

    • The uniqueNameForEndPoint followed by a :before suffix (for example, Test Connection:before) to execute the customizer project before the endpoint
    • The handler to run

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 isAfterCustomizer parameter 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 customizedOperation method with two parameters:

    • An endpoint identifier followed by an :after suffix to execute the customizer project before the endpoint
    • The handler to run

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.