# 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](<https://developer.sailpoint.com/idn/tools/cli/>) in the SailPoint Developer Community.
- To build and upload the SaaS connector customizer, refer to [Building and Uploading to Identity Security Cloud](<https://developer.sailpoint.com/idn/tools/cli/>) in the SailPoint Developer Community.
- To find the commands available to implement in a SaaS connector customizer, refer to [Customizer Commands](<https://developer.sailpoint.com/docs/connectivity/saas-connectivity/customizers/commands>) in the SailPoint Developer Community.

> [!NOTE]
>
> **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:
>
> ```typescript
> // 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
>     })
> ```

> [!NOTE]
>
> **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:
>
> ```typescript
> // 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
>     })
> ```

> [!NOTE]
>
> **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:
>
> ```typescript
> // 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
>     })
> ```

> [!NOTE]
>
> **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:
>
> ```typescript
> // 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-before-operations>)
- [Configure Endpoint Customization After 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:

  ```text
  "isBeforeCustomizer":true
  ```

  > **Note**
  >
  > Refer to [Best Practices: REST API Authentication](<https://developer.sailpoint.com/docs/api/authentication/>) and [REST API - Update Source (Partial)](<https://developer.sailpoint.com/docs/api/update-source-v-1>) 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](<https://developer.sailpoint.com/docs/connectivity/saas-connectivity/customizers/getting-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.

> [!NOTE]
>
> **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.
>
> ```typescript
> 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
>         })
> }
> ```

> [!NOTE]
>
> **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.
>
> ```typescript
> 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](<https://developer.sailpoint.com/docs/api/authentication/>) and [REST API - Update Source (Partial)](<https://developer.sailpoint.com/docs/api/update-source-v-1>) 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](<https://developer.sailpoint.com/docs/connectivity/saas-connectivity/customizers/getting-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.

> [!NOTE]
>
> **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.
>
> ```typescript
> 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
>         })
> }
> ```
