Find Multiple Accounts for Sources Redux – Excel Hack


So after doing the last post on how to find multiple accounts from a single source via API, I was asked by few people and one my clients to see if there is an easy way to do this.

I thought of my trusted Excel to find an easier way to do so. Here are the steps

  • Generate Identities Report via Admin -> Identities -> Identity List UI and download CSV

  • Open the CSV in Excel and look at Column “Source Accounts” ( Column P when I generated it). You will see some data like this. In this example I have a duplicate ServiceNow account for this user. He also has 1 x Workday, 1 x IDN cube and 1 x Okta account.
22222 (Workday [source]), 22222 (IdentityNow), [email protected] (Okta [source]), [email protected] (ServiceNow [source]), [email protected] (ServiceNow [source])
  • Then on another column (Column S in my instance) I created a new header called “ServiceNow [source]” and applied the following formula
=(LEN(P2)-LEN(SUBSTITUTE(LOWER(P2),LOWER($S$1),"")))/LEN($S$1)

Where

  • P2 = The column containing the “Source Accounts” value
  • $S$1 = Column Header “ServiceNow [source]”
  • LOWER() = Used to lowercase both strings as SUBSTITUTE is case sensitive

This will give me count of number of times “ServiceNow [source]” repeats in that line of text.

Done.

You can filter and find the rows you are interested in and give you all the users who have multiple accounts for a single source. You can expand this to other sources like I did above and create multiple columns.

 

How to Integrate ServiceNow ServiceDesk with IdentityNow

There has been a surge recently in integrating SNOW (ServiceNow) with IDN (IdentityNow) to generate tickets during provisioning or certification events. Many use-cases come to mind: For example, on an identity joiner event you might want to create SD ticket to give laptop and phones to an employee. Or give / remove an application access which is not directly integrated with IDN. Below I will show you how to get up and running in less than 30 min with the integration

Notes

  • This integration is fast changing and the guide is correct as of 30th April 2021
  • This integration is an additional license cost to IDN. Please talk to your CSM about it.
  • This assume you already have a SNOW tenant in your company
  • The guide doesn’t go in-depth into advance scenarios requiring Velocity or rules.

Prerequisite

  • SailPoint for Service Desk Installed on SNOW tenant

  • SNOW Governance connector installed on IDN and users correlated between IDN and SNOW connector
  • Configure at least one virtual appliance cluster.
  • Configure at least one source to generate manual tasks for Provisioning.
  • Be familiar with Apache Velocity scripting.
  • Know the authentication option (Basic or OAuth2) to use for IdentityNow to authenticate into ServiceNow. For token generation and OAuth Client setup on the ServiceNow instance, see Create an endpoint for clients to access the instance.
  • Have the permissions requirements for a service account: A Service Desk Administrator must be assigned the x_sap_sdim.admin role.

Gather Information

  • Create a source which is going to be used for provisioning and has entitlements a user can request. In this example a flat file source with 3 entitlements.

  • Create an Access Profile and Requestable Role for this source

  • To set an integration source owner, select an org admin user and get their identity ID via API call {{api-url}}/v3/search 2c91808673a16fb20173b8523ecd0021
  • Get the Cluster ID of your VA via API Call {{api-url}}/beta/managed-clusters 2c918088737cf45e0173b8fb6727040d
  • Get the id for the provision source (Say flat file) created via {{api-url}}/beta/sources/ : 2c91808578daf04e0178dcb83b1a0173
  • Get the id for the SNOW governance source which correlates users between SNOW and IDN via {{api-url}}/beta/sources/ : 2c91808478daf0580178dcff779001b4
  • Get the SNOW Application sys_id from SNOW. 8053xxxedbffb300exxxxxxxdbxxxxcx123

 

Integration

Use the following API to create the integration: https://developer.sailpoint.com/apis/beta/#operation/createServiceDeskIntegration

Please populate the payload (attached separately) and replace the parameters with value

Parameter

Value

<ownerId>

2c91808673a16fb20173b8523ecd0021

<clusterId>

2c918088737cf45e0173b8fb6727040d

<Provision_Source_ID>

2c91808578daf04e0178dcb83b1a0173

<SNOW_Sys_ID>

8053xxxedbffb300exxxxxxxdbxxxxcx123

<SNOW_URL>

https://XXXX.service-now.com/

<requesterSource>

2c91808478daf0580178dcff779001b4

<username>

SNOW user

<password>

User Password

 

{
    "name": "ServiceNow New SDIM",
    "description": "ServiceNow Service Desk Integration for IdentityNow",
    "type": "ServiceNow SDIM",
    "ownerRef": {
        "type": "IDENTITY",
        "id": "<ownerId>"
    },
    "clusterRef": {
        "type": "CLUSTER",
        "id": "<clusterId>"
    },
    "cluster": null,
    "managedSources": [],
    "provisioningConfig": {
        "universalManager": false,
        "managedResourceRefs": [{
                "type": "SOURCE",
                "id": "<Provision_Source_ID>"
            }
        ],
        "planInitializerScript": {
            "source": "\n                \n                    import sailpoint.object.Identity;\n                    import sailpoint.object.Link;\n                    import sailpoint.object.IntegrationConfig;\n                    import sailpoint.object.Application;\n                    import sailpoint.api.IdentityService;\n\n                    /**\n                     * Fields\n                     */\n                    String applicationType = \"ServiceNow\";\n                    // Identity attribute on SailPoint/ UserID on ServiceNow\n                    String userId = \"sys_id\";\n\n                    /**\n                     * Get ServiceNow Account name for identity using application type - ServiceNow\n                     */\n                    private String getLinkByAppType(Identity identity) {\n                        if (identity == null) {\n                            return null;\n                        }\n\n                        List links = identity.getLinks();\n                        String linkName = null;\n                        if (links != null ) {\n                            for (Link link : links) {\n                                String appType = link.getApplication().getType();\n                                if (appType != null && appType.equals(applicationType)) {\n                                    if(link.getAttribute(userId) != null) {\n                                        String userName = link.getAttribute(userId);\n                                        if(userName != null) {\n                                            linkName = userName;\n                                            break;\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        return linkName;\n                    }\n\n                    /**\n                     * Get ServiceNow Account name for Identity using application name\n                     */\n                    private String getLinkByAppName(Identity identity, String requesterSource) {\n                        if (identity == null) {\n                            return null;\n                        }\n\n                        String linkName = null;\n                        if (requesterSource != null) {\n                            Application app = context.getObjectByName(Application.class, requesterSource);\n                            IdentityService identService = new IdentityService(context);\n                            List links = identService.getLinks(identity, app);\n                            Link link = null;\n                            if (links != null && links.size() > 0) {\n                                link = links.get(0);\n                            }\n\n                            if (link != null) {\n                                String userName = (String) link.getAttribute(userId);\n                                if (userName != null) {\n                                    linkName = userName;\n                                }\n                            }\n                        }\n\n                        return linkName;\n                    }\n\n                    ////////////////////////////////////////////////////////////////\n                    // Main\n                    ////////////////////////////////////////////////////////////////\n                    Map arguments = (Map)plan.getArguments();\n                    List requesterList = plan.getRequesters();\n                    Identity requester = null;\n                    if (requesterList != null) {\n                        requester = requesterList.get(0);\n                        if (requester != null && requester.getName() != null) {\n                            requester = context.getObjectByName(Identity.class, requester.getName());\n                        }\n                    }\n\n                    String appName = integration.getName();\n                    if (appName != null) {\n                        Application appObject = context.getObjectByName(Application.class, appName);\n                        requesterSource = appObject.getAttributeValue(\"requesterSource\");\n                    }\n\n                    // Get ServiceNow Account name\n                    if (requesterSource != null) {\n                        openedBy = getLinkByAppName(requester, requesterSource);\n                        requestedFor = getLinkByAppName(identity, requesterSource);\n                    } else {\n                        openedBy = getLinkByAppType(requester);\n                        requestedFor = getLinkByAppType(identity);\n                    }\n\n                    if (requestedFor != null) {\n                        arguments.put(\"requested_for\", requestedFor);\n                    }\n\n                    if (openedBy != null) {\n                        arguments.put(\"opened_by\", openedBy);\n                    }\n                \n            "
        }
    },
    "attributes": {
        "serviceRequest": {
            "checkStatus": {
                "statusMap": {
                    "closed_complete": "Committed",
                    "closed_rejected": "Failed",
                    "requested": "Queued",
                    "in_process": "Queued",
                    "closed_cancelled": "Failed",
                    "closed_incomplete": "Failed",
                    "closed_skipped": "Committed"
                },
                "resource": "/api/now/table/sc_request?number=$ticketId&sysparm_fields=request_state",
                "responseElement": "$.result[0].request_state",
                "checkStatusQueryParam": null
            },
            "provision": {
                "request": {
                    "short_description": "SailPoint Access Request $!plan.arguments.identityRequestId",
                    "opened_by": "$!plan.arguments.opened_by",
                    "req_description": "Service Request created by Service Desk Integration Module (Service Desk).",
                    "description": "#if($request.operation == 'Create') Create Account on application $request.resource #else For $request.id in application $request.resource #end #if ($request.items) $newline #foreach ($item in $request.items) #if ($item.name == '*disabled*' && $item.value == 'true') Disable Account. $newline #elseif ($item.name == '*disabled*' && $item.value == 'false') Enable Account. $newline #elseif ($item.name == '*locked*' && $item.value == 'false') Unlock Account. $newline #else $!item.Operation $item.name: $item.value $newline #end #end #else $newline $!request.Operation Account #end",
                    "requested_for": "$!plan.arguments.requested_for"
                },
                "requestRootElement": "items",
                "resource": "/api/x_sap_sdim/sailpoint_cart_js_api/create_ticket",
                "responseElement": "$.result.request_number",
                "catalogItem": {
                    "<Provision_Source_ID>": "<SNOW_Sys_ID>"                }
            }
        },
        "ticketType": "serviceRequest",
        "authenticationType": "Basic",
        "password": "<password>",
        "requesterSource": "<requesterSource>",
        "url": "<SNOW_URL>",
        "username": "<username>"
    }
}

 

Integration Testing

  • For the provision source, create some Entitlements and create Access Profile and requestable role.
  • Request for a SNOW correlated user for the source

  • In Account Activity a pending request will show for the request with REQ Ticket Number

  • In SNOW “Service Catalog -> Requests” the ticket number shows up

  • Click on RITM and you will see the description of request

  • Once the ticket is closed, the Access Request in IDN will close after interval checks. This frequency can be configured as well (See Configuring a Schedule for Status Checks section in SDIM Integration API Reference Guide)

That should be it. You should be able to create provisioning tickets for the source now.

Hope that helped!!!

 

 

How to attach a rule in IdentityNow #IDN101

Happy New Year Folks!!!

Hopefully this year is better than previous and as always.. stay safe!!!

Anyone who would have worked with IDN would have used or encountered rules. They are wildly used and inevitable component of any deployment to achieve your requirements. So you must be well aware that no client, partner and in fact most of internal SailPoint PS/ES also don’t have access to deploy their rules to their tenants. These rules are reviewed by a specialist team within ES and they upload it to the tenant. There are some very good reasons for doing so due to the SaaS nature of our product and IDN architecture.

There is a lot of work being done to reduce dependency on rules and also a lot in pipeline to make this process simpler. This also does increase our own workload on tickets, review and deployment and we want to reduce it too as IDN has exploded into the world in past few years and new tenants onboarding everyday.

Current Process of Rule upload

  • Send it to SailPoint as an ES ticket. Support will forward it to ES if not as rule review is a billable task.
  • Rule review is done and uploaded to tenant.
  • Ticket is responded to and closed after confirmation.

To make this process faster and seamless do follow some best practices

  • Read the whole rule guide .
  • Run the rule validator and send us the output of it.
  • Make sure the naming convention is followed for the rule names
  • Best to keep the same structure and print of code so it’s easy to see the difference in git for us.

Once the rule has been uploaded to your tenant, there might be a need of additional steps to attach the rule to your source (depending on the type of rule). Once the rule is uploaded you will need the RuleID for some of the rules (mentioned below) to attach it to your source. Please ask the ES person for it if not already given. It will be a long random GUID. We use Postman to execute the API calls but feel free to use your choice of client.

There is a great guide written internally by Neil McGlennon but I am expanding on it.

Few common things found below

  • {ruleID}: This is the rule ID generated after rule is uploaded to your tenant. Ask ES for it if not given already
  • {Rule Name}: This is the name of your rule.
  • {id}: This is the externalID of the source you want to attach it to. It is a long GUID and NOT the shortID found in the URL. You can obtain it by few different methods but simplest is by doing a GET /cc/api/source/get/{shortSourceID} where “shortSourceID” is the ID of the source found in the URL when clicking on it in the tenant.
  • All the API calls use https://{tenantname}.api.identitynow.com/ as the URL (before /beta/…)
  • All of these examples use a PATCH for a partial source update, however PUT operations will work too, as long as the entire source object model is provided.
  • For the PATCH operations, a op key has to be provided. For new configurations this is typically set to add as our example shows, however they can be any of the following:
    • add – Add a new value to the configuration. Use this operation if this is the first time you are setting the value, i.e. it has never been configured before.
    • replace – Use this operation to change the existing value. Use this operation if you are updating the value, i.e. you want to change the configuration.
    • remove – Removes a value from the configuration. Use this operation if you want to unset a value.

Beware! Removals can be destructive if the path isn’t configured properly. This could negatively alter your source config!

Identity Attribute Rule

This rule doesn’t need to be attached via API as it can be seen via UI under Attribute Mappings

 

Account Profile Attribute Generator

This rule doesn’t need to be attached via API as it can be seen via UI under Create Profile for any source which has one.

 

Account Profile Attribute Generator (from Template)

This rule doesn’t need to be attached via API as it can be seen via UI under Create Profile for any source which has one. (as above)

Correlation Rule
PATCH /beta/sources/{id}
Content-Type: application/json-patch+json

[
   {
      "op":"add",
      "path":"/accountCorrelationRule",
      "value":{
         "type":"RULE",
         "id":"{ruleID}",
         "name":"{Rule Name}"
      }
   }
]
Manager Correlation Rule
PATCH /beta/sources/{id}
Content-Type: application/json-patch+json

[
   {
      "op":"add",
      "path":"/managerCorrelationRule",
      "value":{
         "type":"RULE",
         "id":"{ruleID}",
         "name":"{Rule Name}"
      }
   }
]
Before Provisioning Rule
PATCH /beta/sources/{id}
Content-Type: application/json-patch+json

[
   {
      "op":"add",
      "path":"/beforeProvisioningRule",
      "value":{
         "type":"RULE",
         "id":"{ruleID}",
         "name":"{Rule Name}"
      }
   }
]
AfterCreate, AfterModify, AfterDelete, BeforeCreate, BeforeModify, BeforeDelete Rules
PATCH /beta/sources/{id}
Content-Type: application/json-patch+json

Note: Because the value is a list, all available AfterCreate, AfterModify, BeforeCreate, and BeforeModify rules will need to set in the same list.

[
   {
      "op":"add",
      "path":"/connectorAttributes/nativeRules",
      "value":[
         "{Rule Name 1}",
         "{Rule Name 2}"
      ]
   }
]
Build Map Rule
PATCH /beta/sources/{id}
Content-Type: application/json-patch+json

[
   {
      "op":"add",
      "path":"/connectorAttributes/buildMapRule",
      "value":"{Rule Name}"
   }
]
JDBC Provisioning Rule
PATCH /beta/sources/{id}
Content-Type: application/json-patch+json

[
   {
      "op":"add",
      "path":"/connectorAttributes/jdbcProvisionRule",
      "value":"{Rule Name}"
   }
]
WebServiceBeforeOperation Rule
PATCH /beta/sources/{id}
Content-Type: application/json-patch+json

Note: Replace * with the index of operation. For example 0, 1, 2, etc.

[
   {
      "op":"add",
      "path":"/connectorAttributes/connectionParameters/*/beforeRule",
      "value": "{Rule Name}"
   }
]
WebServiceAfterOperation Rule
PATCH /beta/sources/{id}
Content-Type: application/json-patch+json

Note: Replace * with the index of operation. For example 0, 1, 2, etc.

[
   {
      "op":"add",
      "path":"/connectorAttributes/connectionParameters/*/afterRule",
      "value": "{Rule Name}"
   }
]

 

Once any of the rule is attached, it’s ready for use immediately by the source or profile.

Hopefully this covers all rule types and if you have any issues with attaching a rule in your tenant, please feel free to reach out to me or to Support / ES team.

PSA: New Transform Types Available for IdentityNow

Hey Folks!!!

Since the last time we chatted about transforms and had said we are in process of adding new types in future. Well.. here we are with few news ones fresh out of the oven!!!

They will greatly help you achieve your goals without the need of rules. Please do revisit them while doing your design or upliftment. The goal is to minimise dependency on rules and by using transforms it gives you more control over the testing and deployment process.

For some new noteworthy ones

The date math transform allows you to add, subtract and round components of a timestamp to or from an incoming value. It also allows you to work with a referential value of “now” to run operations against the current date and time instead of a fixed value.

Imagine using this for LCS calculation if simple or just to get some dates for different systems say 10 days in future or so.

The username generator transform allows you to specify logic to use when attempting to derive a unique value for an attribute in an account create profile, . Oftentimes this can be as simple as combining parts of a user’s name and/or HR data (e.g., firstName.lastName), but sometimes generator logic such as a uniqueness counter might be needed to find a unique value in the target system (e.g., firstName.lastName1 if firstName.lastName is already taken).

How about ditching an AttributeGenerator rule and using this? 

The UUID generator is a simple transform allows you to create a universal unique id (UUID) in the form of a 36-character string. The underlying code is written in such a way as to provide a 1 in 68,719,476,736 chance of creating a string that actually collides with another string within the tenant.

Generate UUID on the fly

The name normalizer transform allows you to clean or standardize the spelling of strings coming in from source systems. Most commonly, this pertains to names and other proper nouns, but the transform is not necessarily limited to those data elements.

Get rid of the WiERd CasINg

The get reference identity attribute transform is an out-of-the-box rule transform provided via SailPoint’s Cloud Services Deployment Utility rule. It allows you to easily get the identity attribute of another user from within a given identity’s calculation. As a convenience feature, the transform allows you to use “manager” as a referential lookup to the target identity.

Want the manager’s employee number, email, phone and other details listed easily on the profile?? so easy now!!!

And so many more added.. Do review the full list here and see what can benefit you from removing rules and going down the transform path

Nested Transforms for Dummies: Step-by-Step Guide #IDN101

One of the most basic things everyone needs to do in SailPoint IDN (IdentityNow) is to write transforms to create an Identity Profile for business requirements.

SailPoint has a an excellent guide on what a transforms is and detailed list of transforms available for IDN and is pretty comprehensive. 

The simple ones are pretty easy to implement. But we always run into creating complex nested transforms to achieve our goals. It looked daunting to me at first but I started to get the hang of it. I would like to explain in very basic terms how to easily achieve this.

Let’s take a business case here to explain easily.

Requirement

Build an emailPrefix attribute with firstName and lastName from Workday source which will be eventually used to generate an email address.

Logic

Now if we break down the requirement into logic, we need to do the following

  • Get firstName from Workday source
  • Get lastName from Workday source
  • Concat the two with a period (.) in the middle
  • Remove all spaces from the final value.

Since this is an emailPrefix to be used to generate an email attribute, it can’t contain spaces. There can be other requirements like special characters etc but let’s keep it simple here (that is just a matter of proper regex).

Build

Now if you look at the transforms guide you will need the following transforms

To get the attributes from a source – accountAttribute

{
  "attributes": {
    "attributeName": "firstName",
    "sourceName": "Workday"
  },
  "type": "accountAttribute"
}

{
  "attributes": {
    "attributeName": "lastName",
    "sourceName": "Workday"
  },
  "type": "accountAttribute"
}

To concat the two attribute with a period – concat

{
   "type":"concat",
   "attributes":{
      "values":[
         "firstName code block",
         ".",
         "lastName code block"
      ]
   }
}

And then finally we need to do a replace block to remove all spaces from the final result (note the \\s is the put \s as literal in JSON while passing it via REST API)

{
  "type": "replace",
  "attributes": {
      "regex": "\\s",
      "replacement": ""
  }
}

Now we need to join the three block. First we will begin with replacing the “firstName code block” and “lastName code block” with the accountAttribute block we had done above.

{
   "type":"concat",
   "attributes":{
      "values":[
         {
            "attributes":{
               "attributeName":"firstName",
               "sourceName":"Workday"
            },
            "type":"accountAttribute"
         },
         ".",
         {
            "attributes":{
               "attributeName":"lastName",
               "sourceName":"Workday"
            },
            "type":"accountAttribute"
         }
      ]
   }
}

This will give us the concatenated value of “firstName.lastName”. But now we want to remove all spaces from it as it will be used for email address generation. 

If you look at the replace block above, we need to do two additional things to the code

  1. Give it an “id” key as we want to name this final transform for mapping
  2. Give it an “input” key as we want to explicitly define the inputs for this type (i.e. the concatenated string) and not use implicit value (i.e. from the IDN mapping). Do read about the difference in the transform guide.

So the new skeleton code for replace will become

{
    "id": "Calculate Email Prefix",
    "type": "replace",
    "attributes": {
        "input": {},
        "regex": "\\s",
        "replacement": ""
    }
}

The final step is now pretty easy. Replace the entire input value with the built concat value above.

{
    "id":"Calculate Email Prefix",
    "type":"replace",
    "attributes":{
       "input":{
          "type":"concat",
          "attributes":{
             "values":[
                {
                   "attributes":{
                      "attributeName":"firstName",
                      "sourceName":"Workday"
                   },
                   "type":"accountAttribute"
                },
                ".",
                {
                   "attributes":{
                      "attributeName":"lastName",
                      "sourceName":"Workday"
                   },
                   "type":"accountAttribute"
                }
             ]
          }
       },
       "regex":"\\s",
       "replacement":""
    }
 }

And that is it!! You have built your first nested transform. It gives you the immense power to build a deep nested transform for complex logics to get glorious and simplified results in the end.

Learnings

I am not an expert in this and still learning this every day even after playing with it for more than a year but here are my learnings

  • Write down the logic you want to achieve
  • Break it down to individual code blocks
  • Write down the nested logic which will achieve you the result in best way possible (in above example = get the attributes -> concat it -> replace spaces). We could have also done this by say getting each attribute, remove spaces from them individually and then doing a concat of the final result – but this is inefficient and longer code. So understanding the logic and making it smart and short is best way forward.
  • Start working from inside block to outside and encapsulating them to achieve result
  • ALWAYS use a good code editor with syntax highlighter – My fav is VS Code with various plugins (makes it an awesome Swiss army knife for coders).

Hope this helped you!!!

Stay tuned for some more tips and #IDN101