Identity Security Cloud Extensibility: The Art of the Possible

The goal of this article is discussing some extensibility strategies on ISC in comparison with IIQ.

Using transforms for dynamic data and controlled transitions

ISC transforms could be considered a simplified version of IIQ identity/account attribute rules. ISC provides a series of basic transforms or building blocks and custom transforms are built by combining them to produce the desired output. In fact, ISC attribute rules are encapsulated as transforms and used in those situations where a transform may be insufficient. However, transforms are much more powerful than they let on and I’d like to make some examples to illustrate it.

Lookup tables as replacement of IIQ configuration objects

Configuration objects are commonly used to storage configuration options and mapping tables:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configuration PUBLIC "sailpoint.dtd" "sailpoint.dtd">
<Configuration name="SERIConfiguration" significantModified="1710941356571">
   <Attributes>
      <Map>
         <entry key="ActiveDirectory.BaseDN" value="OU=Users,OU=Demo,DC=seri,DC=sailpointdemo,DC=com"/>
         <entry key="Email.Pattern" value="%[email protected]"/>
         <entry key="Password.Default" value="myPassword"/>
         <entry key="Roles.AccessProfile.Container" value="Access Profiles"/>
         <entry key="Roles.Application.Container" value="Application Roles"/>
         <entry key="Roles.Business.Container" value="Business Roles"/>
      </Map>
   </Attributes>
</Configuration>

On ISC, a similar strategy can be followed by creating a lookup table transform:

{
    "name": "Company",
    "type": "lookup",
    "attributes": {
        "table": {
            "SG": "SG Tech Solutions",
            "GB": "GB Innovations",
            "BE": "BE Global Services",
            "CN": "CN Connect Enterprises",
            "US": "US Powerhouse Inc.",
            "JP": "JP Digital Solutions",
            "BR": "BR Creative Ventures",
            "DE": "DE Elite Systems GmbH",
            "default": "ACME Industries"
        }
    }
}

These transforms can be used from other transforms by referencing them. There’s no need to duplicate data across all transforms using this table.

Input overrides or default values

One interesting use case around transforms is the definition of default values if original is not present or overriding the input value with another value if present. Same thing, two different perspectives.

An example of this is checking if an email system provides a different value for an identity than the one provided by the authoritative source. This would be done like:


Notice the email attribute as input and the Email transform on top of it. With a transform like:

{
    "name": "Email",
    "type": "firstValid",
    "attributes": {
        "requiresPeriodicRefresh": true,
        "ignoreErrors": true,
        "values": [
            {
                "type": "accountAttribute",
                "attributes": {
                    "sourceName": "Azure",
                    "attributeName": "userPrincipalName"
                }
            },
            {
                "type": "trim"
            },
            "N/A"
        ]
    }
}

The firstValid transform tries to use Azure account as main source, if present. If not, we use the default input, indirectly referenced by the trim transform. Ultimately, if none is present, we set N/A as default value. IIQ rules can also achieve the same in many different ways.

Controlled identity attribute transitions

While IIQ supports workflows and rules to control identity attribute updates, a clever use of transforms can help us controlling attribute changes based on time, account checks, etc.

In this example, a process sets a timestamp on an auxiliary account’s attribute. The transform checks if that value is present and it hasn’t expired. If it did, it sets the identity attribute that was, in a loopback fashion. If it didn’t it updates the identity attribute with the new value. Input attribute is the auxiliary account’s attribute, the timestamp, so the transform is re-evaluated as soon as it changes, and whenever it happens by other means:

{
    "name": "UID",
    "type": "static",
    "attributes": {
        "value": "#if($present == \"true\")$newValue#else$oldValue#end",
        "oldValue": {
            "type": "firstValid",
            "attributes": {
                "ignoreErrors": true,
                "values": [
                    {
                        "type": "identityAttribute",
                        "attributes": {
                            "name": "uid"
                        }
                    },
                    {
                        "type": "accountAttribute",
                        "attributes": {
                            "sourceName": "HR",
                            "attributeName": "userid"
                        }
                    }
                ]
            }
        },
        "newValue": {
            "type": "identityAttribute",
            "attributes": {
                "name": "newUid"
            }
        },
        "present": {
            "type": "firstValid",
            "attributes": {
                "values": [
                    {
                        "type": "dateCompare",
                        "attributes": {
                            "firstDate": {
                                "type": "trim"
                            },
                            "secondDate": "now",
                            "operator": "lt",
                            "positiveCondition": false,
                            "negativeCondition": true
                        }
                    },
                    false
                ]
            }
        }
    }
}

Workflows for customized identity lifecycle

Virtually every internal and custom process on IIQ is a workflow, also known as business process. ISC has it’s own workflow engine and, although simpler in all aspects than IIQ’s, it can help a great deal with customizing the default identity lifecycle.

Identity lifecycle is generally determined identity attribute changes and lifecycle state transitions. Besides the obvious LCS account management options, in many cases we need to do something else:

Mover micro-certification

Workflow engine provides an out-of-the-box template to trigger a micro-certification on a department change. This can be easily adapted to whatever attribute change means a mover event to you.

Remove leaver standing access

IIQ allows for customized provisioning plans. ISC, on the contrary, doesn’t. However, there are ways around it. Provisioning plans can be customized by before provisioning rules, but this is on a per source basis. For this use case, since it involves a variable number of access items across all sources, this isn’t a valid strategy. The following workflow exploits the concept of an automatic micro-certification with revoke access by default when not completed. The certification picks up all standing access, it’s automatically closed without answer and all standing access is automatically removed:

Dynamic approvals with ETS triggers

We’ve discussed how IIQ workflows are way more complex than ISC’s. ISC workflows are a no-code engine while IIQ workflows heavily rely on code. When ISC workflows are not sufficient, ETS and API can be leveraged from an external platform. From a custom application, AWS Lambda function to an IPaaS platform, we can use whatever system that can process a REST call to customize our business processes.

ETS triggers can be of two types: fire and forget and response required.


While fire and forget triggers cannot change the outcome of a business process, only complement it, response required triggers can. These are currently used for access request decisions, mainly adding a preliminary response (revocation) or adding a dynamic approver to the configured approval workflow.

Here’s an example of how to use both triggers in the context of SOD and risk management. The process automatically revokes a request with a high-risk SOD violation and adds the policy owner as additional approver when the SOD violation is medium-risk:

Similar results can be achieved now by processing these triggers from the workflow engine. Most if not all ETS triggers are readily available as workflow native triggers but triggers can also invoke workflows by pointing them to a workflow HTTP Trigger url.

Delimited file sources for data persistence and passive aggregation

IIQ has mainly four ways of updating an identity cube:

  • Identity mappings
  • UI editing
  • Beanshell code
  • SCIM API

ISC imposes a more restricted data flow limited to identity mappings. Because of this, we cannot simply update an identity cube from our business processes. However, there’s a way to comply with this flow and write identity data back to ISC at will: delimited file sources (NELM/JIT too).

Data persistence

In the event we want to update certain identity attributes on demand, as the result of some business process, we don’t write to the identity cube directly. We instead modify a dedicated delimited file source using the Account API. Write operations are only possible on certain connectors, delimited file being one. By pushing account changes throught the API to the delimited file source and using an override transform like the one described before, we can effectively push changes to identity cubes without modifying the main authoritative source.

Passive aggregation

Based on the same idea, it is possible to push changes from an external system and invert the flow of the typical aggregation. This doesn’t mean it’s a desired practice but, in the right context, it can be a very useful integration strategies to push identity data in real time from an external system.

Automating administration with the SailPoint SDK

There are several ways to automate or streamline IIQ administration:

ISC also has its own administration options:

Before we discuss Yannick Beot’s priceless VS Code plugin, SailPoint SDK can dramatically improve the administration experience and help automating repetitive actions. Currently, the SDK comes in three flavors:

  • TypeScript
  • Go
  • PowerShell

While all programming languages are useful in their own context, for general administration, PowerShell is probably the best option. Most Windows administrators are familiar with it and it’s an excellent way to script API calls from a local or remote runtime. Here’s an example on how to automate the creation of roles on ISC:

param($CsvFile)
Import-Module AzureAD
Import-Module PSSailpoint
$sourceName = "Azure"

function Init {
    $credentials = Import-Clixml -Path "${HOME}\credentials.xml"
    Connect-AzureAD -Credential $credentials
    $data = Get-Data -Path $CsvFile

    $departments = $data | Select-Object -Property "department" -Unique | ForEach-Object { $_.department }
    foreach ($department in $departments) {
        Write-Output "Creating department group $department..."
        New-AzureADMSGroup -DisplayName $department -MailEnabled $true -SecurityEnabled $true -GroupTypes "Unified" -Description "$department department" -Verbose
    }
    $source = Get-Sources | Where-Object name -EQ $sourceName
    Invoke-CCLoadEntitlements -Id $source.connectorAttributes.cloudExternalId
    
    Start-Sleep -Minutes 1
    $Filters = "source.id eq ""$($source.id)"""
    $entitlements = Get-BetaEntitlements -Filters $Filters
    $departmentEntitlements = $entitlements | Where-Object { $.name -In $departments }
    foreach ($entitlement in $departmentEntitlements) {
        Write-Output "Creating access profile for $($entitlement.name)..."
        $ap = @{
            name         = "$($entitlement.name) ($($source.name))"
            description  = "Department access profile for $($source.name)"
            enabled      = $true
            owner        = $source.owner
            source       = @{
                id   = $source.id
                type = "SOURCE"
                name = $source.name
            }
            entitlements = @(
                @{
                    id   = $entitlement.id
                    type = "ENTITLEMENT"
                    name = $entitlement.name
                }
            )
        }
        $AccessProfile = ConvertFrom-JsonToAccessProfile -Json (ConvertTo-Json -InputObject $ap)
        $result = New-AccessProfile -AccessProfile $AccessProfile

        Write-Output "Creating role for $($entitlement.name)..."
        $r = @{
            name           = "$($entitlement.name)"
            description    = "Department role for $($source.name)"
            enabled        = $true
            owner          = $source.owner
            accessProfiles = @(
                @{
                    id   = $result.id
                    type = "ACCESS_PROFILE"
                    name = $result.name
                }
            )
            membership     = @{
                type     = "STANDARD"
                criteria = @{
                    operation   = "EQUALS"
                    key         = @{
                        type     = "IDENTITY"
                        property = "attribute.department"
                    }
                    stringValue = $entitlement.name
                }
            }
        }
        $Role = ConvertFrom-JsonToRole -Json (ConvertTo-Json -InputObject $r)
        $result = New-Role -Role $Role
    }
}

function Get-Data {
    param($Path)
    return Get-Content -Path $Path | ConvertFrom-Csv
}

Init

SaaS Connectivity and loopback connectors

While OpenConnector framework is available for both IIQ and ISC alike, it is not an easy custom development framework and therefore not too popular. ISC SaaS Connectivity framework is a game changer that considerably streamlines custom connector development. It runs VA-less, so it means target systems must be reachable from the Internet, since SailPoint hosts the runtime.

One interesting synergy SaaS Connectivity can capitalize on is with SailPoint SDK. In this case, since the framework runs on Node.js and TypeScript, the TypeScript SDK is incredibly useful when it comes to building loopback connectors. Loopback connectors exploits ISC API to manage ISC as it was any other target system. Some examples of these loopback connectors are:

These connectors are testimony to the power and ease of use of SaaS Connectivity, one of the most powerful ISC’s extensibility options.

Using lifecycle state to drive account management operations

Lifecycle state is not a native concept to IIQ but it is to ISC. The closest IIQ concept we can find is Rapid Setup JML stages, where we can configure a series of policies that kick in when an identity transitions from one JML stage to another.

Lifecycle states are closely related but are more open to interpretation. With a basic combination of active/inactive lifecycle states, we can control joiner and leaver lifecycles and combine them with identity attribute changes to control movers, as described before. More complex lifecycle state combinations can be used, like staging or archive, all names totally arbitrary and flexible. It all depends on the business processes in place and the needs of each identity profile.

While ISC can enable/disable accounts via LCS provisioning options, it cannot delete accounts by default. One common use case is having a deferred deletion of accounts after a leaver lifecycle. This can be achieved by configuring a time-sensitive LCS transform, that, after some time, transitions from inactive (leaver) to something like archive. It is the archive LCS that through the use of attribute sync and a before provisioning rule, can trigger account deletions and fulfill this final lifecycle state.

Lifecycle-driven generic account deletion

Implemented by generic before provisioning rule and time-sensitive LCS transform. The LCS transform transitions from inactive state to archive after 30 days as requested. In turn, Active Directory source extended LCS attribute is synced with identity’s lifecycle state. This is combined with the rule so the rule is guaranteed to run when a LCS change occurs. For the process to be complete, rule configuration is added to the account update provisioning policy in the following way:

  • Attribute sync: cloudLifecycleState → LCS (desired value is archive)
  • Account update provisioning policy: LCS_ARCHIVE_OP = Delete
  • Provisioning policy:
{
    "name": "Update Account",
    "description": null,
    "usageType": "UPDATE",
    "fields": [
        {
            "name": "LCS_ARCHIVE_OP",
            "transform": {
                "type": "static",
                "attributes": {
                    "value": "Delete"
                }
            },
            "attributes": {},
            "isRequired": false,
            "type": "string",
            "isMultiValued": false
        }
    ]
}

Transform:

{
    "name": "Lifecycle State",
    "type": "firstValid",
    "attributes": {
        "values": [
            {
                "type": "dateCompare",
                "attributes": {
                    "firstDate": {
                        "type": "firstValid",
                        "attributes": {
                            "values": [
                                {
                                    "type": "reference",
                                    "attributes": {
                                        "id": "Parse date - HR",
                                        "input": {
                                            "type": "accountAttribute",
                                            "attributes": {
                                                "sourceName": "HR",
                                                "attributeName": "contractStartDate"
                                            }
                                        }
                                    }
                                },
                                {
                                    "type": "dateMath",
                                    "attributes": {
                                        "expression": "now-1d",
                                        "roundUp": true
                                    }
                                }
                            ]
                        }
                    },
                    "secondDate": "now",
                    "operator": "gt",
                    "positiveCondition": "prehire",
                    "negativeCondition": {
                        "type": "dateCompare",
                        "attributes": {
                            "firstDate": {
                                "type": "firstValid",
                                "attributes": {
                                    "values": [
                                        {
                                            "type": "reference",
                                            "attributes": {
                                                "id": "Parse date - HR",
                                                "input": {
                                                    "type": "accountAttribute",
                                                    "attributes": {
                                                        "sourceName": "HR",
                                                        "attributeName": "contractEndDate"
                                                    }
                                                }
                                            }
                                        },
                                        {
                                            "type": "dateMath",
                                            "attributes": {
                                                "expression": "now+1d",
                                                "roundUp": true
                                            }
                                        }
                                    ]
                                }
                            },
                            "secondDate": {
                                "type": "dateMath",
                                "attributes": {
                                    "expression": "now-90d",
                                    "roundUp": true
                                }
                            },
                            "operator": "lt",
                            "positiveCondition": "archive",
                            "negativeCondition": {
                                "type": "dateCompare",
                                "attributes": {
                                    "firstDate": {
                                        "type": "firstValid",
                                        "attributes": {
                                            "values": [
                                                {
                                                    "type": "reference",
                                                    "attributes": {
                                                        "id": "Parse date - HR",
                                                        "input": {
                                                            "type": "accountAttribute",
                                                            "attributes": {
                                                                "sourceName": "HR",
                                                                "attributeName": "contractEndDate"
                                                            }
                                                        }
                                                    }
                                                },
                                                {
                                                    "type": "dateMath",
                                                    "attributes": {
                                                        "expression": "now+1d",
                                                        "roundUp": true
                                                    }
                                                }
                                            ]
                                        }
                                    },
                                    "secondDate": "now",
                                    "operator": "lt",
                                    "positiveCondition": "inactive",
                                    "negativeCondition": "active"
                                }
                            }
                        }
                    }
                }
            },
            "undefined"
        ]
    },
    "internal": false
}

Collecting user input with forms

Forms are a common tool on IIQ. Forms are used to define provisioning policies, user registration, complement/review provisioning plans and custom workflows. ISC also offers its own implementation of forms. However, ISC forms are not rooted low-level as IIQ forms are. This is that IIQ forms can be used to allow for manual input in the provisioning process while ISC cannot. IIQ provisioning forms allow users review and amend the results of a provisioning policy and this is extremely useful to customize provisioning requests.

ISC forms are higher up in the technical layer and are mainly triggered and processed by workflows. This doesn’t mean forms are restricted to the context of workflows, there’s a dedicated API to use them as you see fit. They can be used for many purposes, just not within the provisioning process itself.

Combining forms, workflows, transforms and delimited file sources

While there are many examples on how to use forms, it is true that persisting form responses is often challenging and not built-in. Here’s an example I recently worked on that combines all tools in the belt:

This is an interesting use case. The requirement consists of ensuring the affected user is present in the office when a change affecting their username happens. There are multiple identity attributes that depend on first name and last name, as the username does.

The solution consists of the following pieces:

  • Extra identity attribute: a potential username identity attribute needs to be added in order to trigger the process when it changes.

    Transform: loopback transform that holds the attribute change until the user is confirmed to be in the office. The transform checks for an account timestamp (more on this later) and, if present and in the future, updates the current uid with the newUid value. If not, the old uid value is kept.
{
    "name": "UID",
    "type": "static",
    "attributes": {
        "value": "#if($present == \"true\")$newValue#else$oldValue#end",
        "oldValue": {
            "type": "firstValid",
            "attributes": {
                "values": [
                    {
                        "type": "identityAttribute",
                        "attributes": {
                            "name": "uid"
                        }
                    },
                    {
                        "type": "identityAttribute",
                        "attributes": {
                            "name": "newUid"
                        }
                    }
                ]
            }
        },
        "newValue": {
            "type": "identityAttribute",
            "attributes": {
                "name": "newUid"
            }
        },
        "present": {
            "type": "firstValid",
            "attributes": {
                "values": [
                    {
                        "type": "dateCompare",
                        "attributes": {
                            "firstDate": {
                                "type": "trim"
                            },
                            "secondDate": "now",
                            "operator": "lt",
                            "positiveCondition": false,
                            "negativeCondition": true
                        }
                    },
                    false
                ]
            }
        }
    },
    "internal": false
}
  • Form: a form instance is triggered every time a new UID is detected for an identity. An email is sent and the user confirms hown long they’ll be in the office for. Form-Office presence confirmation.json (2.8 KB)
    . Note: for the moment, the form processing is hardcoded to 2 hours because the action required to compose the timestamp is unable to cast a string like “2” to number.

  • Delimited source: a dedicated delimited source with two attributes, a name to match the identity’s account name and automatically correlate with it and a Confirmed until attribute to store the time selected in the form.
  • Two workflows:
    • Notify UID change: simple workflow to trigger form instance when newUid is detected. Workflow-Notify UID change.json (1.2 KB)
    • Process office confirmation form: workflow that processes form response and updates delimited source with the selected timestamp for the transform to check. [Workflow-Process office confirmation form.json|attachment] (upload://qRTEgZdjIXqlWyFTSvcPUPNF1rj.json) (3.0 KB)

All the pieces together make the system hold the identity attribute change, notify the user and process the notification so the delimited source update makes the transform to be re-evaluated and make all work seamlessly.

Agile administration with VS Code ISC extension

IIQ has its own IDE plugins, one for Eclipse and a newer one for VS Code. They’re extremely helpful but IIQ allows to modify its own internals through Object Browser. While not a really scalable option, it makes a difference with lesser administration tasks.

ISC native administration interface is mainly its API. Most UI commands are API-based and some features are only available through API. This requires a lot of effort from the administrator side to find the suitable endpoints, compose payloads, copy/paste data, etc. This is a cumbersome process that Yannick Beot’s VS Code extension simplifies beyond belief.

In Yannick’s own words:

The SailPoint IdentityNow extension makes it easy to:

  • Connect to several tenants
  • Import and export config of a tenant
  • View, edit, aggregate, test, peek, ping, clone, or reset sources
  • View, create, edit, delete, and test transforms
  • View, create, edit, delete provisioning policies of a source
  • View, create, edit, delete schemas of a source
  • View, edit, enable, disable, export, import and test workflows and view execution history
  • View, create, edit, delete connector rules and export/import the script of a rule
  • View, edit, delete service desk integrations
  • View, edit, delete identity profiles and lifecycle states, and refreshes all the identities under a profile
  • Import/Export Accounts (import for delimited files only), uncorrelated accounts, entitlement details
  • View, edit, create, delete, export, import access profiles
  • View, edit, create, delete, export, import roles
  • View, edit, create, delete, export, import forms
  • View, edit, create search attribute config
  • View, edit identity attributes

Best thing since sliced bread!

SaaS connectors customizers

IIQ offers great flexibility to customize input and outputs of sources through a plethora of rules. This is a very flexible approach and it’s based on Beanshell code.

ISC, while supporting a number of rules, is not that flexible on this field, until now. As a natural SaaS Connectivity evolution, ISC now supports SaaS connector customizers. While SaaS customizers only support SaaS connectors, meaning those which don’t require a VA, it is an even more flexible solution than IIQ rules. Basically, it permits to customize requests and responses of each connector operation individually.

12 Likes

This is a very variable resource for our customer, awesome job @fernando_delosrios

2 Likes

Thanks Bassem, I really appreciate your comments. I hope it helps growing the community too.

2 Likes

This is absolutely incredible! This should be part of THE starting guide for new admins, engineers, architects, and developers on our platform!

I’ve added a nice table of contents to your post so people can find all of the content even better as well!

Thanks for that, Jordan. I’ll come in handy as it’s a bit long.

I encourage others to collaborate with ideas and comments!

1 Like

Controlled identity attribute transitions

Hi @fernando_delosrios, are we allowed to use the identityAttribute transform there?

This transform is not intended for use within an another identity profile attribute’s calculation.


If not, we use the default input, indirectly referenced by the trim transform.

SailPoint should create a transform called input…

Hi Andrei,

The transform works in that context although it’s not its natural one, hence the effect. I never heard it’s not allowed, just it’s not intended.

Hi Fernando,

For me that “not intended” and the explanation means this could lead to a buggy behavior. What else would that “not intended” mean?
It would be nice if SailPoint would make it clear (even better if there would be a warning when using this transform in an incorrect way…)

This transform is not intended for use within an another identity profile attribute’s calculation. Identity attribute calculations are multi-threaded processes, and there is no guarantee that a specific attribute has current data, or even exists, at the time of calculation within any given transform. Referencing identity attributes within another identity attribute’s calculation can lead to identity exceptions.

(emphasis mine)

Sorry my post upset you. By unintended I meant that since you cannot guarantee the identity attribute you reference was updated before accessing it, it can lead to unexpected results if you don’t take that into account. For instance, you shouldn’t reference uid in order to calculate the email, you should reference whatever transform you used for uid directly instead.

This is not what I’m proposing. What I’m proposing is referencing the same identity attribute from the transform that updates its value, which is the previous value. This is totally a controlled situation. That value was either calculated before or never set, and that’s the fact this strategy revolves around.

Feel free to use it or not.