created
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.docusaurus
|
||||||
|
.cache-loader
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
41
README.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Website
|
||||||
|
|
||||||
|
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```
|
||||||
|
$ yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
|
||||||
|
Using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ USE_SSH=true yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Not using SSH:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||||
3
babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||||
|
};
|
||||||
12
blog/2019-05-28-first-blog-post.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
slug: first-blog-post
|
||||||
|
title: First Blog Post
|
||||||
|
authors:
|
||||||
|
name: Gao Wei
|
||||||
|
title: Docusaurus Core Team
|
||||||
|
url: https://github.com/wgao19
|
||||||
|
image_url: https://github.com/wgao19.png
|
||||||
|
tags: [hola, docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
44
blog/2019-05-29-long-blog-post.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
slug: long-blog-post
|
||||||
|
title: Long Blog Post
|
||||||
|
authors: endi
|
||||||
|
tags: [hello, docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the summary of a very long blog post,
|
||||||
|
|
||||||
|
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
|
||||||
|
|
||||||
|
<!--truncate-->
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
|
||||||
20
blog/2021-08-01-mdx-blog-post.mdx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
slug: mdx-blog-post
|
||||||
|
title: MDX Blog Post
|
||||||
|
authors: [slorber]
|
||||||
|
tags: [docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
|
||||||
|
Use the power of React to create interactive blog posts.
|
||||||
|
|
||||||
|
```js
|
||||||
|
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
<button onClick={() => alert('button clicked!')}>Click me!</button>
|
||||||
|
|
||||||
|
:::
|
||||||
BIN
blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg
Normal file
|
After Width: | Height: | Size: 94 KiB |
25
blog/2021-08-26-welcome/index.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
slug: welcome
|
||||||
|
title: Welcome
|
||||||
|
authors: [slorber, yangshun]
|
||||||
|
tags: [facebook, hello, docusaurus]
|
||||||
|
---
|
||||||
|
|
||||||
|
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
|
||||||
|
|
||||||
|
Simply add Markdown files (or folders) to the `blog` directory.
|
||||||
|
|
||||||
|
Regular blog authors can be added to `authors.yml`.
|
||||||
|
|
||||||
|
The blog post date can be extracted from filenames, such as:
|
||||||
|
|
||||||
|
- `2019-05-30-welcome.md`
|
||||||
|
- `2019-05-30-welcome/index.md`
|
||||||
|
|
||||||
|
A blog post folder can be convenient to co-locate blog post images:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The blog supports tags as well!
|
||||||
|
|
||||||
|
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.
|
||||||
17
blog/authors.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
endi:
|
||||||
|
name: Endilie Yacop Sucipto
|
||||||
|
title: Maintainer of Docusaurus
|
||||||
|
url: https://github.com/endiliey
|
||||||
|
image_url: https://github.com/endiliey.png
|
||||||
|
|
||||||
|
yangshun:
|
||||||
|
name: Yangshun Tay
|
||||||
|
title: Front End Engineer @ Facebook
|
||||||
|
url: https://github.com/yangshun
|
||||||
|
image_url: https://github.com/yangshun.png
|
||||||
|
|
||||||
|
slorber:
|
||||||
|
name: Sébastien Lorber
|
||||||
|
title: Docusaurus maintainer
|
||||||
|
url: https://sebastienlorber.com
|
||||||
|
image_url: https://github.com/slorber.png
|
||||||
460
docs/idn_docs/docs/authentication.md
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
# Authentication
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
The quickest way to authenticate and start using SailPoint APIs is to generate a [personal access token](./authentication.md#personal-access-tokens). If you are interested in using OAuth2 for authentication, then please continue to read this document.
|
||||||
|
|
||||||
|
## Finding Your Tenant's OAuth Details
|
||||||
|
|
||||||
|
This document assumes your IDN instance is using the domain name supplied by SailPoint. If your instance is using a vanity URL, then you will need to open the following URL in your browser to get your OAuth info. See [finding your org/tenant name](./getting_started.md#finding-your-org-tenant-name) in the [getting started guide](./getting_started.md) to get your `{tenant}`.
|
||||||
|
|
||||||
|
`https://{tenant}.api.identitynow.com/oauth/info`
|
||||||
|
|
||||||
|
This page will present you with your `authorizeEndpoint` and `tokenEndpoint`, which you will need to follow along with the examples in this document.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tenantId": "cc31a307-8a8d-49e8-93b9-c7cbe20e2e6b",
|
||||||
|
"tenantName": "iga-acme-sb",
|
||||||
|
"authorizeEndpoint": "https://iga-sb.acme.com/oauth/authorize",
|
||||||
|
"tokenEndpoint": "https://iga-sb.api.identitynow.com/oauth/token",
|
||||||
|
"cloudDomainUrl": "https://iga-sb.acme.com",
|
||||||
|
"logoutUrl": "https://iga-sb.acme.com/logout",
|
||||||
|
"pod": "stg01-useast1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
In order to use the IdentityNow REST API, you must first authenticate with IdentityNow and get an `access_token`. This `access_token` will need to be provided in the `Authorization` header of each API request. The steps of the flow are as follows:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **Access Token Request** - The HTTP client (a script, application, Postman, cURL, etc.) makes a request to IdentityNow to get an `access_token`. The details of this are described in the [Authentication Details](#authentication-details) section.
|
||||||
|
2. **Access Token Response** - Assuming the request is valid, IdentityNow will issue an `access_token` to the HTTP client in response.
|
||||||
|
3. **API Request** - The HTTP client makes a request to an IdentityNow API endpoint. Included in that request is the header `Authorization: Bearer {access_token}`.
|
||||||
|
4. **API Response** - Assuming the request and the `access_token` are valid, IdentityNow will return a response to the client. If unexpected errors occur, see the [Troubleshooting](#troubleshooting) section of this document.
|
||||||
|
|
||||||
|
The SailPoint authentication/authorization model is fully [OAuth 2.0](https://oauth.net/2/) compliant, with issued `access_tokens` leveraging the [JSON Web Token (JWT)](https://jwt.io/) standard. This document provides the necessary information for interacting with SailPoint's OAuth2 services.
|
||||||
|
|
||||||
|
## Personal Access Tokens
|
||||||
|
|
||||||
|
A personal access token is a method of authenticating to an API as a user without needing to supply a username and password. The primary use case for personal access tokens is in scripts or programs that don't have an easy way to implement an OAuth 2.0 flow and that need to call API endpoints that require a user context. Personal access tokens are also convenient when using Postman to explore and test APIs.
|
||||||
|
|
||||||
|
>**UPDATE**: Previously, only users with the `Admin` or `Source Admin` role were allowed to generate personal access tokens. Now, all users are able to generate personal access tokens!
|
||||||
|
|
||||||
|
To generate a personal access token from the IdentityNow UI, perform the following steps after logging into your IdentityNow instance:
|
||||||
|
|
||||||
|
1. Select **Preferences** from the drop-down menu under your username, then **Personal Access Tokens** on the left. You can also go straight to the page using this URL, replacing `{tenant}` with your IdentityNow tenant: `https://{tenant}.identitynow.com/ui/d/user-preferences/personal-access-tokens`.
|
||||||
|
|
||||||
|
2. Click **New Token** and enter a meaningful description to help differentiate the token from others.
|
||||||
|
|
||||||
|
>**Note**: The **New Token** button will be disabled when you’ve reached the limit of 10 personal access tokens per user. To avoid reaching this limit, we recommend you delete any tokens that are no longer needed.
|
||||||
|
|
||||||
|
3. Click **Create Token** to generate and view the two components that comprise the token: the `Secret` and the `Client ID`.
|
||||||
|
|
||||||
|
>**IMPORTANT**: After you create the token, the value of the `Client ID` will be visible in the Personal Access Tokens list, but the corresponding `Secret` will not be visible after you close the window. You will need to store the `Secret` somewhere secure.
|
||||||
|
|
||||||
|
4. Copy both values somewhere that will be secure and accessible to you when you need to use the the token.
|
||||||
|
|
||||||
|
To generate a personal access token from the API, see the [API docs](https://developer.sailpoint.com/apis/beta/#operation/createPersonalAccessToken) for details.
|
||||||
|
|
||||||
|
To use a personal access token to generate an `access_token` that can be used to authenticate requests to the API, follow the [Client Credentials Grant Flow](#client-credentials-grant-flow), using the `Client ID` and `Client Secret` obtained from the personal access token.
|
||||||
|
|
||||||
|
## OAuth 2.0
|
||||||
|
|
||||||
|
[OAuth 2.0](https://oauth.net/2/) is an industry-standard protocol for authorization, and provides a variety of authorization flows for web applications, desktop applications, mobile phones, and devices. This specification and its extensions are developed within the [IETF OAuth Working Group](https://www.ietf.org/mailman/listinfo/oauth).
|
||||||
|
|
||||||
|
There are several different authorization flows that OAuth 2.0 supports, and each of these has a grant-type which defines the different use cases. Some of the common ones which might be used with IdentityNow are as follows:
|
||||||
|
|
||||||
|
1. [**Authorization Code**](https://oauth.net/2/grant-types/authorization-code/) - This grant type is used by clients to exchange an authorization code for an `access_token`. This is mainly used for web applications as there is a login into IdentityNow, with a subsequent redirect back to the web application / client.
|
||||||
|
2. [**Client Credentials**](https://oauth.net/2/grant-types/client-credentials/) - This grant type is used by clients to obtain an `access_token` outside the context of a user. Because this is outside of a user context, only a subset of IdentityNow REST APIs may be accessible with this kind of grant type.
|
||||||
|
3. [**Refresh Token**](https://oauth.net/2/grant-types/refresh-token/) - This grant type is used by clients in order to exchange a refresh token for a new `access_token` when the existing `access_token` has expired. This allows clients to continue using the API without having to re-authenticate as frequently. This grant type is commonly used together with `Authorization Code` to prevent a user from having to log in several times per day.
|
||||||
|
|
||||||
|
## JSON Web Token (JWT)
|
||||||
|
|
||||||
|
[JSON Web Token (JWT)](https://jwt.io) is an industry-standard protocol for creating access tokens which assert various claims about the resource who has authenticated. The tokens have a specific structure consisting of a header, payload, and signature.
|
||||||
|
|
||||||
|
A raw JWT might look like this:
|
||||||
|
|
||||||
|
```text
|
||||||
|
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbnRfaWQiOiI1OGViMDZhNC1kY2Q3LTRlOTYtOGZhYy1jY2EyYWZjMDNlNjEiLCJpbnRlcm5hbCI6dHJ1ZSwicG9kIjoiY29vayIsIm9yZyI6ImV4YW1wbGUiLCJpZGVudGl0eV9pZCI6ImZmODA4MTgxNTVmZThjMDgwMTU1ZmU4ZDkyNWIwMzE2IiwidXNlcl9uYW1lIjoic2xwdC5zZXJ2aWNlcyIsInN0cm9uZ19hdXRoIjp0cnVlLCJhdXRob3JpdGllcyI6WyJPUkdfQURNSU4iXSwiY2xpZW50X2lkIjoibktCUE93akpIOExYU2pJbCIsInN0cm9uZ19hdXRoX3N1cHBvcnRlZCI6dHJ1ZSwidXNlcl9pZCI6IjU5NTgyNiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjE1NjU4ODgzMTksImp0aSI6ImM5OGQxMjM2LTQ1MTMtNGM4OS1hMGQwLTBjYjlmMzI3NmI1NiJ9.SAY4ZQkXGi2cY_qz57Ah9_zDq4-bnF-oDJKotXa-LCY
|
||||||
|
```
|
||||||
|
|
||||||
|
If you were to decode the access token data, it might look something like this:
|
||||||
|
|
||||||
|
Header
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"alg": "HS256",
|
||||||
|
"typ": "JWT"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Payload
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"tenant_id": "58eb06a4-dcd7-4e96-8fac-cca2afc03e61",
|
||||||
|
"internal": true,
|
||||||
|
"pod": "cook",
|
||||||
|
"org": "example",
|
||||||
|
"identity_id": "ff80818155fe8c080155fe8d925b0316",
|
||||||
|
"user_name": "slpt.services",
|
||||||
|
"strong_auth": true,
|
||||||
|
"authorities": [
|
||||||
|
"ORG_ADMIN"
|
||||||
|
],
|
||||||
|
"client_id": "nKBPOwjJH8LXSjIl",
|
||||||
|
"strong_auth_supported": true,
|
||||||
|
"user_id": "595826",
|
||||||
|
"scope": [
|
||||||
|
"read",
|
||||||
|
"write"
|
||||||
|
],
|
||||||
|
"exp": 1565888319,
|
||||||
|
"jti": "c98d1236-4513-4c89-a0d0-0cb9f3276b56"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Signature
|
||||||
|
|
||||||
|
```TEXT
|
||||||
|
HMACSHA256(
|
||||||
|
base64UrlEncode(header) + "." +
|
||||||
|
base64UrlEncode(payload),
|
||||||
|
{secret}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can check the JWT access token data online at [jwt.io](https://jwt.io).
|
||||||
|
|
||||||
|
## Authentication Details
|
||||||
|
|
||||||
|
This section details how to call the SailPoint Platform OAuth 2.0 token endpoints to get an `access_token`.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
Before any OAuth 2.0 token requests can be initiated, a Client ID and secret are necessary. As an `ORG_ADMIN`, browse to your API Management Admin Page at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel` and create an API client with the appropriate grant types for your use case. If you are not an admin of your org, you can ask an admin to create this for you. Be sure to save your `Client Secret` somewhere secure, as you will not be able to view or change it later.
|
||||||
|
|
||||||
|
### OAuth 2.0 Token Request
|
||||||
|
|
||||||
|
When authenticating to IdentityNow, the OAuth 2.0 token endpoint resides on the IdentityNow API Gateway at:
|
||||||
|
|
||||||
|
```Text
|
||||||
|
POST https://{tenant}.api.identitynow.com/oauth/token
|
||||||
|
```
|
||||||
|
|
||||||
|
How you call this endpoint to get your token depends largely on the OAuth 2.0 flow and grant type you wish to implement. The details for each grant type within IdentityNow are described in the following sections.
|
||||||
|
|
||||||
|
### Authorization Code Grant Flow
|
||||||
|
|
||||||
|
Further Reading: [https://oauth.net/2/grant-types/authorization-code/](https://oauth.net/2/grant-types/authorization-code/)
|
||||||
|
|
||||||
|
This grant type is used by clients to exchange an authorization code for an `access_token`. This is mainly used for web apps as there is a login into IdentityNow, with a subsequent redirect back to the web app / client.
|
||||||
|
|
||||||
|
The OAuth 2.0 client you are using must have `AUTHORIZATION_CODE` as one of its grant types. The redirect URLs must also match the list in the client as well:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"created": "2019-05-23T02:06:20.685Z",
|
||||||
|
"name": "My Application",
|
||||||
|
"description": "My Application",
|
||||||
|
"id": "b61429f5-203d-494c-94c3-04f54e17bc5c",
|
||||||
|
"secret": null,
|
||||||
|
"grantTypes": [
|
||||||
|
"AUTHORIZATION_CODE"
|
||||||
|
],
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:8080/myApp/code"
|
||||||
|
],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The overall authorization flow is as follows:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. The user clicks the login link on a web app.
|
||||||
|
|
||||||
|
2. The web app sends an authorization request to IdentityNow in the form:
|
||||||
|
|
||||||
|
```Text
|
||||||
|
GET https://{tenant}.identitynow.com/oauth/authorize?client_id={client-id}&client_secret={client-secret}&response_type=code&redirect_uri={redirect-url}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. IdentityNow redirects the user to a login prompt to authenticate to IdentityNow.
|
||||||
|
|
||||||
|
4. The user authenticates to IdentityNow.
|
||||||
|
|
||||||
|
5. Once authentication is successful, IdentityNow issues an authorization code back to the web app.
|
||||||
|
|
||||||
|
6. The web app submits an **OAuth 2.0 Token Request** to IdentityNow in the form:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST https://{tenant}.api.identitynow.com/oauth/token?grant_type=authorization_code&client_id={client-id}&client_secret={client-secret}&code={code}&redirect_uri={redirect-url}
|
||||||
|
```
|
||||||
|
|
||||||
|
>**Note**: the token endpoint URL is `{tenant}.api.identitynow.com`, while the authorize URL is `{tenant}.identitynow.com`. Be sure to use the correct URL when setting up your webapp to use this flow.
|
||||||
|
|
||||||
|
7. IdentityNow validates the token request and submits a response. If successful, the response will contain a JWT `access_token`.
|
||||||
|
|
||||||
|
The query parameters in the OAuth 2.0 token request for the Authorization Code grant are as follows:
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| ------------- | ------------------------------------------------------------ |
|
||||||
|
| grant_type | Set to `authorization_code` for the authorization code grant type. |
|
||||||
|
| client_id | This is the client ID for the API client (e.g. `b61429f5-203d-494c-94c3-04f54e17bc5c`). This can be generated at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel` |
|
||||||
|
| client_secret | This is the client secret for the API client (e.g. `c924417c85b19eda40e171935503d8e9747ca60ddb9b48ba4c6bb5a7145fb6c5`). This can be generated at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel` |
|
||||||
|
| code | This is a code returned by `/oauth/authorize`. |
|
||||||
|
| redirect_uri | This is a URL of the application to redirect to once the token has been granted. |
|
||||||
|
|
||||||
|
Here is an example OAuth 2.0 token request for the Authorization Code grant type.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST \
|
||||||
|
'https://example.api.identitynow.com/oauth/token?grant_type=authorization_code&client_id=b61429f5-203d-494c-94c3-04f54e17bc5c&client_secret=c924417c85b19eda40e171935503d8e9747ca60ddb9b48ba4c6bb5a7145fb6c5&code=6688LQJB0y652z6ZjFmkCKuBUjv2sTIqKS2JthWrZ7qlPgI9TClJ6FnpweEhO6w7&redirect_uri=https://myappdomain.com/oauth/redirect' \
|
||||||
|
-H 'cache-control: no-cache'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Credentials Grant Flow
|
||||||
|
|
||||||
|
Further Reading: [https://oauth.net/2/grant-types/client-credentials/](https://oauth.net/2/grant-types/client-credentials/)
|
||||||
|
|
||||||
|
This grant type is used by clients to obtain an access token outside the context of a user. This is probably the simplest authentication flow, but comes with a major drawback; API endpoints that require [user level permissions](https://documentation.sailpoint.com/saas/help/common/users/user_level_matrix.html) will not work. [Personal Access Tokens](#personal-access-tokens) are a form of Client Credentials that have a user context, so they do not share this drawback. However, the APIs that can be invoked with a personal access token depend on the permissions of the user that generated it.
|
||||||
|
|
||||||
|
An OAuth 2.0 client using the Client Credentials flow must have `CLIENT_CREDENTIALS` as one of its grantTypes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"created": "2019-05-23T02:06:20.685Z",
|
||||||
|
"name": "My Application",
|
||||||
|
"description": "My Application",
|
||||||
|
"id": "b61429f5-203d-494c-94c3-04f54e17bc5c",
|
||||||
|
"secret": null,
|
||||||
|
"grantTypes": [
|
||||||
|
"CLIENT_CREDENTIALS"
|
||||||
|
],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[Personal Access Tokens](#personal-access-tokens) are implicly granted a `CLIENT_CREDENTIALS` grant type.
|
||||||
|
|
||||||
|
The overall authorization flow looks like this:
|
||||||
|
|
||||||
|
1. The client submits an **OAuth 2.0 Token Request** to IdentityNow in the form:
|
||||||
|
|
||||||
|
```Text
|
||||||
|
POST https://{tenant}.api.identitynow.com/oauth/token?grant_type=client_credentials&client_id={client-id}&client_secret={client-secret}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. IdentityNow validates the token request and submits a response. If successful, the response will contain a JWT access token.
|
||||||
|
|
||||||
|
The query parameters in the OAuth 2.0 Token Request for the Client Credentials grant are as follows:
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| ------------- | ------------------------------------------------------------ |
|
||||||
|
| grant_type | Set to `CLIENT_CREDENTIALS` for the authorization code grant type. |
|
||||||
|
| client_id | This is the client ID describing for the API client (e.g. `b61429f5-203d-494c-94c3-04f54e17bc5c`). This can be generated at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel` or by [creating a personal access token](#personal-access-tokens). |
|
||||||
|
| client_secret | This is the client secret describing for the API client (e.g. `c924417c85b19eda40e171935503d8e9747ca60ddb9b48ba4c6bb5a7145fb6c5`). This can be generated at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel` or by [creating a personal access token](#personal-access-tokens). |
|
||||||
|
|
||||||
|
Here is an example request to generate an `access_token` using Client Credentials.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST \
|
||||||
|
'https://{tenant}.api.identitynow.com/oauth/token?grant_type=client_credentials&client_id={client_id}&client_secret={client_secret}' \
|
||||||
|
-H 'cache-control: no-cache'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Refresh Token Grant Flow
|
||||||
|
|
||||||
|
Further Reading: [https://oauth.net/2/grant-types/refresh-token/](https://oauth.net/2/grant-types/refresh-token/)
|
||||||
|
|
||||||
|
This grant type is used by clients in order to exchange a refresh token for a new `access_token` once the existing `access_token` has expired. This allows clients to continue to have a valid `access_token` without the need for the user to login as frequently.
|
||||||
|
|
||||||
|
The OAuth 2.0 client you are using must have `REFRESH_TOKEN` as one of its grant types, and is typically used in conjunction with another grant type, like `CLIENT_CREDENTIALS` or `AUTHORIZATION_CODE`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"created": "2019-05-23T02:06:20.685Z",
|
||||||
|
"name": "My Application",
|
||||||
|
"description": "My Application",
|
||||||
|
"id": "b61429f5-203d-494c-94c3-04f54e17bc5c",
|
||||||
|
"secret": null,
|
||||||
|
"grantTypes": [
|
||||||
|
"REFRESH_TOKEN",
|
||||||
|
"AUTHORIZATION_CODE"
|
||||||
|
],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The overall authorization flow looks like this:
|
||||||
|
|
||||||
|
1. The client application receives an `access_token` and a `refresh_token` via one of the other OAuth grant flows, like `AUTHORIZATION_CODE`.
|
||||||
|
2. The client application notices that the `access_token` is about to expire, based on the `expires_in` attribute contained within the JWT token.
|
||||||
|
3. The client submits an **OAuth 2.0 Token Request** to IdentityNow in the form:
|
||||||
|
|
||||||
|
```Text
|
||||||
|
POST https://{tenant}.api.identitynow.com/oauth/token?grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token={refresh_token}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. IdentityNow validates the token request and submits a response. If successful, the response will contain a new `access_token` and `refresh_token`.
|
||||||
|
|
||||||
|
The query parameters in the OAuth 2.0 Token Request for the Refresh Token grant are as follows:
|
||||||
|
|
||||||
|
| Key | Description |
|
||||||
|
| ------------- | ------------------------------------------------------------ |
|
||||||
|
| grant_type | Set to `refresh_token` for the authorization code grant type. |
|
||||||
|
| client_id | This is the client ID for the API client (e.g. `b61429f5-203d-494c-94c3-04f54e17bc5c`). This can be generated at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`. |
|
||||||
|
| client_secret | This is the client secret for the API client (e.g. `c924417c85b19eda40e171935503d8e9747ca60ddb9b48ba4c6bb5a7145fb6c5`). This can be generated at `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`. |
|
||||||
|
| refresh_token | This is the `refresh_token` that was provided along with the now expired `access_token`. |
|
||||||
|
|
||||||
|
Here is an example call OAuth 2.0 Token Request for the Refresh Token grant.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X POST \
|
||||||
|
'https://example.api.identitynow.com/oauth/token?grant_type=refresh_token&client_id=b61429f5-203d-494c-94c3-04f54e17bc5c&client_secret=c924417c85b19eda40e171935503d8e9747ca60ddb9b48ba4c6bb5a7145fb6c5&refresh_token=ey...4M' \
|
||||||
|
-H 'cache-control: no-cache'
|
||||||
|
```
|
||||||
|
|
||||||
|
## OAuth 2.0 Token Response
|
||||||
|
|
||||||
|
A successful request to `https://{tenant}.api.identitynow.com/oauth/token` will contain a response body similar to this:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbnRfaWQiOiI1OGViMDZhNC1kY2Q3LTRlOTYtOGZhYy1jY2EyYWZjMDNlNjEiLCJpbnRlcm5hbCI6ZmFsc2UsInBvZCI6ImNvb2siLCJvcmciOiJuZWlsLXRlc3QiLCJpZGVudGl0eV9pZCI6ImZmODA4MTgxNTVmZThjMDgwMTU1ZmU4ZDkyNWIwMzE2IiwidXNlcl9uYW1lIjoic2xwdC5zZXJ2aWNlcyIsInN0cm9uZ19hdXRoIjp0cnVlLCJhdXRob3JpdGllcyI6WyJPUkdfQURNSU4iXSwiZW5hYmxlZCI6dHJ1ZSwiY2xpZW50X2lkIjoiZmNjMGRkYmItMTA1Yy00Y2Q3LWI5NWUtMDI3NmNiZTQ1YjkwIiwiYWNjZXNzVHlwZSI6Ik9GRkxJTkUiLCJzdHJvbmdfYXV0aF9zdXBwb3J0ZWQiOmZhbHNlLCJ1c2VyX2lkIjoiNTk1ODI2Iiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl0sImV4cCI6MTU2NTg5MTA2MywianRpIjoiOTQ5OWIyOTktOTVmYS00N2ZiLTgxNWMtODVkNWY2YjQzZTg2In0.zJYfjIladuGHoLXr92EOJ3A9qGNkiG5UJ9eqrtSYXAQ",
|
||||||
|
"token_type": "bearer",
|
||||||
|
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbnRfaWQiOiI1OGViMDZhNC1kY2Q3LTRlOTYtOGZhYy1jY2EyYWZjMDNlNjEiLCJpbnRlcm5hbCI6ZmFsc2UsInBvZCI6ImNvb2siLCJvcmciOiJuZWlsLXRlc3QiLCJpZGVudGl0eV9pZCI6ImZmODA4MTgxNTVmZThjMDgwMTU1ZmU4ZDkyNWIwMzE2IiwidXNlcl9uYW1lIjoic2xwdC5zZXJ2aWNlcyIsInN0cm9uZ19hdXRoIjp0cnVlLCJhdXRob3JpdGllcyI6WyJPUkdfQURNSU4iXSwiZW5hYmxlZCI6dHJ1ZSwiY2xpZW50X2lkIjoiZmNjMGRkYmItMTA1Yy00Y2Q3LWI5NWUtMDI3NmNiZTQ1YjkwIiwiWYNjZXNzVHlwZSI6Ik9GRkxJTkUiLCJzdHJvbmdfYXV0aF9zdXBwb3J0ZWQiOmZhbHNlLCJ1c2VyX2lkIjoiNTk1ODI2Iiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl0sImF0aSI6Ijk0OTliMjk5LTk1ZmEtNDdmYi04MTVjLTg1ZDVmNmI0M2U4NiIsImV4cCI6MTU2NTk3NjcxMywianRpIjoiODliODk1ZDMtNTdlNC00ZDAwLWI5ZjctOTFlYWVjNDcxMGQ3In0.pfDcB0sGChdHk-oDNmiIxsKFLxq9CcPQV5-eXWgIcp4",
|
||||||
|
"expires_in": 749,
|
||||||
|
"scope": "read write",
|
||||||
|
"accessType": "OFFLINE",
|
||||||
|
"tenant_id": "58eb06a4-dcd7-4e96-8fac-cca2afc03e61",
|
||||||
|
"internal": false,
|
||||||
|
"pod": "cook",
|
||||||
|
"strong_auth_supported": false,
|
||||||
|
"org": "example",
|
||||||
|
"user_id": "595826",
|
||||||
|
"identity_id": "ff80818155fe8c080155fe8d925b0316",
|
||||||
|
"strong_auth": true,
|
||||||
|
"enabled": true,
|
||||||
|
"jti": "9499b299-95fa-47fb-815c-85d5f6b43e86"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `access_token` contains the JSON Web Token which is subsequently used in any further REST API calls through the IdentityNow API gateway. To use the `access_token`, simply include it in the `Authorization` header as a `Bearer` token. For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X GET \
|
||||||
|
'https://{tenant}.api.identitynow.com/v3/account-activities' \
|
||||||
|
-H 'Authorization: Bearer {access_token}' \
|
||||||
|
-H 'cache-control: no-cache'
|
||||||
|
```
|
||||||
|
|
||||||
|
The `expires_in` describes the lifetime, in seconds, of the `access_token`. For example, the value 749 means that the `access_token` will expire in 12.5 minutes from the time the response was generated. The exact expiration date is also contained within the `access_token`. You can view this expiration time by decoding the JWT `access_token` using a tool like [jwt.io](https://jwt.io/).
|
||||||
|
|
||||||
|
The `refresh_token` contains a JSON Web Token for use in a [Refresh Token](#refresh-token-grant-flow) grant flow. The `refresh_token` will only be present if the API client has the `REFRESH_CODE` grant flow.
|
||||||
|
|
||||||
|
The `user_id` and `identity_id` define the identity context of the person that authenticated. This is not set for the Client Credentials grant type since it doesn't have a user context.
|
||||||
|
|
||||||
|
## Which OAuth 2.0 Grant Flow should I use?
|
||||||
|
|
||||||
|
Deciding which OAuth 2.0 grant flow you should use largely depends on your use case.
|
||||||
|
|
||||||
|
### Daily Work or Quick Actions
|
||||||
|
|
||||||
|
For daily work or short, quick administrative actions, you may not really need to worry about grant types, as an access token can easily be obtained in the user interface. In order to see this:
|
||||||
|
|
||||||
|
1. Login to IdentityNow.
|
||||||
|
2. Go to `https://{tenant}.identitynow.com/ui/session`.
|
||||||
|
3. The `accessToken` is visible in the user interface.
|
||||||
|
4. Use this access token in the `Authorization` header when making API calls. If the access token expires, log back into Identity Now and retrieve the new access token.
|
||||||
|
|
||||||
|
While this is very simple to use, this is only valid for a short period of time (a few minutes).
|
||||||
|
|
||||||
|
### Postman
|
||||||
|
|
||||||
|
If you are using the popular HTTP client, [Postman](https://www.getpostman.com), you have a couple of options on how you might setup your authorization. You can just leverage the accessToken as mentioned above, or you can also configure Postman to use OAuth 2.0 directly.
|
||||||
|
|
||||||
|
### Web Applications
|
||||||
|
|
||||||
|
If you are making a web application, the best grant flow to use is the [Authorization Code](#authorization-code-grant-flow) grant flow. This will allow users to be directed to IdentityNow to login, and then redirected back to the web application via a URL redirect. This also works well with SSO, strong authentication, or pass-through authentication mechanisms.
|
||||||
|
|
||||||
|
SailPoint does not recommend using a password grant flow for web applications as it would involve entering IdentityNow credentials in the web application. This flow also doesn't allow you to work with SSO, strong authentication, or pass-through authentication.
|
||||||
|
|
||||||
|
### Scripts or Programs
|
||||||
|
|
||||||
|
If you are writing scripts or programs that leverage the IdentityNow APIs, which OAuth 2.0 grant from you should use typically depends on what you are doing, and which user context you need to operate under.
|
||||||
|
|
||||||
|
Because scripts, code, or programs do not have an interactive web-interface it is difficult, but not impossible, to implement a working [Authorization Code](#authorization-code-grant-flow) flow. Most scripts or programs typically run as a [Client Credentials](#client-credentials-grant-flow). If your APIs can work under an API context without a user, then [Client Credentials](#client-credentials-grant-flow) is ideal. However, if your APIs need a user or admin context, then the [Personal Access Token](#personal-access-tokens) approach will be more suitable.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
Having issues? Follow these steps.
|
||||||
|
|
||||||
|
1. **Verify the API End Point Calls**
|
||||||
|
|
||||||
|
1. Verify the structure of the API call:
|
||||||
|
1. Verify that the API calls are going through the API gateway:
|
||||||
|
`https://{tenant}.api.identitynow.com`
|
||||||
|
2. Verify you are calling their version correctly:
|
||||||
|
- Private APIs: `https://{tenant}.api.identitynow.com/cc/api/{endpoint}`
|
||||||
|
- V2 APIs: `https://{tenant}.api.identitynow.com/v2/{endpoint}`
|
||||||
|
- V3 APIs: `https://{tenant}.api.identitynow.com/v3/{endpoint}`
|
||||||
|
- Beta APIs: `https://{tenant}.api.identitynow.com/beta/{endpoint}`
|
||||||
|
3. Verify that the API calls have the correct headers (e.g., `content-type`), query parameters, and body data.
|
||||||
|
2. If the HTTP response is **401 Unauthorized** , this is an indication that either there is no `Authorization` header or the `access_token` is invalid. Verify that the API calls are supplying the `access_token` in the `Authorization` header correctly (ex. `Authorization: Bearer {access_token}`) and that the `access_token` has not expired.
|
||||||
|
3. If the HTTP response is **403 Forbidden**, this is an indication that the `access_token` is valid, but the user you are running as doesn't have access to this endpoint. Check the access rights which are associated with the user.
|
||||||
|
>**Note**: This can also be due to calling an API which expects a user, but your authorization grant type might not have a user context. Calling most administrative APIs with a CLIENT_CREDENTIAL grant will often produce this result.
|
||||||
|
|
||||||
|
2. **Verify the OAuth 2.0 Client**
|
||||||
|
|
||||||
|
1. Verify that the OAuth 2.0 Client is not a Legacy OAuth client. Legacy OAuth clients will not work.
|
||||||
|
This is very apparent by looking at the Client ID, as OAuth 2.0 Client IDs have dashes. Here is an example:
|
||||||
|
Legacy Client ID: `G6xLlBBOKIcOAQuK`
|
||||||
|
OAuth 2.0 Client ID: `b61429f5-203d-494c-94c3-04f54e17bc5c`
|
||||||
|
|
||||||
|
2. Verify the OAuth 2.0 Client ID exists. This can be verified by calling:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /beta/oauth-clients/{client-id}
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /beta/oauth-clients/
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also view all of the active clients in the UI by going to `https://{tenant}.identitynow.com/ui/admin/#admin:global:security:apimanagementpanel`.
|
||||||
|
|
||||||
|
3. Verify that the OAuth 2.0 Client grant types match the OAuth 2.0 grant type flow you are trying to use. For instance, this client will work with [Authorization Code](#authorization-code-grant-flow) and [Client Credentials](#client-Credentials-grant-flow) flows, but not [Refresh Token](#refresh-token-grant-flow) flows:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"created": "2019-05-23T02:06:20.685Z",
|
||||||
|
"name": "My Application",
|
||||||
|
"description": "My Application",
|
||||||
|
"id": "b61429f5-203d-494c-94c3-04f54e17bc5c",
|
||||||
|
"secret": null,
|
||||||
|
"grantTypes": [
|
||||||
|
"AUTHORIZATION_CODE",
|
||||||
|
"CLIENT_CREDENTIALS"
|
||||||
|
],
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. If using an A[Authorization Code](#authorization-code-grant-flow) flow, verify the redirect URL(s) for your application match the `redirectUris` value in the client. You can check this using the [oauth-clients](https://developer.sailpoint.com/apis/beta/#operation/getOauthClient) endpoint.
|
||||||
|
|
||||||
|
3. **Verify the OAuth 2.0 Calls**
|
||||||
|
|
||||||
|
1. Verify that the OAuth call flow is going to the right URLs, with the correct query parameters and data values. A common source of errors is using the wrong host for authorization and token API calls. The token endpoint URL is `{tenant}.api.identitynow.com`, while the authorize URL is `{tenant}.identitynow.com`.
|
||||||
218
docs/idn_docs/docs/custom_connectors/commands/account_create.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# Account Create
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:---------------------------:|
|
||||||
|
| Input | StdAccountCreateInput |
|
||||||
|
| Output | StdAccountCreateOutput |
|
||||||
|
|
||||||
|
### Example StdAccountCreateInput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"email": "example@gmail.com",
|
||||||
|
"department": "external",
|
||||||
|
"displayName": "John Doe",
|
||||||
|
"password": "test",
|
||||||
|
"entitlements": [
|
||||||
|
"user",
|
||||||
|
"administrator"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Example StdAccountCreateOutput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"displayName": "John Doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Description
|
||||||
|
The account create command triggers whenever IDN is told to provision entitlements for an identity on the target source, but no account for the identity on the target source exists yet. For example, if you create an access profile that grants a group on the target source and then add that access profile to a role, any identity matching that role’s membership criteria will be granted to the group. IDN determines which identities do not have accounts on the target source and triggers the account create command for each identity. If an identity already has an account, then it invokes the account update command.
|
||||||
|
|
||||||
|
## The Provisioning Plan
|
||||||
|
The account create command accepts a provisioning plan from IDN and creates the corresponding account(s) in the target source. When you configure your source in IDN, you must set up ‘Create Profile’ to tell IDN how to provision new accounts for your source.
|
||||||
|
|
||||||
|
You can create the provisioning plan through the ```accountCreateTemplate``` in the ```connector-spec.json``` file, and you can also modify its behavior in IDN using the create profile screen:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
‘Create Profile’ provides the instructions for the provisioning plan and determines which attributes are sent to your connector code. For example, if all the account attributes in the preceding image are configured for a value, then the following JSON payload is sent to your connector:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"password": "secretPassword",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The provisioning plan does not include any disabled attributes. In the earlier image, ```password``` is disabled, so the payload to your connector does not not include a field for ```password```:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The provisioning plan presents multi-valued entitlements in two different ways:
|
||||||
|
|
||||||
|
If a multi-valued entitlement, like groups, has only one value, then the provisioning plan represents it as a string value:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": "user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
If a multi-valued entitlement has more than one value, then the plan represents it as an array:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Your connector code must handle the possibility of both cases. The following code example from [AirtableAccount.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/models/AirtableAccount.ts) shows how to handle a multi-valued attribute:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
public static createWithStdAccountCreateInput(record: StdAccountCreateInput): AirtableAccount {
|
||||||
|
const account = new AirtableAccount();
|
||||||
|
...
|
||||||
|
if (record.attributes['entitlements'] != null) {
|
||||||
|
if (!Array.isArray(record.attributes['entitlements'])) {
|
||||||
|
account.entitlments = [record.attributes['entitlements']]
|
||||||
|
} else {
|
||||||
|
account.entitlments = record.attributes['entitlements']
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
account.entitlments = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## The return object
|
||||||
|
When the account is returned to IDN, any values you set are updated in IDN. So if an account ID is auto-generated on the source system, you must send the account ID back to IDN so IDN is aware of it for future account update activities. This is useful for the compound key type.
|
||||||
|
|
||||||
|
## Password Handling
|
||||||
|
There are three main ways to handle passwords on a source:
|
||||||
|
|
||||||
|
1. SSO, LDAP, or other federated authentication mechanisms are the preferred means of providing user login on a target source. If your source can integrate with a federated login service, use that service. If your source requires you to provide a password when you create accounts, even with a federated login, it is best to create a strong, random password. Your users will use the federated login, so they never need to know this password.
|
||||||
|
|
||||||
|
2. If your source has a password reset feature at login, it is best to initially create the account with a strong, random password the user does not have access to. Once the account is created, make the user request a password reset to set their own password. This method is the safest alternative to federated authentication because the initial password is strong and never known to anyone, and the user can generate his or her own password through secure channels.
|
||||||
|
|
||||||
|
3. The least secure method is setting a static password in the create profile that is well known among your users. This approach is not recommended. It does not require any automated communications with your users.
|
||||||
|
|
||||||
|
There are two ways you can generate random passwords:
|
||||||
|
|
||||||
|
1. Use the “Create Password” generator in ‘Create Profile.’ (This can also be configured in the ```accountCreateTemplate```)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. Disable the 'password' field.
|
||||||
|
|
||||||
|
Use ‘Create Profile’ and generate a random password in code. There are some JavaScript libraries that can generate random strings suitable for passwords, like [random-string](https://www.npmjs.com/package/random-string) and [crypto-random-string](https://www.npmjs.com/package/crypto-random-string). Import either one of these libraries into your code to use them. The following example from [airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/airtable.ts) uses a ternary operator to ensure the password is always provided. If the provisioning plan provides a password, use that value. If the provisioning plan does not provide a password, generate a random one.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async createAccount(input: StdAccountCreateInput): Promise<AirtableAccount> {
|
||||||
|
const account = AirtableAccount.createWithStdAccountCreateInput(input);
|
||||||
|
|
||||||
|
return this.airTableBase('Users').create({
|
||||||
|
"displayName": account.displayName,
|
||||||
|
"email": account.email,
|
||||||
|
"id": account.id,
|
||||||
|
"enabled": account.enabled ? 'true' : 'false',
|
||||||
|
"department": account.department,
|
||||||
|
"firstName": account.firstName,
|
||||||
|
"lastName": account.lastName,
|
||||||
|
"locked": account.locked ? 'true' : 'false',
|
||||||
|
"password": account.password ? account.password : crypto.randomBytes(20).toString('hex'),
|
||||||
|
"entitlements": account.entitlments.join(',')
|
||||||
|
}).then(record => {
|
||||||
|
const airtableRecord = AirtableAccount.createWithRecords(record)
|
||||||
|
return airtableRecord
|
||||||
|
}).catch(err => {
|
||||||
|
throw new ConnectorError('error while getting accounts: ' + err)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Testing in IdentityNow
|
||||||
|
|
||||||
|
One way to test whether the account create code works in IDN is to set up an access profile and role that grants members an entitlement from the connector’s target source. Start by creating an access profile that grants one or more entitlements from the target source.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Next, create a role that uses the access profile created in the previous step.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Modify the role membership to use ‘Identity List’ and select one or more users that do not have accounts in the target source yet.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click the ‘Update’ button in the upper right corner to initiate the account provisioning process. Doing so creates the account(s) on the target source once the process is complete.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Account Delete
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:---------------------------:|
|
||||||
|
| Input | StdAccountDeleteInput |
|
||||||
|
| Output | StdAccountDeleteOutput |
|
||||||
|
|
||||||
|
### Example StdAccountDeleteInput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Example StdAccountDeleteOutput
|
||||||
|
```javascript
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
## Description
|
||||||
|
The account delete command sends one attribute from IDN, the identity to delete. This can be passed to your connector to delete the account from the source system.
|
||||||
|
|
||||||
|
Enable account delete in IDN through a BeforeProvisioning rule. The connector honors whichever operation the provisioning plan sends. For more information, see the [documentation](https://community.sailpoint.com/t5/IdentityNow-Articles/IdentityNow-Rule-Guide/ta-p/76665) and an [example implementation](https://community.sailpoint.com/t5/IdentityNow-Wiki/IdentityNow-Rule-Guide-Before-Provisioning-Rule/ta-p/77415).
|
||||||
|
|
||||||
|
The following snippet shows an example of account delete command implementation:
|
||||||
|
|
||||||
|
[index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts)
|
||||||
|
```javascript
|
||||||
|
.stdAccountDelete(async (context: Context, input: StdAccountDeleteInput, res: Response<StdAccountDeleteOutput>) => {
|
||||||
|
const account = await airtable.getAccount(input.key)
|
||||||
|
res.send(await airtable.deleteAccount(account.airtableId))
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
[airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/airtable.ts)
|
||||||
|
```javascript
|
||||||
|
async deleteAccount(airTableid: string): Promise<Record<string, never>> {
|
||||||
|
return this.airTableBase('Users').destroy(airTableid,
|
||||||
|
).then(() => {
|
||||||
|
return {}
|
||||||
|
}).catch(err => {
|
||||||
|
throw new ConnectorError('error while deleting account: ' + err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
# Account Discover Schema
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:----------------------------:|
|
||||||
|
| Input | undefined |
|
||||||
|
| Output | StdTestConnectionOutput |
|
||||||
|
|
||||||
|
### Example StdTestConnectionOutput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"displayAttribute": "id",
|
||||||
|
"identityAttribute": "email",
|
||||||
|
"groupAttribute": "entitlements",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "displayName",
|
||||||
|
"type": "string",
|
||||||
|
"description": "Display Name of the account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "string",
|
||||||
|
"description": "unique Id of the account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"type": "string",
|
||||||
|
"description": "Email of the account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entitlements",
|
||||||
|
"type": "string",
|
||||||
|
"entitlement": true,
|
||||||
|
"managed": true,
|
||||||
|
"multi": true,
|
||||||
|
"description": "The groups the user belongs to presented as an array of strings"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Description
|
||||||
|
The account discover schema command tells IDN to dynamically create the account schema for the source rather than use the account schema provided by the connector in connector-spec.json. It is often ideal to statically define the account schema because it is generally more performant and easier to develop and reason about the code. However, some sources have schemas that can be different for each customer deployment. It can also be difficult to determine which account attributes to statically expose, which requires the schema to be dynamically generated. SalesForce is an example of a source that can have thousands of account attributes, which makes it impractical to statically define a set of attributes that satisfies all connector users. Although the SalesForce connector defines a standard set of account attributes out of the box, it also allows schema discovery for users looking for more attributes.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
If your connector requires dynamic schema discovery, you must add std:account:discover-schema to the list of commands in connector-spec.json. Because the account schema is dynamic, you do not need to specify an accountSchema or an accountCreateTemplate object in connector-spec.json. Your connector-spec.json file will look similar to this example from the [Airtable connector](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/connector-spec.json).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"name": "airtable-v4",
|
||||||
|
"keyType": "simple",
|
||||||
|
"commands": [
|
||||||
|
"std:account:list",
|
||||||
|
"std:account:read",
|
||||||
|
"std:entitlement:list",
|
||||||
|
"std:entitlement:read",
|
||||||
|
"std:test-connection",
|
||||||
|
"std:account:update",
|
||||||
|
"std:account:discover-schema",
|
||||||
|
"std:account:create",
|
||||||
|
"std:account:delete",
|
||||||
|
"std:account:disable",
|
||||||
|
"std:account:enable",
|
||||||
|
"std:account:unlock"
|
||||||
|
],
|
||||||
|
"sourceConfig": [
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"key": "apiKey",
|
||||||
|
"label": "API Key",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "airtableBase",
|
||||||
|
"label": "airtable base ID",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"entitlementSchemas": [
|
||||||
|
{
|
||||||
|
"type": "group",
|
||||||
|
"displayAttribute": "name",
|
||||||
|
"identityAttribute": "id",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique ID of the group (ex. admin)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string",
|
||||||
|
"description": "The display name of the group (ex. Admin)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accountCreateTemplate": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"key": "email",
|
||||||
|
"label": "Email",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"initialValue": {
|
||||||
|
"type": "identityAttribute",
|
||||||
|
"attributes": {
|
||||||
|
"name": "email"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "id",
|
||||||
|
"label": "id",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"initialValue": {
|
||||||
|
"type": "identityAttribute",
|
||||||
|
"attributes": {
|
||||||
|
"name": "uid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "password",
|
||||||
|
"label": "Password",
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"initialValue": {
|
||||||
|
"type": "generator",
|
||||||
|
"attributes": {
|
||||||
|
"name": "Create Password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "department",
|
||||||
|
"label": "Department",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"initialValue": {
|
||||||
|
"type": "identityAttribute",
|
||||||
|
"attributes": {
|
||||||
|
"name": "department"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "displayName",
|
||||||
|
"label": "Display Name",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"initialValue": {
|
||||||
|
"type": "identityAttribute",
|
||||||
|
"attributes": {
|
||||||
|
"name": "displayName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "firstName",
|
||||||
|
"label": "First Name",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"initialValue": {
|
||||||
|
"type": "identityAttribute",
|
||||||
|
"attributes": {
|
||||||
|
"name": "firstname"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "lastName",
|
||||||
|
"label": "Last Name",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"initialValue": {
|
||||||
|
"type": "identityAttribute",
|
||||||
|
"attributes": {
|
||||||
|
"name": "lastname"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Programmatically build an account schema
|
||||||
|
There are many ways to programmatically build the account schema for a source. This section will cover one such method. To start, register your command in the main connector file, [index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const connector = async () => {
|
||||||
|
|
||||||
|
// Get connector source config
|
||||||
|
const config = await readConfig()
|
||||||
|
|
||||||
|
// Use the vendor SDK, or implement own client as necessary, to initialize a client
|
||||||
|
const airtable = new AirtableClient(config)
|
||||||
|
|
||||||
|
return createConnector()
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
.stdAccountDiscoverSchema(async (context: Context, input: undefined, res: Response<StdAccountDiscoverSchemaOutput>) => {
|
||||||
|
const account = await airtable.getAccountSchema()
|
||||||
|
|
||||||
|
res.send(account)
|
||||||
|
})
|
||||||
|
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, implement the ```discoverSchema()``` function in your client code. The following function calls the necessary endpoints to get the full schema of the user account you want to represent in IDN. After you receive a response from your call, you must build your account schema object that will return to IDN. The response has a structure like the accountSchema property in the connector-spec.json file. The following is an example from [airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/airtable.ts).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async getAccountSchema(): Promise<StdAccountDiscoverSchemaOutput> {
|
||||||
|
return this.airTableBase('Users').select({
|
||||||
|
view: 'Grid view'
|
||||||
|
}).firstPage().then(records => {
|
||||||
|
const recordArray: StdAccountDiscoverSchemaOutput = {
|
||||||
|
"identityAttribute": 'email',
|
||||||
|
"displayAttribute": 'id',
|
||||||
|
"groupAttribute": 'entitlments',
|
||||||
|
"attributes": []
|
||||||
|
}
|
||||||
|
recordArray.attributes = []
|
||||||
|
for (const record of records) {
|
||||||
|
const fieldset = record.fields
|
||||||
|
for (const [key] of Object.entries(fieldset)) {
|
||||||
|
if (key === 'entitlements') {
|
||||||
|
recordArray.attributes.push(
|
||||||
|
{
|
||||||
|
"name": key,
|
||||||
|
"description": key,
|
||||||
|
"type": "string",
|
||||||
|
"entitlement": true,
|
||||||
|
"managed": true,
|
||||||
|
"multi": true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
recordArray.attributes.push(
|
||||||
|
{
|
||||||
|
"name": key,
|
||||||
|
"description": key,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return recordArray
|
||||||
|
}).catch(err => {
|
||||||
|
throw new ConnectorError('error while getting accounts: ' + err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This code produces the following payload that will be sent back to IDN.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"identityAttribute": "email",
|
||||||
|
"displayAttribute": "id",
|
||||||
|
"groupAttribute": "entitlments",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"description": "id",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "enabled",
|
||||||
|
"description": "enabled",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "department",
|
||||||
|
"description": "department",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "locked",
|
||||||
|
"description": "locked",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entitlements",
|
||||||
|
"description": "entitlements",
|
||||||
|
"type": "string",
|
||||||
|
"entitlement": true,
|
||||||
|
"managed": true,
|
||||||
|
"multi": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"description": "password",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "displayName",
|
||||||
|
"description": "displayName",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lastName",
|
||||||
|
"description": "lastName",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email",
|
||||||
|
"description": "email",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "firstName",
|
||||||
|
"description": "firstName",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There are many properties in this payload, so you may want to remove some, but it can be hard to determine which properties to keep in a dynamic way. If you can programmatically determine which properties to remove, you can alter the ```discoverSchema()``` function to remove them.
|
||||||
|
|
||||||
|
## Test in IdentityNow
|
||||||
|
To test the account discover schema command in IDN, ensure that you upload your latest connector code and create a new source in IDN. After you configure and test your source connection, go to the ‘Account Schema’ page. You will see an empty schema.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To discover the schema for this source, click the ‘Options’ dropdown in the upper right and select ‘Discover Schema.’
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
IDN then asks you to assign attributes to ‘Account ID’ and 'Account Name.'
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Save the schema. You now have a populated account schema. A user of this source must provide further details, like descriptions and identifying which attributes are entitlements.
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Account Enable/Disable
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:------------------------------------:|
|
||||||
|
| Input - Enable | StdAccountEnableInput |
|
||||||
|
| Output - Enable | StdAccountEnableOutput |
|
||||||
|
| Input - Disable | StdAccountDisableInput |
|
||||||
|
| Output -Disable | StdAccountDisableOutput |
|
||||||
|
|
||||||
|
### Example StdAccountEnableInput/StdAccountDisableInput
|
||||||
|
```javascript
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Example StdAccountEnableOutput/StdAccountDisableOutput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"displayName": "John Doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Description
|
||||||
|
You typically invoke the account enable and account disable commands during the joiner, mover, leaver (JML) lifecycle. An identity’s leaving from the organization or change to a role that does not require access to one or more accounts triggers the account disable command. An identity’s rejoining the organization or move to a role that grants access to a previously disabled account triggers the account enable command.
|
||||||
|
|
||||||
|
Disabling accounts is generally preferred if the source supports account disabling so the account data remains for later reactivation or inspection. If the source does not support account disabling or deleting accounts is preferred when an identity leaves the organization, the connector performs the necessary steps to delete an account with the account disable function.
|
||||||
|
|
||||||
|
>🚧 It is important to note that although SaaS Connectivity supports the account delete command, IDN never sends the account delete command, only the account enable command. The connector’s developer determines the appropriate action for account disable on the source.
|
||||||
|
|
||||||
|
Account enable/disable is similar to implementing the account update command. If you have implemented your source call to modify any of the values on your source, then you can use the same method to implement the command. The following code implements enable and disable:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
.stdAccountDisable(async (context: Context, input: StdAccountDisableInput, res: Response<StdAccountDisableOutput>) => {
|
||||||
|
let account = await airtable.getAccount(input.key)
|
||||||
|
const change: AttributeChange = {
|
||||||
|
op: AttributeChangeOp.Set,
|
||||||
|
attribute: 'enabled',
|
||||||
|
value: 'false'
|
||||||
|
}
|
||||||
|
account = await airtable.changeAccount(account, change)
|
||||||
|
res.send(account.toStdAccountDisableOutput())
|
||||||
|
})
|
||||||
|
|
||||||
|
.stdAccountEnable(async (context: Context, input: StdAccountEnableInput, res: Response<StdAccountEnableOutput>) => {
|
||||||
|
let account = await airtable.getAccount(input.key)
|
||||||
|
const change: AttributeChange = {
|
||||||
|
op: AttributeChangeOp.Set,
|
||||||
|
attribute: 'enabled',
|
||||||
|
value: 'true'
|
||||||
|
}
|
||||||
|
account = await airtable.changeAccount(account, change)
|
||||||
|
res.send(account.toStdAccountEnableOutput())
|
||||||
|
})
|
||||||
|
```
|
||||||
145
docs/idn_docs/docs/custom_connectors/commands/account_list.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# Account List
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:-------------------------:|
|
||||||
|
| Input | undefined |
|
||||||
|
| Output | StdAccountListOutput |
|
||||||
|
|
||||||
|
### Example StdAccountListOutput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"displayName": "John Doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Description
|
||||||
|
The account list command aggregates all accounts from the target source into IdentityNow. IDN calls this command during a manual or scheduled account aggregation.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
For you to be able to implement this endpoint, the web service must expose an API for listing user accounts and entitlements (i.e. roles or groups). Sometimes, a target source’s API has a single endpoint providing all the attributes and entitlements a source account contains. However, some APIs may break these attributes and entitlements into separate API endpoints, requiring you to make multiple calls to gather all an account's necessary data. The following code from [airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/airtable.ts) shows the necessary steps to create a complete account from the various endpoints the API offers:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async getAllAccounts(): Promise<AirtableAccount[]> {
|
||||||
|
return this.airTableBase('Users').select({
|
||||||
|
view: 'Grid view'
|
||||||
|
}).firstPage().then(records => {
|
||||||
|
const recordArray: Array<AirtableAccount> = []
|
||||||
|
for (const record of records) {
|
||||||
|
recordArray.push(AirtableAccount.createWithRecords(record))
|
||||||
|
}
|
||||||
|
return recordArray
|
||||||
|
}).catch(err => {
|
||||||
|
throw new ConnectorError('error while getting accounts: ' + err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The following code snippet from [index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts) shows how to register the account list command on the connector object:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const connector = async () => {
|
||||||
|
|
||||||
|
// Get connector source config
|
||||||
|
const config = await readConfig()
|
||||||
|
|
||||||
|
// Use the vendor SDK, or implement own client as necessary, to initialize a client
|
||||||
|
const airtable = new AirtableClient(config)
|
||||||
|
|
||||||
|
return createConnector()
|
||||||
|
.stdAccountList(async (context: Context, input: undefined, res: Response<StdAccountListOutput>) => {
|
||||||
|
const accounts = await airtable.getAllAccounts()
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
res.send(account.toStdAccountListOutput())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
IDN expects each user in the target source to be converted into a format IDN understands. The specific attributes the web service returns depend on what your source provides.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
public toStdAccountListOutput(): StdAccountListOutput {
|
||||||
|
return this.buildStandardObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildStandardObject(): StdAccountListOutput | StdAccountCreateOutput | StdAccountReadOutput | StdAccountListOutput {
|
||||||
|
return {
|
||||||
|
key: SimpleKey(this.id),
|
||||||
|
disabled: !this.enabled,
|
||||||
|
locked: this.locked,
|
||||||
|
attributes: {
|
||||||
|
id: this.id,
|
||||||
|
displayName: this.displayName,
|
||||||
|
department: this.department,
|
||||||
|
firstName: this.firstName,
|
||||||
|
lastName: this.lastName,
|
||||||
|
email: this.email,
|
||||||
|
entitlements: this.entitlments,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The result of the account list command is not an array of objects but several individual objects. This is the format IDN expects, so if you see something like the following result while testing, it is normal:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"displayName": "John Doe",
|
||||||
|
"department": "sailpoint admins",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john",
|
||||||
|
"displayName": "John Doe External",
|
||||||
|
"department": "external",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe",
|
||||||
|
"email": "example@gmail.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Account Read
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:-------------------------:|
|
||||||
|
| Input | StdAccountReadInput |
|
||||||
|
| Output | StdAccountReadOutput |
|
||||||
|
|
||||||
|
### Example StdAccountReadInput
|
||||||
|
```javascript
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Example StdAccountReadOutput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"displayName": "John Doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Description
|
||||||
|
The account read command aggregates a single account from the target source into IdentityNow. IDN can call this command during a “one-off” account refresh, which you can trigger by aggregating an individual account in IDN.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
Implementation of account read is similar to account list's implementation, except the code only needs to get one account, not all the accounts. The following snippet is from [airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/airtable.ts):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async getAccount(identity: SimpleKeyType | CompoundKeyType): Promise<AirtableAccount> {
|
||||||
|
const id = <SimpleKeyType>identity
|
||||||
|
let found = false
|
||||||
|
|
||||||
|
return this.airTableBase('Users').select({
|
||||||
|
view: 'Grid view',
|
||||||
|
filterByFormula: `({Id} = '${id.simple.id}')`
|
||||||
|
}).firstPage().then(records => {
|
||||||
|
const recordArray: Array<AirtableAccount> = []
|
||||||
|
for (const record of records) {
|
||||||
|
found = true
|
||||||
|
recordArray.push(AirtableAccount.createWithRecords(record))
|
||||||
|
}
|
||||||
|
return recordArray[0]
|
||||||
|
}).catch(err => {
|
||||||
|
throw new ConnectorError('error while getting account: ' + err)
|
||||||
|
}).finally(() => {
|
||||||
|
// if the account is not found, throw the special NotFound error type
|
||||||
|
if (!found) {
|
||||||
|
throw new ConnectorError("Account not found", ConnectorErrorType.NotFound)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
One special case of this command is the ```NotFound``` type. On line 20, if an account is not found, the ```ConnectorError``` is thrown with the ```ConnectorErrorType.NotFound``` type. This tells IDN the account does not exist, and IDN then triggers the account create logic to generate the account.
|
||||||
|
|
||||||
|
The following code snippet from [index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts) shows how to register the account read command on the connector object:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Connector must be exported as module property named connector
|
||||||
|
export const connector = async () => {
|
||||||
|
|
||||||
|
// Get connector source config
|
||||||
|
const config = await readConfig()
|
||||||
|
|
||||||
|
// Use the vendor SDK, or implement own client as necessary, to initialize a client
|
||||||
|
const airtable = new AirtableClient(config)
|
||||||
|
|
||||||
|
return createConnector()
|
||||||
|
.stdAccountRead(async (context: Context, input: StdAccountReadInput, res: Response<StdAccountReadOutput>) => {
|
||||||
|
const account = await airtable.getAccount(input.key)
|
||||||
|
|
||||||
|
res.send(account.toStdAccountReadOutput())
|
||||||
|
})
|
||||||
|
...
|
||||||
|
```
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Account Unlock
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:-------------------------:|
|
||||||
|
| Input | StdAccountUnlockInput |
|
||||||
|
| Output | StdAccountUnlockOutput |
|
||||||
|
|
||||||
|
### Example StdAccountUnlockInput
|
||||||
|
```javascript
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Example StdAccountUnlockOutput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"displayName": "John Doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Description
|
||||||
|
The account lock and account unlock commands provide ways to temporarily prevent access to an account. IDN only supports the unlock command, so accounts must be locked on the source level, but they can be unlocked through IDN, and IDN can store the account's status.
|
||||||
|
|
||||||
|
Implementing account unlock is similar to the other commands that update attributes on an account. The following code unlocks an account:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
.stdAccountUnlock(async (context: Context, input: StdAccountUnlockInput, res: Response<StdAccountUnlockOutput>) => {
|
||||||
|
let account = await airtable.getAccount(input.key)
|
||||||
|
const change: AttributeChange = {
|
||||||
|
op: AttributeChangeOp.Set,
|
||||||
|
attribute: 'locked',
|
||||||
|
value: 'false'
|
||||||
|
}
|
||||||
|
account = await airtable.changeAccount(account, change)
|
||||||
|
res.send(account.toStdAccountUnlockOutput())
|
||||||
|
})
|
||||||
|
```
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Account Update
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:---------------------------:|
|
||||||
|
| Input | StdAccountUpdateInput |
|
||||||
|
| Output | StdAccountUpdateOutput |
|
||||||
|
|
||||||
|
### Example StdAccountUpdateInput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"op": <Set|Add|Remove>,
|
||||||
|
"attribute": <account attribute to modify>,
|
||||||
|
"value": <the value to use for the operation>
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Example StdAccountUpdateOutput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disabled": false,
|
||||||
|
"locked": false,
|
||||||
|
"attributes": {
|
||||||
|
"id": "john.doe",
|
||||||
|
"displayName": "John Doe",
|
||||||
|
"email": "example@sailpoint.com",
|
||||||
|
"entitlements": [
|
||||||
|
"administrator",
|
||||||
|
"sailpoint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The account update command triggers whenever IDN is told to modify an identity's attributes or entitlements on the target source. For example, granting an identity a new entitlement through a role, changing an identity’s lifecycle state, or modifying an identity attribute tied to an account attribute all trigger the account update command.
|
||||||
|
|
||||||
|
## Input Schema
|
||||||
|
The payload from IDN contains the ID of the identity to modify, the configuration items the connector needs to call the source API, and one or more change operations to apply to the identity. Each operation has the following special considerations:
|
||||||
|
|
||||||
|
- **Set:** Set tells the connector to overwrite the current value of the attribute or entitlement with the new value provided in the payload. The entire entitlement array resets if there are multi-valued entitlements.
|
||||||
|
|
||||||
|
- **Add:** Add only works for multi-valued entitlements. Add tells the connector to add one or more values to the entitlement. Add is often useful for group entitlements when new groups are added to the identity. If only one entitlement is added, it is represented as a ```string```. If more than one entitlement is added, it represented as an ```array of strings```.
|
||||||
|
|
||||||
|
- **Remove:** Remove is similar to add, but it also works for attributes or single-valued entitlements. If you apply remove to multi-valued entitlements, doing so tells the connector to remove the value(s) from the entitlement. If only one entitlement is removed, it is represented as a ```string```. If more than one entitlement is removed, it is represented as an ```array of strings```. If you apply remove to a single-valued entitlement or account attribute, doing so tells the connector to set the value to ```null``` or ```empty```.
|
||||||
|
|
||||||
|
The following example payload tells the connector to perform the following update actions:
|
||||||
|
|
||||||
|
- Set the title of the account to “Developer Advocate.”
|
||||||
|
|
||||||
|
- Add the account to two groups on the source: “Engineering” and “Support.”
|
||||||
|
|
||||||
|
- Remove the account from the “Moderator” group.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"type": "std:account:update",
|
||||||
|
"input": {
|
||||||
|
"identity": "95",
|
||||||
|
"changes": [
|
||||||
|
{"op": "Set", "attribute": "title", "value": "Developer Advocate"},
|
||||||
|
{"op": "Add", "attribute": "groups", "value": ["Engineering", "Support"]},
|
||||||
|
{"op": "Remove", "attribute": "groups", "value": "Moderator"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response Schema
|
||||||
|
After the connector applies the operations defined in the input payload, the connector must respond to IDN with the changes to the account so IDN can update the identity accordingly. If an account update operation results in no changes to the account, the connector responds with an empty object ```{}```. If the update operation results in one or more changes to the account, the connector responds with the complete account as it exists in the source, just like an account read response. IDN can parse the response and apply the differences accordingly.
|
||||||
|
|
||||||
|
## Testing in IdentityNow
|
||||||
|
You can test the account update command the way you test the [Account Create](./account_create.md) command. Follow the steps in “Testing in IdentityNow” from “Account Create” to set up an access profile and role. Be sure to run the aggregation so the account(s) are created in the target source. Once the account(s) are created in the target source, modify the access profile to grant an additional entitlement. Return to the role and click the ‘Update’ button in the upper right corner. Doing so triggers the account update command because the accounts are already created in the target source. Once the update is complete, ensure the account(s) have the additional entitlement.
|
||||||
|
|
||||||
|
Note: Testing the account update command for removing entitlements using this method does not work. You can remove the entitlement from the access profile and run an update, but IDN will not send an update command to the connector to remove the entitlement. We are looking for suggestions on how to test the removal of entitlements.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Entitlement List
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:---------------------------:|
|
||||||
|
| Input | StdEntitlementListInput |
|
||||||
|
| Output | StdEntitlementListOutput |
|
||||||
|
|
||||||
|
### Example StdEntitlementListInput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"type": "group"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Example StdEntitlementListOutput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "administrator"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "group",
|
||||||
|
"attributes": {
|
||||||
|
"id": "administrator",
|
||||||
|
"name": "Administrator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The entitlement list command triggers during a manual or scheduled entitlement aggregation operation within IDN. This operation gathers a list of all entitlements available on the target source, usually multi-valued entitlements like groups or roles. This operation provides IDN administrators with a list of entitlements available on the source so they can create access profiles and roles accordingly, and it provides IDN with more details about the entitlements. The entitlement schema’s minimum requirements are name and ID, but you can add other values, such as created date, updated date, status, etc.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Defining the Schema
|
||||||
|
The entitlement schema is defined in the [connector-spec.json](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/connector-spec.json) file. Currently, only the multi-valued “group” type is supported. The following values are the minimum requirements, but you can add more attributes.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
...
|
||||||
|
"entitlementSchemas": [
|
||||||
|
{
|
||||||
|
"type": "group",
|
||||||
|
"displayAttribute": "name",
|
||||||
|
"identityAttribute": "id",
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"type": "string",
|
||||||
|
"description": "Unique ID of the group (ex. admin)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "string",
|
||||||
|
"description": "The display name of the group (ex. Admin)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
This can be implemented in the main connector file, [index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
...
|
||||||
|
.stdEntitlementList(async (context: Context, input: StdEntitlementListInput, res: Response<StdEntitlementListOutput>) => {
|
||||||
|
const groups = await airtable.getAllEntitlements()
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
res.send(group.toStdEntitlementListOutput())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
...
|
||||||
|
...
|
||||||
|
...
|
||||||
|
public toStdEntitlementListOutput(): StdEntitlementListOutput {
|
||||||
|
return this.buildStandardObject();
|
||||||
|
}
|
||||||
|
private buildStandardObject(): StdEntitlementReadOutput | StdEntitlementListOutput {
|
||||||
|
return {
|
||||||
|
key: SimpleKey(this.id),
|
||||||
|
type: 'group',
|
||||||
|
attributes: {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Entitlement Read
|
||||||
|
|
||||||
|
>📘 At this time Entitlement Read is not triggered from IDN for any specific workflow and as such it is not necessary to implement this in order to have a fully functional connector.
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:---------------------------:|
|
||||||
|
| Input | StdEntitlementReadInput |
|
||||||
|
| Output | StdEntitlementReadOutput |
|
||||||
|
|
||||||
|
### Example StdEntitlementReadInput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "john.doe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "group"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Example StdEntitlementReadOutput
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"key": {
|
||||||
|
"simple": {
|
||||||
|
"id": "administrator"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "group",
|
||||||
|
"attributes": {
|
||||||
|
"id": "administrator",
|
||||||
|
"name": "Administrator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Response Schema
|
||||||
|
|
||||||
|
Entitlement read fetches a single entitlement’s attributes and returns the resulting object to IDN, similar to how entitlement list does. You can implement this in the main connector file, [index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
...
|
||||||
|
.stdEntitlementRead(async (context: Context, input: StdEntitlementReadInput, res: Response<StdEntitlementReadOutput>) => {
|
||||||
|
const group = await airtable.getEntitlement(input.key)
|
||||||
|
|
||||||
|
res.send(group.toStdEntitlementReadOutput())
|
||||||
|
})
|
||||||
|
...
|
||||||
|
...
|
||||||
|
...
|
||||||
|
public toStdEntitlementReadOutput(): StdEntitlementReadOutput {
|
||||||
|
return this.buildStandardObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildStandardObject(): StdEntitlementReadOutput | StdEntitlementListOutput {
|
||||||
|
return {
|
||||||
|
key: SimpleKey(this.id),
|
||||||
|
type: 'group',
|
||||||
|
attributes: {
|
||||||
|
id: this.id,
|
||||||
|
name: this.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# Test Connection
|
||||||
|
|
||||||
|
| Input/Output | Data Type |
|
||||||
|
|:-------------|:-------------------------:|
|
||||||
|
| Input | undefined |
|
||||||
|
| Output | StdTestConnectionOutput |
|
||||||
|
|
||||||
|
### Example StdTestConnectionOutput
|
||||||
|
```javascript
|
||||||
|
{}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
The test connection command ensures the connector can communicate with the target web service. It validates API credentials, host names, ports, and other configuration items. To implement this command, look for either a health endpoint or a simple GET endpoint. Some web services implement a health endpoint that returns status information about the service, which can be useful to test a connection. If no health endpoint exists, use a simple GET endpoint that takes few to no parameters to ensure the connector can make a successful call to the web service.
|
||||||
|
|
||||||
|
Use ‘Test Connection’ in the IDN UI after an admin has finished entering configuration information for a new instance of the connector.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
In [index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts), add the test connection function handler to your connector. Within this function, send a simple request to your web service to ensure the connection works. The web service this connector targets has a JavaScript SDK, so define your own function like the following example to test the connection:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const connector = async () => {
|
||||||
|
|
||||||
|
// Get connector source config
|
||||||
|
const config = await readConfig()
|
||||||
|
|
||||||
|
// Use the vendor SDK, or implement own client as necessary, to initialize a client
|
||||||
|
const airtable = new AirtableClient(config)
|
||||||
|
|
||||||
|
return createConnector()
|
||||||
|
.stdTestConnection(async (context: Context, input: undefined, res: Response<StdTestConnectionOutput>) => {
|
||||||
|
res.send(await airtable.testConnection())
|
||||||
|
})
|
||||||
|
...
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To implement the ```testConnection()``` function, use the following function created in the web service client code, [airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/airtable.ts).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
/**
|
||||||
|
* Test connection by listing users from the Airtable instance.
|
||||||
|
* This will make sure the apiKey has the correct access.
|
||||||
|
* @returns empty struct if response is 2XX
|
||||||
|
*/
|
||||||
|
async testConnection(): Promise<any> {
|
||||||
|
return this.airTableBase('Users').select({
|
||||||
|
view: 'Grid view'
|
||||||
|
}).firstPage().then(records => {
|
||||||
|
return {}
|
||||||
|
}).catch(err => {
|
||||||
|
throw new ConnectorError('unable to connect')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This function calls an endpoint on the target web service to list all users. If the call is successful, the web service returns an empty object, which is okay because you do not need to do anything with the data. Your only goal is to ensure that you can make API calls with the provided configuration.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Common CLI/SDK Commands
|
||||||
|
|
||||||
|
- **Development**
|
||||||
|
- Create a project on your local system: ```sp conn init "my-project"```
|
||||||
|
- Test your connector locally: ```npm run dev```
|
||||||
|
- **Deployment**
|
||||||
|
- Create an empty connector in your IDN Org (used to get id so you can upload): ```sp conn create "my-project"```
|
||||||
|
- Build a project: ```npm run pack-zip```
|
||||||
|
- Upload your connector to your IDN Org: ```sp conn upload -c [connectorID | connectorAlias] -f dist/[connector filename].zip```
|
||||||
|
- **Exploring**
|
||||||
|
- List connectors in your IDN Org: ```sp conn list```
|
||||||
|
- List your connector tags: ```sp conn tags list -c [connectorID | connectorAlias]```
|
||||||
|
- **Testing and Debugging**
|
||||||
|
- Test your connector on the IDN Org: ```sp connectors invoke [action] -c [connectorID | connectorAlias] -p config.json```
|
||||||
|
- Get a list of actions: ```sp conn invoke -h```
|
||||||
|
- Run read-only integration tests against your connector: ```sp conn validate -p config.json -c [connectorID | connectorAlias] -r```
|
||||||
|
- Tail IDN Org connector logs: ```sp conn logs tail```
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Getting Started with the CLI
|
||||||
|
|
||||||
|
> 📘 **Currently in Beta**
|
||||||
|
>
|
||||||
|
> Connector development using this SDK and CLI is currently in beta. To participate, please sign up for the beta [here](https://app.smartsheet.com/b/form/1e4a7f063de4496b9c6d33f191996950). Once your tenant is activated, you can access and create new connectors.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
To build the CLI, the following packages are required:
|
||||||
|
- Golang >= 1.17
|
||||||
|
- Make >= 3.81
|
||||||
|
|
||||||
|
To develop a connector, the following packages are required:
|
||||||
|
- Node >= 14.17.3
|
||||||
|
|
||||||
|
## IDE
|
||||||
|
|
||||||
|
Although you can develop connectors in a text editor, use an Integrated Development Environment (IDE) for a better experience. There are many IDEs that support Javascript/Typescript, including [Visual Sudio Code](https://code.visualstudio.com/Download), a free IDE with native support for Javascript/Typescript. VS Code provides syntax highlight, debugging, hints, code completion, and other helpful options.
|
||||||
|
|
||||||
|
## Install CLI
|
||||||
|
|
||||||
|
SailPoint provides a CLI tool to manage the connectors' lifecycles. To install and set up the CLI, follow the instructions in this repository's README file (TBD. This repository is not public yet):
|
||||||
|
|
||||||
|
[SailPoint CLI on GitHub](https://github.com/sailpoint-oss/sp-connector-cli)
|
||||||
|
|
||||||
|
## Create new project
|
||||||
|
|
||||||
|
To create an empty connector project, run ```sp conn init "my-project"``` in your terminal. The CLI init command creates a new folder with your project name in the location where you run the command.
|
||||||
|
|
||||||
|
Run npm install to change the directory to the project folder and install the dependencies. You may need to provide your GitHub credentials because the CLI tool depends on a SailPoint internal GitHub repository.
|
||||||
|
|
||||||
|
### Source Files
|
||||||
|
The initial project source directory contains three main files:
|
||||||
|
|
||||||
|
- **index.ts:** Use this file to register all the available commands the connector supports, provide the necessary configuration options to the client code implementing the API for the source, and pass data the client code obtains to IdentityNow. This file can either use a vendor supplied client Software Development Kit (SDK) to interact with the web service or reference custom client code within the project.
|
||||||
|
|
||||||
|
- **my-client.ts:** Use this template to create custom client code to interact with a web service’s APIs. If the web service does not provide an SDK, you can modify this file to implement the necessary API calls to interact with the source web service.
|
||||||
|
|
||||||
|
- **connector-spec.ts** This file describes how the connector works to IDN. More information about the connector spec is available in the next section. At a high level, it has the information for the following:
|
||||||
|
- What commands the connector supports
|
||||||
|
- What config values the user must provide when creating the connector
|
||||||
|
- Defining the account schema
|
||||||
|
- Defining the entitlment schema
|
||||||
|
- Defining the account create template that maps fields from IDN to the connector
|
||||||
|
|
||||||
|
These files are templates that provide guidance to begin implementing the connector on the target web service. Although you can implement a connector's entire functionality within these three files (or even just one if the web service provides an SDK), you can implement your own code architecture, like breaking out common utility functions into a separate file or creating separate files for each operation.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Test, Build and Deploy Using the CLI
|
||||||
|
## Testing Your Connector
|
||||||
|
|
||||||
|
You can use the following Postman Collection file to locally run tests for each of the commands.
|
||||||
|
|
||||||
|
[Postman Collection](../../../files/SaaS_Connectivity.postman_collection)
|
||||||
|
|
||||||
|
As you implement command handlers, you must test them. The connector SDK provides some utility methods to locally run your connector. To start, run ```npm run dev``` within the connector project folder. This script locally starts an Express server on port 3000, which can be used to invoke a command against the connector. You do not need to restart this process after making changes to connector code. Once the Express server is started, you can send ```POST``` requests to ```localhost:3000``` and test your command handlers. For example, you can run ```POST localhost:3000``` with the following payload to run the stdAccountRead handler method.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "std:account:read",
|
||||||
|
"input": {
|
||||||
|
"identity": "john.doe"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **type:** The command handler’s name. It also refers to the operation being performed.
|
||||||
|
- **input:** Input to provide to the command handler.
|
||||||
|
- **config:** The configuration values required to test locally. A ```token``` value is not required, but the default project specifies ```token```, so you must include it in your request to begin.
|
||||||
|
|
||||||
|
## Create and upload connector bundle
|
||||||
|
Follow these steps to use the CLI to package a connector bundle, create it in your IdentityNow org, and upload it to IdentityNow.
|
||||||
|
### Package connector files
|
||||||
|
You must compress the files in the connector project into a zip file before uploading them to IdentityNow.
|
||||||
|
|
||||||
|
Use the CLI to run ```npm run pack-zip``` to build and package the connector bundle. Put the resulting zip file in the ```dist``` folder.
|
||||||
|
|
||||||
|
|
||||||
|
### Create connector in your org
|
||||||
|
Before uploading the zip file, you must create an entry for the connector in your IdentityNow org. Run ```sp conn create "my-project"``` to create a connector entry.
|
||||||
|
|
||||||
|
The response to this command contains a connector ID you can use to manage this connector.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sp conn create "example-connector"
|
||||||
|
+--------------------------------------+----------------------------+
|
||||||
|
| ID | ALIAS |
|
||||||
|
+--------------------------------------+----------------------------+
|
||||||
|
| a9360354-2f9d-4111-bff6-7cd53184a61e | example-connector |
|
||||||
|
+--------------------------------------+----------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
Run ```sp conn list``` to retrieve the connector ID at any time.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sp conn list
|
||||||
|
+--------------------------------------+----------------------------+
|
||||||
|
| ID | ALIAS |
|
||||||
|
+--------------------------------------+----------------------------+
|
||||||
|
| 39fe3f4f-3559-4e1f-98bb-2f6d0bcb13dc | airtable-hr |
|
||||||
|
| a9360354-2f9d-4111-bff6-7cd53184a61e | example-connector |
|
||||||
|
+--------------------------------------+----------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Upload connector zip file to IdentityNow
|
||||||
|
Run ```sp conn upload -c [connectorID | connectorAlias] -f dist/[connector filename].zip``` to upload the zip file built from the previous step to IdentityNow.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sp conn upload -c example-connector -f dist/example-connector-0.1.0.zip
|
||||||
|
+--------------------------------------+---------+
|
||||||
|
| CONNECTOR ID | VERSION |
|
||||||
|
+--------------------------------------+---------+
|
||||||
|
| a9360354-2f9d-4111-bff6-7cd53184a61e | 7 |
|
||||||
|
+--------------------------------------+---------+
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The first version upload of connector zip file also creates the ```latest``` tag, pointing to the latest version of the connector file. After uploading the connector bundle zip file, you can run ```sp conn tags list -c example-connector``` to see the connector tags.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sp conn tags list -c example-connector
|
||||||
|
+--------------------------------------+----------+----------------+
|
||||||
|
| ID | TAG NAME | ACTIVE VERSION |
|
||||||
|
+--------------------------------------+----------+----------------+
|
||||||
|
| 8cd99eea-cfe1-424f-abfd-6494292b13a8 | latest | 3 |
|
||||||
|
+--------------------------------------+----------+----------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test connector in IdentityNow
|
||||||
|
Follow these steps to test a connector bundle in both IdentityNow and the IdentityNow user interface (UI).
|
||||||
|
|
||||||
|
### Test connector bundle in IdentityNow
|
||||||
|
The connector CLI provides ways to test invoking commands with any connector upload version. Before running a command, create a file, **config.json**, in the root project folder. Include any configuration items required to interact with the target web service in this file, such as API token, username, password, organization, version, etc. The following snippet is an example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "123acsa494fbasd#asd"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This file is required and requires at least one key value even if your connector does not require anything.
|
||||||
|
|
||||||
|
Next, invoke the command using the connector ID and config.json. For example, this command invokes std:account:list command on the connector:
|
||||||
|
|
||||||
|
```sp connectors invoke account-list -c example-connector -p config.json```
|
||||||
|
|
||||||
|
You will receive a list of JSON objects for each account the connector contains.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sp connectors invoke account-list -c example-connector -p config.json
|
||||||
|
2022/06/29 11:06:07 Running "std:account:list" with "{}"
|
||||||
|
{"key":{"simple":{"id":"john.doe"}},"disabled":false,"locked":false,"attributes":{"id":"john.doe","displayName":"John Doe","entitlements":["administrator","sailpoint"]}}
|
||||||
|
```
|
||||||
|
|
||||||
|
>⚠️ Sensitive information!
|
||||||
|
>
|
||||||
|
> Ensure that you add config.json to your .gitignore file so you do not accidentally store secrets in your code repository.
|
||||||
|
|
||||||
|
## Test connector from IdentityNow UI
|
||||||
|
Go to your IdentityNow org’s source section. Create a source from the connector you just uploaded. This connector will display in the dropdown list: **example-connector (tag: latest)**
|
||||||
|
|
||||||
|
After creating a source, you can to test connection, aggregate account, etc. from the IdentityNow UI.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Connector specification file (connector-spec.json)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
The connector spec file tells IDN how the connector should interact between IDN and the custom connector. It is the glue between IDN and the connector, so understanding the different sections are key to understanding how to build a custom connectors.
|
||||||
|
|
||||||
|
## Sample File
|
||||||
|
|
||||||
|
To see a sample spec file, see this link: [connector-spec.json](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/connector-spec.json)
|
||||||
|
|
||||||
|
## Description of Fields
|
||||||
|
|
||||||
|
The following describes in detail the different fields in the connector spec:
|
||||||
|
|
||||||
|
- **name:** The name of the connector as it appears in IDN. Tags can be appended to this name.
|
||||||
|
|
||||||
|
- **keyType:** Either “simple” or “compound” This determines which type of key your connector expects to receive and send back for each of the commands. This must always be indicated in your connector spec - the connector returns the correct type for each command that returns a key type.
|
||||||
|
- For example, the stdAccountRead command input is the StdAccountReadInput. if you select keyType as “simple,” then the StdAccountReadInput.key will be the type SimpleKey.
|
||||||
|
|
||||||
|
- **commands:** The list of commands the connector supports. A full list of available commands can be found here.
|
||||||
|
|
||||||
|
- **sourceConfig** A list of configuration items you must provide when you create a source in IDN. The order of these items is preserved in the UI.
|
||||||
|
- **type:** This is always “section” - it indicates a new section.
|
||||||
|
- **key:** The name of the configuration item as it is referenced in code.
|
||||||
|
- **label:** The name of the configuration item as it appears in the UI.
|
||||||
|
- **required** (Optional): Set to 'false' by default. Valid values are 'true' or 'false.' You must populate required configuration items in the IDN source configuration wizard before continuing.
|
||||||
|
- **type:** The configuration items' types. The following types are valid:
|
||||||
|
- text
|
||||||
|
- password
|
||||||
|
- url
|
||||||
|
- email
|
||||||
|
- number
|
||||||
|
- checkbox
|
||||||
|
- json
|
||||||
|
- **accountSchema:** The schema for an account in IDN populated by data from the source.
|
||||||
|
- **displayAttribute:** Identifies the attribute (defined below) used to map to ```Account Name``` in the IdentityNow account schema. This should be a unique value even though it is not required because the connector will use this value to correlate accounts in IDN to accounts in the source system.
|
||||||
|
- **identityAttribute:** Identifies the attribute (defined below) used to map to ```Account ID``` in the IdentityNow account schema. This must be a globally unique identifier, such as email address, employee ID, etc.
|
||||||
|
- **groupAttribute:** Identifies the attribute used to map accounts to entitlements. For example, a web service can define ```groups``` that users are members of, and the ```groups``` grant entitlements to each user. In this case, **groupAttribute** is “groups,” and there is also an account attribute called “groups”.
|
||||||
|
- **attributes:** One or more attributes that map to a user’s attribute on the target source. Each attribute defines the following:
|
||||||
|
- **name:** The attribute’s name as it appears in IDN.
|
||||||
|
- **type:** The attribute’s type. Possible values are ```string```, ```boolean```, ```long```, and ```int```.
|
||||||
|
- **description:** A helpful description of the attribute. This is useful to source owners when they are trying to understand the account schema.
|
||||||
|
- **managed:** This indicates whether the entitlements are manageable through IDN or read-only.
|
||||||
|
- **entitlement:** This boolean indicates whether the attribute is an entitlement. Entitlements give identities privileges on the source system. Use this indication to determine which fields to synchronize with accounts in IDN for tasks such as separation of duties and role assignment. The boolean indicates whether the attribute is an entitlement.
|
||||||
|
- **multi:** This indicates entitlements that are stored in an array format. This one field can store multiple entitlements for a single account.
|
||||||
|
- **entitlementSchemas:** A list of entitlement schemas in IDN populated by data from the source.
|
||||||
|
- **type:** The entitlement’s type. Currently, only ```group``` is supported.
|
||||||
|
- **displayAttribute:** The entitlement attribute’s name. This can be the ```name``` or another human friendly identifier for a group.
|
||||||
|
- **identityAttribute:** The entitlement attribute’s unique ID. This can be the ```id``` or another unique key for a group.
|
||||||
|
- **attributes:** The entitlement’s list of attributes. This list of attributes is an example: ```id```, ```name```, and ```description```.
|
||||||
|
- **name:** The name of the attribute as it appears in IDN.
|
||||||
|
- **type:** The attribute’s type. Possible values are ```string```, ```boolean```, ```long```, and ```int```.
|
||||||
|
- **description:** A helpful description the attribute. This is useful to source owners when they are trying to understand the account schema.
|
||||||
|
- **accountCreateTemplate:** A map of identity attributes IDN will pass to the connector to create an account in the target source.
|
||||||
|
- **key:** The unique identifier of the attribute. This is also the name that is presented in the Create Profile screen in IDN.
|
||||||
|
- **label:** A friendly name for presentation purposes.
|
||||||
|
- **type:** The attribute’s type. Possible values are ```string```, ```boolean```, ```long```, and ```int```.
|
||||||
|
- **initialValue (Optional):** Use this to specify identitAttribute mapping, generator or default values.
|
||||||
|
- **type:** The initial value type. Possible values are ```identityAttribute```, ```generator```, ```static```.
|
||||||
|
- **attributes:** Attributes change depending on the type selected.
|
||||||
|
- **name:** Use this to identify the mapping for identityAttribute type, or the generator to use (```Create Password```, ```Create Unique Account ID```).
|
||||||
|
- **value:** Use this as the default value for the static type.
|
||||||
|
- **maxSize:** Use this for the Create Unique Account ID generator type. This value specifies the maximum size of the username to be generated.
|
||||||
|
- **maxUniqueChecks:** Use this for the Create Unique Account ID generator type. This value specifies the maximum retries in case a unique ID is not found with the first random generated user.
|
||||||
|
- **template:** Use this for the Create Unique Account ID generator type. This value specifies the template used for generation. Example: ```"$(firstname).$(lastname)$(uniqueCounter)"```.
|
||||||
|
- **required (Optional):** Determines whether the account create operation requires this attribute. It defaults to ```false```. If it is ```true``` and IdentityNow encounters an identity missing this attribute, IDN does not send the account to the connector for account creation.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Example Connectors
|
||||||
|
|
||||||
|
- [Airtable connector](https://github.com/sailpoint-oss/airtable-example-connector) is a real connector that works like a flat file data source and is great for demonstrating how a connector works.
|
||||||
|
|
||||||
|
- [Discourse Connector](https://github.com/sailpoint-oss/discourse-connector-2) is a real connector that works with the [Discourse service](https://www.discourse.org/). The documentation for each command references code from this example application.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Postman Collection
|
||||||
|
|
||||||
|
Use the following Postman Collection file to run tests for each of the commands locally.
|
||||||
|
|
||||||
|
[Postman Collection](../../../files/SaaS_Connectivity.postman_collection)
|
||||||
35
docs/idn_docs/docs/custom_connectors/in_depth/api_calls.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
Calling API endpoints sequentially for hundreds or thousands of accounts is slow. If several API calls are required to build a user’s account, then it is recommended that you use asynchronous functions to speed up this task. Asynchronous functions allow your program to execute several commands at once, which is especially important for high latency commands like calling API endpoints - each call to an endpoint can take anywhere from several milliseconds to several seconds. The following code snippet from [discourse-client.ts](https://github.com/sailpoint-oss/discourse-connector-2/blob/main/Discourse/src/discourse-client.ts) shows how you can use asynchronous functions to quickly build a list of account profiles for your source’s users:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async getUsers(): Promise<User[]> {
|
||||||
|
// First, get the members of the group. This will call a single endpoint to get all the users of a group.
|
||||||
|
const groupMembers = await this.getGroupMembers(this.primaryGroup!)
|
||||||
|
|
||||||
|
// To get the full user representation, we need to invoke a single API call for each user.
|
||||||
|
// Because there can be hundreds, or even thousands of users, this would take several minutes
|
||||||
|
// if run sequentially. We use Promise.all to execute a list of API calls in parallel.
|
||||||
|
let users = await Promise.all(groupMembers.map(member => this.getUser(member.id!.toString())))
|
||||||
|
|
||||||
|
// Emails aren't included in the above call. Once again, we need to execute several API calls
|
||||||
|
// in parallel.
|
||||||
|
const emails = await Promise.all(groupMembers.map(member => this.getUserEmailAddress(member.username!)))
|
||||||
|
|
||||||
|
// Finally, we need to update our user accounts with the emails we obtained. This code
|
||||||
|
// doesn't make network calls, so it doesn't need to use Promise.all.
|
||||||
|
for (let i = 0; i < groupMembers.length; i++) {
|
||||||
|
users[i].email = emails[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
- Line 3 gets all the user IDs for a default group to which all the users you want to track are assigned.
|
||||||
|
|
||||||
|
- Line 6 gets more attributes for each user present in the group. There can be hundreds of users who need their attributes fetched, so use Promise.all to build and execute the API calls asynchronously, speeding up the operation’s completion time.
|
||||||
|
|
||||||
|
- Line 9 uses the same strategy as Line 6, except it calls another endpoint that will get each user’s email address, which isn’t present in the previous API call. Use Promise.all again to speed up the operation.
|
||||||
|
|
||||||
|
- Line 12-14 combines the data you gathered from the preceding calls to complete your user accounts.
|
||||||
|
|
||||||
|
>📘 As a general guideline, any time you must execute several API calls that all call the same endpoint, it is recommended that you use Promise.all to speed up the operation.
|
||||||
77
docs/idn_docs/docs/custom_connectors/in_depth/debugging.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Debugging
|
||||||
|
|
||||||
|
## Debug locally
|
||||||
|
An easy way to debug locally is to use ```console.log()``` to print debug information to your console. You can add ```console.log()``` statements anywhere, and the messages they print can contain static text or variables. For example, to see the contents of an input object when you are invoking the ```stdAccountCreate``` command, you can craft the following debug logic:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export const connector = async () => {
|
||||||
|
|
||||||
|
return createConnector()
|
||||||
|
|
||||||
|
.stdAccountCreate(async (context: Context, input: StdAccountCreateInput, res: Response<StdAccountCreateOutput>) => {
|
||||||
|
// Print the contents of input to the console. Must use
|
||||||
|
// JSON.stringify() to print the contents of an object.
|
||||||
|
console.log(`Input received for account create: ${JSON.stringify(input)}`)
|
||||||
|
if (!input.attributes.id) {
|
||||||
|
throw new ConnectorError('identity cannot be null')
|
||||||
|
}
|
||||||
|
const user = await airtable.createAccount(input)
|
||||||
|
logger.info(user, "created user in Airtable")
|
||||||
|
res.send(user.toStdAccountCreateOutput())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```console.log()``` statements work anywhere, and they work when you deploy your connector to IDN. However, these statements can create clutter in your code. You will often have to clean up debug statements once you are done.
|
||||||
|
|
||||||
|
If your IDE supports debugging JavaScript, then your IDE’s built-in debugger can be a powerful and easy way to debug your code.
|
||||||
|
|
||||||
|
## Debug in VS Code
|
||||||
|
### Debug through the javascript debug terminal
|
||||||
|
|
||||||
|
In VS Code, open a javascript debug terminal window and run the npm run dev command.
|
||||||
|
|
||||||
|
```npm run dev```
|
||||||
|
|
||||||
|
Now you can set breakpoints in your typescript files in VS Code:
|
||||||
|

|
||||||
|
|
||||||
|
### Debug through the VS Code Debug configuration
|
||||||
|
To simplify the debugging process, you can consolidate the debugging steps into a VS Code launch configuration. The following snippet is an example of how you would do so:
|
||||||
|
|
||||||
|
**Launch.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Program via NPM",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run-script",
|
||||||
|
"dev"
|
||||||
|
],
|
||||||
|
"windows": {
|
||||||
|
"runtimeExecutable": "npm.cmd",
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
},
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"type": "node"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
With these configurations set, you can run the debugger by selecting the options shown in the following image:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Debug in IdentityNow
|
||||||
|
You can use the ```sp conn logs``` command to gain insight into how your connector is performing while running in IDN. See the section on logging for more information.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Error Handling
|
||||||
|
Any time code can fail due to validation issues, connectivity or configuration errors, handle the error and provide information back to the user about what went wrong. If you handle your errors properly, it will be easier to debug and pinpoint what happened in your connector when something goes wrong.
|
||||||
|
|
||||||
|
## Connector Errors
|
||||||
|
The connector SDK has a built-in ConnectorError to use in your project to handle most generic errors:
|
||||||
|
|
||||||
|
[airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/airtable.ts)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ConnectorError } from "@sailpoint/connector-sdk"
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
export class AirtableClient {
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
async getAllAccounts(): Promise<AirtableAccount[]> {
|
||||||
|
return this.airTableBase('Users').select({
|
||||||
|
view: 'Grid view'
|
||||||
|
}).firstPage().then(records => {
|
||||||
|
const recordArray: Array<AirtableAccount> = []
|
||||||
|
for (const record of records) {
|
||||||
|
recordArray.push(AirtableAccount.createWithRecords(record))
|
||||||
|
}
|
||||||
|
return recordArray
|
||||||
|
}).catch(err => {
|
||||||
|
throw new ConnectorError('error while getting accounts: ' + err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Errors
|
||||||
|
You can also create custom errors and use them in your code to give more meaningful and specific responses to error states. For example, when you are configuring your connector, it is recommended that you throw an ```InvalidConfigurationError``` instead of a generic ConnectorError. To do this, create the custom error:
|
||||||
|
|
||||||
|
[invalid-configuration-error.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/errors/invalid-configuration-error.ts)
|
||||||
|
```javascript
|
||||||
|
import { ConnectorError, ConnectorErrorType } from '@sailpoint/connector-sdk'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when an application missing configuration during initialization
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class InvalidConfigurationError extends ConnectorError {
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param message Error message
|
||||||
|
* @param type ConnectorErrorType they type of error
|
||||||
|
*/
|
||||||
|
constructor(message: string, type?: ConnectorErrorType) {
|
||||||
|
super(message, type)
|
||||||
|
this.name = 'InvalidConfigurationError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then throw the error in your code:
|
||||||
|
|
||||||
|
[airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/airtable.ts)
|
||||||
|
```javascript
|
||||||
|
import { InvalidConfigurationError } from "./errors/invalid-configuration-error"
|
||||||
|
|
||||||
|
export class AirtableClient {
|
||||||
|
private readonly airTableBase: Airtable.Base
|
||||||
|
constructor(config: any) {
|
||||||
|
// Fetch necessary properties from config.
|
||||||
|
// Following properties actually do not exist in the config -- it just serves as an example.
|
||||||
|
if (config.apiKey == null) {
|
||||||
|
throw new InvalidConfigurationError('token must be provided from config')
|
||||||
|
}
|
||||||
|
if (config.airtableBase == null) {
|
||||||
|
throw new InvalidConfigurationError('airtableBase base id needed')
|
||||||
|
}
|
||||||
|
Airtable.configure({apiKey: config.apiKey})
|
||||||
|
this.airTableBase = Airtable.base(config.airtableBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
23
docs/idn_docs/docs/custom_connectors/in_depth/linting.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Linting
|
||||||
|
To add linting to your project, simple install the linter using NPM:
|
||||||
|
|
||||||
|
|
||||||
|
```npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin``
|
||||||
|
|
||||||
|
Then add the ```.eslintrc.yml``` file on the project root:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env:
|
||||||
|
browser: true
|
||||||
|
es2021: true
|
||||||
|
extends:
|
||||||
|
- eslint:recommended
|
||||||
|
- plugin:@typescript-eslint/recommended
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
parserOptions:
|
||||||
|
ecmaVersion: latest
|
||||||
|
sourceType: module
|
||||||
|
plugins:
|
||||||
|
- '@typescript-eslint'
|
||||||
|
rules: {}
|
||||||
|
```
|
||||||
170
docs/idn_docs/docs/custom_connectors/in_depth/logging.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# Logging
|
||||||
|
|
||||||
|
## Printing Logs with the CLI
|
||||||
|
Fetch logs from IDN by issuing the ```sp conn logs``` command:
|
||||||
|
```bash
|
||||||
|
$ sp conn logs
|
||||||
|
|
||||||
|
[2022-07-14T11:04:24.276-04:00] ERROR | connectorMessage ▶︎ {"commandType":"std:test-connection","invocationId":"49213a1c-0ba5-48f4-bceb-b6b5b0ec18d5","message":"Connector error ConnectorError: unable to connect, check your connection parameters and API key\n at /app/index.js:1:441187\n at runMicrotasks (\u003canonymous\u003e)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async /app/index.js:1:441923\n at async Connector._exec (/app/index.js:1:5872)\n at async /usr/bin/index.js:1:77407 {\n type: 'generic'\n}\n","requestId":"cca732a2-084d-4433-9bd5-ed22fa397d8d","version":2}
|
||||||
|
[2022-07-14T11:04:24.310-04:00] INFO | commandOutcome ▶︎ {"commandType":"std:test-connection","completed":true,"elapsed":62,"error":"[ConnectorError] unable to connect, check your connection parameters and API key","message":"command failed","requestId":"cca732a2-084d-4433-9bd5-ed22fa397d8d","version":2}
|
||||||
|
[2022-07-14T11:04:24.442-04:00] INFO | invokeCommand ▶︎ Command execution started : std:test-connection, for connector version 29.
|
||||||
|
[2022-07-14T11:04:24.442-04:00] INFO | invokeCommand ▶︎ Command invocation complete : std:test-connection, for connector version: 29. Elapsed time 144.178µs
|
||||||
|
[2022-07-14T11:04:24.812-04:00] INFO | commandOutcome ▶︎ {"commandType":"std:test-connection","completed":true,"elapsed":369,"message":"command completed","requestId":"cca732a2-084d-4433-9bd5-ed22fa397d8d","version":29}
|
||||||
|
[2022-07-14T11:04:24.890-04:00] INFO | invokeCommand ▶︎ Command execution started : std:test-connection, for connector version 8.
|
||||||
|
[2022-07-14T11:04:24.890-04:00] INFO | invokeCommand ▶︎ Command invocation complete : std:test-connection, for connector version: 8. Elapsed time 125.749µs
|
||||||
|
[2022-07-14T11:04:24.941-04:00] INFO | commandOutcome ▶︎ {"commandType":"std:test-connection","completed":true,"elapsed":49,"message":"command completed","requestId":"cca732a2-084d-4433-9bd5-ed22fa397d8d","version":8}
|
||||||
|
```
|
||||||
|
To tail the logs to see output as it happens, execute the ```sp conn logs tail``` command.
|
||||||
|
|
||||||
|
It can also be helpful to execute the logs command along with grep to filter your results to a specific connector or text:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sp conn logs | grep 'connector version 29'
|
||||||
|
[2022-07-14T11:04:24.442-04:00] INFO | invokeCommand ▶︎ Command execution started : std:test-connection, for connector version 29.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging with console.log
|
||||||
|
anywhere that you use console.log in your code will expose the output to the logs. The following example has a printed statement in the index.ts file:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Connector must be exported as module property named connector
|
||||||
|
export const connector = async () => {
|
||||||
|
|
||||||
|
...
|
||||||
|
// Use the vendor SDK, or implement own client as necessary, to initialize a client
|
||||||
|
const airtable = new AirtableClient(config)
|
||||||
|
return createConnector()
|
||||||
|
.stdTestConnection(async (context: Context, input: undefined, res: Response<StdTestConnectionOutput>) => {
|
||||||
|
// print the output to the console
|
||||||
|
console.log('testing connector logging')
|
||||||
|
res.send(await airtable.testConnection())
|
||||||
|
})
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
```
|
||||||
|
When you run the ```sp conn logs``` command, you will see the following in the output:
|
||||||
|
```bash
|
||||||
|
$ sp conn logs tail
|
||||||
|
|
||||||
|
[2022-07-14T11:23:05.418-04:00] INFO | connectorMessage ▶︎ {"commandType":"std:test-connection","invocationId":"e5c73502-2c03-4b22-aa0d-5b67655e8f2d","message":"testing connector logging\n","requestId":"93370aa663d94bebb509bf5661f18650","version":9}
|
||||||
|
[2022-07-14T11:23:06.085-04:00] INFO | commandOutcome ▶︎ {"commandType":"std:test-connection","completed":true,"elapsed":1071,"message":"command completed","requestId":"93370aa663d94bebb509bf5661f18650","version":9}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging using the SDK
|
||||||
|
|
||||||
|
Use the built in logging tool to simplify the logging process and enhance your logger’s capabilities. To start, import the logger from the sdk:
|
||||||
|
|
||||||
|
```import { logger as SDKLogger } from '@sailpoint/connector-sdk'```
|
||||||
|
|
||||||
|
Next, add a simple configuration for the logger to use throughout your application.
|
||||||
|
|
||||||
|
[logger.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/logger/logger.ts)
|
||||||
|
```javascript
|
||||||
|
import { logger as SDKLogger } from '@sailpoint/connector-sdk'
|
||||||
|
|
||||||
|
export const logger = SDKLogger.child(
|
||||||
|
// specify your connector name
|
||||||
|
{ connectorName: 'Airtable' }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can import your logger into your project and start logging.
|
||||||
|
|
||||||
|
[index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Connector must be exported as module property named connector
|
||||||
|
export const connector = async () => {
|
||||||
|
|
||||||
|
...
|
||||||
|
// Use the vendor SDK, or implement own client as necessary, to initialize a client
|
||||||
|
const airtable = new AirtableClient(config)
|
||||||
|
return createConnector()
|
||||||
|
.stdAccountList(async (context: Context, input: undefined, res: Response<StdAccountListOutput>) => {
|
||||||
|
const accounts = await airtable.getAllAccounts()
|
||||||
|
|
||||||
|
// use the logger to send accounts that are fetched
|
||||||
|
logger.info(accounts, "fetched the following accounts from Airtable")
|
||||||
|
for (const account of accounts) {
|
||||||
|
res.send(account.toStdAccountListOutput())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuring the SDK to Mask Sensitive Values
|
||||||
|
|
||||||
|
The SDK Logger uses [Pino](https://github.com/pinojs/pino) under the hood, which has the built-in capability to search and remove json paths that can contain sensitive information.
|
||||||
|
|
||||||
|
>🚧 Never expose any Personal Identifiable Information in any logging operations.
|
||||||
|
|
||||||
|
Start by looking at line 116 to 122 in your logger configuration, which looks like the one below:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { logger as SDKLogger } from '@sailpoint/connector-sdk'
|
||||||
|
|
||||||
|
export const logger = SDKLogger.child(
|
||||||
|
// specify your connector name
|
||||||
|
{ connectorName: 'Airtable' },
|
||||||
|
// This is optional for removing specific information you might not want to be logged
|
||||||
|
{
|
||||||
|
redact: {
|
||||||
|
paths: [
|
||||||
|
'*.password',
|
||||||
|
'*.username',
|
||||||
|
'*.email',
|
||||||
|
'*.id',
|
||||||
|
'*.firstName',
|
||||||
|
'*.lastName',
|
||||||
|
'*.displayName'
|
||||||
|
],
|
||||||
|
censor: '****',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
Now compare that with the object you want to remove information from while still logging information in it:
|
||||||
|
|
||||||
|
[AirtableAccount.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/models/AirtableAccount.ts)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
export class AirtableAccount {
|
||||||
|
airtableId!: string
|
||||||
|
displayName!: string
|
||||||
|
email!: string
|
||||||
|
id!: string
|
||||||
|
department!: string
|
||||||
|
firstName!: string
|
||||||
|
lastName!: string
|
||||||
|
enabled = true
|
||||||
|
locked = false
|
||||||
|
password!: string
|
||||||
|
entitlments!: Array<string>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Now when you log the contents of an ```AirtableAccount``` object, you will see all the fields redacted. For example, in [index.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/index.ts) we log the ```accounts``` in the following code snippet:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
.stdAccountList(async (context: Context, input: undefined, res: Response<StdAccountListOutput>) => {
|
||||||
|
const accounts = await airtable.getAllAccounts()
|
||||||
|
|
||||||
|
logger.info(accounts, "fetched the following accounts from Airtable")
|
||||||
|
for (const account of accounts) {
|
||||||
|
res.send(account.toStdAccountListOutput())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
which results in the following log output:
|
||||||
|
```bash
|
||||||
|
$ sp conn logs
|
||||||
|
|
||||||
|
[2022-07-14T11:19:29.368-04:00] INFO | invokeCommand ▶︎ Command invocation complete : std:account:list, for connector version: 8. Elapsed time 111.836542ms
|
||||||
|
[2022-07-14T11:19:30.629-04:00] INFO | connectorMessage ▶︎ {"0":{"airtableId":"recdUN76q9KibYMir","department":"sailpoint admins","displayName":"****","email":"****","enabled":true,"entitlments":["administrator","sailpoint"],"firstName":"****","id":"****","lastName":"****","locked":false},"1":{"airtableId":"recXJEzpeySmtlIOF","department":"external","displayName":"****","email":"****","enabled":true,"entitlments":["administrator"],"firstName":"****","id":"****","lastName":"****","locked":false},"2":{"airtableId":"recnsv3VJ1K4k867v","department":"external","displayName":"****","email":"****","enabled":true,"entitlments":[""],"firstName":"****","id":"****","lastName":"****","locked":false},"commandType":"std:account:list","connectorName":"Airtable","invocationId":"541bcc2f-1d42-4c78-b201-de3ea46552e0","message":"fetched the following accounts from Airtable","requestId":"379a8a4510944daf9d02b51a29ae863e","version":8}
|
||||||
|
[2022-07-14T11:19:30.678-04:00] INFO | commandOutcome ▶︎ {"commandType":"std:account:list","completed":true,"elapsed":1290,"message":"command completed","requestId":"379a8a4510944daf9d02b51a29ae863e","version":8}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can see that any of the PII information has now been transformed into "****"
|
||||||
88
docs/idn_docs/docs/custom_connectors/in_depth/rate_limits.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Handling Rate Limits
|
||||||
|
APIs often implement rate limits to prevent any one user from abusing the API or using an unfair amount of resources, limiting what other users of the API can do. The rate limits can manifest in many ways, but one of the most common ways is the 429 (Too Many Requests) HTTP status code. You must check the documentation of the API you are using to see whether it enforces rate limits and how it notifies you when you reach that limit. An example of rate limit documentation for Stripe’s API can be found [here](https://stripe.com/docs/rate-limits).
|
||||||
|
|
||||||
|
If you are using a vendor supplied client library for the API, check the documentation for that client library to see whether it handles rate limits for you. If it does, you do not need to worry about rate limits. If it does not or if you have to implement your own library for interacting with the target API, you must handle rate limiting yourself. If you are implementing your own library for the target API, the easiest way to handle rate limits is to use the [axios-retry](https://www.npmjs.com/package/axios-retry) NPM package in conjunction with the [axios](https://www.npmjs.com/package/axios) HTTP request library. Start by including both packages in the dependencies section of your ```package.json``` file:
|
||||||
|
|
||||||
|
```json
|
||||||
|
...
|
||||||
|
"dependencies": {
|
||||||
|
"@sailpoint/connector-sdk": "github:sailpoint-oss/sp-connector-sdk-js#main",
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"axios-retry": "^3.2.4"
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, run ```npm install``` in your project directory to install the packages. Once they are installed, go to the section of your code that handles API calls to your source and wrap your Axios HTTP client object in an Axios retry object. In the following snippet, the code automatically retries an API call that fails with a 429 error code three times, using exponential back-off between each API call. You can configure this better to suit your API’s rate limit. The following code snippet from [discourse-client.ts](https://github.com/sailpoint-oss/discourse-connector-2/blob/main/src/discourse-client.ts) shows the code necessary to set up the retry logic:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ConnectorError } from "@sailpoint/connector-sdk"
|
||||||
|
import axios, { AxiosInstance } from "axios"
|
||||||
|
import axiosRetry from "axios-retry"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DiscourseClient is the client that communicates with Discourse APIs.
|
||||||
|
*/
|
||||||
|
export class DiscourseClient {
|
||||||
|
private readonly apiKey?: string
|
||||||
|
private readonly apiUsername?: string
|
||||||
|
private readonly baseUrl?: string
|
||||||
|
|
||||||
|
httpClient: AxiosInstance
|
||||||
|
|
||||||
|
constructor(config: any) {
|
||||||
|
// Fetch necessary properties from config.
|
||||||
|
this.apiKey = config?.apiKey
|
||||||
|
if (this.apiKey == null) {
|
||||||
|
throw new ConnectorError('apiKey must be provided from config')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.apiUsername = config?.apiUsername
|
||||||
|
if (this.apiUsername == null) {
|
||||||
|
throw new ConnectorError('apiUsername must be provided from config')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.baseUrl = config?.baseUrl
|
||||||
|
if (this.baseUrl == null) {
|
||||||
|
throw new ConnectorError('baseUrl must be provided from config')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.httpClient = axios.create({
|
||||||
|
baseURL: this.baseUrl,
|
||||||
|
headers: {
|
||||||
|
'Api-Key': this.apiKey,
|
||||||
|
'Api-Username': this.apiUsername
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wrap our Axios HTTP client in an Axios retry object to automatically
|
||||||
|
// handle rate limiting. By default, this logic will retry a given
|
||||||
|
// API call 3 times before failing. Read the documentation for
|
||||||
|
// axios-retry on NPM to see more configuration options.
|
||||||
|
axiosRetry(this.httpClient, {
|
||||||
|
retryDelay: axiosRetry.exponentialDelay,
|
||||||
|
retryCondition: (error) => {
|
||||||
|
// Only retry if the API call recieves an error code of 429
|
||||||
|
// this logic can be replaced with whatever approach is necessary for your connector
|
||||||
|
return error.response!.status === 429
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Because ```axios-retry``` wraps an ```axios``` object, you can make API calls like you normally would with Axios without any special options or configuration.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
private async getUserEmailAddress(username: string): Promise<string> {
|
||||||
|
// Use our axios httpClient object like we normally would. If this call
|
||||||
|
// fails with a 429, it will automatically wait 30 seconds before retrying
|
||||||
|
// the call. It will do this three times before hard failing. The catch
|
||||||
|
// function will catch any other error besides a 429.
|
||||||
|
const response = await this.httpClient.get<UserEmail>(`/u/${username}/emails.json`).catch(error => {
|
||||||
|
throw new ConnectorError(`Failed to retrieve email for user ${username}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.data.email!
|
||||||
|
}
|
||||||
|
```
|
||||||
209
docs/idn_docs/docs/custom_connectors/in_depth/testing.md
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# Testing
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
When you set up a new project, the following test files are created: ```index.spec.ts``` and ```my-client.spec.ts```. Execute the tests immediately using npm test.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
> test-project-5@0.1.0 test
|
||||||
|
> jest --coverage
|
||||||
|
|
||||||
|
PASS src/my-client.spec.ts
|
||||||
|
PASS src/index.spec.ts
|
||||||
|
--------------|---------|----------|---------|---------|-------------------
|
||||||
|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
|
||||||
|
--------------|---------|----------|---------|---------|-------------------
|
||||||
|
All files | 72 | 100 | 75 | 70.83 |
|
||||||
|
index.ts | 56.25 | 100 | 50 | 53.33 | 29-56
|
||||||
|
my-client.ts | 100 | 100 | 100 | 100 |
|
||||||
|
--------------|---------|----------|---------|---------|-------------------
|
||||||
|
|
||||||
|
Test Suites: 2 passed, 2 total
|
||||||
|
Tests: 7 passed, 7 total
|
||||||
|
Snapshots: 0 total
|
||||||
|
Time: 1.937 s
|
||||||
|
Ran all test suites.
|
||||||
|
{"level":"INFO","message":"Running test connection"}
|
||||||
|
```
|
||||||
|
You can also view the results in an html report by viewing the ```index.html``` file inside the ```coverage/lcov-report``` folder:
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Testing Techniques
|
||||||
|
|
||||||
|
[Jest](https://jestjs.io/docs/getting-started) is a testing framework provided for javascript that focuses on simplicity. CLI includes it when it generates the project. It is recommended to use Jest to test your code.
|
||||||
|
|
||||||
|
Testing your code is important because it can highlight implementation issues before they get into production. If your tests are setup with good descriptions, the tests can also help explain why certain conditions are important in the code, so if a new developer breaks a test, he or she will know what broke and why the functionality is important.
|
||||||
|
|
||||||
|
If you have good tests setup, then you can quickly identify and fix changes or updates that occur in dependent sources.
|
||||||
|
|
||||||
|
Jest provides [many different ways to test your code](https://jestjs.io/docs/using-matchers). Some techniques are listed below:
|
||||||
|
|
||||||
|
### Test a method and evaluate the response using ```expect```
|
||||||
|
```javascript
|
||||||
|
it('get users populates correct fields', async () => {
|
||||||
|
|
||||||
|
// Execute the method
|
||||||
|
let res = await discourseClient.getUsers()
|
||||||
|
|
||||||
|
// Check the response, and make sure it is an array with exactly 2 elements
|
||||||
|
expect(res.length).toBe(2)
|
||||||
|
|
||||||
|
// Evaluate the response email and ensure it matches the expected result
|
||||||
|
expect(res[0].email === 'test.test@test.com')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- Line 4 executes the method.
|
||||||
|
|
||||||
|
- Line 7 asserts that the response is an array with 2 elements.
|
||||||
|
|
||||||
|
- Line 10 evaluates the email field in the array to ensure it matches the expected result.
|
||||||
|
|
||||||
|
### Test a method to ensure it calls another method using ```spyOn```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
it('password is generated when not provided', async () => {
|
||||||
|
|
||||||
|
// Create the spy for later use. We want to know details about this method.
|
||||||
|
const spy = jest.spyOn(DiscourseClient.prototype as any, "generateRandomPassword")
|
||||||
|
|
||||||
|
// Execute the method
|
||||||
|
let res = await discourseClient.createUser({ "email": "", "username": "test" })
|
||||||
|
|
||||||
|
// Validate that the internal method "generateRandomPassword" was called
|
||||||
|
expect(spy).toBeCalled();
|
||||||
|
|
||||||
|
// Validate the email field matches the expected result
|
||||||
|
expect(res.email === 'test.test@test.com')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
- Line 4 sets up the spy. “generateRandomPassword” is an internal method that gets called when the password is not provided.
|
||||||
|
|
||||||
|
- Line 7 executes the method.
|
||||||
|
|
||||||
|
- Line 10 checks the spy to ensure that the internal method was called.
|
||||||
|
|
||||||
|
## Setting up Mock Services
|
||||||
|
|
||||||
|
The easiest way to mock your client is to set up a mock service that returns data just like your service would in production so you can test all your functions and data manipulation in your unit tests.
|
||||||
|
|
||||||
|
Mocks help test your code without actually invoking your service and allow you to simulate the kind of response your client expects to receive. They can also help you pinpoint where failures occur in case something changes on your service. By using a mock service, you can test your entire application without connecting to your service.
|
||||||
|
|
||||||
|
Create a mock file
|
||||||
|
Jest provides a way to set up a mock service. It stores your mock files in a folder called \_\_mocks__. If you name your typescript files the exact same as the files they are mocking, those mock implementations will be called instead when your unit tests are running. In the following example, a mock has been created to simulate calls to the airtable client:
|
||||||
|
|
||||||
|
[airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/__mocks__/airtable.ts)
|
||||||
|
```javascript
|
||||||
|
import { AttributeChange, CompoundKeyType, ConnectorError, ConnectorErrorType, SimpleKeyType, StdAccountCreateInput, StdAccountDiscoverSchemaOutput } from "@sailpoint/connector-sdk"
|
||||||
|
import { AirtableAccount } from "../models/AirtableAccount"
|
||||||
|
import { AirtableEntitlement } from "../models/AirtableEntitlement"
|
||||||
|
import { InvalidConfigurationError } from "../errors/invalid-configuration-error"
|
||||||
|
import accountJson from "./account.json"
|
||||||
|
import entitlementJson from "./entitlement.json"
|
||||||
|
import schemaJson from "./schema.json"
|
||||||
|
|
||||||
|
export class AirtableClient {
|
||||||
|
constructor(config: any) {
|
||||||
|
// Fetch necessary properties from config.
|
||||||
|
// Following properties actually do not exist in the config -- it just serves as an example.
|
||||||
|
if (config.apiKey == null) {
|
||||||
|
throw new InvalidConfigurationError('token must be provided from config')
|
||||||
|
}
|
||||||
|
if (config.airtableBase == null) {
|
||||||
|
throw new InvalidConfigurationError('airtableBase base id needed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllAccounts(): Promise<AirtableAccount[]> {
|
||||||
|
const recordArray: Array<AirtableAccount> = []
|
||||||
|
const account = Object.assign(new AirtableAccount(), accountJson)
|
||||||
|
recordArray.push(account)
|
||||||
|
return recordArray
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeAccount(account: AirtableAccount, changes: AttributeChange): Promise<AirtableAccount> {
|
||||||
|
account.updateFieldByName(changes.attribute, changes.value, changes.op)
|
||||||
|
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllEntitlements(): Promise<AirtableEntitlement[]> {
|
||||||
|
|
||||||
|
const recordArray: Array<AirtableEntitlement> = []
|
||||||
|
const entitlement = Object.assign(new AirtableEntitlement(), entitlementJson)
|
||||||
|
recordArray.push(entitlement)
|
||||||
|
return recordArray
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccount(identity: SimpleKeyType | CompoundKeyType): Promise<AirtableAccount> {
|
||||||
|
const id = <SimpleKeyType>identity
|
||||||
|
|
||||||
|
const account = Object.assign(new AirtableAccount(), accountJson)
|
||||||
|
if (id.simple.id === "1234") {
|
||||||
|
return account
|
||||||
|
} else {
|
||||||
|
throw new ConnectorError("Account not found", ConnectorErrorType.NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccountSchema(): Promise<StdAccountDiscoverSchemaOutput> {
|
||||||
|
return schemaJson
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAccount(airTableid: string): Promise<Record<string, never>> {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAccount(input: StdAccountCreateInput): Promise<AirtableAccount> {
|
||||||
|
return Object.assign(new AirtableAccount(), accountJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEntitlement(identity: SimpleKeyType | CompoundKeyType): Promise<AirtableEntitlement> {
|
||||||
|
return Object.assign(new AirtableEntitlement(), entitlementJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
async testConnection(): Promise<any> {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The method signatures are exactly the same on this mock file as the signature sin the "real" [airtable.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/airtable.ts). The only difference is that the response objects from all the calls are made without actually calling any external dependencies, so it can be run quickly in a unit test without having to make api calls to a real client
|
||||||
|
|
||||||
|
### Define json mock objects
|
||||||
|
The responses are stored in directly imported json files. This helps keep the code focused on the logic and allows the response objects to be more easily generated directly from a tool like Postman without requiring any major formatting of the response. Enable this situation by setting ```"resolveJsonModule": true``` in your ```tsconfig.json```. The following response file is an example:
|
||||||
|
|
||||||
|
[account.json](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/src/__mocks__/account.json)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"airtableId": "1234",
|
||||||
|
"displayName": "Test User",
|
||||||
|
"email": "test@test.com",
|
||||||
|
"id": "1234",
|
||||||
|
"enabled": true,
|
||||||
|
"locked": false,
|
||||||
|
"department": "accounting",
|
||||||
|
"firstName": "test",
|
||||||
|
"lastName": "user",
|
||||||
|
"password": "password1234",
|
||||||
|
"entitlments": ["ent1", "ent2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use the mock in your tests
|
||||||
|
The mock is defined in the test file, and Jest does the rest. Jest overrides all the calls to use the methods in the ```__mocks__``` folder.
|
||||||
|
|
||||||
|
[index.spec.ts](https://github.com/sailpoint-oss/airtable-example-connector/blob/main/test/index.spec.ts)
|
||||||
|
```javascript
|
||||||
|
import { connector } from '../src/index'
|
||||||
|
import { StandardCommand } from '@sailpoint/connector-sdk'
|
||||||
|
import { PassThrough } from 'stream'
|
||||||
|
|
||||||
|
// setup your mock object
|
||||||
|
jest.mock('../src/airtable')
|
||||||
|
```
|
||||||
23
docs/idn_docs/docs/custom_connectors/overview.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# SaaS Connectivity
|
||||||
|
|
||||||
|
SaaS Connectivity is a cloud based connector runtime that makes developing and deploying web service connectors easier than Connector 1.0 does. However, because the cloud hosts SaaS Connectivity, not a Virtual Appliance (VA), SaaS Connectivity is limited in the types of applications it can connect to. For example, you cannot use SaaS Connectivity to connect to on-prem services that can only communicate within an intranet (no public internet access). This excludes JDBC and Mainframe applications, to name a few.
|
||||||
|
|
||||||
|
## What are connectors?
|
||||||
|
|
||||||
|
Connectors are the bridges between the SailPoint Identity Now (IDN) SaaS platform and the source systems that IDN needs to communicate with and aggregate data from. An example of a source system IDN may need to communicate with would be an Oracle HR system or GitHub. In these cases, IDN synchronizes data between systems to ensure account entitlements and state are correct through the organization.
|
||||||
|
|
||||||
|
## Why are we introducing a new connector?
|
||||||
|
|
||||||
|
VA connectors always communicate with external sources through the Virtual Appliance (VA) as seen in the diagram below:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
VA connectors can be disadvantageous because you need an on-prem virtual appliance to have any external connectivity with them, even when that connectivity is a SaaS service like Salesforce.com.
|
||||||
|
|
||||||
|
It is also challenging to create a custom connector in the VA Connector framework. Therefore, there are generic connectors available such as flat file, JDBC and webservice connectors. These options provide flexibility in configuring almost any source, but this configuration can be complex. For example, when you create a JDBC connector, you must use SQL to define the data model.
|
||||||
|
|
||||||
|
The new Cloud connectors work differently - they run on the IDN platform instead (see diagram below).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
With this process, you can run an entire IDN instance without a VA. The new connector also includes a CLI tool to manage cloud connectors and an SDK to create custom connectors. Because it is simpler to create a custom connector, you can create specific connectors for a variety of sources, and the connectors' configuration can be much simpler. For example, you can now configure a formerly complicated webservice connector by providing two parameters (Base URL and API Key) in a custom cloud connector.
|
||||||
71
docs/idn_docs/docs/getting_started.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Getting Started with SailPoint APIs
|
||||||
|
|
||||||
|
## Finding your Org/Tenant Name
|
||||||
|
|
||||||
|
You will need to know your org/tenant name in order to form the proper URL for an API request. You can find your org/tenant name by logging into IdentityNow, navigating to the Admin UI, and clicking on the Dashboard dropdown and selecting the Overview page. The org name is displayed within the Org Details section of the dashboard. If you do not have admin access, you can find your tenant name, as well as the API base URL that you will use for API calls, by viewing your session details when logged into your IdentityNow instance. Simply change your URL to the following: `https://{your-IdentityNow-hostname}.com/ui/session`, where `{your-IdentityNow-hostname}` is your company's domain name for accessing IdentityNow. The session detail you want is the `baseUrl`, which will have the form of `https://{tenant}.api.identitynow.com`.
|
||||||
|
|
||||||
|
## Making Your First API Call
|
||||||
|
|
||||||
|
To get started, you will need create a [personal access token](./authentication.md#personal-access-tokens), which can then be used to generate access tokens to authenticate your API calls. To generate a personal access token from the IdentityNow UI, perform the following steps after logging into your IdentityNow instance:
|
||||||
|
|
||||||
|
1. Select **Preferences** from the drop-down menu under your username, then **Personal Access Tokens** on the left. You can also go straight to the page using this URL, replacing `{tenant}` with your IdentityNow tenant: `https://{tenant}.identitynow.com/ui/d/user-preferences/personal-access-tokens`.
|
||||||
|
|
||||||
|
2. Click **New Token** and enter a meaningful description to help differentiate the token from others.
|
||||||
|
|
||||||
|
>**Note**: The **New Token** button will be disabled when you’ve reached the limit of 10 personal access tokens per user. To avoid reaching this limit, we recommend you delete any tokens that are no longer needed.
|
||||||
|
|
||||||
|
3. Click **Create Token** to generate and view the two components that comprise the token: the `Secret` and the `Client ID`.
|
||||||
|
|
||||||
|
>**IMPORTANT**: After you create the token, the value of the `Client ID` will be visible in the Personal Access Tokens list, but the corresponding `Secret` will not be visible after you close the window. You will need to store the `Secret` somewhere secure.
|
||||||
|
|
||||||
|
4. Copy both values somewhere that will be secure and accessible to you when you need to use the the token.
|
||||||
|
|
||||||
|
5. To create an `access_token` that can be used to authenticate API requests, use the following cURL command, replacing `{tenant}` with your IdentityNow tenant. The response body will contain an `access_token`, which will look like a long string of random characters.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --location --request POST 'https://{tenant}.api.identitynow.com/oauth/token?grant_type=client_credentials&client_id={client_id}&client_secret={secret}'
|
||||||
|
```
|
||||||
|
|
||||||
|
6. To test your `access_token`, execute the following cURL command, replacing `{tenant}` with your IdentityNow tenant and `access_token` with the token you generated in the previous step. If successful, you should get a JSON representation of an identity in your tenant.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --request GET --url 'https://{tenant}.api.identitynow.com/v3/public-identities?limit=1' --header 'authorization: Bearer {access_token}'
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information about SailPoint Platform authentication, see [API Authentication](./authentication.md)
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
|
||||||
|
There is a rate limit of 100 requests per `access_token` per 10 seconds for V3 API calls through the API gateway. If you exceed the rate limit, expect the following response from the API.
|
||||||
|
|
||||||
|
**HTTP Status Code**: 429 Too Many Requests
|
||||||
|
|
||||||
|
**Headers**:
|
||||||
|
|
||||||
|
* **Retry-After**: {seconds to wait before rate limit resets}
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
Each API resource requires a specific level of authorization attached to your `access_token`. Please review the authorization constraints for each API endpoint. Tokens generated outside of a user context, like the [Client Credentials](./authentication.md#client-credentials-grant-flow) we generated above to make your first API call, will be limited in the endpoints that it can call. If your token doesn't have permission to call an endpoint, you will receive the following response:
|
||||||
|
|
||||||
|
**HTTP Status Code**: 403 Forbidden
|
||||||
|
|
||||||
|
**Response Body**:
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
"detailCode": "403 Forbidden",
|
||||||
|
"trackingId": "fca9eb2227514d6d90cd4a1d1cdc255c",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"locale": "en-US",
|
||||||
|
"localeOrigin": "DEFAULT",
|
||||||
|
"text": "The server understood the request but refuses to authorize it."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using an API Tool
|
||||||
|
|
||||||
|
There are several API tools that make exploring and testing APIs easier than using the command line or a programming language. One such tool is [Postman](https://www.postman.com/downloads/). To import the SailPoint REST APIs into a tool like Postman, you must first download the REST specification. Navigate to [https://developer.sailpoint.com/apis/v3](https://developer.sailpoint.com/apis/v3) and click the "Download OpenAPI specification" button. You can then import the JSON file in Postman by using the [import wizard](https://learning.postman.com/docs/getting-started/importing-and-exporting-data/) within Postman.
|
||||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 484 KiB |
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/idn_docs/docs/img/custom_connectors/in_depth/debugging1.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/idn_docs/docs/img/custom_connectors/in_depth/debugging2.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
docs/idn_docs/docs/img/custom_connectors/in_depth/testing1.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/idn_docs/docs/img/custom_connectors/in_depth/testing2.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 80 KiB |
BIN
docs/idn_docs/docs/img/http-client-identity-now.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/idn_docs/docs/img/sp-config-export.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
docs/idn_docs/docs/img/sp-config-import.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
docs/idn_docs/docs/img/transforms/account_schema.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
docs/idn_docs/docs/img/transforms/account_summary.png
Normal file
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/idn_docs/docs/img/transforms/create_source.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
docs/idn_docs/docs/img/transforms/how_transforms_work_1.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/idn_docs/docs/img/transforms/how_transforms_work_2.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/idn_docs/docs/img/transforms/how_transforms_work_3.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/idn_docs/docs/img/transforms/how_transforms_work_4.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/idn_docs/docs/img/transforms/identity_profile.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/idn_docs/docs/img/transforms/mappings_tab.png
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
docs/idn_docs/docs/img/transforms/sailpoint_logo.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/idn_docs/docs/img/transforms/what_are_transforms_1.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/idn_docs/docs/img/user-web-app-identity-now.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
576
docs/idn_docs/docs/sp_config.md
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
# Overview
|
||||||
|
|
||||||
|
This is a guide on using the SailPoint SaaS Configuration APIs in order to import and export configurations from the SailPoint SaaS system. This is intended to be used to get configurations in bulk in support of environmental promotion, go-live, or tenant-to-tenant configuration management processes and pipelines.
|
||||||
|
|
||||||
|
For more details around how to manage configurations, refer to [SailPoint SaaS Change Management and Deployment Best Practices](https://community.sailpoint.com/t5/IdentityNow-Articles/SailPoint-SaaS-Change-Management-and-Deployment-Best-Practices/ta-p/189871).
|
||||||
|
|
||||||
|
## Audience
|
||||||
|
|
||||||
|
This document is intended for technically proficient administrators, implementers, integrators or even developers. No coding experience is necessary, but being able to understand JSON data structures and make REST API web-service calls is needed to fully understand this.
|
||||||
|
|
||||||
|
## Supported Objects
|
||||||
|
|
||||||
|
| **Object** | **Object Type** | **Export** | **Import** |
|
||||||
|
| :-------------------------- | :--------------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
|
||||||
|
| Event Trigger Subscriptions | `TRIGGER_SUBSCRIPTION` |  |  |
|
||||||
|
| Identity Profiles | `IDENTITY_PROFILE` |  |  |
|
||||||
|
| Rules | `RULE` |  |  |
|
||||||
|
| Sources | `SOURCE` |  |  |
|
||||||
|
| Transforms | `TRANSFORM` |  |  |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Note:** The available supported objects are also available via REST API too! See List Configuration Objects in the **API Reference** section of this document.
|
||||||
|
|
||||||
|
**Rule Import and Export -** Rules are allowed to be exported from one tenant and imported into another. Cloud Rules have already been reviewed and installed in other tenants, and Connector Rules do not require a rule review. During the import and export process rules cannot be changed in the migration process, as these are validated by the usage of `jwsHeader` and `jwsSignature` in the object.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Exporting Configurations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Need to have credentials as a tenant administrator (`ORG_ADMIN`)
|
||||||
|
- Understanding of what objects you’d like to export
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Process
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **Start Export** - Start the export process by configuring a JSON payload for the export options. This should then be sent to `POST /beta/sp-config/export`.
|
||||||
|
2. **Response with Export Status** - An export status will given in response. This contains a `jobId` and a `status` which should be used to subsequently monitor the process. Initially this might have a status of `NOT_STARTED`.
|
||||||
|
3. **Get Export Status** - Using the `jobId` from the previous status, call `GET /beta/sp-config/export/{id}` where the `{id}` is the `jobId`.
|
||||||
|
4. **Response with Export Status** - An export status will given in response. This contains a `jobId` and a `status` which should be used to subsequently monitor the process. After a period of time, the process `status` should move to either `COMPLETE` or `FAILED`. Depending on the amount of objects being exported, this could take awhile. It might be ncessary to iterate over steps 3 and 4 until the status reflects a completion. If it takes too long, the export process may expire.
|
||||||
|
5. **Get Export Results** - Once the status is `COMPLETE`, download the export results by calling `GET /beta/sp-config/export/{id}/download` where the `{id}` is the `jobId`.
|
||||||
|
6. **Response with Export Results** - In response, the export process should produce a set of JSON objects which you can download as an export result set. These should reflect the objects that were selected in the export options earlier.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Importing Configurations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Need to have credentials as a tenant administrator (`ORG_ADMIN`)
|
||||||
|
- Prepare any objects to be imported into the system, as well as import options.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Process
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
1. **Start Import** - Start the import process by configuring a JSON payload for the import options. This should then be sent to `POST /beta/sp-config/import`.
|
||||||
|
2. **Response with Import Status** - An import status will given in response. This contains a `jobId` and a `status` which should be used to subsequently monitor the process. Initially this might have a status of `NOT_STARTED`.
|
||||||
|
3. **Get Import Status** - Using the `jobId` from the previous status, call `GET /beta/sp-config/import/{id}` where the `{id}` is the `jobId`.
|
||||||
|
4. **Response with Import Status** - An import status will given in response. This contains a `jobId` and a `status` which should be used to subsequently monitor the process. After a period of time, the process `status` should move to either `COMPLETE` or `FAILED`. Depending on the amount of objects being imported, this could take awhile. It might be ncessary to iterate over steps 3 and 4 until the status reflects a completion. If it takes too long, the import process may expire.
|
||||||
|
5. **Get Import Results** - Once the status is `COMPLETE`, download the import results by calling `GET /beta/sp-config/import/{id}/download` where the `{id}` is the `jobId`.
|
||||||
|
6. **Response with Import Results** - In response, the import process should produce listing of object that successfully imported, as well as any errors, warnings, or information about the import process. This result set should reflect the objects that were selected to be imported earlier.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## API Reference Guide
|
||||||
|
|
||||||
|
| **Description** | **REST API End-Point** |
|
||||||
|
| :------------------ | :----------------------------------------- |
|
||||||
|
| List Config Objects | `GET /beta/sp-config/config-objects` |
|
||||||
|
| Export Objects | `POST /beta/sp-config/export` |
|
||||||
|
| Export Status | `GET /beta/sp-config/export/{id}` |
|
||||||
|
| Export Results | `GET /beta/sp-config/export/{id}/download` |
|
||||||
|
| Import Objects | `POST /beta/sp-config/import` |
|
||||||
|
| Import Status | `GET /beta/sp-config/import/{id}` |
|
||||||
|
| Import Results | `GET /beta/sp-config/import/{id}/download` |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### List Configuration Objects
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Lists all available objects which can be imported and exported into the system.
|
||||||
|
|
||||||
|
**Request**
|
||||||
|
|
||||||
|
```json
|
||||||
|
GET /beta/sp-config/config-objects
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"objectType": "SOURCE",
|
||||||
|
"resolveByIdUrl": {
|
||||||
|
"url": "diana://v3/sources/sources/$id",
|
||||||
|
"query": null
|
||||||
|
},
|
||||||
|
"resolveByNameUrl": {
|
||||||
|
"url": "diana://v3/sources/sources/",
|
||||||
|
"query": {
|
||||||
|
"filters": "name eq \"$name\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exportUrl": "diana://v3/sources/sources/export",
|
||||||
|
"exportLimit": 10,
|
||||||
|
"importUrl": "diana://v3/sources/sources/import",
|
||||||
|
"importLimit": 10,
|
||||||
|
"referenceExtractors": null,
|
||||||
|
"signatureRequired": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectType": "TRIGGER_SUBSCRIPTION",
|
||||||
|
"resolveByIdUrl": {
|
||||||
|
"url": "ets://trigger-subscriptions/$id",
|
||||||
|
"query": null
|
||||||
|
},
|
||||||
|
"resolveByNameUrl": {
|
||||||
|
"url": "ets://trigger-subscriptions/",
|
||||||
|
"query": {
|
||||||
|
"filters": "name eq \"$name\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exportUrl": "ets://trigger-subscriptions/export",
|
||||||
|
"exportLimit": 10,
|
||||||
|
"importUrl": "ets://trigger-subscriptions/import",
|
||||||
|
"importLimit": 10,
|
||||||
|
"referenceExtractors": null,
|
||||||
|
"signatureRequired": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectType": "RULE",
|
||||||
|
"resolveByIdUrl": {
|
||||||
|
"url": "rms://rules/$id",
|
||||||
|
"query": null
|
||||||
|
},
|
||||||
|
"resolveByNameUrl": {
|
||||||
|
"url": "rms://rules",
|
||||||
|
"query": {
|
||||||
|
"filters": "name eq \"$name\""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exportUrl": "rms://rules/export",
|
||||||
|
"exportLimit": 10,
|
||||||
|
"importUrl": "rms://rules/import",
|
||||||
|
"importLimit": 10,
|
||||||
|
"referenceExtractors": null,
|
||||||
|
"signatureRequired": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"objectType": "TRANSFORM",
|
||||||
|
"resolveByIdUrl": {
|
||||||
|
"url": "trams://v3/transforms/$id",
|
||||||
|
"query": null
|
||||||
|
},
|
||||||
|
"resolveByNameUrl": {
|
||||||
|
"url": "trams://v3/transforms",
|
||||||
|
"query": {
|
||||||
|
"name": "$name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exportUrl": "trams://v3/export-transforms",
|
||||||
|
"exportLimit": 10,
|
||||||
|
"importUrl": "trams://v3/import-transforms",
|
||||||
|
"importLimit": 10,
|
||||||
|
"referenceExtractors": null,
|
||||||
|
"signatureRequired": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Export Objects
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Starts an export process to export selected objects from the system.
|
||||||
|
|
||||||
|
**Request**
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /beta/sp-config/export
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"description": "Export from Neil's SailPoint tenant",
|
||||||
|
"excludeTypes": [
|
||||||
|
"TRIGGER_SUBSCRIPTION"
|
||||||
|
],
|
||||||
|
"includeTypes": [
|
||||||
|
"SOURCE",
|
||||||
|
"RULE",
|
||||||
|
"TRANSFORM"
|
||||||
|
],
|
||||||
|
"objectOptions": {
|
||||||
|
"SOURCE": {
|
||||||
|
"includedIds": [
|
||||||
|
],
|
||||||
|
"includedNames": [
|
||||||
|
"Active Directory"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RULE": {
|
||||||
|
"includedIds": [
|
||||||
|
],
|
||||||
|
"includedNames": [
|
||||||
|
"JDBCProvisioning Rule Adapter"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"TRANSFORM": {
|
||||||
|
"includedIds": [
|
||||||
|
],
|
||||||
|
"includedNames": [
|
||||||
|
"Calculate Display Name",
|
||||||
|
"Default Email",
|
||||||
|
"Determine Email",
|
||||||
|
"Account Status to Lifecycle State"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
202 Accepted
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"jobId": "19169053-66d2-40c6-8772-9e2bf55edcf6",
|
||||||
|
"status": "NOT_STARTED",
|
||||||
|
"type": "EXPORT",
|
||||||
|
"message": null,
|
||||||
|
"description": "Export from Neil's SailPoint tenant",
|
||||||
|
"expiration": "2021-09-03T15:55:29.127Z",
|
||||||
|
"created": "2021-08-27T15:55:29.127Z",
|
||||||
|
"modified": "2021-08-27T15:55:29.127Z",
|
||||||
|
"completed": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Export Status
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Gets the status of an export process.
|
||||||
|
|
||||||
|
**Request**
|
||||||
|
|
||||||
|
```json
|
||||||
|
GET /beta/sp-config/export/{id}
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"jobId": "19169053-66d2-40c6-8772-9e2bf55edcf6",
|
||||||
|
"status": "COMPLETE",
|
||||||
|
"type": "EXPORT",
|
||||||
|
"message": null,
|
||||||
|
"description": "Export from Neil's SailPoint tenant",
|
||||||
|
"expiration": "2021-09-03T15:55:29Z",
|
||||||
|
"created": "2021-08-27T15:55:29.127Z",
|
||||||
|
"modified": "2021-08-27T15:55:37.59Z",
|
||||||
|
"completed": "2021-08-27T15:55:37.583Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Export Results
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Gets the results of an export process.
|
||||||
|
|
||||||
|
**Request**
|
||||||
|
|
||||||
|
```json
|
||||||
|
GET /beta/sp-config/export/{id}/download
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"timestamp": "2021-08-27T15:55:37.37883Z",
|
||||||
|
"tenant": "neil-test",
|
||||||
|
"description": "Export from Neil's SailPoint tenant",
|
||||||
|
"options": {
|
||||||
|
"excludeTypes": [
|
||||||
|
"TRIGGER_SUBSCRIPTION"
|
||||||
|
],
|
||||||
|
"includeTypes": [
|
||||||
|
"SOURCE",
|
||||||
|
"RULE",
|
||||||
|
"TRANSFORM"
|
||||||
|
],
|
||||||
|
"objectOptions": {
|
||||||
|
"SOURCE": {
|
||||||
|
"includedIds": [],
|
||||||
|
"includedNames": [
|
||||||
|
"Active Directory"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"RULE": {
|
||||||
|
"includedIds": [],
|
||||||
|
"includedNames": [
|
||||||
|
"JDBCProvisioning Rule Adapter"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"TRANSFORM": {
|
||||||
|
"includedIds": [],
|
||||||
|
"includedNames": [
|
||||||
|
"Calculate Display Name"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"objects": [{
|
||||||
|
"version": 1,
|
||||||
|
"self": {
|
||||||
|
"name": "Active Directory [source-101921]",
|
||||||
|
"id": "2c91808363dae20e0163dbef5a7d2de9",
|
||||||
|
"type": "SOURCE"
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"id": "2c91808363dae20e0163dbef5a7d2de9",
|
||||||
|
"name": "Active Directory",
|
||||||
|
"type": "Active Directory - Direct",
|
||||||
|
"connectorClass": "sailpoint.connector.ADLDAPConnector",
|
||||||
|
"connectorScriptName": "active-directory",
|
||||||
|
"description": "SailPoint Active Directory",
|
||||||
|
"deleteThreshold": 10.0,
|
||||||
|
"provisionAsCsv": false,
|
||||||
|
"owner": {
|
||||||
|
"type": "IDENTITY",
|
||||||
|
"id": "ff80818155fe8c080155fe8d925b0316",
|
||||||
|
"name": "slpt.services"
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"self": {
|
||||||
|
"name": "JDBCProvisioning Rule Adapter",
|
||||||
|
"id": "ff8081815bc9f6c6015bd491edbe0023",
|
||||||
|
"type": "RULE"
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"description": null,
|
||||||
|
"type": "JDBCProvision",
|
||||||
|
"signature": {
|
||||||
|
"input": [],
|
||||||
|
"output": null
|
||||||
|
},
|
||||||
|
"sourceCode": {
|
||||||
|
"version": "1.0",
|
||||||
|
"script": "\n\t \nimport sailpoint.services.JDBCProvisioning;\nreturn JDBCProvisioning.provision( application, schema, connection, plan );\n\t"
|
||||||
|
},
|
||||||
|
"attributes": null,
|
||||||
|
"id": "ff8081815bc9f6c6015bd491edbe0023",
|
||||||
|
"name": "JDBCProvisioning Rule Adapter",
|
||||||
|
"created": "2017-05-04T17:46:25.086Z",
|
||||||
|
"modified": "2018-06-29T15:45:42.375Z"
|
||||||
|
},
|
||||||
|
"jwsHeader": "eyJhbGciOiJFUzI1NiJ9",
|
||||||
|
"jwsSignature": "cbvjeLOIJajrJBs1dLc60p8rJ46wYnUYyEAG1ECPn7ahIvy9G109oyjfNAGkR6eguewW2NEzP0mJcK6vOEtbfw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"self": {
|
||||||
|
"name": "Calculate Display Name",
|
||||||
|
"id": "24e5ad57-c12d-4e62-92fe-88c40b39ad6b",
|
||||||
|
"type": "TRANSFORM"
|
||||||
|
},
|
||||||
|
"object": {
|
||||||
|
"id": "24e5ad57-c12d-4e62-92fe-88c40b39ad6b",
|
||||||
|
"name": "Calculate Display Name",
|
||||||
|
"type": "trim",
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"firstName": {
|
||||||
|
"attributes": {
|
||||||
|
"name": "firstname"
|
||||||
|
},
|
||||||
|
"type": "identityAttribute"
|
||||||
|
},
|
||||||
|
"lastName": {
|
||||||
|
"attributes": {
|
||||||
|
"name": "lastname"
|
||||||
|
},
|
||||||
|
"type": "identityAttribute"
|
||||||
|
},
|
||||||
|
"department": {
|
||||||
|
"attributes": {
|
||||||
|
"name": "department"
|
||||||
|
},
|
||||||
|
"type": "identityAttribute"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"attributes": {
|
||||||
|
"name": "employeeType"
|
||||||
|
},
|
||||||
|
"type": "identityAttribute"
|
||||||
|
},
|
||||||
|
"value": "#if ( $type == 'E' ) $lastName $firstName $department #elseif ( $type == 'H' ) $lastName $firstName $department #elseif ( $type == 'S' ) $lastName $firstName $department #else $lastName $firstName (Contractor) #end"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Import Objects
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Starts an import process to import selected objects into the system.
|
||||||
|
|
||||||
|
**Request**
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /beta/sp-config/import
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
|
||||||
|
data: (File) data.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
202 Accepted
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"jobId": "333e8c69-2c38-4caa-9212-ba62b4f2623d",
|
||||||
|
"status": "NOT_STARTED",
|
||||||
|
"type": "IMPORT",
|
||||||
|
"message": null,
|
||||||
|
"description": null,
|
||||||
|
"expiration": "2021-06-11T02:59:56.569Z",
|
||||||
|
"created": "2021-06-04T02:59:56.569Z",
|
||||||
|
"modified": "2021-06-04T02:59:56.57Z",
|
||||||
|
"completed": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: Import also has a “preview” option if you would like to see what an import might look like without actually having to import and change your tenant. Any errors discovered during reference or resource resolution will be provided. To use this, simply set query option `preview` to `true`.
|
||||||
|
|
||||||
|
Example: `POST /beta/sp-config/import?preview=true`
|
||||||
|
|
||||||
|
|
||||||
|
### Import Status
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Gets the status of an import process.
|
||||||
|
|
||||||
|
**Request**
|
||||||
|
|
||||||
|
```json
|
||||||
|
GET /beta/sp-config/import/{id}
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"jobId": "333e8c69-2c38-4caa-9212-ba62b4f2623d",
|
||||||
|
"status": "FAILED",
|
||||||
|
"type": "IMPORT",
|
||||||
|
"message": "Import halted because of errors. Download results for error details.",
|
||||||
|
"description": null,
|
||||||
|
"expiration": "2021-06-11T02:59:56Z",
|
||||||
|
"created": "2021-06-04T02:59:56.569Z",
|
||||||
|
"modified": "2021-06-04T02:59:57.568Z",
|
||||||
|
"completed": "2021-06-04T02:59:57.563Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Import Results
|
||||||
|
|
||||||
|
**Description**
|
||||||
|
|
||||||
|
Gets the results of an import process.
|
||||||
|
|
||||||
|
**Request**
|
||||||
|
|
||||||
|
```json
|
||||||
|
GET /beta/sp-config/import/{id}/download
|
||||||
|
Authorization: Bearer {token}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response**
|
||||||
|
|
||||||
|
```json
|
||||||
|
200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"results": {
|
||||||
|
"TRIGGER_SUBSCRIPTION": {
|
||||||
|
"infos": [],
|
||||||
|
"warnings": [],
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"key": null,
|
||||||
|
"text": "There was an error importing 'TRIGGER_SUBSCRIPTION' with name 'Access Request sub' and id 'f69b6ae7-0709-4432-becd-0a98bb802c3a' Exception com.sailpoint.ets.exception.DuplicatedSubscriptionException",
|
||||||
|
"detail": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"importedObjects": [
|
||||||
|
{
|
||||||
|
"name": "test",
|
||||||
|
"id": "4dd6fab1-1a5b-4928-a2e7-a3926f818bc7",
|
||||||
|
"type": "TRIGGER_SUBSCRIPTION"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "test identity changed",
|
||||||
|
"id": "03dae3d2-2482-4818-92c4-01b0e70e2408",
|
||||||
|
"type": "TRIGGER_SUBSCRIPTION"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
143
docs/idn_docs/docs/standard_collection_parameters.md
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# Standard Collection Parameters
|
||||||
|
|
||||||
|
Many collection endpoints in the IdentityNow APIs support a generic syntax for paginating, filtering
|
||||||
|
and sorting the results.
|
||||||
|
|
||||||
|
A collection endpoint has the following characteristics:
|
||||||
|
|
||||||
|
* The HTTP verb is always GET
|
||||||
|
* The last component in the URL is a plural noun (ex. `/v3/public-identities`)
|
||||||
|
* The return value from a successful request is always an array of JSON objects. This array may be empty if there are no results
|
||||||
|
|
||||||
|
## Paginating Results
|
||||||
|
|
||||||
|
Pagination is achieved with the following optional query parameters.
|
||||||
|
|
||||||
|
|||||
|
||||||
|
|--- |--- |--- |--- |
|
||||||
|
|Name|Description|Default|Constraints|
|
||||||
|
|**limit**|Integer that specifies the maximum number of records to return in a single API call. If not specified a default limit will be used.|250|Maxiumum of 250 records per page|
|
||||||
|
|**offset**|Integer that specifies the offset of the first result from the beginning of the collection. **offset** is record based, not page based, and the index starts at 0. For example, **offset=0** and **limit=20** will return records 0-19, while **offset=1** and **limit=20** will return records 1-20.|0|Between 0 and the last record index.
|
||||||
|
|**count**|Boolean that indicates whether a total count will be returned, factoring in any filter parameters, in the **X-Total-Count** response header. The value will be the total size of the collection that would be returned if **limit** and **offset** were ignored. For example, if the total number of records is 1000, then count=true would return 1000 in the **X-Total-Count** header. Since requesting a total count can have performance impact, it is recommended not to send **count=true** if no use is being made of that value.|false|Must be **true** or **false**|
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* GET `/v3/public-identities?limit=2`
|
||||||
|
* GET `/v3/public-identities?limit=20&offset=4`
|
||||||
|
* GET `/v3/public-identities?count=true`
|
||||||
|
|
||||||
|
## Filtering Results
|
||||||
|
|
||||||
|
Any collection with a `filters` parameter supports filtering. This means that an item will only be included in the returned array if the filters expression evaluates to true for that item. Check the available request parameters for the collection endpoint you are using to see if it supports filtering.
|
||||||
|
|
||||||
|
### Data Types
|
||||||
|
|
||||||
|
Filter expressions are applicable to fields of the following types:
|
||||||
|
|
||||||
|
* Numeric
|
||||||
|
* Boolean: either **true** or **false**
|
||||||
|
* Strings. Enumerated values are a special case of this.
|
||||||
|
* Date-time. In V3, all date time values are in ISO-8601 format, as specified in [RFC 3339 - Date and Time on the Internet: Timestamps](https://tools.ietf.org/html/rfc3339).
|
||||||
|
|
||||||
|
### Filter Syntax
|
||||||
|
|
||||||
|
The syntax of V3 filters is similar to, but not exactly the same as, that specified by the SCIM standard. Some key differences are
|
||||||
|
|
||||||
|
* A slightly different set of supported operators
|
||||||
|
* Case-sensitivity of operators. All V3 filter operators are in lowercase; it is illegal to specify "EQ" instead of "eq".
|
||||||
|
|
||||||
|
### Primitive Operators
|
||||||
|
|
||||||
|
These filter operators apply directly to fields and their values:
|
||||||
|
|
||||||
|
||||
|
||||||
|
|--- |--- |--- |
|
||||||
|
|Operator|Description|Example|
|
||||||
|
|ca|True if the collection-valued field contains all the listed values.|groups ca ("Venezia","Firenze")|
|
||||||
|
|co|True if the value of the fieldcontains the specified value as a substring.(Applicable to string-valued fields only.)|name co "Rajesh"|
|
||||||
|
|eq|True if the value of the field indicated by the first operand isequal to the value specified by the second operand.|identitySummary.id eq "2c9180846e85e4b8016eafeba20c1314"|
|
||||||
|
|ge|True if the value of the field indicated by the first operand isgreater or equal to the value specified by the second operand.|daysUntilEscalation ge 7 name ge "Genaro"|
|
||||||
|
|gt|True if the value of the field indicated by the first operand isgreater than the value specified by the second operand.|daysUntilEscalation gt 7 name gt "Genaro" created gt 2018-12-18T23:05:55Z|
|
||||||
|
|in|True if the field value is in the list of values.|accountActivityItemId in ("2c9180846b0a0583016b299f210c1314","2c9180846b0a0581016b299e82560c1314")|
|
||||||
|
|le|True if the value of the field indicated by the first operand is less or equal to the value specified by the second operand.|daysUntilEscalation le 7 name le "Genaro"|
|
||||||
|
|lt|True if the value of the field indicated by the first operand is less than the value specified by the second operand.|daysUntilEscalation lt 7 name lt "Genaro" created lt 2018-12-18T23:05:55Z|
|
||||||
|
|ne|True if the value of the field indicated by the first operand isnot equal to the value specified by the second operand.|type ne "ROLE"|
|
||||||
|
|pr|True if the field is present, that is, not null.|pr accountRequestInfo|
|
||||||
|
|sw|True if the value of the field starts with the specified value.(Applicable to string-valued fields only.)|name sw "Rajesh"|
|
||||||
|
|
||||||
|
### Composite Operators
|
||||||
|
|
||||||
|
These operators are applied to other filter expressions:
|
||||||
|
|
||||||
|
||||
|
||||||
|
|--- |--- |--- |
|
||||||
|
|Operator|Description|Example|
|
||||||
|
|and|True if both the filter-valued operands are true.|startDate gt 2018 and name sw "Genaro"|
|
||||||
|
|not|True if the filter-valued operand is false.|not groups ca ("Venezia","Firenze")|
|
||||||
|
|or|True if either of the filter-valued operands are true.|startDate gt 2018 or name sw "Genaro"|
|
||||||
|
|
||||||
|
### Escaping Special Characters in a Filter
|
||||||
|
|
||||||
|
Certain characters must be escaped before they can be used in a filter expression. For example, the following filter expression that attempts to find all sources with the name `#Employees` will produce a 400 error:
|
||||||
|
|
||||||
|
`/v3/sources?filters=name eq "#Employees"`
|
||||||
|
|
||||||
|
To properly escape this filter, you will need to do the following:
|
||||||
|
|
||||||
|
`/v3/sources?filters=name eq "%23Employees"`
|
||||||
|
|
||||||
|
If you are searching for a string that contains double quotes, you will need to use the following escape sequence:
|
||||||
|
|
||||||
|
`/v3/sources/?filters=name eq "\"Employees\""`
|
||||||
|
|
||||||
|
The following table lists the special characters that are incompatible with `filters`, and how to escape them.
|
||||||
|
|||
|
||||||
|
|--- |--- |
|
||||||
|
|Character|Escape Sequence|
|
||||||
|
|#|%23|
|
||||||
|
|%|%25|
|
||||||
|
|&|%26|
|
||||||
|
| \\ | \\\\ |
|
||||||
|
|"| \\" |
|
||||||
|
|
||||||
|
### Known Limitations
|
||||||
|
|
||||||
|
Although filter expressions are a very general mechanism, individual API endpoints will only support filtering on a specific set of fields that are relevant to that endpoint, and will frequently only support a subset of operations for each field. For example, an endpoint might allow filtering on the name field but not support use of the co operator on that field. Consult the documentation for each API endpoint to determine what fields and operators can be used.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* `/v3/public-identities?filters=email eq "john.doe@example.com"`
|
||||||
|
* `/v3/public-identities?filters=firstname sw "john" or email sw "joe"`
|
||||||
|
|
||||||
|
Attempts to use an unsupported filter expression will result in a 400 Bad Request response.
|
||||||
|
|
||||||
|
**NOTES:**
|
||||||
|
|
||||||
|
* Spaces in URLs must be escaped with `%20`. Most programming languages, frameworks, libraries, and tools will do this for you, but some won't. In the event that your tool doesn't escape spaces, you will need to format your query as `/v3/public-identities?filters=email%20eq%20"john.doe@example.com"`
|
||||||
|
|
||||||
|
* Unless explicitly noted otherwise, strings are compared lexicographically. Most comparisons are not case sensitive. Any situations where the comparisons are case sensitive will be called out.
|
||||||
|
|
||||||
|
* Date-times are compared temporally; an earlier date-time is less than a later date-time.
|
||||||
|
|
||||||
|
* The usual precedence/associativity of the composite operators applies, with **not** having higher priority than **and**, which in turn has higher priority than **or**. Parentheses can be used to override this precedence.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
`not prop1 eq val1 or prop2 eq val2 and prop3 eq val3` is equivalent to `(not (prop1 eq val1)) or ((prop2 eq val2) and (prop3 eq val3))`
|
||||||
|
|
||||||
|
and
|
||||||
|
|
||||||
|
`not (prop1 eq val1 or prop2 eq val2) and prop3 eq val3` is equivalent to `(not ((prop1 eq val1) or (prop2 eq val2))) and (prop3 eq val3)`
|
||||||
|
|
||||||
|
### Sorting Results
|
||||||
|
|
||||||
|
Sorting of results is supported with the standard `sorters` parameter. Its syntax is a set of comma-separated field names. Each field name may be optionally prefixed with a "-" character, which indicates the sort is descending based on the value of that field. Otherwise, the sort is ascending.
|
||||||
|
|
||||||
|
For example, to sort primarily by **type** in ascending order, and secondarily by **modified date** in descending order, use `sorters=type,-modified`
|
||||||
|
|
||||||
|
## Putting it all Together
|
||||||
|
|
||||||
|
Pagination, filters, and sorters can be mixed and match to achieve the desired output for a given collection endpoint. Here are some examples:
|
||||||
|
|
||||||
|
* `/v3/public-identities?limit=20&filters=firstname eq "john"&sorters=-name` returns the first 20 identities that have a first name of John and are sorted in descending order by full name.
|
||||||
|
* `/v3/account-activities?limit=10&offset=2&sorters=-created` sorts the results by descending created time, so the most recent activities appear first. The limit and offset returns the 3rd page of this sorted response with 10 records displayed.
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
# Building Transforms in IdentityNow
|
||||||
|
|
||||||
|
In SailPoint's cloud services, transforms allow you to manipulate attribute values while aggregating from or provisioning to a source. This guide provides a reference to help you understand the purpose, configuration, and usage of transforms.
|
||||||
|
|
||||||
|
## What are Transforms?
|
||||||
|
|
||||||
|
Transforms are configurable objects that define easy ways to manipulate attribute data without requiring you to write code. Transforms are configurable building blocks with sets of inputs and outputs:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Because there is no code to write, an administrator can configure these using a JSON object structure and uploading them into IdentityNow using [IdentityNow's Transform REST APIs](https://developer.sailpoint.com/apis/v3/#tag/Transforms).
|
||||||
|
|
||||||
|
> **NOTE**: Sometimes transforms are referred to as Seaspray, the codename for transforms. IdentityNow Transforms and Seaspray are essentially the same.
|
||||||
|
|
||||||
|
## How Transforms Work
|
||||||
|
|
||||||
|
Transforms typically have an input(s) and output(s). The way the transformation happens mainly depends on the type of transform. Refer to [Operations in IdentityNow Transforms](../transform_operations/transform_operations.md#operations-in-identitynow-transforms) for more information.
|
||||||
|
|
||||||
|
For example, a [Lower transform](../transform_operations/operations/lower.md) transforms any input text strings into lowercase versions as output. So if the input were "Foo", the lower case output of the transform would be "foo":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
There are other types of transforms too. For example an [E.164 Phone transform](../transform_operations/operations/e164_phone.md) transforms any input phone number strings into an E.164 formatted version as output. So if the input were "(512) 346-2000" the output would be "+1 5123462000":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Multiple Transform Inputs
|
||||||
|
|
||||||
|
In the previous examples, each transform had a single input. Some transforms can specify more than one input. For example, the [Concat transform](../transform_operations/operations/concatenation.md) concatenates one or more strings together. If "Foo" and "Bar" were inputs, the transformed output would be "FooBar":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Complex Nested Transforms
|
||||||
|
|
||||||
|
For more complex use cases, a single transform may not be enough. It is possible to link several transforms together. IdentityNow calls these 'nested' transforms because they are transform objects within other transform objects.
|
||||||
|
|
||||||
|
An example of a nested transform would be using the previous [Concat transform](../transform_operations/operations/concatenation.md) and passing its output as an input to another [Lower transform](../transform_operations/operations/lower.md). If the inputs "Foo" and "Bar" were passed into the transforms, the ultimate output would be "foobar," concatenated and lower-cased.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
There is no hard limit for the number of transforms that can be nested. However, the more transforms applied, the more complex the nested transform will be, which can make it difficult to understand and maintain.
|
||||||
|
|
||||||
|
## Configuring Transform Behavior
|
||||||
|
|
||||||
|
Some transforms can specify an attributes map that configures the transform behavior. Each transform type has different configuration attributes and different uses. To better understand what is configurable per transform, refer to the Transform Types section and the associated Transform guide(s) that cover each transform.
|
||||||
|
|
||||||
|
It is possible to extend the earlier complex nested transform example. If a Replace transform, which replaces certain strings with replacement text, were added, and the transform were configured to replace "Bar with "Baz," the output would be added as an input to the Concat and Lower transforms:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The output of the Replace transform would be "Baz," which is then passed as an input to the Concat transform along with "Foo," producing an output of "FooBaz." This is then passed as an input into the Lower transform, producing a final output of "foobaz."
|
||||||
|
|
||||||
|
### Transform Syntax
|
||||||
|
|
||||||
|
Transforms are JSON objects. Prior to this, the transforms have been shown as flows of building blocks to help illustrate basic transform ideas. However at the simplest level, a transform looks like this:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Lowercase Department",
|
||||||
|
"type": "lower",
|
||||||
|
"attributes": {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There are three main components of a transform object:
|
||||||
|
|
||||||
|
1. `name` - This specifies the name of the transform. It refers to a transform in the IdentityNow API or User Interface. Only provide a name on the root-level transform. Nested transforms do not have names.
|
||||||
|
|
||||||
|
2. `type` - This specifies the transform type, which ultimately determines the transform's behavior.
|
||||||
|
|
||||||
|
3. `attributes` - This specifies any attributes or configurations for controlling how the transform works. As mentioned earlier in [Configuring Transform Behavior](#configuring-transform-behavior), each transform type has different sets of attributes available.
|
||||||
|
|
||||||
|
### Implicit vs Explicit Input
|
||||||
|
|
||||||
|
A special configuration attribute available to all transforms is input. If the input attribute is not specified, this is referred to as implicit input, and the system determines the input based on what is configured. If the input attribute is specified, then this is referred to as explicit input, and the system's input is ignored in favor of whatever the transform explicitly specifies. A good way to understand this concept is to walk through an example. Imagine that IdentityNow has the following:
|
||||||
|
|
||||||
|
- An account on Source 1 with department set to "Services."
|
||||||
|
- An account on Source 2 with department set to "Engineering."
|
||||||
|
|
||||||
|
The following two examples explain how a transform with an implicit or explicit input would work with those sources.
|
||||||
|
|
||||||
|
### Implicit Input
|
||||||
|
|
||||||
|
An identity profile is configured the following way:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
As an example, the "Lowercase Department" transform being used is written the following way:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Lowercase Department",
|
||||||
|
"type": "lower",
|
||||||
|
"attributes": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that the attributes has no input. This is an implicit input example. The transform uses the input provided by the attribute you mapped on the identity profile.
|
||||||
|
|
||||||
|
In this example, the transform would produce "services" when the source is aggregated because Source 1 is providing a department of "Services," which then gets lowercased per the transform.
|
||||||
|
|
||||||
|
### Explicit Input
|
||||||
|
|
||||||
|
As an example, the "Lowercase Department" has been changed the following way:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Lowercase Department",
|
||||||
|
"type": "lower",
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"type": "accountAttribute",
|
||||||
|
"attributes": {
|
||||||
|
"attributeName": "department",
|
||||||
|
"sourceName": "Source 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that there is an `input` in the attributes. This is an explicit input example. The transform uses the value Source 2 provides for the "department" attribute, ignoring your configuration in the identity profile.
|
||||||
|
|
||||||
|
In this example, the transform would produce "engineering" because Source 2 is providing a department of "Engineering," which then gets lowercased, per the transform. Though the system is still providing an implicit input of Source 1's department attribute, the transform ignores this and uses the explicit input specified as Source 2's department attribute.
|
||||||
|
|
||||||
|
> **Note**: This is also an example of a nested transform.
|
||||||
|
|
||||||
|
## Transform Usage
|
||||||
|
|
||||||
|
You typically use transforms when data in IdentityNow or on a source is not normalized for its intended destination and must be mapped, generated, or otherwise altered to meet data standards. Transforms have a variety of applications across IdentityNow's feature sets, ranging from account correlation for access reviews to provisioning new accounts in target sources.
|
||||||
|
|
||||||
|
You mainly use transforms in two places:
|
||||||
|
|
||||||
|
1. The identity - on an identity profile for identity attribute calculation. These are calculated during any identity refresh process.
|
||||||
|
|
||||||
|
2. The account - on a source profile for determining new account attribute values for provisioning operations (like account creation).
|
||||||
|
|
||||||
|
### Identity Transforms
|
||||||
|
|
||||||
|
Identity attribute transforms are configured on the identity profile. Use them to determine identity attribute values calculated during an identity refresh process.
|
||||||
|
|
||||||
|
**Configuration**
|
||||||
|
|
||||||
|
These can be configured in IdentityNow by going to **Admin** > **Identities** > **Identity Profiles** > (An Identity Profile) > **Mappings** (tab). These can also be configured with IdentityNow REST APIs.
|
||||||
|
|
||||||
|
From this screen the installed, available transforms can be added to an identity profile to transform identity attributes. Select a transform next to an identity attribute. Once the transform is configured, click **Save**.
|
||||||
|
|
||||||
|
**Testing Transforms**
|
||||||
|
|
||||||
|
Once the transform is saved, you can preview the example transform data with the Preview function. This provides a live preview of the newly saved transforms applied to the identity data.
|
||||||
|
|
||||||
|
**Applying Transforms**
|
||||||
|
|
||||||
|
Select Update to apply the transform updates. This starts an identity refresh process to recalculate and update identity attributes for all identities in the system.
|
||||||
|
|
||||||
|
> **Note**: This process can take some time.
|
||||||
|
|
||||||
|
### Account Transforms
|
||||||
|
|
||||||
|
Account attribute transforms are configured on the account create profiles. They determine the templates for new accounts created during provisioning events.
|
||||||
|
|
||||||
|
**Configuration**
|
||||||
|
|
||||||
|
These can be configured in IdentityNow by going to **Admin** > **Sources** > (A Source) > **Accounts** (tab) > **Create Profile**. These can also be configured with IdentityNow REST APIs.
|
||||||
|
|
||||||
|
You can select the installed, available transforms from this interface. Alternately, you can add more complex transforms with REST APIs.
|
||||||
|
|
||||||
|
For more information on the IdentityNow REST API endpoints used to managed transform objects in APIs, refer to [IdentityNow REST APIs](https://developer.sailpoint.com/apis/v3/#tag/Transforms).
|
||||||
|
|
||||||
|
> **Note**: For details about authentication against REST APIs, refer to the [authentication docs](../../authentication.md).
|
||||||
|
|
||||||
|
Testing Transforms
|
||||||
|
|
||||||
|
To test a transform for an account create profile, you must generate a new account creation provisioning event. This involves granting access to an identity that does not already have an account on this source; an account is created as a byproduct of the access assignment. This can be initiated with access request or even role assignment.
|
||||||
|
|
||||||
|
Applying Transforms
|
||||||
|
|
||||||
|
Once the transforms are saved to the account profile, they are automatically applied for any subsequent provisioning events.
|
||||||
|
|
||||||
|
## Testing Seaspray Transforms
|
||||||
|
|
||||||
|
**Testing Transforms in Identity Profile Mappings**
|
||||||
|
|
||||||
|
To test a transform for identity data, go to **Identities** > **Identity Profiles** and click **Mappings**. Select the transform to map one of your identity attributes, click **Save**, and preview your identity data.
|
||||||
|
|
||||||
|
**Testing Transforms for Account Attributes**
|
||||||
|
|
||||||
|
To test a transform for account data, you must provision a new account on that source. For example, you can create an access request that would result in a new account on that source, or you can assign a new role.
|
||||||
|
|
||||||
|
## Transform Best Practices
|
||||||
|
|
||||||
|
- **Designing Complex Transforms** - Start with small transform 'building blocks' and add to them. It can be helpful to diagram out the inputs and outputs if you are using many transforms.
|
||||||
|
|
||||||
|
- **JSON Editor** - Because transforms are JSON objects, it is recommended that you use a good JSON editor. Atom, Sublime Text, and Microsoft Code work well because they have JSON formatting and plugins that can do JSON validation, completion, formatting, and folding. This is very useful for large complex JSON objects.
|
||||||
|
|
||||||
|
- **Leverage Examples** - Many implementations use similar sets of transforms, and a lot of common solutions can be found in examples. Feel free to share your own transform examples on Compass!
|
||||||
|
|
||||||
|
- **Same Problem, Multiple Solutions** - There can be multiple ways to solve the same problem, but use the solution that makes the most sense to your implementation and is easiest to administer and understand.
|
||||||
|
|
||||||
|
- **Encapsulate Repetition** - If you are copying and pastings the same transforms over and over, it can be useful to make a transform a stand-alone transform and just have other transforms reference it by using the reference type.
|
||||||
|
|
||||||
|
- **Plan for Bad Data** - Data will not always be perfect, so plan for data failures and try to ensure transforms still produce workable results, in case data is missing, malformed, or has incorrect values.
|
||||||
|
|
||||||
|
## Using Transforms vs. Rules
|
||||||
|
|
||||||
|
Sometimes it can be difficult to decide when to implement a transform and when to implement a rule. Both transforms and rules can calculate values for identity or account attributes.
|
||||||
|
|
||||||
|
Despite their functional similarity, transforms and rules have very different implementations. Transforms are JSON-based configurations, editable with IdentityNow's transform REST APIs. Rules are implemented with code (typically BeanShell, a Java-like syntax), they must abide by the [IdentityNow Rule Guidelines](https://community.sailpoint.com/docs/DOC-12122), and they require SailPoint in order to be reviewed and installed into the tenant. Rules, however, can do things that transforms cannot in some cases.
|
||||||
|
|
||||||
|
Because transforms have easier and more accessible implementations, they are generally recommended. With transforms, any IdentityNow administrator can view, create, edit, and delete transforms directly with REST API without SailPoint involvement.
|
||||||
|
|
||||||
|
If something cannot be done with a transform, then consider using a rule. When you are transitioning from a transform to a rule, you must take special consideration when you decide where the rule executes.
|
||||||
|
|
||||||
|
- If you are calculating identity attributes, you can use [Identity Attribute rules](https://community.sailpoint.com/docs/DOC-12616) instead of identity transforms.
|
||||||
|
|
||||||
|
- If you are calculating account attributes (during provisioning), you can use [Attribute Generator rules](https://community.sailpoint.com/docs/DOC-12645) instead of account transforms.
|
||||||
|
|
||||||
|
- All rules you build must abide by the [IdentityNow Rule Guidelines](https://community.sailpoint.com/docs/DOC-12122).
|
||||||
|
|
||||||
|
If you use a rule, make note of it for administrative purposes. The best practice is to check in these types of artifacts into some sort of version control (e.g., GitHub, et. Al.) for records.
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# Creating Your First Transform
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide explains how to use [IdentityNow's Transform REST APIs](https://developer.sailpoint.com/apis/v3/#tag/Transforms) to do the following:
|
||||||
|
|
||||||
|
- [List Transforms in your IdentityNow Tenant](#list-transforms-in-your-identitynow-tenant)
|
||||||
|
- [Create a Transform](#create-a-transform)
|
||||||
|
- [Get a Transform by Id](#get-transform-by-id)
|
||||||
|
- [Update a Transform](#update-a-transform)
|
||||||
|
- [Delete a Transform](#delete-a-transform)
|
||||||
|
|
||||||
|
## List Transforms in your IdentityNow Tenant
|
||||||
|
|
||||||
|
To call the APIs for transforms you need a personal access token and your tenant's name to provide with the request. To get a personal access token, see [Personal Access Tokens](../../authentication.md#personal-access-tokens). To get the name of your tenant, see [Finding Your Organization Tenant Name](../../getting_started.md#finding-your-org-tenant-name)
|
||||||
|
|
||||||
|
Before you create your first custom transform, see what transforms are already in the tenant. You can get this information by calling the [List Transforms API](https://developer.sailpoint.com/apis/v3/#operation/getTransformsList).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --location --request GET 'https://{tenant}.api.identitynow.com/v3/transforms' --header 'Authorization: Bearer {token}'
|
||||||
|
```
|
||||||
|
|
||||||
|
The response body contains an array of transform objects containing the following values:
|
||||||
|
|
||||||
|
- **id** - The id of the transform
|
||||||
|
- **name** - The name of the transform
|
||||||
|
- **type** - The type of transform, see [Transform Operations](./transform_operations.md#transform-operations)
|
||||||
|
- **attributes** - Object of attributes related to the transform
|
||||||
|
- **internal** - A `true` or `false` attribute to determine whether the transform is internal or custom
|
||||||
|
- **true** - The transform is internal and cannot be modified without contacting Sailpoint.
|
||||||
|
- **false** - The tranform is custom and can be modified with the API.
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "2b5191bb-051f-4edf-8283-3962b4a0f7a5",
|
||||||
|
"name": "ISO3166 Country Format",
|
||||||
|
"type": "iso3166",
|
||||||
|
"attributes": null,
|
||||||
|
"internal": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "484e717d-2841-4bab-9bbf-6f48d8096965",
|
||||||
|
"name": "Calculate Partners State",
|
||||||
|
"type": "substring",
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"attributeName": "Location",
|
||||||
|
"sourceName": "Partner Accounts"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
"end": -1.0,
|
||||||
|
"begin": {
|
||||||
|
"attributes": {
|
||||||
|
"substring": ","
|
||||||
|
},
|
||||||
|
"type": "indexOf"
|
||||||
|
},
|
||||||
|
"beginOffset": 2.0
|
||||||
|
},
|
||||||
|
"internal": false
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create a Transform
|
||||||
|
|
||||||
|
This [lookup transform](./operations/lookup.md) takes the input value of an attribute, locates it in the table provided, and returns its corresponding value. If your input value is not found in the lookup table, the transform returns the default value. Replace `{tenant}` and `{token}` with the values you got ealier.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --location --request POST 'https://{tenant}.api.identitynow.com/v3/transforms' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'Authorization: Bearer {token}' \
|
||||||
|
--data-raw '{
|
||||||
|
"name": "Country Code To Timezone",
|
||||||
|
"type": "lookup",
|
||||||
|
"attributes": {
|
||||||
|
"table": {
|
||||||
|
"EN-US": "CST",
|
||||||
|
"ES-MX": "CST",
|
||||||
|
"EN-GB": "GMT",
|
||||||
|
"default": "GMT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "b23788a0-41a2-453b-89ae-0d670fa0cb6a",
|
||||||
|
"name": "Country Code To Timezone",
|
||||||
|
"type": "lookup",
|
||||||
|
"attributes": {
|
||||||
|
"table": {
|
||||||
|
"EN-US": "CST",
|
||||||
|
"ES-MX": "CST",
|
||||||
|
"EN-GB": "GMT",
|
||||||
|
"default": "GMT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you have created the transform, you can find it in IdentityNow by going to **Admin** > **Identities** > **Identity Profiles** > (An Identity Profile) > **Mappings** (tab).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For more information about creating transforms, see [Create Transform](https://developer.sailpoint.com/apis/v3/#operation/createTransform).
|
||||||
|
|
||||||
|
## Get Transform By ID
|
||||||
|
|
||||||
|
To get the transform created with the API, call the `GET` endpoint, using the `id` returned by the create API response.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --location --request GET 'https://{tenant}.api.identitynow.com/v3/transforms/b23788a0-41a2-453b-89ae-0d670fa0cb6a' \
|
||||||
|
--header 'Authorization: Bearer {token}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "b23788a0-41a2-453b-89ae-0d670fa0cb6a",
|
||||||
|
"name": "Country Code To Timezone",
|
||||||
|
"type": "lookup",
|
||||||
|
"attributes": {
|
||||||
|
"table": {
|
||||||
|
"EN-US": "CST",
|
||||||
|
"ES-MX": "CST",
|
||||||
|
"EN-GB": "GMT",
|
||||||
|
"default": "GMT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information about getting a transform by its `id,` see [Transform by ID](https://developer.sailpoint.com/apis/v3/#operation/getTransform).
|
||||||
|
|
||||||
|
## Update a Transform
|
||||||
|
|
||||||
|
To update a transform, call the `PUT` endpoint with the updated transform body. This example adds another item to the lookup table, `EN-CA.`
|
||||||
|
|
||||||
|
>**NOTE** Modifying the `name` or `type` field results in a bad request.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --location --request PUT 'https://{tenant}.api.identitynow.com/v3/transforms/b23788a0-41a2-453b-89ae-0d670fa0cb6a' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--header 'Authorization: Bearer {token}' \
|
||||||
|
--data-raw '{
|
||||||
|
"name": "Country Code To Timezone",
|
||||||
|
"type": "lookup",
|
||||||
|
"attributes": {
|
||||||
|
"table": {
|
||||||
|
"EN-US": "CST",
|
||||||
|
"ES-MX": "CST",
|
||||||
|
"EN-GB": "GMT",
|
||||||
|
"EN-CA": "MST",
|
||||||
|
"default": "GMT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "b23788a0-41a2-453b-89ae-0d670fa0cb6a",
|
||||||
|
"name": "Country Code To Timezone",
|
||||||
|
"type": "lookup",
|
||||||
|
"attributes": {
|
||||||
|
"table": {
|
||||||
|
"EN-US": "CST",
|
||||||
|
"ES-MX": "CST",
|
||||||
|
"EN-GB": "GMT",
|
||||||
|
"EN-CA": "MST",
|
||||||
|
"default": "GMT"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"internal": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information about updating transforms, see [Update a transform](https://developer.sailpoint.com/apis/v3/#operation/updateTransform).
|
||||||
|
|
||||||
|
## Delete a Transform
|
||||||
|
|
||||||
|
To delete the transform, call the `DELETE` endpoint with the `id` of the transform to delete. The server responds with a 204 when the transform is successfully removed.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --location --request DELETE 'https://{tenant}.api.identitynow.com/v3/transforms/b23788a0-41a2-453b-89ae-0d670fa0cb6a' \
|
||||||
|
--header 'Authorization: Bearer {token}'
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information about deleting transforms, see [Delete Transform](https://developer.sailpoint.com/apis/v3/#operation/deleteTransform).
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that you understand the lifecycle of transforms, see this [complex usecase](./temporary_password_usecase.md) using a nested transform structure to create a temporary password that can be sent to each user.
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# IdentityNow Transforms - Account Attribute
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the account attribute transform to look up an account for a particular source on an identity and return a specific attribute value from that account.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - If there are multiple accounts, then IdentityNow by default takes the value from the oldest account (based on the account created date). You can configure this behavior by specifying `accountSortAttribute` and `accountSortDescending` attributes.
|
||||||
|
> - If there are multiple accounts and the oldest account has a null attribute value, by default IdentityNow moves to the next account that can have a value (if there are any). You can override this behavior with the `accountReturnFirstLink` property.
|
||||||
|
> - You can filter the multiple accounts returned based on the data they contain so that you can target specific accounts. This is often used to target accounts that are "active" instead of those that are not.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The account attribute transform's configuration can take several attributes as inputs. The following example shows a fully configured transform with all required and optional attributes.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "Workday",
|
||||||
|
"attributeName": "DEPARTMENT",
|
||||||
|
"accountSortAttribute": "created",
|
||||||
|
"accountSortDescending": true,
|
||||||
|
"accountReturnFirstLink": true,
|
||||||
|
"accountPropertyFilter": "(DEPARTMENT == \"Engineering\")",
|
||||||
|
"accountFilter": "!(nativeIdentity.startsWith(\"*DELETED*\"))"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute",
|
||||||
|
"name": "Account Attribute Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `accountAttribute.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **sourceName** - This is a reference to the source to search for accounts.
|
||||||
|
- This is a reference by a source's display name attribute (e.g., Active Directory). If the display name is updated, this reference must also be updated.
|
||||||
|
- As an alternative, you can provide an `applicationId` or `applicationName` instead.
|
||||||
|
- `applicationId` - This is a reference by a source's external GUID/ID attribute (e.g., "ff8081815a8b3925015a8b6adac901ff")
|
||||||
|
- `applicationName` - This is a reference by a source's immutable name attribute (e.g., "Active Directory \[source\]")
|
||||||
|
- **attributeName** - The name of the attribute on the account to return. This should match the name of the account attribute name visible in the user interface or on the source schema.
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This is a `true` or `false` value indicating whether the transform logic must be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **accountSortAttribute** - This configuration's value is a string name of the attribute to use when determining the ordering of returned accounts when there are multiple entries.
|
||||||
|
- Accounts can be sorted by any schema attribute.
|
||||||
|
- If no sort attribute is defined, the transform will default to "created" (ascending sort on created date - oldest object wins).
|
||||||
|
- **accountSortDescending** - This configuration's value is a boolean (true/false). It controls the sort order when there are multiple accounts.
|
||||||
|
- If not defined, the transform will default to false (ascending order)
|
||||||
|
- **accountReturnFirstLink** - This configuration's value is a boolean (true/false). It controls which account to source a value from for an attribute. If this flag is set to true, the transform returns the value from the first account in the list, even if it is null. If this flag is set to false, the transform returns the first non-null value.
|
||||||
|
- If the configuration's value is not defined, the transform will default to the false setting.
|
||||||
|
- **accountFilter** - This expression queries the database to narrow search results. This configuration's value is a sailpoint.object.Filter expression for searching against the database. The default filter always includes the source and identity, and any subsequent expressions are combined in an AND operation with the existing search criteria.
|
||||||
|
- Only certain searchable attributes are available:
|
||||||
|
- `nativeIdentity` - This is the account ID.
|
||||||
|
- `displayName` - This is the account name.
|
||||||
|
- `entitlements` - This boolean value determine whether the account has entitlements.
|
||||||
|
- **accountPropertyFilter** - Use this expression to search and filter accounts in memory. This configuration's value is a sailpoint.object.Filter expression for searching against the returned resultset.
|
||||||
|
- All account attributes are available for filtering because this operation is performed in memory.
|
||||||
|
- Examples:
|
||||||
|
- `(status != "terminated")`
|
||||||
|
- `(department == "Engineering")`
|
||||||
|
- `(groups.containsAll({"Admin"}) || location == "Austin")`
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
HR systems can have multiple HR records for a person, especially in rehire and conversion scenarios. In order to get the correct identity data, you must get data from only the latest active accounts.
|
||||||
|
|
||||||
|
- `sourceName` is "Corporate HR" because that is the name of the authoritative source.
|
||||||
|
|
||||||
|
- `attributeName` is "HIREDATE" because that is the attribute you want from the authoritative source.
|
||||||
|
|
||||||
|
- `accountSortAttribute` is "created" because you want to sort on created dates in case there are multiple accounts.
|
||||||
|
|
||||||
|
- `accountSortDescending` is true because you want to sort based on the newest or latest account from the HR system.
|
||||||
|
|
||||||
|
- `accountReturnFirstLink` is true because you want to return the value of HIREDATE, event if it is null.
|
||||||
|
|
||||||
|
- `accountPropertyFilter` is filtering the accounts to look at only active accounts. Terminated accounts will not appear (assuming there are no data issues).
|
||||||
|
|
||||||
|
> **Note** You cannot use accountFilter here because WORKER_STATUS\_\_c is not a searchable attribute, but accountPropertyFilter works instead.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"attributeName": "HIREDATE",
|
||||||
|
"sourceName": "Corporate HR",
|
||||||
|
"accountSortAttribute": "created",
|
||||||
|
"accountSortDescending": true,
|
||||||
|
"accountReturnFirstLink": true,
|
||||||
|
"accountPropertyFilter": "(WORKER_STATUS__c == \"active\")"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute",
|
||||||
|
"name": "Account Attribute Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
When you are mapping values like a username, focus on primary accounts from a particular source or accounts that are not service accounts.
|
||||||
|
|
||||||
|
- `sourceName` is "Active Directory" because that is the source this data is coming from.
|
||||||
|
- `attributeName` is "sAMAccountName" because you are mapping the username of the user.
|
||||||
|
- `accountFilter` is an expression filtering the accounts to make sure they are not service accounts.
|
||||||
|
> **Note**: `accountPropertyFilter` also would have worked here.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"attributeName": "sAMAccountName",
|
||||||
|
"sourceName": "Active Directory",
|
||||||
|
"accountFilter": "!(displayName.startsWith(\"SVC-\"))"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute",
|
||||||
|
"name": "Account Attribute Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# IdentityNow Transforms - Concatenation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the concatenation transform to join two or more string values into a combined output. The concatenation transform often joins elements such as first and last name into a full display name, but it has many other uses.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The concatenation transform requires an array list of `values` that need to be joined. These values can be static strings or the return values of other nested transforms.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"values": ["John", " ", "Smith"]
|
||||||
|
},
|
||||||
|
"type": "concat",
|
||||||
|
"name": "Concatenation transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `concat.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **values** - This is the array of items to join.
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform joins the user's first name from the "HR Source" with his/her last name, adds a space between them, and then adds a parenthetical note that the user is a contractor at the end.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "FirstName"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
" ",
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "LastName"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
" (Contractor)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "concat",
|
||||||
|
"name": "Test Concat Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform joins the user's job title with his/her job code value and adds a hyphen between those two pieces of data.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "JobTitle"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
"-",
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "JobCode"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "concat",
|
||||||
|
"name": "Test Concat Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# IdentityNow Transforms - Conditional
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the conditional transform to output different values depending on simple conditional logic. This is a convenience transform - the same capability can be implemented with a "static" transform, but this transform has greater simplicity and null-safe error checking.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - The two operands within the transform cannot be null; if they are, an IllegalArgumentException is thrown.
|
||||||
|
> - The `expression` attribute must be "eq," or the transform will throw an IllegalArgumentException.
|
||||||
|
> - All attribute string values are case-sensitive, so differently cased strings (e.g., "engineering" and "Engineering") will not return as matched.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
In addition to the `type` and `name` attributes, the conditional transform requires an `expression,` a `positiveCondition,` and a `negativeCondition.` If the expression evaluates to false, the transform returns the negative condition; otherwise it returns the positive condition.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"expression": "foo eq foo",
|
||||||
|
"positiveCondition": "true",
|
||||||
|
"negativeCondition": "false"
|
||||||
|
},
|
||||||
|
"type": "conditional",
|
||||||
|
"name": "Conditional Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `conditional.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **expression** - This comparison statement follows the structure of `ValueA eq ValueB` where `ValueA` and `ValueB` are static strings or outputs of other transforms; the `eq` operator is the only valid comparison.
|
||||||
|
- **positiveCondition** - This is the output of the transform if the expression evaluates to true.
|
||||||
|
- **negativeCondition** - This is the output of the transform if the expression evaluates to false.
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform takes the user's HR-defined department attribute and compares it to the value of "Science." If this is the user's department, the transform returns "true;" otherwise it returns "false."
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"expression": "$department eq Science",
|
||||||
|
"positiveCondition": "true",
|
||||||
|
"negativeCondition": "false",
|
||||||
|
"department": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "department"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "conditional",
|
||||||
|
"name": "Test Conditional Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform extends the previous one by returning the output of another Seaspray transform depending on the result of the expression. You can assign Seaspray transforms' outputs to variables and then reference them within the `positiveCondition` and `negativeCondition` attributes.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"expression": "$department eq Science",
|
||||||
|
"positiveCondition": "$scienceBuilding",
|
||||||
|
"negativeCondition": "$adminBuilding",
|
||||||
|
"department": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "department"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
"scienceBuilding": {
|
||||||
|
"attributes": {
|
||||||
|
"value": "Building S"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
},
|
||||||
|
"adminBuilding": {
|
||||||
|
"attributes": {
|
||||||
|
"value": "Building A"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "conditional",
|
||||||
|
"name": "Test Conditional Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# IdentityNow Transforms - Date Compare
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the date compare transform to compare two dates and, depending on the comparison result, return one value if one date is after the other or return a different value if it is before the other. A common use case is to calculate lifecycle states (e.g., the user is "active" if the current date is greater than or equal to the user's hire date, etc.).
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - In addition to explicit date values, the transform recognizes the "now" keyword that always evaluates to the exact date and time when the transform is evaluating.
|
||||||
|
> - All dates **must** be in [ISO8601 format](https://en.wikipedia.org/wiki/ISO_8601) in order for the date compare transform to evaluate properly.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The date compare transform takes as an input the two dates to compare, denoted as `firstDate` and `secondDate`. The transform also requires an `operator` designation so it knows which condition to evaluate for. Lastly, the transform requires both a `positiveCondition` and a `negativeCondition` -- the former returns if the comparison evaluates to true; the latter returns if the comparison evaluates to false.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"firstDate": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "termination_date"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
"secondDate": "now",
|
||||||
|
"operator": "gt",
|
||||||
|
"positiveCondition": "active",
|
||||||
|
"negativeCondition": "terminated"
|
||||||
|
},
|
||||||
|
"type": "dateCompare",
|
||||||
|
"name": "Date Compare Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `dateCompare.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **firstDate** - This is the first date to consider (i.e., the date that would be on the left hand side of the comparison operation).
|
||||||
|
- **secondDate** - This is the second date to consider (i.e., the date that would be on the right hand side of the comparison operation).
|
||||||
|
- **operator** - This is the comparison to perform. The following values are valid:
|
||||||
|
- **LT**: Strictly less than: firstDate < secondDate
|
||||||
|
- **LTE**: Less than or equal to: firstDate <= secondDate
|
||||||
|
- **GT**: Strictly greater than: firstDate > secondDate
|
||||||
|
- **GTE**: Greater than or equal to: firstDate >= secondDate
|
||||||
|
- **positiveCondition** - This is the value to return if the comparison is true.
|
||||||
|
- **negativeCondition** - This is the value to return if the comparison is false.
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform accomplishes a basic lifecycle state calculation. It compares the user's termination date with his/her HR record. If the current datetime (denoted by `now`) is less than that date, the transform returns "active." If the current datetime is greater than that date, the transform returns "terminated."
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"firstDate": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "termination_date"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
"secondDate": "now",
|
||||||
|
"operator": "gt",
|
||||||
|
"positiveCondition": "active",
|
||||||
|
"negativeCondition": "terminated"
|
||||||
|
},
|
||||||
|
"type": "dateCompare",
|
||||||
|
"name": "Date Compare Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform compares the user's hire date to a fixed date in the past. If the user was hired prior to January 1, 1996, the transform returns "legacy." If the user was hired later than January 1, 1996, it returns "regular."
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"firstDate": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "hire_date"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
"secondDate": {
|
||||||
|
"attributes": {
|
||||||
|
"input": "12/31/1995",
|
||||||
|
"inputFormat": "M/d/yyyy",
|
||||||
|
"outputFormat": "ISO8601"
|
||||||
|
},
|
||||||
|
"type": "dateFormat"
|
||||||
|
},
|
||||||
|
"operator": "lte",
|
||||||
|
"positiveCondition": "legacy",
|
||||||
|
"negativeCondition": "regular"
|
||||||
|
},
|
||||||
|
"type": "dateCompare",
|
||||||
|
"name": "Date Compare Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# IdentityNow Transforms - Date Format
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the date format transform to convert datetime strings from one format to another. This is often useful when you are syncing data from one system to another, where each application uses a different format for date and time data.
|
||||||
|
|
||||||
|
This transform leverages the Java SimpleDateFormat syntax; see the [References](#references) section for more information on this standard.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
- In addition to explicit SimpleDateFormat syntax, the date format transform also recognizes several built-in "named" constructs:
|
||||||
|
- **ISO8601:** This is the date format corresponding to the ISO8601 standard. The exact format is expressed as yyyy-MM-dd'T'HH:mm:ss.SSSX.
|
||||||
|
- **LDAP:** This is the date format corresponding to the LDAP date format standard, also expressed as yyyyMMddHHmmss.Z.
|
||||||
|
- **PEOPLE_SOFT:** This is the date format format used by People Soft, also expressed as MM/dd/yyyy.
|
||||||
|
- **EPOCH_TIME_JAVA:** This represents the incoming date value as the elapsed time in milliseconds from midnight, January 1st, 1970
|
||||||
|
- **EPOCH_TIME_WIN32:** This represents the incoming date value as the elapsed time in 100-nanosecond intervals from midnight, January 1st, 1601.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The date format transform takes whatever value provided as the input, parses the datetime based on the `inputFormat` provided, and then reformats it into the desired `outputFormat.`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"inputFormat": "EPOCH_TIME_JAVA",
|
||||||
|
"outputFormat": "ISO8601"
|
||||||
|
},
|
||||||
|
"type": "dateFormat",
|
||||||
|
"name": "Date Format Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `dateFormat.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **inputFormat** - This string value indicates either the explicit SimpleDateFormat or the built-in named format of the incoming data.
|
||||||
|
- If no inputFormat is provided, the transform assumes that it is in [ISO8601 format](https://en.wikipedia.org/wiki/ISO_8601).
|
||||||
|
- **outputFormat** - This string value indicates either the explicit SimpleDateFormat or the built-in named format that the data should be formatted into.
|
||||||
|
- If no outputFormat is provided, the transform assumes that it is in [ISO8601 format](https://en.wikipedia.org/wiki/ISO_8601).
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform takes the incoming Java epoch-based timestamp and formats it as an ISO8601 compatible string.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: 144642632190
|
||||||
|
Output: 1974-08-02T02:30:32.190-00
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"inputFormat": "EPOCH_TIME_JAVA",
|
||||||
|
"outputFormat": "ISO8601"
|
||||||
|
},
|
||||||
|
"type": "dateFormat",
|
||||||
|
"name": "Date Format Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform takes the incoming date, formatted as a common US date string, and formats it to match the date structure of most database systems.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: 4/1/1975
|
||||||
|
Output: 1975-04-01
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"inputFormat": "M/d/yyyy",
|
||||||
|
"outputFormat": "yyyy-MM-dd"
|
||||||
|
},
|
||||||
|
"type": "dateFormat",
|
||||||
|
"name": "Date Format Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [http://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html](http://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html)
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# IdentityNow Transforms - Date Math
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the date math transform to add, subtract, and round components of a timestamp's 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.
|
||||||
|
|
||||||
|
The output format for the DateMath transform is "yyyy-MM-dd'T'HH:mm." When you use this transform inside another transform (e.g., [dateCompare](./date_compare.md)), make sure to convert to [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) first.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - The input datetime value must always be in [ISO8601 format](https://en.wikipedia.org/wiki/ISO_8601), in UTC time zone:
|
||||||
|
|
||||||
|
- yyyy-MM-ddThh:mm:ss:nnnZ
|
||||||
|
- 2020-10-28T12:00:00.000Z, as an example
|
||||||
|
- The dateFormat transform can help get data into this format.
|
||||||
|
|
||||||
|
> - The industry standard for rounding is actually date/time truncation. When rounding down, the fractional value is truncated from the incoming data. When rounding up, the fractional value is truncated and the next unit of time is added. Refer to the Transform Structure section below for examples.
|
||||||
|
> - When you are rounding, the "week" unit of time is not supported as a metric, and attempting to round up or down a week will result in an error.
|
||||||
|
> - If you are using the "now" keyword and an input date is also applied as the implicitly or explicitly definted input parameter, the transform prefers using "now" and ignores the data in the `input` attribute.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The date math transform takes the input value and executes addition, subtraction and/or rounding operations to that value based on an `expression` configuration value. As indicated earlier, the input datetime must be in [ISO8601 format](https://en.wikipedia.org/wiki/ISO_8601). The `expression` value leverages the following abbreviations to indicate which date or time component to evaluate:
|
||||||
|
|
||||||
|
> - "y" - year
|
||||||
|
> - "M" - month
|
||||||
|
> - "w" - week
|
||||||
|
> - "d" - day
|
||||||
|
> - "h" - hour
|
||||||
|
> - "m" - minute
|
||||||
|
> - "s" - second
|
||||||
|
> - "now" - the current instant in time
|
||||||
|
|
||||||
|
Also, the operational logic is defined by usage of one of the following symbols:
|
||||||
|
|
||||||
|
> - "+" - add; This must be followed by a valid time unit.
|
||||||
|
> - "-" - subtract; This must be followed by a valid time unit.
|
||||||
|
> - "/" - round; This must be followed by a valid time unit.
|
||||||
|
|
||||||
|
Some examples of expressions are:
|
||||||
|
|
||||||
|
> - `"expression": "now"` returns the current date and time.
|
||||||
|
> - `"expression": "now/h"` returns the current date and time, rounded to the hour.
|
||||||
|
> - `"expression": "now+1w"` returns one week from the current date and time.
|
||||||
|
> - `"expression": "now+1y+1M+2d-4h+1m-3s/s"` returns the current date and time plus one year, one month, two days, minus four hours, plus one minute and minus three seconds, rounded to the second.
|
||||||
|
> - `"expression": "+3M"` returns the date and time that would be three months more than the value provided as an input to the transform.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"expression": "+3M/h",
|
||||||
|
"roundUp": true,
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "startDate"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "dateMath",
|
||||||
|
"name": "Test Date Math Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
|
||||||
|
- **type** - This must always be set to `dateMath.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **expression** - A string value of the date and time components to operate on, along with the math operations to execute. Multiple operations on multiple components are supported.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **roundUp** - This `true` or `false` value indicates whether the transform rounds up or down when the `expression` defines a rounding ("/") operation. If this value is not provided, the transform defaults to `false.`
|
||||||
|
|
||||||
|
- `true` indicates the transform should round up (i.e., truncate the fractional date/time component indicated and then add one unit of that component).
|
||||||
|
- `false` indicates the transform should round down (i.e., truncate the fractional date/time component indicated).
|
||||||
|
- `input` - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform takes the current date, subtracts five days from it, and rounds down to the lowest day.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"expression": "now-5d/d",
|
||||||
|
"roundUp": false
|
||||||
|
},
|
||||||
|
"type": "dateMath",
|
||||||
|
"name": "Date Math Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform takes the `startDate` attribute from a user's record in the "HR Source," converts it from its native format to an [ISO8601-formatted](https://en.wikipedia.org/wiki/ISO_8601) string, and then adds twelve hours to it. The final value is then rounded up to the next second.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"expression": "+12h/s",
|
||||||
|
"roundUp": true,
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "startDate"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
"inputFormat": "MMM dd yyyy, HH:mm:ss.SSS",
|
||||||
|
"outputFormat": "ISO8601"
|
||||||
|
},
|
||||||
|
"type": "dateFormat"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "dateMath",
|
||||||
|
"name": "Date Math Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform take the `HIREDATE` from Workday and converts it to [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) to be used in the Date Math transform. The Date Math transform then creates a new Date of `HIREDATE + 1.` Since that is then outputted in the format "yyyy-MM-dd'T'HH:mm," you can then use it in a [dateFormat](./date_format.md) transform to give a WIN32 formatted date.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "dateFormat",
|
||||||
|
"name": "WD - HireDate",
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"expression": "+1d",
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"attributeName": "HIREDATE",
|
||||||
|
"sourceName": "Workday"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
"inputFormat": "MM/dd/yyyy",
|
||||||
|
"outputFormat": "ISO8601"
|
||||||
|
},
|
||||||
|
"type": "dateFormat"
|
||||||
|
},
|
||||||
|
"roundUp": true
|
||||||
|
},
|
||||||
|
"type": "dateMath"
|
||||||
|
},
|
||||||
|
"inputFormat": "yyyy-MM-dd'T'HH:mm",
|
||||||
|
"outputFormat": "EPOCH_TIME_WIN32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# IdentityNow Transforms - Decompose Diacritial Marks
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the decompose diacritical marks transform to clean or standardize symbols used within language to inform the reader how to say or pronounce a letter. These symbols are often incompatible with downstream applications and must be standardized to another character set such as ASCII.
|
||||||
|
|
||||||
|
The following are examples of diacritical marks:
|
||||||
|
|
||||||
|
> - Ā
|
||||||
|
> - Ĉ
|
||||||
|
> - Ň
|
||||||
|
> - Ŵ
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The transform for decompose diacritical marks requires only the transform's `type` and `name` attributes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "decomposeDiacriticalMarks",
|
||||||
|
"name": "Decompose Diacritical Marks Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `decomposeDiacriticalMarks.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "Āric"
|
||||||
|
Output: "Aric"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "decomposeDiacriticalMarks",
|
||||||
|
"name": "Test Decompose Diacritical Marks Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform takes the user's "LastName" attribute from the "HR Source" and replaces any diacritical marks with ASCII-compatible values.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "Dubçek"
|
||||||
|
Output: "Dubcek"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "LastName"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "decomposeDiacriticalMarks",
|
||||||
|
"name": "Decompose Diacritical Marks Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
# IdentityNow Transforms - E.164 Phone
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the E.164 phone transform to convert an incoming phone number string into an E.164-compatible number.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - If the input string to the transform does not represent a valid phone number, the transform returns null.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The E.164 phone transform only requires the transform's `type` and `name` attributes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "e164phone",
|
||||||
|
"name": "Test E.164Phone Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
|
||||||
|
- **type** - This must always be set to `E.164phone.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
- **defaultRegion** - This is an optional attribute used to define the phone number region to format into. If no defaultRegion is provided, the transform takes US as the default country. The format of the country code must be in [ISO 3166-1 alpha-2 format](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform transforms a phone number seperated by `-` into the E.164 Phone format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "512-777-2222"
|
||||||
|
Output: "+1512459222"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"value": "512-777-2222"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "e164phone",
|
||||||
|
"name": "E.164Phone Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform transforms a phone number seperated by `.` into the E.164 Phone format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "779.284.2727"
|
||||||
|
Output: "+17792842727"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"value": "779.284.2727"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "e164phone",
|
||||||
|
"name": "E.164Phone Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform transforms a phone number and country region code into the E.164 Phone format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "0412345678"
|
||||||
|
defaultRegion: "AU"
|
||||||
|
|
||||||
|
Output: "+61412345678"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"value": "0412345678"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
},
|
||||||
|
"defaultRegion": "AU"
|
||||||
|
},
|
||||||
|
"type": "e164phone",
|
||||||
|
"name": "E.164Phone Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [https://en.wikipedia.org/wiki/E.164](https://en.wikipedia.org/wiki/E.164)
|
||||||
|
- [https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# IdentityNow Transforms - First Valid
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the first valid transform to perform if/then/else operations on multiple different data points to return the first piece of data that is not null. This is often useful for the SailPoint User Name (uid) attribute in which case each identity requires a value, but the desired information is not available yet (e.g., Active Directory username). In these cases, you can use a first valid transform to populate the uid attribute with the user's linked Active Directory account information if the uid attribute is not null. If the attribute is null, use a different attribute from a source that the user does have, like his/her employee number.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The first valid transform requires an array list of `values` that you must consider. These can be static strings or other nested transforms' return values. Remember that the transform returns the first entry in the array that evaluates to a non-null value, so you are recommended to provide the entries in the array in descending order of preference.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "Active Directory",
|
||||||
|
"attributeName": "sAMAccountName"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "Okta",
|
||||||
|
"attributeName": "login"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "employeeID"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "firstValid",
|
||||||
|
"name": "Test First Valid Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `firstValid.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **values** - This is an array of attributes to evaluate for existence.
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **ignoreErrors** - This `true` or `false` value indicates whether to proceed to the next option if an error (like an NPE) occurs.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform first attempts to return the user's sAMAccountName from his/her Active Directory account. In the event that the user does not have an Active Directory account, the transform then attempts to return the user's Okta login. If the Okta login is also blank, the transform returns the user's employee ID from his/her HR record.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "Active Directory",
|
||||||
|
"attributeName": "sAMAccountName"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "Okta",
|
||||||
|
"attributeName": "login"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "employeeID"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "firstValid",
|
||||||
|
"name": "First Valid Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform is often useful for populating the Work Email identity attribute. Since the Work Email attribute is a required field for a valid identity, it cannot be blank. However, often new hires do not have an Active Directory account and/or email provisioned until after the user has been provisioned. A common practice in this situation is to return a static string of "none" to ensure that this required attribute does not remain empty.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "Active Directory",
|
||||||
|
"attributeName": "mail"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"value": "none"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "firstValid",
|
||||||
|
"name": "First Valid Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform is often useful for populating an attribute called Manager DN. It pulls the manager of the identity and then gets the Identity Attribute "Network DN" for the manager. "Network DN" pulls directly from distinguishedName in AD. With this transform, you can set a user's manager's DN as an Identity Attribute to allow for Attribute Sync down to AD. Without ignoreErrors set to true, this transform throws a Null Pointer Exception (NPE) for any user without a manager. With ignoreErrors set to true, the first value in the firstValid throws an error for users without managers, but the error is ignored, and the transform selects the empty string to set the Manager DN Identity Attribute to.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"ignoreErrors": "true",
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"value": "$identity.manager.attributes.networkDn"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
},
|
||||||
|
""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"name": "Example_Transform_ManagerDN",
|
||||||
|
"type": "firstValid"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# IdentityNow Transforms - Generate Random String
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the generate random string transform as an out-of-the-box rule transform provided through SailPoint's Cloud Services Utility rule. The transform allows you to generate a random string of any length, using true/false flags to denote whether the stringe includes numbers and/or special characters.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - The generate random string transform shares some common features with two other transforms: [random numeric](./random_numeric.md) and [random alphanumeric](./random_alphanumeric.md). In most cases, either of these other two out-of-the-box transforms are recommended. However, the one advantage of the generate random string transform is its support for special characters, so a common use for this transform is generating random passwords that meet basic complexity requirements.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The structure of a generate random string transform requires the `name` of the referenced rule to be the "Cloud Services Deployment Utility" rule built by SailPoint. You must also must set `operation` to `generateRandomString,` provide a `length,` and provide the true/false attributes for `includeNumbers` and `includeSpecialChars.` Last, you must include the `type` and `name` attributes required for all transforms:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "Cloud Services Deployment Utility",
|
||||||
|
"operation": "generateRandomString",
|
||||||
|
"includeNumbers": "true",
|
||||||
|
"includeSpecialChars": "true",
|
||||||
|
"length": "16"
|
||||||
|
},
|
||||||
|
"type": "rule",
|
||||||
|
"name": "Generate Random String Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `rule.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **attributes.name** - This must always be set to "Cloud Services Deployment Utility."
|
||||||
|
- **operation** - This must always be set to "generateRandomString."
|
||||||
|
- **includeNumbers** - You must set this value to `true` or `false` to indicate whether the generator logic includes numbers.
|
||||||
|
- **includeSpecialChars** - You must set this value to `true` or `false` to indicate whether the generator logic includes the followin special characters:
|
||||||
|
- !
|
||||||
|
- @
|
||||||
|
- \#
|
||||||
|
- \$
|
||||||
|
- %
|
||||||
|
- &
|
||||||
|
- \*
|
||||||
|
- (
|
||||||
|
- )
|
||||||
|
- \+
|
||||||
|
- <
|
||||||
|
- \>
|
||||||
|
- ?
|
||||||
|
- **length** - This is the required length ofthe randomly generated string.
|
||||||
|
> **Note** Due to identity attribute data constraints, the maximum allowable value is 450 characters.
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform generates a 16-character random string containing letters, numbers and special characters.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "Cloud Services Deployment Utility",
|
||||||
|
"operation": "generateRandomString",
|
||||||
|
"includeNumbers": "true",
|
||||||
|
"includeSpecialChars": "true",
|
||||||
|
"length": "16"
|
||||||
|
},
|
||||||
|
"type": "rule",
|
||||||
|
"name": "Generate Random String Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform generates an 8-character random string containing only letters and numbers.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "Cloud Services Deployment Utility",
|
||||||
|
"operation": "generateRandomString",
|
||||||
|
"includeNumbers": "true",
|
||||||
|
"includeSpecialChars": "false",
|
||||||
|
"length": "8"
|
||||||
|
},
|
||||||
|
"type": "rule",
|
||||||
|
"name": "Generate Random String Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# IdentityNow Transforms - Get End of String
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the get end of string transform as an out-of-the-box rule transform provided through SailPoint's Cloud Services Deployment Utility rule. The transform allows you to get the rightmost N characters of a string.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The structure of a get end of string transform requires the `name` of the referenced rule to be the "Cloud Services Deployment Utility" rule built by SailPoint. You must also set `operation` to `getEndOfString,` and provide a `numChars` value. Last, you must include the `type` and `name` attributes required for all transforms:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "Cloud Services Deployment Utility",
|
||||||
|
"operation": "getEndOfString",
|
||||||
|
"numChars": "4"
|
||||||
|
},
|
||||||
|
"type": "rule",
|
||||||
|
"name": "Get End Of String Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `rule.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **attributes.name** - This must always be set to "Cloud Services Deployment Utility."
|
||||||
|
- **operation** - This must always be set to "getEndOfString."
|
||||||
|
- **numChars** - This specifies how many of the rightmost characters within the incoming string the transform returns. If the value of numChars is greater than the string length, the transform returns null.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform returns the last four characters of the input string "abcd1234."
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "abcd1234"
|
||||||
|
Output: "1234"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "Cloud Services Deployment Utility",
|
||||||
|
"operation": "getEndOfString",
|
||||||
|
"numChars": "4"
|
||||||
|
},
|
||||||
|
"type": "rule",
|
||||||
|
"name": "Get End Of String Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform returns a null value because the incoming string length is only 15 characters long, but the transform requests the rightmost 16 characters.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "Cloud Services Deployment Utility",
|
||||||
|
"operation": "getEndOfString",
|
||||||
|
"numChars": "16",
|
||||||
|
"input": "This is a test."
|
||||||
|
},
|
||||||
|
"type": "rule",
|
||||||
|
"name": "Get End Of String Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# IdentityNow Transforms - Get Reference Identity Attribute
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the get reference identity attribute transform as an out-of-the-box rule provided through SailPoint's Cloud Services Deployment Utility rule. The transform allows you to get the identity attribute of another user from within a given identity's calculation. For your convenience, the transform allows you to use "manager" as a referential lookup to the target identity.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The structure of a get reference identity transform requires the `name` of the referenced rule to be the "Cloud Services Deployment Utility" rule built by SailPoint. Additionally, you must set the `operation` to `getReferenceIdentityAttribute` and specify a `uid` attribute that correlates to the identity whose attribute is desired. Last, you must include the `type` and `name` attributes required for all transforms:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "Cloud Services Deployment Utility",
|
||||||
|
"operation": "getReferenceIdentityAttribute",
|
||||||
|
"uid": "manager",
|
||||||
|
"attributeName": "email"
|
||||||
|
},
|
||||||
|
"type": "rule",
|
||||||
|
"name": "Get Reference Identity Attribute Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `rule.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **attributes.name** - This must always be set to "Cloud Services Deployment Utility."
|
||||||
|
- **operation** - This must always be set to "getReferenceIdentityAttribute."
|
||||||
|
- **uid** - This is the SailPoint User Name (uid) value of the identity whose attribute is desired.
|
||||||
|
- For your convenience, you can use the "manager" keyword to look up the user's manager and then get that manager's identity attribute.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform gets the user's manager's email address.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "Cloud Services Deployment Utility",
|
||||||
|
"operation": "getReferenceIdentityAttribute",
|
||||||
|
"uid": "manager",
|
||||||
|
"attributeName": "email"
|
||||||
|
},
|
||||||
|
"type": "rule",
|
||||||
|
"name": "Get Reference Identity Attribute Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform gets the alternate phone number for the user identified as "corporate.admin."
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "Cloud Services Deployment Utility",
|
||||||
|
"operation": "getReferenceIdentityAttribute",
|
||||||
|
"uid": "corporate.admin",
|
||||||
|
"attributeName": "phone"
|
||||||
|
},
|
||||||
|
"type": "rule",
|
||||||
|
"name": "Get Reference Identity Attribute Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# IdentityNow Transforms - Identity Attribute
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the identity attribute transform to get the value of a user's identity attribute. This transform is often useful within a source's account create or disable profile.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - 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.*
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The transform for identity attributes requires the desired identity attribute's system `name,` along with the `type` and `name` attributes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "email"
|
||||||
|
},
|
||||||
|
"type": "identityAttribute",
|
||||||
|
"name": "Identity Attribute Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `identityAttribute.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **attributes.name** - The system (camel-cased) name of the identity attribute to bring in.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform returns a user's SailPoint User Name attribute.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "uid"
|
||||||
|
},
|
||||||
|
"type": "identityAttribute",
|
||||||
|
"name": "Identity Attribute Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform returns a user's Employee Number attribute.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"name": "identificationNumber"
|
||||||
|
},
|
||||||
|
"type": "identityAttribute",
|
||||||
|
"name": "Identity Attribute Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# IdentityNow Transforms - Index Of
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the index of transform to get the location of a specific substring within an incoming value. This transform is often useful in conjunction with the substring transform for getting parts of strings that can be dynamic in length or composition. If the substring you are searching for does not occur within the data, the transform returns -1.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - If the substring you are searching for occurs multiple times within the incoming data, the transform returns the location of the first occurrence. If you want the last occurrence of a substring, use the [Last Index Of](./last_index_of.md) transform. If you want an occurrence that is neither first nor last, use the [Substring](./substring.md) transform.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The indexOf transform requires only the substring which you want to search for, along with the `type` and `name` attributes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"substring": "admin_"
|
||||||
|
},
|
||||||
|
"type": "indexOf",
|
||||||
|
"name": "Index Of Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `indexOf.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **substring** - This is the string whose beginning location within the incoming data you want to find.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
The "admin_" substring occurs at the very beginning of the input string, so this transform returns 0.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "admin_jsmith"
|
||||||
|
Output: "0"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"substring": "admin_"
|
||||||
|
},
|
||||||
|
"type": "indexOf",
|
||||||
|
"name": "Index Of Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
Though the letter "b" occurs multiple times throughout the input string, the first time it occurs is within the index location 1, so the transform returns that value.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "abcabcabc"
|
||||||
|
Output: "1"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"substring": "b"
|
||||||
|
},
|
||||||
|
"type": "indexOf",
|
||||||
|
"name": "Index Of Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# IdentityNow Transforms - ISO3166
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the ISO3166 transform to convert an incoming string into an [ISO 3166](https://en.wikipedia.org/wiki/ISO_3166) country code value. The incoming data must be either a recognized country name or country code:
|
||||||
|
|
||||||
|
- The alpha2 country code (e.g. "ES")
|
||||||
|
- The alpha3 country code (e.g. "ESP)
|
||||||
|
- The numeric country code (e.g. 724)
|
||||||
|
- The English name for the country (e.g. Spain)
|
||||||
|
- The native name for the country (e.g. España)
|
||||||
|
|
||||||
|
The output value can be any of these three values:
|
||||||
|
|
||||||
|
- Two-character country code (e.g., "US")
|
||||||
|
- Three-character country code (e.g., "USA")
|
||||||
|
- Numeric country code (e.g., "840")
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
If the input string to the transform does not represent a valid country code or country name, the transform returns null.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The transform for iso3166 only requires the transform's `type` and `name` attributes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "iso3166",
|
||||||
|
"name": "ISO3166 Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
|
||||||
|
- **type** - This must always be set to `iso3166.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **format** - Use this optional value to denote which ISO 3166 format to return. The following values are valid:
|
||||||
|
- `alpha2` - Two-character country code (e.g., "US"). This is the default value if you do not provide a format.
|
||||||
|
- `alpha3` - Three-character country code (e.g., "USA")
|
||||||
|
- `numeric` - The numeric country code (e.g., "840")
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Because no specific format is provided, this transform defaults to the alpha2 output and returns "US."
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "United States of America"
|
||||||
|
Output: "US"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "iso3166",
|
||||||
|
"name": "ISO3166 Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
Because the desired format is specified as numeric, the output of this transform returns "724."
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "ES"
|
||||||
|
Output: "724"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"format": "numeric"
|
||||||
|
},
|
||||||
|
"type": "iso3166",
|
||||||
|
"name": "ISO3166 Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [https://en.wikipedia.org/wiki/ISO_3166](https://en.wikipedia.org/wiki/ISO_3166)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# IdentityNow Transforms - Last Index Of
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the last index of transform to get the last location of a specific substring within an incoming value. This transform is often useful in conjunction with the substring transform for getting parts of strings that can be dynamic in length or composition. If the substring you are searching for does not occur within the data, the transform returns -1.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
If the substring you are searching for occurs multiple times within the incoming data, the transform returns the location of the last occurrence. If you want the first occurrence of a substring, use the [Index Of](./index_of.md) transform. If you want an occurrence that is neither first nor last, use the [Substring](./substring.md) transform.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The lastIndexOf transform requires only the substring you want to search for, along with the transform's `type` and `name` attributes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"substring": "admin_"
|
||||||
|
},
|
||||||
|
"type": "lastIndexOf",
|
||||||
|
"name": "Last Index Of Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `lastIndexOf.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **substring** - This is the string whose beginning location within the incoming data you want to find.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
The "admin_" substring only occurs once at the very beginning of the input string, so this transform returns 0.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "admin_jsmith"
|
||||||
|
Output: "0"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"substring": "admin_"
|
||||||
|
},
|
||||||
|
"type": "lastIndexOf",
|
||||||
|
"name": "Last Index Of Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
While the letter "b" occurs multiple times throughout the input string, the last time it occurs is within index location 7, so this transform returns that value.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "abcabcabc"
|
||||||
|
Output: "7"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"substring": "b"
|
||||||
|
},
|
||||||
|
"type": "lastIndexOf",
|
||||||
|
"name": "Last Index Of Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# IdentityNow Transforms - Left Pad
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the left pad transform to pad an incoming string with a user-supplied character out to a specific number of characters. This transform is often useful for data normalization situations in which data such as employee IDs are not uniform in length but need to be for downstream systems.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - If the input to the left pad transform is null, the transform returns a null value.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
In addition to the standard `type` and `name` attributes, the left pad transform requires the `length` attribute, which tells the transform how many characters to pad the incoming string to.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"padding": "0",
|
||||||
|
"length": "5"
|
||||||
|
},
|
||||||
|
"type": "leftPad",
|
||||||
|
"name": "Left Pad Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `leftPad.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **length** - This is an integer value for the final output string's desired length.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **padding** - This string value represents the character the transform will pad the incoming data to to get to the desired length.
|
||||||
|
- If no padding value is provided, the transform defaults to a single space (" ") character for padding.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform takes the incoming attribute configured in the Identity Profile attribute UI and ensures it is padded out to 8 characters in length by adding "0"s to the left.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "1234"
|
||||||
|
Output: "00001234"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"padding": "0",
|
||||||
|
"length": "8"
|
||||||
|
},
|
||||||
|
"type": "leftPad",
|
||||||
|
"name": "Left Pad Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform takes the user's employeeID attribute from the HR source and ensures it is padded out to 7 characters in length by adding "x"s to the left.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "1234"
|
||||||
|
Output: "xxx1234"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"padding": "x",
|
||||||
|
"length": "7",
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "employeeID"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "leftPad",
|
||||||
|
"name": "Left Pad Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# IdentityNow Transforms - Lookup
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the lookup transform to take in an incoming string value and compare it to a list of key-value pairs to determine which output to return. If the incoming data matches a key, the transform returns the corresponding value. If the incoming key does not match a key, the transform returns the table's optional default value.
|
||||||
|
|
||||||
|
### Other Considerations
|
||||||
|
|
||||||
|
> - If the input does not match any key value within the table and no default value is provided, the transform returns null.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
In addition to the `type` and `name` attributes, the structure of a lookup transform involves a `table` entry of key-value pairs:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"table": {
|
||||||
|
"USA": "Americas",
|
||||||
|
"FRA": "EMEA",
|
||||||
|
"AUS": "APAC",
|
||||||
|
"default": "Unknown Region"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "lookup",
|
||||||
|
"name": "Lookup Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `lookup.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **table** - This is a JSON object of key-value pairs. The key is the string the transform tries to match to the input, and the value is the output string the transform returns if it matches the key.
|
||||||
|
> **Note** This is a use for the optional default key value here: if none of the three countries in the earlier example matches the input string, the transform returns "Unknown Region" for the attribute mapped to this transform.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform tries to map a telephone area code to a city in Texas. There is no `default` entry in the table map, so the transform returns null if there is no provided area code that is not one of the provided four values.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"table": {
|
||||||
|
"512": "Austin",
|
||||||
|
"281": "Houston",
|
||||||
|
"214": "Dallas",
|
||||||
|
"210": "San Antonio"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "lookup",
|
||||||
|
"name": "Lookup Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform extends the previous one to show how multiple key values can be mapped to the same output value. However, duplicate key values are not allowed, so this will throw an error.
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"table": {
|
||||||
|
"512": "Austin",
|
||||||
|
"281": "Houston",
|
||||||
|
"713": "Houston",
|
||||||
|
"832": "Houston",
|
||||||
|
"214": "Dallas",
|
||||||
|
"210": "San Antonio"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "lookup",
|
||||||
|
"name": "Test Lookup Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# IdentityNow Transforms - Lower
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the lower transform to convert an input string into all lowercase letters.
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The lower transform only requires the transform's `type` and `name` attributes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "lower",
|
||||||
|
"name": "Lower Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
- **type** - This must always be set to `lower.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform takes in the input "ACTIVE" and produces "active" as the output.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input:"ACTIVE"
|
||||||
|
Output:"active"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"value": "ACTIVE"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "lower",
|
||||||
|
"name": "Lower Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform takes in the input "All-Access" and produces "all-access" as the output.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input:"All-Access"
|
||||||
|
Output:"all-access"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transform Request Body**:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"value": "All-Access"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "lower",
|
||||||
|
"name": "Lower Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# IdentityNow Transforms - Name Normalizer
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use the name normalizer transform to clean or standardize the spelling of strings coming in from source systems. The most common use for this transform is for names and other proper nouns, but the transform is not necessarily limited to those data elements.
|
||||||
|
|
||||||
|
The normalization logic within the transform handles a wide range of use cases:
|
||||||
|
|
||||||
|
- Proper casing/capitalization of names
|
||||||
|
- Any string containing either a space, a hyphen or an apostrophe - the transform splits these by that character and capitalizes the first character of each resulting substring.
|
||||||
|
- Special replacements of patterns that include "MC" and "MAC" (or case-based variations of those two strings)
|
||||||
|
- The transform automatically converts "MC" to "Mc" and "MAC" to "Mac" when they are part of a patronymic last name.
|
||||||
|
- Consistent capitalization of strings that are part of a toponymic surname or a generational suffix:
|
||||||
|
- Convert "VON" to "von"
|
||||||
|
- Convert "DEL" to "del"
|
||||||
|
- Convert "OF" to "of"
|
||||||
|
- Convert "DE" to "de"
|
||||||
|
- Convert "LA" to "la"
|
||||||
|
- Convert "Y" to "y"
|
||||||
|
- Convert Roman numeral suffixes to all capitalized letters (e.g., "iii" becomes "III")
|
||||||
|
|
||||||
|
## Transform Structure
|
||||||
|
|
||||||
|
The name normalizer transform only requires the transform's `type` and `name` attributes:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "normalizeNames",
|
||||||
|
"name": "Name Normalizer Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
- **Required Attributes**
|
||||||
|
|
||||||
|
- **type** - This must always be set to `normalizeNames.`
|
||||||
|
- **name** - This is a required attribute for all transforms. It represents the name of the transform as it will appear in the UI's dropdown menus.
|
||||||
|
|
||||||
|
- **Optional Attributes**
|
||||||
|
- **requiresPeriodicRefresh** - This `true` or `false` value indicates whether the transform logic should be reevaluated every evening as part of the identity refresh process.
|
||||||
|
- **input** - This is an optional attribute that can explicitly define the input data passed into the transform logic. If no input is provided, the transform takes its input from the source and attribute combination configured with the UI.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
This transform takes a static value and normalizes it to a consistent format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "jOHN VON SmITh"
|
||||||
|
Output: "John von Smith"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"value": "jOHN VON SmITh"
|
||||||
|
},
|
||||||
|
"type": "static"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "normalizeNames",
|
||||||
|
"name": "Name Normalizer Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p> </p>
|
||||||
|
|
||||||
|
This transform takes the user's "LastName" attribute from the "HR Source" and normalizes the name to a consistent format.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
Input: "Dr. JOHN D. O'BRIEN"
|
||||||
|
Output: "Dr. John D. O'Brien"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"attributes": {
|
||||||
|
"input": {
|
||||||
|
"attributes": {
|
||||||
|
"sourceName": "HR Source",
|
||||||
|
"attributeName": "LastName"
|
||||||
|
},
|
||||||
|
"type": "accountAttribute"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "normalizeNames",
|
||||||
|
"name": "Name Normalizer Transform"
|
||||||
|
}
|
||||||
|
```
|
||||||