Reassign Object Owners Using IdentityNow Workflows

Introduction

Object ownership in IdentityNow refers to identities that own sources, roles, access profiles, workflows, and more. Object ownership is an important concept in IdentityNow as it identifies the person who is responsible for managing the object, which may involve tasks like manually provisioning entitlements and accounts or certifying access on a source. When an identity changes roles within a company or leaves the company, this creates a scenario where they may no longer be the appropriate owner of one or more of these objects. It is important to proactively reassign the owner of these objects to reduce risk and avoid untimely delays in governance operations.

IdentityNow provides a robust set of APIs that can be used to modify the owner of many of these objects. This blog posts demonstrates how to change the owner for the following items.

  • Sources
  • Roles
  • Access Profiles
  • Entitlements
  • Identity Profiles
  • Governance Groups
  • Workflows

Applications (Apps) are not officially supported at this time because the public version of the API is not yet ready. Once there is a public version of the Apps API, we can include them in this list.

Object reassignment can be solved using a programming language of your choice, but I want to demonstrate how Workflows and Forms can be used to accomplish the same task within IdentityNow.

Workflow files

All of the workflows created in this blog post are available in the Colab. Please follow the link below to download the template files for use in your Workflow’s environment. You can either create the workflows from scratch, or configure the templates according to the instructions in this blog post.

Understanding the options for looping

Loop Limits

Before we implement the workflows needed to solve this use case, let’s discuss how we are going to approach this problem and the limitations we need to consider. The first hurdle we will run into trying to implement this in a single workflow is the limit of 100 items for loops. While this limit might not be a problem for certain objects, like sources or identity profiles (who owns more than 100 sources? :sweat_smile:), other objects, like roles and access profiles could easily exceed that limit. Therefore, we need to plan for a scenario where we need to iterate over more than 100 items to make this a robust solution. Loops is viable for certain objects, like workflow reassignment, but loops will not be a viable solution for others. Therefore, we must look at the alternative iteration method of recursive workflows.

Synchronous recursion

Synchronous recursion is configured to run synchronously, which means the recursion will process each item in the list in the order in which they are received. For example, if the list [1,2,3] is provided to the recursive workflow, it will process 1 first, then 2 , and finally 3. Most of the object types can utilize synchronous recursion, and the resulting workflow will usually be very simple to build and understand. The only caveat to this approach is that it is much slower than asynchronous recursion since each workflow needs to finish before executing the next workflow in the list.

    flowchart TD
    Z[Invoke workflow with previous and new owner ID] --> A
    A[External Trigger] --> B
    B[Get a source owned by previous owner] --> C{If source was found}
    C -->|no| D[End workflow]
    C -->|yes| E[Change source owner to new owner]
    E --> F
    F[Invoke this workflow with previous and new owner ID again] --> A

Synchronous recursion will not work if you use the search API to get the list of objects to be reassigned. This is because the search API has a latency between when an action is taken on an object and when the updated data is synced into the search database. If you attempt to use the search API to support synchronous recursion, you may end up in a scenario where the recursion attempts to reassign an object that has already been reassigned, which may result in errors or a significant increase in execution time as it waits for the search database to update.

Asynchronous recursion

Asynchronous recursion isn’t possible for the the specific use case of object reassignment. A standard recursive workflow requires us to pass the list of data that needs to be processed to each iteration of the recursion. Because of the way inline variables work, you cannot reference arrays in inline variables. This is a problem because the workflow must know two things: The details of the object to reassign, and the ID of the new owner to assign it to.

Synchronous recursion works because we can simply pass the previous and new owner ID to each iteration of recursion, and we can use an operational API, like list sources, to get the next item in the list of objects to reassign within the workflow rather than relying on the items to be passed into the External Trigger as an array. Asynchronous recursion cannot work this way, and we must provide the full array of items to work on to the External Trigger. Since asynchronous recursion requires the full array of items in the trigger payload, we cannot add any context variables, like new owner ID, which would require inline variables and therefore would not work.

You can read more about the cons of recursion and why this nuance of inline variables matters. Just know that for now we must use synchronous recursion to solve this use case.

Creating the form

Before we implement our workflows, let’s create the form that will be sent to the identity’s manager. This form will ask the manager to provide a new owner of the objects. Start by navigating to the Forms dashboard and creating a new form.

Next, create a Text Field with the following configuration:

Apply the changes and then click on the form settings button. Navigate to “Inputs” and create the following input. This input will allow our workflow to supply the name of the previous owner that left the company.

Click on “Conditions” and create the following condition. This will disable the previous owner text field so the recipient of the form knows that it can’t be modified and is only there for informational purposes.

Exit the form settings menu and add a “Select Field” to the form with the following details. This will allow the form recipient to select a new identity to assign the objects to.

Make sure you save the form, and then go to the Workflows dashboard to create a new workflow.

Creating the reassignment workflows

Many of the object types that support reassignment follow very similar steps. Rather than go into detail on how to implement reassignment for each object type, I will cover two scenarios that should allow you to complete the reassignment workflows for all the object types.

Reassign sources, roles, access profiles, and entitlements

This method will require a separate workflow that will be invoked by the main workflow. It will also require a non-search API that can filter the results by owner.id. For example, list sources, list access profiles, list roles, and list entitlements can all be reassigned since they support the owner.id filter param.

To demonstrate this method, create a new workflow called “Reassign Sources” and add the “External Trigger” as the first step. Generate a “New Access Token” and save the “Client ID”, “Client Secret”, and “Client URL” somewhere safe so you can reference them later. This external trigger will expect the following input to be provided when it is invoked:

{
    "previousOwnerId": "1de880867624cbd7017642d8c8c81f67",
    "newOwnerId": "3bd180867624cbd7017642d8c8c9ffca"
}

previousOwnerId is the identity ID of the identity that left the company, and newOwnerId is the identity ID of new owner of the sources.

Next, add an “HTTP Request” action beneath the trigger. This action will be used to fetch the first source owned by the reassignFrom identity using the list sources endpoint. Configure it as follows.

  • Token URL is in the following format: https://{tenant}.api.identitynow.com/oauth/token
  • Client ID and Client Secret are the obtained from your personal access token. See this guide for more information on generating a PAT.
  • Request URL is https://{tenant}.api.identitynow.com/v3/sources?filters=owner.id%20eq%20%22{{$.trigger.previousOwnerId}}%22&limit=1
  • Leave the Request Content Type empty

After the HTTP Request, we need to check if a source was returned or not. This comparison operator will determine how we proceed with execution. If an item was returned, then we reassign the owner and execute this same workflow again to see if there is another source to reassign. If there was no item returned, then there are no more sources to reassign and we can end the recursive loop. Configure the comparison operator as follows.

If this comparison evaluates to True then we need to reassign the source owner using an HTTP Request action to call the source update endpoint. Add a new HTTP Request action beneath the “True” branch of the comparison operator, and configure it as follows.

image

  • The Authentication section should be setup exactly like the first HTTP Request.
  • Request URL is https://{tenant}.api.identitynow.com/v3/sources/{{$.hTTPRequest.body[0].id}}
  • Request Body is [{"op":"replace","path":"/owner","value":{"id":"{{$.trigger.newOwnerId}}"}}]

The last step in this workflow is to invoke this workflow again to reassign any remaining sources that the identity was an owner of. This is the “recursive” part of the workflow where it will invoke itself to mimic looping. To invoke this same workflow, create one final HTTP Request action beneath the “HTTP Request 1” action and configure it as follows.

image

  • Token URL will be the same as the other two HTTP Requests.
  • Client ID and Client Secret will be the values you saved when generating the access token for the External Trigger. This will not be your PAT credentials.
  • Request URL will be the URL that you saved when generating the access token for the External Trigger. It should look similar to this: https://devrel.api.identitynow.com/beta/workflows/execute/external/c79e0079-562c-4df5-aa73-xxxxxxxxxx
  • Request Body will be the exact same information that was received by the External Trigger.

Once all steps are configured, your finished source reassignment workflow should look like this.

To implement access profiles, roles, and entitlements, simply duplicate the “Reassign Sources” workflow for each and modify the URLs accordingly. You will also need to generate a new access token for the External Trigger for each and update the last HTTP Request to use the new client credentials and URL.

Reassign identity profiles

Unlike “Reassign Sources”, the identity profiles endpoint doesn’t support filtering by owner.id. This complicates things, and it makes recursion impossible. However it is still possible to implement this without recursion and using regular loops. Before we can proceed, you must determine the upper range of identity profiles that you can have in your environment. For example, let’s assume you will never have more than 150 identity profiles in your environment. This may seem like a very high number, but it will allow me to demonstrate how to use pagination to feed the loops data in 100 item increments.

To demonstrate this method, create a new workflow called “Reassign Identity Profiles” and add the “External Trigger” as the first step. Generate a “New Access Token” and save the “Client ID”, “Client Secret”, and “Client URL” somewhere safe so you can reference them later. This external trigger will expect the following input to be provided when it is invoked:

{
    "previousOwnerId": "1de880867624cbd7017642d8c8c81f67",
    "newOwnerId": "3bd180867624cbd7017642d8c8c9ffca",
    "offset": 0
}
  • previousOwnerId is the identity ID of the identity that left the company
  • newOwnerId is the identity ID of new owner of the sources.
  • offset is the pagination offset that will be used to fetch the next 100 items for the loop.

Since list identity profiles doesn’t support the owner.id filter param, we must invoke the appropriate number of HTTP Request calls to meet our maximum, in this case 150. These invocations will happen in the main workflow, so we don’t need to worry about them quite yet. Instead, this workflow will handle the steps for fetching the next 100 items from the supplied offset and reassigning each object accordingly. Add a single HTTP Request below the External Trigger, and configure it accordingly:

  • Add your PAT details to the authentication section. This is detailed in the “Reassigning Sources” section above.
  • Set the Request URL to https://{tenant}.api.identitynow.com/v3/identity-profiles?limit=100&offset={{$.trigger.offset}}&sorters=name. Replace {tenant} with your tenant name. This will get 100 items from the offset provided in the trigger input.

Next, add a loop below the HTTP Request and configure it as follows:

  • Loop Input is $.hTTPRequest.body
  • Loop Context is $.trigger

Inside the loop, add a Compare Strings operator, and configure it as follows:

  • Value 1 is $.loop.loopInput.owner.id
  • Comparison Operator is “Equals”
  • Value 2 should be a “Choose Variable” with $.loop.context.previousOwnerId as the value.

Setup the two branches as follows.

HTTP Request 1 should be configured as follows to allow it to update the identity profile to the new owner.

  • Configure the authentication the same as the first HTTP Request.
  • Request URL is https://{tenant}.api.identitynow.com/beta/identity-profiles/{{$.loop.loopInput.id}}. Replace {tenant} with your tenant name.
  • Method is “PATCH”
  • Request Body is:
[
  { 
    "op":"replace",
    "path":"/owner",
    "value": {
      "id":"{{$.loop.context.newOwnerId}}"
    }
  }
]

The final workflow should look like this.

To invoke this workflow from the main workflow, simply add the appropriate number of HTTP Requests to the main workflow to cover the maximum number of identity profiles your environment can have. If your confident your environment won’t have more than 150, then the first HTTP Request will send this payload to the External Trigger of this workflow.

{
  "previousOwnerId": "{identity ID}",
  "newOwnerId": "{identity ID}",
  "offset": 0
}

This will allow the first invocation of the workflow to retrieve and process the first 100 identity profiles.

The second HTTP Request will advance the offset by 100 so the workflow can retrieve and process the next 100 items.

{
  "previousOwnerId": "{identity ID}",
  "newOwnerId": "{identity ID}",
  "offset": 100
}

Just keep sending HTTP Requests and advancing the offset by 100 until you reach your desired limit. This is a brute force approach to the problem, but given the fact that there should not be many identity profiles it is very feasible to do this.

Reassigning governance groups

Reassigning governance groups works the same as identity profiles. You must determine the upper limit for the number of governance groups you expect to have in your tenant, and then invoke the workflow for each set of 100 governance groups (determined by the offset) until you reach that limit. Use list governance groups to get each page of 100 governance groups, and use update governance group to change the owner. If you are certain that identities will never have more than 100 governance groups, then you can skip the pagination using list governance groups and instead make a single API call to the search API. The query you will use is as follows.

{
    "indices": [
        "identities"
    ],
    "query": {
        "query": "2c9180867624cbd7017642d8c8c81f67"
    },
    "queryResultFilter": {
        "includes": [
            "owns.governanceGroups"
        ]
    }
}

This will produce the following response:

[
    {
        "owns": {
            "governanceGroups": [
                {
                    "name": "Test Group",
                    "id": "7b7a4d82-fad4-46ec-8d5a-842843d09f0e"
                },
                {
                    "name": "Test Group 2",
                    "id": "12538ecf-60d0-44b4-9273-d1ba578ef384"
                }
            ]
        },
        "_type": "identity",
        "type": "identity",
        "_version": "v7"
    }
]

You can then loop over the governanceGroups to quickly reassign them. This strategy of using the search API to reassign objects that will not have more than 100 items for a given identity can also be used for entitlements, sources, access profiles, and roles.

Reassigning workflows

Reassigning workflow owners is a rather simple affair and does not require pagination or recursion. Since workflows are limited to 100 objects per tenant at this time, a single loop can handle the reassignment. As with the other reassignment workflows, begin by creating a new workflow called “Reassign Workflows” and add an External Trigger. Generate a new access token for the trigger and save the details somewhere safe. This trigger will accept the following payload.

{
  "previousOwnerId": "{identity ID}",
  "newOwnerId": "{identity ID}"
}

Next, add an HTTP request that will invoke the following GET endpoint, replacing {tenant} with your tenant name.

https://{tenant}.api.identitynow.com/beta/workflows

Next, add a loop to the workflow and configure it as follows:

  • Loop Input is $.hTTPRequest.body
  • Context is $.trigger

Inside the loop, add a Compare Strings operator, and configure it as follows:

  • Value 1 is $.loop.loopInput.owner.id
  • Comparison Operator is “Equals”
  • Value 2 should be a “Choose Variable” with $.loop.context.previousOwnerId as the value.

Setup the two branches as follows.

HTTP Request 1 should be configured as follows to allow it to update the identity profile to the new owner.

  • Configure the authentication the same as the first HTTP Request.
  • Request URL is https://{tenant}.api.identitynow.com/beta/workflows/{{$.loop.loopInput.id}}. Replace {tenant} with your tenant name.
  • Method is “PATCH”
  • Request Body is:
[
  { 
    "op":"replace",
    "path":"/owner",
    "value": {
      "id":"{{$.loop.context.newOwnerId}}",
      "type": "IDENTITY"
    }
  }
]

The final workflow should look like this.

When invoking this workflow from the main workflow, simply provide the previous and the new owner ID in the payload.

{
  "previousOwnerId": "{identity ID}",
  "newOwnerId": "{identity ID}"
}

Creating the main workflow

The main workflow will most likely be triggered by Identity Attributes changed event where the lifecycle state has changed to a leaver value, like “terminated”. It’s always a good idea to add a trigger filter to avoid invoking workflows for events that you don’t want to process. For this trigger, we will add the following filter so we only execute this workflow when an identity is terminated.

$.changes[?(@.attribute == "cloudLifecycleState" && @.newValue == "terminated")]

Your trigger should look like this.

Next, add a Get Identity action below the trigger. Configure this action to get the details of the terminated identity.

This trigger and Get Identity will provide us with the name and ID of the terminated identity and their manager, which we will use to send the form we created earlier to the manager. Add a “Form” action beneath the trigger, and configure it as follows.

Once the manager submits the form, the workflow will resume with the identity of the new owner. The next step is to begin invoking the External Trigger of each reassignment workflow to start processing the reassignment.

Invoke source, entitlement, access profile, and role reassignments

Source, entitlement, access profile, and role reassignments will each have their own separate workflow that will need to be invoked by their External Trigger. To invoke each of these reassignment workflows, simply. add an HTTP Request for each one after the Form action and configure it as follows.

  • Authentication Type is “OAuth 2.0 - Client Credentials Grant”
  • Token URL is https://{tenant}.api.identitynow.com/oauth/token. Replace {tenant} with your tenant name.
  • Client ID is the client ID generated from the External Trigger for the particular workflow you are invoking.
  • Client Secret is the client secret generated from the External Trigger for the particular workflow you are invoking.
  • Request URL is the URL generated from the External Trigger for the particular workflow you are invoking.
  • Method is “POST”
  • Request Content Type is “JSON”
  • Request Body is
{
  "previousOwnerId":"{{$.trigger.identity.id}}",
  "newOwnerId": "{{$.form.formData.newOwner}}"
}

If you are reassigning all four object types, then you should have four HTTP Requests. Your workflow will look like this so far.

Invoke identity profiles reassignment

To invoke the identity profiles reassignment workflow, add an HTTP Request action and configure it to invoke the External Trigger of the identity profiles reassignment workflow. The payload for should be as follows:

{
  "newOwnerId": "{{$.form.formData.newOwner}}",
  "previousOwnerId": "{{$.trigger.identity.id}}",
  "offset": 0
}

If your organization can have more than 100 identity profiles, then copy this action as many times as necessary to reach the limit of your org. For example, if your org may have up to 300 identity profiles, copy this action twice and increase each offset by 100. The payloads for each copy would look like this.

{
  "newOwnerId": "{{$.form.formData.newOwner}}",
  "previousOwnerId": "{{$.trigger.identity.id}}",
  "offset": 100
}
{
  "newOwnerId": "{{$.form.formData.newOwner}}",
  "previousOwnerId": "{{$.trigger.identity.id}}",
  "offset": 200
}

Invoke governance groups reassignment

Invoking the governance group reassignment workflow is the same as invoking the identity profiles reassignment workflow. You must determine up front how many governance groups you expect to have in your tenant in order to know how many HTTP Requests to create to invoke the reassignment workflow. For example, if your tenant will never have more than 250 governance groups, then you will need three HTTP Requests, which will be configured the same as identity profiles above. The only difference is that you must configure the HTTP Request to use the client credentials for your governance group reassignment workflow.

Invoke workflows reassignment

Invoking the workflows reassignment workflow is exactly the same as for sources, roles, access profiles, and entitlements. Configure a single HTTP Request to use the credentials of the External Trigger for the workflows reassignment workflow, and send a POST request with the following request body.

{
  "newOwnerId": "{{$.form.formData.newOwner}}",
  "previousOwnerId": "{{$.trigger.identity.id}}"
}
7 Likes

@colin_mckibben This is pretty cool, thanks for building and sharing. Just curious, do you know about the roadmap to add HTTP response handling into Workflows? Currently, if there isn’t a 2** response, the Workflow fails. Some API endpoints return data with 4** responses and would like to know when IDN can handle those.

Hello Nicholas. We have a backlog roadmap item to handle this by allowing admins to specify the action/activity when an error is returned, i.e. Continue On Error as opposed to existing/failing. I cannot commit to a given date for now but would you mind submitting some additional details around how you would want to handle the response?

Also could you add comments and votes to this IDEA:
https://ideas.sailpoint.com/ideas/GOV-I-1912

Thanks @tburt! I actually don’t know that this idea would meet my needs exactly. I would want to be able to receive the response code and any response body (if one exists) and be able to carry on the workflow based on data. I have one use case that can just move on when the response is not a 2** (it currently just fails) and another that would need to assess the response body, which contains “No users found.” I did vote that one, as it would be useful in itself. Thanks again.

@cassidiopia

The response codes do/should live in the execution log of the workflow today (again caveat on whether or not the application returns the response details). The issue is that most 400 errors are not something that would typically require a retry. Things like bad request, unauthorized, forbidden, etc… are not something we would want to issue a retry for. We do, however, already handle some of the 500 error types as retry natively on the backend. Again things that make sense like Service Unavailable, Gateway Timeout, etc… are automatically retried.

Having said all of that, I believe the real value add would be to allow admins the ability to configure how they want the workflow to proceed when some of these errors are hit. For example you could configure the “Continue on error” option and pass the 401 “Unauthorized” error into a follow on comparison and send an email notifying someone as opposed to exiting the workflow. This is the AHA idea referenced above.

Again, the execution logs will pick up and capture any response codes if the application returns that response detail.

1 Like

Ahh, yes, that makes sense. I was under-thinking it from the POV of it resolving timeout issues, but the functionality would do the same. I’m aware the response code and body are currently logged, so just being able to continue on would be the win!

Thanks!

1 Like

Can this workflow or a similar one be utilized to reassign Tasks that remain assigned to an identity in a terminated state? Setting up a reassignment after the fact will only affect new work tasks. As an admin, it would be great to be able to oversee/override such things in the UI to be able to push tasks along as needed.

Without digging deeper into the APIs required to do this, it sounds like a similar and plausible use case for workflows to handle.

Hi @colin_mckibben, it seems that inactive identities are included for selection. Am I missing some setting to exclude them? Is this a desired behavior?

You can change the “Option Type” to be “Search” and then create a search query to only return the identities you want to be selectable.

Thanks for your reply, @colin_mckibben .

It would be nice to have a solution without limitations. If you want to select all active users, using the search query you are limiting the number of users you can have in the dropdown box, right?

If the search query returns more than 10,000 items, only the first 10,000 items will be returned.

Fortunately we haven’t reached this limitation in the current project, but nonetheless, it’s a bummer to take into consideration all this limitations (loops, number of users, active/inactive users, …)

On the release notes from Nov 18, 2024 ( https://community.sailpoint.com/t5/SaaS-Release-Notes/tkb-p/saas-release-notes ) it was mentioned that all dropdown lists would filter out inactive identities. Why is here not the case? Is this a known bug?

When selecting an identity from a dropdown list, inactive identities are filtered out on all pages except for the Update Correlation overlay of the Account Management UI.