Enhancement: Web Services SaaS Connector - Endpoint Customization

Description

The SailPoint Web Services SaaS Connector now supports customization to endpoints, providing additional and greater flexibilities to do the data massaging based on your requirement. This works for any endpoint that is configured in the Web Services connector.

This capability provides similar flexibilities as before and after operation rule of VA based Web Services connector.

Before Endpoint Customization:

  • The Web Service SaaS Connector supports adding customization to the configured endpoints. This customization is used by the Web Services SaaS Connector before performing operations such as Test Connection and Aggregation operations.
  • To add the customization before a particular endpoint, you can do the followings:
    • Configure the endpoint as usual.
    • Add the isBeforeCustomizer flag to indicate the customization is present to the particular endpoint via the API. Refer to SailPoint Developer Community.
    • Create a customizer project, add the method customizedOperation with two parameters - endpoint identifier followed by a suffix, :before for executing customizer before the endpoint and the handler to run.

After Endpoint Customization:

  • Web Service SaaS Connector supports adding customization to the configured endpoints. This customization is used by the Web Services SaaS Connector after performing operations such as Test Connection and Aggregation.
  • To add the customization after a particular endpoint, follow below steps
    • Configure the endpoint as usual.
    • Add the isAfterCustomizer flag to indicate the customization is present to that particular endpoint via the API. Refer to, SailPoint Developer Community.
    • Create a customizer project and add the customizedOperation method with two parameters: the endpoint identifier (followed by the suffix :after to run the customizer after the endpoint) and the handler to run.

Documentation

Release Details

  • Identity Security Cloud - Now Available!
2 Likes

I think adding an option to execute a call or skipping a call based on some conditions should also be added in the webservices connector. Currently it’s doable via before and after rules but if it could be done in the endpoint level that will be great

1 Like

is this like Before Operation and After Operation for non-SaaS web services?

Is the idea that you dont need SailPoint to upload/approve rules for these connectors, that you can do it yourself?

This looks promising and seems to add exactly what was missing from customizers when using them with Web Services SaaS connectors; the option to have a customizer per operation. I will definately experiment with these, as it might be possible to transfer some of the Web Services connectors into their SaaS based counterpart.

One thing to note, which I believe is a downstep towards using VA based Web Services with beforeRule and afterRule is this.
With the VA based setup, your rule can be source agnostic, i.e. if you have several sources that need a similar use case, you can reuse the same rule and you can tie the rule to ANY endpoint.
With this new SaaS + customizer approach, your customizer code determines which operations it acts on, so you are forced to give you operations that exact name. I believe this might be, in certain contexts, less clear. Especially when looking at a source, at a glance from the API, you cannot immediately see what customizations there are on a certain operation. You can only see that a source has a before or after customizer.

I can’t wait to start using the apache velocity logic in Web Services and Web Services SaaS based sources similarly to notification templates and transforms. This will remove the need of having rules under the hood for many applications.
Here are some examples where we will use this. Note that my syntax might be wrong in these examples.

  1. Example where an application only supports PUT account and not PATCH account. So if the provisioning plan contains lastName, but does not contain firstName, we should keep the value currently in the target application (which we just retrieved whose value is in the response variable).

  2. Example where an application does not support adding a group to an account, but only supports giving the full list of groups to an account. So we should get the current list, add the required ones, and push the result.

  3. Example like above but with removing groups from such an application. So we should get the current list, remove the required ones, and push the result.

These would be very powerful functionality:

  1. Directly visible and configurable in the UI
  2. No need for custom rules for such use cases
  3. Elegant. Easily readable on what the logic does
  4. Multiple purpose. You can achieve many use cases with this one simple addition. Whether it is edge case handling (empty string v.s. null v.s. empty array), have XML tags removed depending on whether the plan has a value, and many more.

The documentation does mention that the Web Service (SaaS) connector supports velocity, but the documentation is limited and the syntax is different than expected. I would love some clarification on how to use velocity in the Web Services (SaaS) connector. And I am curious on the plans to also offer Velocity on the regular Web Services connector for the applications where VA’s are still required.

Please see this documentation here:

Kind regards,
Angelo

5 Likes

@dinesh_mishra - I was testing the before and after operation customizer. I have few questions-

  1. which keyword should we use for fetching the processedResponseObject in after customizer? I tried using input.processedResponse.response similar as rawresponse but didn’t work.

  2. In After customizer, when I am returning my modified data list then always it gives error r.get() function doesn’t exist. I can see in the logs that list is in perfect json syntax. This seems to be some SDK issue. Attaching the logs and code below- <
    customizedOperation(‘Account Aggregation:after’, async (context: Context, input:any): Promise => {

            let res1 = input.rawResponse.response
    
            const header: Map<string, any> =  input.\_requestConfig.headers
    
           *//  const val= header.get("x-api-key");*
    
            *// logger.info(\`Header key value: ${val}\`);*
    
            *// logger.info(JSON.stringify(res1,null,2));*
    
           
    
            let parsedObj;
    
            if(typeof res1 === 'string'){
    
                try{
    
                    parsedObj = JSON.parse(res1);
    
                }catch(e)
    
                {
    
                    logger.error("failed to convert json");
    
                    parsedObj={};
    
                }
    
            }
    
             else{
    
                parsedObj= res1;
    
             }   
    
            
    
            logger.info('After Parsing the object')
    
            const result:{id:number; first_name:string;last_name:string; display_name:string}\[\]=
    
            parsedObj.data.map((item:{id:number;first_name:string;last_name:string})=>({
    
                id:item.id,
    
                first_name:item.first_name,
    
                last_name:item.last_name,
    
                display_name:\`${item.first_name} ${item.last_name}\`
    
            }))
    
                      
    
            logger.info(\`Returning ${result.length} accounts\`)
    
            logger.info(\`Full result: ${JSON.stringify(result,null,2)}\`);
    
            
    
            const returnMap = { data: result }
    
             
    
           return returnMap;
    
            
    
        }) 
    

11/02/2025, 17:00:54.461 INFO [connectorMessage] {“level”:“INFO”,“message”:“After Parsing the object”}

11/02/2025, 17:00:54.461 INFO [connectorMessage] {“level”:“INFO”,“message”:“Returning 6 accounts”}

11/02/2025, 17:00:54.461 INFO [connectorMessage] {“level”:“INFO”,“message”:“Full result: [\n {\n \“id\”: 1,\n \“first_name\”: \“George\”,\n \“last_name\”: \“Bluth\”,\n \“display_name\”: \“George Bluth\”\n },\n {\n \“id\”: 2,\n \“first_name\”: \“Janet\”,\n \“last_name\”: \“Weaver\”,\n \“display_name\”: \“Janet Weaver\”\n },\n {\n \“id\”: 3,\n \“first_name\”: \“Emma\”,\n \“last_name\”: \“Wong\”,\n \“display_name\”: \“Emma Wong\”\n },\n {\n \“id\”: 4,\n \“first_name\”: \“Eve\”,\n \“last_name\”: \“Holt\”,\n \“display_name\”: \“Eve Holt\”\n },\n {\n \“id\”: 5,\n \“first_name\”: \“Charles\”,\n \“last_name\”: \“Morris\”,\n \“display_name\”: \“Charles Morris\”\n },\n {\n \“id\”: 6,\n \“first_name\”: \“Tracey\”,\n \“last_name\”: \“Ramos\”,\n \“display_name\”: \“Tracey Ramos\”\n }\n]”}

11/02/2025, 17:00:54.461 INFO [connectorMessage] ExecutionMediator.ts Paging logic processing ended for endpoint ‘Account Aggregation’. Paging terminated: true

11/02/2025, 17:00:54.461 INFO [connectorMessage] WebserviceConnector.ts Pagination hasNextPage: false

11/02/2025, 17:00:54.461 ERROR [connectorMessage] Account aggregation failed. r.get is not a function

11/02/2025, 17:00:54.489 INFO [commandOutcome] Command failed with [ConnectorError] Account aggregation failed. r.get is not a function: std:account:list, for connector version 52 and customizer version 17. output_count=0 output_bytes=0 keep_alive_count=0 state_count=0. Elapsed time 1580ms

11/02/2025, 17:00:54.461 INFO [connectorMessage] ExecutionMediator.ts Paging logic processing ended for endpoint ‘Account Aggregation’. Paging terminated: true

11/02/2025, 17:00:54.461 INFO [connectorMessage] WebserviceConnector.ts Pagination hasNextPage: false

11/02/2025, 17:00:54.461 ERROR [connectorMessage] Account aggregation failed. r.get is not a function

11/02/2025, 17:00:54.489 INFO [commandOutcome] Command failed with [ConnectorError] Account aggregation failed. r.get is not a function: std:account:list, for connector version 52 and customizer version 17. output_count=0 output_bytes=0 keep_alive_count=0 state_count=0. Elapsed time 1580ms”