Scripted REST Connector
Important
Connectors continue to be released outside the IDM release. For the latest documentation, refer to the ICF documentation.
The Scripted REST connector is an implementation of the Scripted Groovy Connector Toolkit. It can interact with any REST API, using Groovy scripts for the ICF operations. This connector type lets you develop a fully functional REST-based connector for in-house applications, or for any cloud-based application not yet supported with the standard set of ForgeRock connectors.
To use this connector, you must write a Groovy script for each operation that you want the connector to perform (create, read, update, delete, authenticate, and so on). No sample scripts are bundled with the connector, but IDM customers have access to the Scripted REST connector source code at https://stash.forgerock.org/projects/GA/repos/connectors-customers-ga/browse/scriptedrest-connector?at=refs%2Ftags%2F1.5.20.12. This repository includes sample scripts for all the ICF operations.
You cannot configure the Scripted REST connector through the UI. Configure the connector over REST, as described in "Configure Connectors Over REST".
Alternatively, a sample connector configuration and scripts are provided in the /path/to/openidm/samples/scripted-rest-with-dj/
directory and described in Connect to DS With ScriptedREST. The scripts provided with this sample demonstrate how the connector can be used but most likely cannot be used as is in your deployment. They are a good starting point on which to base your customization. For information about writing your own scripts, see Writing Scripted Connectors With the Groovy Connector Toolkit.
Script Custom Behavior
Important
Connectors continue to be released outside the IDM release. For the latest documentation, refer to the ICF documentation.
The Scripted REST connector uses the Apache HTTP client library. Unlike the Scripted SQL connector, which uses JDBC drivers and a Tomcat JDBC connection pool, the Scripted REST connector includes a special script to customize the Apache HTTP client.
This customizer script lets you customize the Apache HTTP client connection pool, proxy, default headers, timeouts, and so on.
The customizer script is referenced in the connector configuration, in the CustomizerScriptFileName
property:
{ ... "configurationProperties": { ... "customizerScriptFileName": "CustomizerScript.groovy", ... } }
The script can implement two predefined Groovy closures — init {}
and decorate {}
.
init {}
The Apache HTTP client provides an HTTPClientBuilder class, to build an instance of the HTTPClient. The Scripted REST connector injects this builder into the
init
closure when the connector is first instantiated. Theinit
closure is the ideal place to customize the HTTP client with the builder.You can customize the following elements of the client:
Connection pool
Connection timeouts
Proxy
Default HTTP headers
Certificate handling
/** * A customizer script defines the custom closures to interact with the default implementation and customize it. * Here, the {@link HttpClientBuilder} is passed to the customize closure. This is where the pooling, the headers, * the timeouts etc... should be defined. */ customize { init { HttpClientBuilder builder -> //SETUP: org.apache.http def c = delegate as ScriptedRESTConfiguration def httpHost = new HttpHost(c.serviceAddress?.host, c.serviceAddress?.port, c.serviceAddress?.scheme) PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager() // Increase max total connection to 200 cm.setMaxTotal(200) // Increase default max connection per route to 20 cm.setDefaultMaxPerRoute(20) // Increase max connections for httpHost to 50 cm.setMaxPerRoute(new HttpRoute(httpHost), 50) builder.setConnectionManager(cm) // configure timeout on the entire client RequestConfig requestConfig = RequestConfig.custom()/* * . * setConnectionRequestTimeout * ( 50). * setConnectTimeout * (50) * .setSocketTimeout * (50) */.build(); builder.setDefaultRequestConfig(requestConfig) ... } }
Call the builder methods to fit your requirements. The
init{}
closure does not need to return anything.decorate {}
The
init
closure configures a Java instance of the HTTP client, which is injected into every CRUD script. In addition to the libraries provided by the Apache HTTP client, Groovy provides a number of libraries to deal with requests and responses.The
decorate
closure lets you inject a "decorated" instance of the HTTP client into your scripts. For example, the sample scripts use thegroovyx.net.http.RESTClient
library.This excerpt of a sample
delete
script shows the injection of thehttpClient
andconnection
variables into the script. Theconnection
variable is the output of thedecorate
closure.def operation = operation as OperationType def configuration = configuration as ScriptedRESTConfiguration def httpClient = connection as HttpClient def connection = customizedConnection as RESTClient def log = log as Log def objectClass = objectClass as ObjectClass def options = options as OperationOptions def uid = uid as Uid log.info("Entering " + operation + " Script"); switch (objectClass) { case ObjectClass.ACCOUNT: connection.delete(path: '/api/users/' + uid.uidValue); break case ObjectClass.GROUP: connection.delete(path: '/api/groups/' + uid.uidValue); }
Important
When you use the defaultRequestHeaders
configuration property to set HTTP request headers, the syntax requires an =
sign rather than a :
. For example, to generate a request header such as "Authorization: Bearer rg1cwAeQJxEf"
, you must set the following value for defaultRequestHeaders
in the connector configuration:
"defaultRequestHeaders" : [ "Authorization = Bearer rg1cwAeQJxEf" ]
This example shows how to use the customizer script to implement OAuth2 authentication in the Scripted REST connector.
Although grant types are largely standardized across OAuth2 authentication providers, the way in which different providers handle flows, headers, attribute names, and so on, often differs. This makes it difficult to include a single implementation of OAuth2 authentication in the Scripted REST connector. To make sure that OAuth2 authentication works in your specific use case, you use the customizer script, which can be adapted without requiring a new version of the connector itself.
The Scripted REST connector includes a simple implementation of the OAuth2 Client Credentials grant type. The connector needs to get an access token, using the Client ID and the Client Secret, cache it, and renew it when it expires or when the server revokes it. The Apache client provides interceptors for requests and responses. These interceptors can be used in the customizer script to manage the access token:
In the request: If the access token is absent or expired, renew the token and cache it in the Scripted REST connector property bag.
In the response: If the server returns a 401 error, delete the Access Token from the connector property bag. This will ensure that the next connector request gets a new access token. The HTTP POST query to get the access token is also handled by the customizer script.
This example shows a complete customizer script for the OAuth2 implementation:
init { HttpClientBuilder builder -> switch (ScriptedRESTConfiguration.AuthMethod.valueOf(c.defaultAuthMethod)) { // ...... case ScriptedRESTConfiguration.AuthMethod.OAUTH: // define a request interceptor to set the Authorization header if absent or expired HttpRequestInterceptor requestInterceptor = { HttpRequest request, HttpContext context -> if (null == context.getAttribute("oauth-request")) { def exp = c.propertyBag.tokenExpiration as Long if (c.propertyBag.accessToken == null || exp < System.currentTimeMillis() / 1000) { new NewAccessToken(c).clientCredentials() } request.addHeader(new BasicHeader(HttpHeaders.AUTHORIZATION, "Bearer " + c.propertyBag.accessToken)) } } // define a response interceptor to catch a 401 response code and delete access token from cache HttpResponseInterceptor responseInterceptor = { HttpResponse response, HttpContext context -> if (HttpStatus.SC_UNAUTHORIZED == response.statusLine.statusCode) { if (c.propertyBag.accessToken != null) { c.propertyBag.remove("accessToken") Log.getLog(ScriptedRESTConnector.class).info("Code 401 - accessToken removed") } } } builder.addInterceptorLast(requestInterceptor) builder.addInterceptorLast(responseInterceptor) break default: throw new IllegalArgumentException() } } class NewAccessToken { static final String GRANT_TYPE = "grant_type" static final String REFRESH_TOKEN = "refresh_token" static final String CLIENT_CREDENTIALS = "client_credentials" static final String CLIENT_ID = "client_id" static final String CLIENT_SECRET = "client_secret" static final String OAUTH_REQUEST = "oauth-request" Log logger = Log.getLog(NewAccessToken.class) ScriptedRESTConfiguration c = null final CloseableHttpClient client = null final HttpPost post = null NewAccessToken(ScriptedRESTConfiguration conf) { this.c = conf this.client = c.getHttpClient() this.post = new HttpPost(c.getOAuthTokenEndpoint()) post.setHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded") post.setHeader(HttpHeaders.ACCEPT, "application/json") } @Synchronized void clientCredentials() { boolean expired = (c.propertyBag.tokenExpiration as Long) < System.currentTimeMillis() / 1000 if (c.propertyBag.accessToken == null || expired ) { if (c.propertyBag.tokenExpiration != null && expired) { logger.info("Token expired!") } logger.info("Getting new access token...") final List<NameValuePair> pairs = new ArrayList<>() pairs.add(new BasicNameValuePair(GRANT_TYPE, CLIENT_CREDENTIALS)) pairs.add(new BasicNameValuePair(CLIENT_ID, c.getOAuthClientId())) pairs.add(new BasicNameValuePair(CLIENT_SECRET, SecurityUtil.decrypt(c.getOAuthClientSecret()))) post.setEntity(new UrlEncodedFormEntity(pairs)) CloseableHttpResponse response = null try { HttpClientContext ctx = HttpClientContext.create() ctx.setAttribute(OAUTH_REQUEST, true) response = client.execute(post, ctx) int statusCode = response.getStatusLine().getStatusCode() if (HttpStatus.SC_OK == statusCode) { def jsonSlurper = new JsonSlurper() def oauthResponse = jsonSlurper.parseText(EntityUtils.toString(response.getEntity())) c.propertyBag.accessToken = oauthResponse.access_token c.propertyBag.tokenExpiration = System.currentTimeMillis() / 1000 + oauthResponse.expires_in as Long } else { throw new InvalidCredentialException("Retrieve Access Token failed with code: " + statusCode) } } catch (ClientProtocolException ex) { logger.info("Trace: {0}", ex.getMessage()) throw new ConnectorException(ex) } catch (IOException ex) { logger.info("Trace: {0}", ex.getMessage()) throw new ConnectionFailedException(ex) } finally { try { if (response != null) { response.close() } } catch (IOException e) { logger.info("Can't close HttpResponse") } } } } }
Using the Scripted REST Connector With a Proxy Server
Important
Connectors continue to be released outside the IDM release. For the latest documentation, refer to the ICF documentation.
If the IDM server is hosted behind a firewall and requests to the resource are routed through a proxy, you must specify the proxy host and port in the connector configuration.
To specify the proxy server details, set the proxyAddress
property in the connector configuration. For example:
"configurationProperties": { ... "proxyAddress": "http://myproxy:8080", ... }
Implemented Interfaces
Important
Connectors continue to be released outside the IDM release. For the latest documentation, refer to the ICF documentation.
This table lists the ICF interfaces that are implemented for the scripted REST connector:
OpenICF Interfaces Implemented by the Scripted REST Connector
The Scripted REST Connector implements the following OpenICF interfaces.
- Authenticate
Provides simple authentication with two parameters, presumed to be a user name and password.
- Create
Creates an object and its
uid
.- Delete
Deletes an object, referenced by its
uid
.- Resolve Username
Resolves an object by its username and returns the
uid
of the object.- Schema
Describes the object types, operations, and options that the connector supports.
- Script on Connector
Enables an application to run a script in the context of the connector. Any script that runs on the connector has the following characteristics:
The script runs in the same execution environment as the connector and has access to all the classes to which the connector has access.
The script has access to a
connector
variable that is equivalent to an initialized instance of the connector. At a minimum, the script can access the connector configuration.The script has access to any script-arguments passed in by the application.
- Script on Resource
Runs a script on the target resource that is managed by this connector.
- Search
Searches the target resource for all objects that match the specified object class and filter.
- Sync
Polls the target resource for synchronization events, that is, native changes to objects on the target resource.
- Test
Tests the connector configuration. Testing a configuration checks all elements of the environment that are referred to by the configuration are available. For example, the connector might make a physical connection to a host that is specified in the configuration to verify that it exists and that the credentials that are specified in the configuration are valid.
This operation might need to connect to a resource, and, as such, might take some time. Do not invoke this operation too often, such as before every provisioning operation. The test operation is not intended to check that the connector is alive (that is, that its physical connection to the resource has not timed out).
You can invoke the test operation before a connector configuration has been validated.
- Update
Updates (modifies or replaces) objects on a target resource.
Configuration Properties
Important
Connectors continue to be released outside the IDM release. For the latest documentation, refer to the ICF documentation.
This table lists the configuration properties for the scripted REST connector:
Scripted REST Connector Configuration
The Scripted REST Connector has the following configurable properties.
Configuration properties
Property | Type | Default | Encrypted [a] | Required [b] |
---|---|---|---|---|
customSensitiveConfiguration | GuardedString | null | ||
Custom Sensitive Configuration script for Groovy ConfigSlurper | ||||
| ||||
customConfiguration | String | null | ||
Custom Configuration script for Groovy ConfigSlurper | ||||
| ||||
[a] Indicates whether the property value is considered confidential, and therefore encrypted in OpenIDM. [b] A list of operations in this column indicates that the property is required for those operations. |
Operation Script Files
Property | Type | Default | Encrypted [a] | Required [b] | ||
---|---|---|---|---|---|---|
createScriptFileName | String | null |
| |||
The name of the file used to perform the CREATE operation. | ||||||
| ||||||
customizerScriptFileName | String | null | ||||
The script used to customize some function of the connector. Read the documentation for more details. | ||||||
| ||||||
authenticateScriptFileName | String | null |
| |||
The name of the file used to perform the AUTHENTICATE operation. | ||||||
| ||||||
scriptOnResourceScriptFileName | String | null |
| |||
The name of the file used to perform the RUNSCRIPTONRESOURCE operation. | ||||||
| ||||||
deleteScriptFileName | String | null |
| |||
The name of the file used to perform the DELETE operation. | ||||||
| ||||||
resolveUsernameScriptFileName | String | null |
| |||
The name of the file used to perform the RESOLVE_USERNAME operation. | ||||||
| ||||||
searchScriptFileName | String | null |
| |||
The name of the file used to perform the SEARCH operation. | ||||||
| ||||||
updateScriptFileName | String | null |
| |||
The name of the file used to perform the UPDATE operation. | ||||||
| ||||||
schemaScriptFileName | String | null |
| |||
The name of the file used to perform the SCHEMA operation. | ||||||
| ||||||
testScriptFileName | String | null |
| |||
The name of the file used to perform the TEST operation. | ||||||
| ||||||
syncScriptFileName | String | null |
| |||
The name of the file used to perform the SYNC operation. | ||||||
| ||||||
[a] Indicates whether the property value is considered confidential, and therefore encrypted in OpenIDM. [b] A list of operations in this column indicates that the property is required for those operations. |
Groovy Engine configuration
Property | Type | Default | Encrypted [a] | Required [b] |
---|---|---|---|---|
targetDirectory | File | null | ||
Directory into which to write classes. | ||||
| ||||
warningLevel | int | 1 | ||
Warning Level of the compiler | ||||
| ||||
scriptExtensions | String[] | ['groovy'] | ||
Gets the extensions used to find groovy files | ||||
| ||||
minimumRecompilationInterval | int | 100 | ||
Sets the minimum of time after a script can be recompiled. | ||||
| ||||
scriptBaseClass | String | null | ||
Base class name for scripts (must derive from Script) | ||||
| ||||
scriptRoots | String[] | null | ||
The root folder to load the scripts from. If the value is null or empty the classpath value is used. | ||||
| ||||
tolerance | int | 10 | ||
The error tolerance, which is the number of non-fatal errors (per unit) that should be tolerated before compilation is aborted. | ||||
| ||||
debug | boolean | false | ||
If true, debugging code should be activated | ||||
| ||||
classpath | String[] | [] | ||
Classpath for use during compilation. | ||||
| ||||
disabledGlobalASTTransformations | String[] | null | ||
Sets a list of global AST transformations which should not be loaded even if they are defined in META-INF/org.codehaus.groovy.transform.ASTTransformation files. By default, none is disabled. | ||||
| ||||
verbose | boolean | false | ||
If true, the compiler should produce action information | ||||
| ||||
sourceEncoding | String | UTF-8 | ||
Encoding for source files | ||||
| ||||
recompileGroovySource | boolean | false | ||
If set to true recompilation is enabled | ||||
| ||||
[a] Indicates whether the property value is considered confidential, and therefore encrypted in OpenIDM. [b] A list of operations in this column indicates that the property is required for those operations. |
Basic Configuration Properties
Property | Type | Default | Encrypted [a] | Required [b] |
---|---|---|---|---|
username | String | null | ||
The Remote user to authenticate with | ||||
| ||||
password | GuardedString | null | ||
The Password to authenticate with | ||||
| ||||
serviceAddress | URI | null | ||
The service URI (example: http://myservice.com/api) | ||||
| ||||
proxyAddress | URI | null | ||
The optional Proxy server URI (example: http://myproxy:8080) | ||||
| ||||
proxyUsername | String | null | ||
The username to authenticate with the proxy server | ||||
| ||||
proxyPassword | GuardedString | null | ||
The password to authenticate with the proxy server | ||||
| ||||
defaultAuthMethod | String | BASIC | ||
Authentication method used. Defaults to BASIC. | ||||
| ||||
defaultContentType | String | application/json | ||
Default HTTP request content type. Defaults to JSON. Can be: TEXT, XML, HTML, URLENC, BINARY | ||||
| ||||
defaultRequestHeaders | String[] | null | ||
Placeholder for default HTTP request headers. | ||||
| ||||
OAuthTokenEndpoint | URI | null | ||
When using OAUTH, this property defines the endpoint where a new access token should be queried for (https://myserver.com/oauth2/token) | ||||
| ||||
OAuthClientId | String | null | ||
The client identifier | ||||
| ||||
OAuthClientSecret | GuardedString | null | ||
Secure client secret for OAUTH | ||||
| ||||
OAuthRefreshToken | GuardedString | null | ||
The refresh token used to renew the access token for the refresh_token grant type | ||||
| ||||
OAuthScope | String | null | ||
The optional scope | ||||
| ||||
OAuthGrantType | String | CLIENT_CREDENTIALS | ||
The grant type to use. Can be CLIENT_CREDENTIALS (default) | REFRESH_TOKEN | AUTHORIZATION_CODE | ||||
| ||||
[a] Indicates whether the property value is considered confidential, and therefore encrypted in OpenIDM. [b] A list of operations in this column indicates that the property is required for those operations. |