In the ForgeRock Identity Cloud (Identity Cloud) managed environment, syncing identities via a remote server provides necessary flexibility in integrating the ForgeRock Identity Platform (Platform) with external systems.
Scripted implementations present a relatively easy way to extend this flexibility further and almost indefinitely, including the option to develop a new connector when the available solutions do not meet one’s requirements. Thus, a scripted connector can address edge cases, aid with proofing a concept, serve as a demo, and potentially outline future development plans for a standard feature.
The following content overlays the existing ever-evolving official docs with additional details on developing connectors based on the Groovy Connector Toolkit for the Java Remote Connector Server (RCS).
Additional information could also be found on the ForgeRock Backstage site.
Use the links under the Contents section to quickly navigate to an area of interest. If you feel lost in a long chapter, navigate to the closest Back to contents link and try again.
The quoted paragraphs, such as this one, indicate that the content they provide is supplementary and optional.
Contents
- Choosing IDE
- Interacting with RCS via IDM’s REST
- Debugging Scripts
- Scripting Context
- Scripted Groovy Connector Bindings
-
Scripted Groovy Connection Configuration
- “configurationProperties”
-
“systemActions”
- Defining System Action
-
Invoking via IDM’s REST
- Parts of the Request
- Invoking from an IDM Script
- “run on resource” vs “run on connector”
- Support in Connectors
- Commonly Used References
-
Part 2
- Scripted Groovy Connector (Toolkit)
- Registering Connection in IDM
- Deployment Requirements
- Platform UI
- IDM’s REST
- Create Configuration
- Use a Provisioner File
- Example
- Schema Script
- Object Classes
- objectClass(Closure closure)
- type(String type)
- attribute(String name[, Class type[, Set flags]])
- attribute(AttributeInfo attributeInfo)
- attributes(Closure closure)
- defineObjectClass(ObjectClassInfo objectClassInfo[, . . . ])
- objectClass(Closure closure)
- Example Data
- Users
- Groups
- Example Schema Script
- Original Data Structure
- Flat Representation of Data
- Object Classes
- Search Script
- Requesting Search Operation
- IDM’s REST
- IDM Script
- Responding with Data
- Filtering Results
- Read by Resource ID
- Query Definition
- Paging and Sorting
- Page Size
- Sorting
- Tracking Position in Paged Results
- Attributes to Get
- Example Search Script
- Flat Representation of Data
- Requesting Search Operation
- Test Script
- Registering Connection in IDM
- Conclusion
- Commonly Used References
- Scripted Groovy Connector (Toolkit)
Choosing IDE
For a Java RCS, you will write scripts in the Apache Groovy programming language (Groovy). Consult the IDE integration support for Groovy document when you choose an IDE for your scripted RCS development.
In general, you can get a better support for Groovy in a Java-specialized IDE, like IntelliJ IDEA (IntelliJ).
In a non-Java or a polyglottal IDE, you might be able to effectively maintain your RCS scripts, but Groovy-related features may not be readily available or have limited functionality and support available.
For example, as of this writing, no Groovy debugger extension is available for Visual Code Studio—a very popular code editor. This means that if you want to do remote debugging and attach a debugger to your RCS process, you will have to use something like IntelliJ.
Interacting with RCS via IDM’s REST
A remote connector is a system object, and as such, you can interact with it via IDM’s REST—which is a convenient option to validate your work during development.
You will need to authorize your requests to IDM’s REST as an IDM administrator. In Identity Cloud, this means including an OAuth 2.0 bearer token in the Authorization
header of your request. The token needs to be obtained with a client mapped to an IDM subject associated with the admin role.
The easiest way of accomplishing this type of authorization is signing in to the IDM admin UI, and using the browser console for making HTTP request with jQuery
. Internally, jQuery
uses XMLHttpRequest
(XHR), and such requests are automatically authorized by the IDM admin UI. In addition, browser console can serve well as a JavaScript text editor. It will provide an interactive playground with code highlighting, autocompletion, and error checking. It will add necessary headers to your requests.
For example (where [ . . . ]
denotes omission from the original content), you could check what scripted connectors are bundled with your connector server:
IDM admin UI browser console
(async function () {
var connectorServerName = 'rcs';
/**
* Get an array of available connector references.
*/
var { connectorRef } = await $.ajax({
method: 'POST',
url: '/openidm/system?_action=availableConnectors'
});
/**
* Get a list of scripted connectors for an RCS.
*/
var scriptedConnectorRef = connectorRef.filter((connectorRef) => {
return connectorRef.connectorHostRef === connectorServerName;
}).filter((connectorRef) => {
return connectorRef.connectorName.toLowerCase().includes('scripted');
});
console.log('scriptedConnectorRef', JSON.stringify(scriptedConnectorRef, null, 4));
}());
[
{
"connectorHostRef": "rcs",
"displayName": "Scripted SQL Connector",
"bundleVersion": "1.5.20.15",
"systemType": "provisioner.openicf",
"bundleName": "org.forgerock.openicf.connectors.scriptedsql-connector",
"connectorName": "org.forgerock.openicf.connectors.scriptedsql.ScriptedSQLConnector"
},
{
"connectorHostRef": "rcs",
"displayName": "Scripted REST Connector",
"bundleVersion": "1.5.20.15",
"systemType": "provisioner.openicf",
"bundleName": "org.forgerock.openicf.connectors.scriptedrest-connector",
"connectorName": "org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConnector"
},
{
"connectorHostRef": "rcs",
"displayName": "Scripted Poolable Groovy Connector",
"bundleVersion": "1.5.20.15",
"systemType": "provisioner.openicf",
"bundleName": "org.forgerock.openicf.connectors.groovy-connector",
"connectorName": "org.forgerock.openicf.connectors.groovy.ScriptedPoolableConnector"
},
{
"connectorHostRef": "rcs",
"displayName": "Scripted Groovy Connector",
"bundleVersion": "1.5.20.15",
"systemType": "provisioner.openicf",
"bundleName": "org.forgerock.openicf.connectors.groovy-connector",
"connectorName": "org.forgerock.openicf.connectors.groovy.ScriptedConnector"
}
]
In this example, the
connectorServerName
variable corresponds to the namesake setting in theConnectorServer.properties
file described in the Configure a remote server doc. In Identity Cloud, you can see individual Connector Servers and Server Clusters in the Platform admin UI under Identities > Connect; you can use either a server or a cluster name to identify your RCS.
This technique will be used in the examples included in this writing.
Alternatively, you could use an actual IDE like Postman for crafting your requests to IDM’s REST.
You can also obtain your access token separately and use it with cURL.
For example:
curl 'https://openam-dx-kl03.forgeblocks.com/openidm/system?_action=availableConnectors' \
-X POST \
-H 'Authorization: Bearer $ACCESS_TOKEN' \
-H 'Content-Length: 0'
Debugging Scripts
Debugging Scripts > Try and Catch
If an unhandled exception occurs in your RCS scripts, depending on the script, it may result in a malformed or blank screen in the UI, an unnecessarily detailed error message sent to the client side, and/or lack of debug information in the RCS logs.
Therefore, you should wrap your code with a try/catch
block, send custom error messages to the logs output, and potentially throw a custom exception.
For example:
SearchScript.groovy
try {
// code
} catch (e) {
throw new UnsupportedOperationException('Error occurred during ' + operation + ' operation')
}
UnsupportedOperationException is a Java exception, which, among some other most commonly used Java classes, is automatically provided in Groovy scripts.
Should an exception occur, a request for search operation would return the specified error content:
{"code":404,"reason":"Not Found","message":"Error occurred during SEARCH operation"}
If it is supported, the browser response will be reflected in the UI, for example:
If you throw custom exceptions in your code, you can preserve those custom messages by catching a specific exception type.
For example:
SearchScript.groovy
[ . . . ]
try {
switch (objectClass.objectClassValue) {
case 'users':
[ . . . ]
default:
throw new UnsupportedOperationException(operation.name() + ' operation of type: ' + objectClass.getObjectClassValue() + ' is not supported.')
}
} catch (UnsupportedOperationException e) {
/**
* Preserve and re-throw the custom exception on unrecognized object class.
*/
throw e
} catch (e) {
throw new UnsupportedOperationException('Error occurred during ' + operation + ' operation')
}
For debugging purposes, you can output more detailed information about an exception in the RCS logs.
Debugging Scripts > Custom Logs
You can use methods of the Log class to output custom logs from your connector scripts by passing in a String with the debugging content.
For example:
SearchScript.groovy
try {
[ . . . ]
} catch (e) {
log.error e.message
throw new UnsupportedOperationException('Error occurred during ' + operation + ' operation')
}
RCS logs
[rcs] Oct 20, 2022 12:41:12 AM INFO ERROR SearchScript: No such property: getResourceData for class: SearchScript
Using methods of the Log
class might require some extra processing applied to the content you are trying to output:
-
To output an object information without referencing its individual properties you may need to convert it to a String first. Otherwise, you could get a wordy error.
For example:
TestScript.groovy
try { log.info operation } catch (e) { log.error e.message }
RCS logs
[rcs] Oct 20, 2022 11:43:45 PM ERROR TestScript: No signature of method: org.identityconnectors.common.logging.Log.info() is applicable for argument types: (org.forgerock.openicf.connectors.groovy.OperationType) values: [TEST]%0APossible solutions: info(java.lang.String, [Ljava.lang.Object;), isInfo(), info(java.lang.Throwable, java.lang.String, [Ljava.lang.Object;), isOk(), find(), any()
You can convert an object to string by using its
.toString()
method or by prepending your log with a String:For example:
TestScript.groovy
try { log.info 'Operation: ' + operation } catch (e) { log.error e.message }
RCS logs
[rcs] Oct 20, 2022 11:47:24 PM INFO TestScript: Operation: TEST
-
If you try to output raw JSON describing an object, its curly braces will be interpreted as formatting syntax (by internally used MessageFormat). This might produce an error if the content of the curly braces is not a number.
For (an error) example:
TestScript.groovy
try { log.info '{"key": "value"}' } catch (e) { log.error e.message }
RCS logs
[rcs] Oct 19, 2022 06:55:08 PM ERROR TestScript: cant parse argument number: "key": "value"
To mitigate this issue, you should parse the JSON first and convert the resulting object to a String.
For example:
TestScript.groovy
import groovy.json.JsonSlurper try { log.info 'JSON object: ' + (new JsonSlurper().parseText('{"key": "value"}')) } catch (e) { log.error e.message }
RCS logs
[rcs] Oct 19, 2022 06:56:41 PM INFO TestScript: JSON object: [key:value]
Methods of the Log
class add additional information to the output: a timestamp, the log level, and the source reference. You should use Log
for debugging output that is to stay in the code and be used in the final application.
During the development phase, however, for a quick temporary output, you could use the println method. System.out.println
will automatically apply .toString()
method available in all Java objects to the content it outputs. This will allow to print out content of different types of variables without additional processing. It will be up to you to provide any extra info in the output.
Much of the commonly used Java functionality is imported in Groovy by default, including the
java.lang.*
package whereprintln
comes from. Hence, you don’t need to use the fullSystem.out.println
statement.
For example:
TestScript.groovy
println operation
RCS logs
[rcs] TEST
To outline string values in the printed out content of an object, you can use inspect() method in Groovy.
For example:
SearchScript.groovy
println binding.variables.query()
println binding.variables.query().inspect()
RCS Logs
[rcs] [not:false, operation:GREATERTHAN, left:__NAME__, right:m]
[rcs] ['not':false, 'operation':'GREATERTHAN', 'left':'__NAME__', 'right':'m']
Debugging Scripts > Attaching Debugger to Kubernetes Deployment
Attaching a debugger to your RCS process will allow to pause a connector execution at select break points in your code and inspect the current state of your connector scripting context. Doing so can help to locate and eliminate programming errors.
An RCS can be deployed within a Docker container; in this case, it will run in a remote Java Virtual Machine (JVM). In order to attach a debugger to this process from your local development setup, you will need to perform the following steps:
-
You can enable debugging by invoking your RCS JVM with the Java Debug Wire Protocol (JDWP) options.
For a Kubernetes deployment, you can specify the JDWP options in a few alternative ways:
-
Engage the ForgeRock Open Identity Connector Framework (ICF) defaults.
You could rely on the default JDWP options defined for the RCS Docker container. You can do it by supplying the expected
jpda
argument to the ICF’s Docker ENTRYPOINT script:/opt/openicf/bin/docker-entrypoint.sh
:[ . . . ] if [ "$1" = "jpda" ] ; then if [ -z "$JPDA_TRANSPORT" ]; then JPDA_TRANSPORT="dt_socket" fi if [ -z "$JPDA_ADDRESS" ]; then JPDA_ADDRESS="5005" fi if [ -z "$JPDA_SUSPEND" ]; then JPDA_SUSPEND="n" fi if [ -z "$JPDA_OPTS" ]; then JPDA_OPTS="-agentlib:jdwp=transport=$JPDA_TRANSPORT,address=$JPDA_ADDRESS,server=y,suspend=$JPDA_SUSPEND" fi OPENICF_OPTS="$OPENICF_OPTS $JPDA_OPTS" shift fi [ . . . ]
JDWP is a part of Java Platform Debugger Architecture; hence, the JPDA abbreviation is used in the ICF code.
In a Kubernetes manifest for your RCS, the
jpda
argument can be added to the command that calls the/opt/openicf/bin/docker-entrypoint.sh
script.For example:
rcs.yaml
[ . . . ] command: ['bash', '-c'] args: - export OPENICF_OPTS="-Dconnectorserver.connectorServerName=$HOSTNAME [ . . . ]" && /opt/openicf/bin/docker-entrypoint.sh jpda; [ . . . ]
-
Provide custom JDWP options at RCS launch.
Alternatively, you can include your (custom) JDWP options in the
OPENICF_OPTS
environment variable defined in your Kubernetes manifest.For example:
rcs.yaml
[ . . . ] command: ['bash', '-c'] args: - export OPENICF_OPTS="-Dconnectorserver.connectorServerName=$HOSTNAME [ . . . ] -agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n" && /opt/openicf/bin/docker-entrypoint.sh; [ . . . ]
-
Provide (custom) JDWP options at runtime.
You can dynamically apply an environment variable to your RCS containers by using kubectl set env command. With this command, you can update
JAVA_OPTS
for the JVM running in the containers.For example:
Terminal
$ kubectl set env statefulsets/rcs -c rcs JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n"
If you use Skaffold, updating
JAVA_OPTS
will restart your StatefulSet/Deployment.You can check the updated environment with the
--list
option.For example:
Terminal
$ kubectl set env statefulsets/rcs -c rcs --list=true
# StatefulSet rcs, container rcs JAVA_OPTS= -agentlib:jdwp=transport=dt_socket,address=5005,server=y,suspend=n
You can remove the variable and its effects with the (negative)
JAVA_OPTS-
option.For example:
Terminal
$ kubectl set env statefulsets/rcs -c rcs JAVA_OPTS-
In all cases, the JDWP address option is the remote JVM’s TCP/IP port, to which your local debugger will eventually connect. It will be a local to the RCS instance port, but to distinguish it from the port on the debugger machine, we will call it “remote”.
Optionally, you can include the host information in the address option, an IP or the
localhost
designation, to limit where the debugger connection could be made from; for example:address=127.0.0.1:5005
.If you omit the host identifier in the Java Development Kit (JDK) 9 and above, the connection will be limited to
localhost
. In the older versions of JDK, if no host is specified, a connection would be allowed from any IP. To achieve the same behavior in JDK 9+, you can use a wildcard as the host value; for example,address=*:5005
. It is considered the best practice, however, to limit connections to a specific IP.In the case of attaching a debugger to RCS deployed within a Kubernetes cluster, leaving the host information out, and thus limiting the debugger connection to localhost, is the easiest option and the one taken in the default JDWP options defined in the ICF.
-
-
You will need to allow your local debugger to communicate with the RCS process via the remote debugging port specified in the JDWP options.
Your RCS deployment and its debugging port are unlikely to be exposed externally. This means, you will need to let your debugger access the remote process by forwarding connections made to a local port on your machine to a remote port on the RCS pods in your Kubernetes cluster.
Here, the local port is the one you will use in your debugger configuration; the remote port is the one that you specified in the JDWP
address
option. In the following example, the “local” port is on the left and the “remote” one is on the right:Terminal
$ kubectl port-forward statefulsets/rcs 5005:5005 Forwarding from 127.0.0.1:5005 -> 5005 Forwarding from [::1]:5005 -> 5005
-
Configure Debugger and Start Debugging
IntelliJ is a popular IDE that has rich and refined support for Java and Groovy; and thus, it is probably going to be your best option for developing Groovy scripts for RCS. Below, find an example of how you can configure IntelliJ for remote debugging and attach its debugger to your RCS process:
-
Create a new IntelliJ project.
For example, you can use
File > New > Project from Existing Sources...
and point it to the folder that contains your project files—such as README, configuration, etc.—and the actual scripts; then, the folder content could be accessed and maintained underProject > Project Files
. Do not import any sources at this point; you will add the scripts you need to debug as a module in the next step.Open the project.
-
Add a new module with
File > New Module from Existing Sources...
and point it to your connector’s scripts location.If/when you have more than one connector in your RCS, mark only the connector-specific scripts as the source files in the Import Module dialog. Creating a separate module from the existing scripts for each connector will let you reference the module in a debugging configuration and thus limit its scope to the scripts for a particular connector.
Otherwise, if you included files with the same name for more than one connector in a module, and set a breakpoint in one of the namesake scripts, the debugger could open a file with the same name for a different connector—the first script file with this name that was found in the module sources.
The module files will serve as the sources of your (RCS) application, which is one of the prerequisites for attaching a debugger in IntelliJ.
For example, two modules registered for a project might appear under the Project Files in the following manner:
-
Select
Run
>Edit Configurations...
-
Select
Add New Configuration
(+
), then selectRemote JVM Debug
from the list of predefined configuration templates. -
In the
Configuration
tab, provide values (or verify the defaults) for the following settings:-
Name
: your-rcs-connector-debugging-configuration-name -
Debugger mode
: Attach to remote JVM -
Host
: localhostThe host to which the debugger will connect. Choose localhost because we, actually, attempt to debug locally (that is, the debugger runs locally and connects to a local port, and then it is forwarded to a remote port in the Kubernetes cluster); you could also use
127.0.0.1
or::1
as the Host value. -
Port
: 5005The local port the debugger will connect to, from which connections will be forwarded (with the
kubectl port-forward [ . . . ]
command) to the remote port. -
Command line arguments for remote JVM (for JDK 9 or later)
: JDK 9 or laterThis input is to provide a template for your JDWP options according to your previous choices, and you will see the following:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
Note, however, that:
-
The
*
prefix inJDK 9+
means that the connection will be allowed on the remote host from any IP.Leaving the host information out and having just the port specified will limit connections to the localhost only, which is the safest option that will work in this case.
Removing the wildcard or replacing it with a specific identifier (for example, an IP or
localhost
) is considered the best practice. In reality, however, it is unlikely that any IP/port will be made public on your RCS; hence, limiting debugging connections to your JVM might be a minor consideration in this case. -
The remote JVM port is populated with the same number as your local debugger port, for it assumes that the two ports, local and remote, are the same.
If the remote debugging port in your RCS were different from the local one, you could still use this input for getting your JDWP options template, and simply update the port with the actual remote port that you will use for debugging.
Naturally, in such case, you would also need to port-forward your local connections to that port with the
kubectl port-forward [ . . . ]
command.For example, if your remote JVM port is
5006
, your actual JDWP options could look like the following:Terminal
$ kubectl set env statefulsets/rcs -c rcs JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006"
and your port-forwarding command would be:
Terminal
kubectl port-forward statefulsets/rcs 5005:5006
-
-
Use module classpath
: your-rcs-connector-module-nameHere, you reference the module created from your existing connector’s scripts—so that the debugger looks for the breakpoints positions only in those files.
-
The end result may look similar to the following:
Select
Apply
orOK
.
-
-
Start debugging.
For example, you can select the bug button in the upper right of your IDE UI:
-
Add breakpoints.
If everything is set up correctly, you should be able to see the breakpoints verified and employed when you are using your connector.
For example:
-
If you want to use debugger with another scripted connector in the same IntelliJ project:
-
Add a new module via
File > New Module from Existing Sources...
, and point it to the other connector’s scripts. -
Under
Run > Edit Configurations...
, add a new remote JVM debugging configuration, and select the new module in theUse module classpath:
input. -
Select the new debug configuration before you start your debug session.
For example:
-
For additional details, consult the IntelliJ docs on setting debugging environment and debugging.
-
This should help understand the process of attaching a debugger to your RCS instance running in a Kubernetes cluster. Change it according to your specific requirements.
Debugging Scripts > Attaching Debugger to RCS within Docker Container
If you run RCS in a stand-alone Docker container, as described in Deploying Java Remote Connector Server in a Docker Container, you can publish the debugger port in the docker run command using the –publish, -p flag.
Because localhost reference in a stand-alone container will depend on the host platform, you might not be able to use the default JDWP options defined by the ICF. Also, there is no standard way to update the environment variables in a running Docker container. Thus, including the JDWP options in the OPENICF_OPTS
environment variable at the time an RCS container is created (with the docker run command) is probably the most practical way of enabling debugging, because it will allow to include the host information in the address option. For example, on a macOS, you can reference the host machine address with address=0.0.0.0:5005
inside a Docker container. Alternatively, you can allow a debugger connection from any host with address=*:5005
in the JDWP options. To prevent external access, you can bind the debugging port on the host machine to the localhost IP in the --publish, -p
flag.
For example:
.env file
OPENICF_OPTS=-Dconnectorserver.url=wss://openam-dx-kl04.forgeblocks.com/openicf/0 -Dconnectorserver.tokenEndpoint=https://openam-dx-kl04.forgeblocks.com/am/oauth2/realms/root/realms/alpha/access_token -Dconnectorserver.connectorServerName=rcs-docker-1 -Dconnectorserver.clientId=RCSClient -Dconnectorserver.clientSecret=YA...H? -agentlib:jdwp=transport=dt_socket,address=*:5005,server=y,suspend=n
$ docker run --rm --env-file .env -p 127.0.0.1:5005:5005 rcs
From this point, you can proceed to the step 3. Configure Debugger and Start Debugging described in the Kubernetes Deployment chapter.
Scripting Context
Scripting Context > Bindings
A Groovy script can receive externally defined content via the binding object defined in the script’s top-level scope. In connector scripts, the variable bindings are defined according to the connector type (Groovy, Scripted REST, or Scripted SQL for a scripted connector) and the script operation type, which are derived from the respective connection configuration registered in IDM.
For example:
provisioner.openicf-<connector-name>.json
{
"connectorRef": {
"bundleName": "org.forgerock.openicf.connectors.groovy-connector",
"connectorName": "org.forgerock.openicf.connectors.groovy.ScriptedConnector",
[ . . . ]
},
[ . . . ]
"configurationProperties": {
"scriptExtensions": [
"groovy"
],
"scriptRoots": [
"/opt/openicf/scripts/groovy"
],
"scriptOnResourceScriptFileName": null,
"authenticateScriptFileName": null,
"createScriptFileName": null,
"customizerScriptFileName": null,
"deleteScriptFileName": null,
"resolveUsernameScriptFileName": null,
"schemaScriptFileName": "SchemaScript.groovy",
"searchScriptFileName": "SearchScript.groovy",
"syncScriptFileName": null,
"testScriptFileName": "TestScript.groovy",
"updateScriptFileName": null,
"scriptBaseClass": null,
[ . . . ]
},
"systemActions" : [
{
"scriptId" : "script-1",
"actions" : [
{
"systemType" : ".*ScriptedConnector",
"actionType" : "groovy",
"actionSource" : "println 'actionSource bindings: '; println binding.variables;"
}
]
}
],
[ . . .]
}
You can inspect variable bindings passed to the script (and thus, directly accessible in the script top-level scope) as shown in the following example:
SchemaScript.groovy
println 'operation: ' + operation
RCS logs
[rcs] operation: SCHEMA
You can also output the entire content of the binding.variables
property:
SchemaScript.groovy
println binding.variables.inspect()
RCS logs
['builder':org.forgerock.openicf.connectors.groovy.ICFObjectBuilder@290017f2, 'operation':SCHEMA, 'configuration':org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@5a34f2f9, 'log':org.identityconnectors.common.logging.Log@32095aee]
The binding.variables
property is an instance of the java.util.LinkedHashMap class. You can loop over its entries and get more detailed information about individual bindings:
For example:
SchemaScript.groovy
binding.variables.each { key, value ->
def className = value ? value.class.name : ''
println key + ': ' + className
}
RCS logs
[rcs] Bindings:
[rcs] builder: org.forgerock.openicf.connectors.groovy.ICFObjectBuilder
[rcs] operation: org.forgerock.openicf.connectors.groovy.OperationType
[rcs] configuration: org.forgerock.openicf.connectors.groovy.ScriptedConfiguration
[rcs] log: org.identityconnectors.common.logging.Log
Registering Connection in IDM and Schema Script (Part 2) chapters provide details on enabling and requesting schema operation, which invokes the schema script.
Global Variables
Bindings, both the binding
instance and the individual variable bindings, behave as global variables. This opens a possibility of reassigning them accidentally and thus breaking something in your code.
To avoid a situation like this in complex and involved scripts, you could reassign variable bindings to the namesake local variables. You can further clarify bindings’ designations by referencing their types.
For example:
TestScript.groovy
import org.identityconnectors.common.logging.Log
import org.forgerock.openicf.connectors.groovy.OperationType
import org.forgerock.openicf.connectors.groovy.ScriptedConfiguration
def operation = operation as OperationType
def configuration = configuration as ScriptedConfiguration
def log = log as Log
[ . . . ]
Doing so and adding corresponding dependencies to your scripted connector project can enable your IDE to show bindings’ class information and provide additional code completion options.
For example, in IntelliJ, you can add your dependencies as modules. Even if you don’t manage your connector scripts as a Java project, for the
identityconnectors
andopenicf
packages, you could importjava-framework
andgroovy-common
from the General Access Connectors repository; and thus, allow your IDE to show the additional information about your variables.
Scripted Groovy Connector Bindings
In ICF operations with Groovy scripts, you can navigate through descriptions and examples of use for common and operation-specific bindings (that is, externally defined input variables) available in connector scripts.
Below, find additional information about some common properties accessible from the scripting context:
Scripted Groovy Connector Bindings > configuration
The configuration
binding is an instance of org.forgerock.openicf.connectors.groovy.ScriptedConfiguration
, which provides access to connection properties registered in IDM and some additional properties defined by the ICF framework.
You can print out the entire binding content by inspecting configuration.properties
, which returns a map:
SearchScript.groovy
println configuration.properties.inspect()
To make a map output more readable and/or informative, you could print the map keys individually and recursively when a key itself is a map.
For example:
SearchScript.groovy
/**
* @param object Any variable.
* @param label String
* Optional label to precede the output.
* @param space int
* Optional indentation size.
* @todo Extend to handle complex types other than Map.
*/
def printObjectProperties = { object, label='Map', space=4 ->
def printProperties
printProperties = { propertyValue, propertyName, indent ->
def message = indent + propertyName + ' (' + propertyValue.getClass().name + '):'
if (propertyValue instanceof Map) {
println message
propertyValue.each { entry ->
printProperties entry.value, entry.key, indent + ' ' * space
}
} else {
println message + ' ' + propertyValue.inspect()
}
}
printProperties object, label, ''
}
printObjectProperties configuration.properties, 'configuration.properties'
RCS logs
[rcs] configuration.properties (java.util.LinkedHashMap):
[rcs] imports ([Ljava.lang.String;): ['org.identityconnectors.framework.common.objects.*']
[rcs] disabledGlobalASTTransformations ([Ljava.lang.String;): [null]
[rcs] scriptOnResourceScriptFileName (org.codehaus.groovy.runtime.NullObject): null
[rcs] parentLoader (org.identityconnectors.framework.impl.api.local.BundleClassLoader): org.identityconnectors.framework.impl.api.local.BundleClassLoader@12bbfc54
[rcs] deleteScriptFileName (org.codehaus.groovy.runtime.NullObject): null
[rcs] propertyBag (java.util.concurrent.ConcurrentHashMap):
[rcs] myCustomProperties (java.util.LinkedHashMap):
[rcs] count (java.lang.Integer): 0
[rcs] config (org.apache.groovy.json.internal.LazyMap):
[rcs] key1 (java.lang.String): 'value1'
[rcs] key2 (org.apache.groovy.json.internal.LazyMap):
[rcs] key1 (java.lang.String): 'value1'
[rcs] myCustomMethods (java.util.LinkedHashMap):
[rcs] getSum (SearchScript$_run_closure2): SearchScript$_run_closure2@61997137
[rcs] scriptExtensions ([Ljava.lang.String;): ['groovy']
[rcs] syncScriptFileName (org.codehaus.groovy.runtime.NullObject): null
[rcs] defaultCustomizerScriptName (java.lang.String): '/org/forgerock/openicf/connectors/groovy/CustomizerScript.groovy'
[rcs] recompileGroovySource (java.lang.Boolean): false
[rcs] createScriptFileName (org.codehaus.groovy.runtime.NullObject): null
[rcs] tolerance (java.lang.Integer): 10
[rcs] customSensitiveConfiguration (org.codehaus.groovy.runtime.NullObject): null
[rcs] customizerClass (org.codehaus.groovy.runtime.NullObject): null
[rcs] minimumRecompilationInterval (java.lang.Integer): 100
[rcs] authenticateScriptFileName (org.codehaus.groovy.runtime.NullObject): null
[rcs] warningLevel (java.lang.Integer): 1
[rcs] targetDirectory (org.codehaus.groovy.runtime.NullObject): null
[rcs] schemaScriptFileName (java.lang.String): 'SchemaScript.groovy'
[rcs] class (java.lang.Class): class org.forgerock.openicf.connectors.groovy.ScriptedConfiguration
[rcs] scriptBaseClass (org.codehaus.groovy.runtime.NullObject): null
[rcs] customConfiguration (org.codehaus.groovy.runtime.NullObject): null
[rcs] connectorMessages (org.identityconnectors.framework.impl.api.ConnectorMessagesImpl): org.identityconnectors.framework.impl.api.ConnectorMessagesImpl@3ea2423
[rcs] debug (java.lang.Boolean): false
[rcs] classpath ([Ljava.lang.String;): []
[rcs] releaseClosure (org.codehaus.groovy.runtime.NullObject): null
[rcs] updateScriptFileName (org.codehaus.groovy.runtime.NullObject): null
[rcs] sourceEncoding (java.lang.String): 'UTF-8'
[rcs] customizerScriptFileName (org.codehaus.groovy.runtime.NullObject): null
[rcs] testScriptFileName (org.codehaus.groovy.runtime.NullObject): null
[rcs] verbose (java.lang.Boolean): false
[rcs] groovyScriptEngine (groovy.util.GroovyScriptEngine): groovy.util.GroovyScriptEngine@53a79e33
[rcs] searchScriptFileName (java.lang.String): 'SearchScript.groovy'
[rcs] scriptRoots ([Ljava.lang.String;): ['/opt/openicf/scripts/groovy']
[rcs] resolveUsernameScriptFileName (org.codehaus.groovy.runtime.NullObject): null
You can also access each configuration property individually:
SearchScript.groovy
println 'Script Roots: ' + configuration.scriptRoots.inspect()
RCS logs
[rcs] Script Roots: ['/opt/openicf/scripts/groovy']
Most of this content corresponds directly to the keys in the connection configuration at the time it is registered in IDM, where you can specify some of the connector configuration properties. ICF defines additional properties in the connector configuration.
Scripted Groovy Connector Bindings > configuration
> configuration.propertyBag
If you need to keep a custom global reference accessible from any connector script, you can save it in the configuration.propertyBag
property, which is a map. This will allow to dynamically define, cache, and share within a connector instance anything that can be passed as a variable: a primitive or a reference to an object, such as a map or a closure.
The content saved in configuration.propertyBag
will be reset when “configurationProperties.customConfiguration” and “configurationProperties.customSensitiveConfiguration” properties have been changed in the connection configuration, the RCS is restarted, or the IDM connector instance service (OSGi) is restarted.
You can populate configuration.propertyBag
at different stages of a connector life cycle:
Scripted Groovy Connector Bindings > configuration.propertyBag
> In Connection Configuration
Initial content for configuration.propertyBag
can be provided via “configurationProperties.customConfiguration” and “configurationProperties.customSensitiveConfiguration” keys in a connection configuration.
Doing so, the connection-specific content—which could be settings, secrets, references, etc.—can be saved in IDM environment, with an option to use property value substitution and Environment-Specific Variables and Secrets (ESVs) (in Identity Cloud).
See the Connection Configuration > “configurationProperties” > “customConfiguration” and “customSensitiveConfiguration” chapter for details.
Note that if “configurationProperties.customConfiguration” or “configurationProperties.customSensitiveConfiguration” properties have been changed in the connection configuration, the configuration.propertyBag
content will be reset on the connector that is using this configuration.
Scripted Groovy Connector Bindings > configuration.propertyBag
> In Customizer Script
In connector implementations based on the Scripted Groovy Connector Toolkit, ICF uses the Stateful Configuration interface. This means that a configuration instance is initialized once during the connector’s management cycle and before any ICF operations with Groovy scripts are performed.
You can use custom configuration initialization in the connector server environment via a script referenced under the “configurationProperties.customizerScriptFileName” key in a connection configuration.
During the initialization process, the customizer script will have access to the configuration instance and can be used to update configuration.propertyBag
content, which will become globally available for all the scripts implementing ICF operations at runtime.
Note that while a stateful connector configuration is initialized only once, the customizer script runs every time when the corresponding connection configuration is updated in IDM; you can leverage this fact while developing your customizer script functionality.
Scripted Groovy Connector Bindings > configuration.propertyBag
> In Customizer Script > For Groovy Connector
In a plain Groovy connector, the customizer script will have access to the following bindings:
-
log
, which is an instance of the org.identityconnectors.common.logging.Log class that can be used for outputting debugging information. -
configuration
, which is an instance oforg.forgerock.openicf.connectors.groovy.ScriptedConfiguration
, and which provides access to its.propertyBag
property and the other connector configuration properties.
Thus, you can interact with the configuration.propertyBag
content in a customizer script.
For example:
CustomizerScript.groovy
configuration.propertyBag.myCustomProperties = [:]
configuration.propertyBag.myCustomProperties.count = 0
configuration.propertyBag.apiConfig.apiKey = System.getenv('API_KEY')
[ . . . ]
SearchScript.groovy
println configuration.propertyBag.apiConfig.apiKey
[ . . . ]
RCS logs
[rcs] bm90LWFuLWFwaS1rZXk
Similarly, the customizer script can reference other connector server resources, and it could also be driven by the existing propertyBag
content defined in “custom(Sensitive)Configuration”.
Scripted Groovy Connector Bindings > configuration.propertyBag
> In Customizer Script > For Scripted SQL Connector
Everything said in regard to configuration.propertyBag
in a plain Groovy connector applies to a Scripted SQL one, except its configuration instance is extended to org.forgerock.openicf.connectors.scriptedsql.ScriptedSQLConfiguration
and contains additional, Scripted SQL-specific properties.
Scripted Groovy Connector Bindings > configuration
> configuration.propertyBag
> In Customizer Script > For Scripted REST Connector
Scripted REST connector does not define configuration
binding in the top-level scope of its customizer script. Instead, it allows to use init
, decorate
, and release
methods inside a closure passed into the customize
method, which is defined in the script’s top-level scope.
The init
method accepts a closure, in which you can configure a passed in HTTPClientBuilder instance, which will be used to build customized CloseableHttpClient. The init
closure runs when the connector configuration is initialized.
The decorate
method accepts a closure in which you can customize further an instance of the HTTP client built with the HTTPClientBuilder. Then, the HTTP client is provided as the customizedConnection
binding for making requests in the connector’s scripts. The decorate closure runs on every ICF operation.
The
release
closure runs when the stateful configuration is released. It could be used to dispose any custom objects not handled by the ICF. Otherwise you don not need to call the release method.
The closures passed into init
, decorate
, and release
methods have their delegates assigned an instance of the org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConfiguration
class, which extends org.forgerock.openicf.connectors.groovy.ScriptedConfiguration
and thus provides access to propertyBag
and the other connector’s configuration properties.
This means that in the init
closure you can update the propertyBag
content during the connector configuration initialization and before any scripted ICF operations are performed, and in the decorate
closure, you can update the propertyBag
content at runtime.
For example:
CustomizerScript.groovy
[ . . . ]
import org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConfiguration
import org.apache.http.impl.client.HttpClientBuilder
import org.apache.http.client.HttpClient
customize {
init { HttpClientBuilder builder ->
def c = delegate as ScriptedRESTConfiguration
c.propertyBag.myCustomProperties = [:]
c.propertyBag.myCustomProperties.count = 0
c.propertyBag.apiConfig.apiKey = System.getenv('API_KEY')
[ . . . ]
}
decorate { HttpClient httpClient ->
def c = delegate as ScriptedRESTConfiguration
c.propertyBag.myCustomProperties.count += 1
}
}
Comprehensive examples of customizer script for scripted REST connector could be found in an IDM installation in Samples provided with IDM, in the accessible for ForgeRock customers General Access Connectors repository, and in the Scripted REST connector documentation. In particular, the OAuth2 Authentication Implementation example demonstrates how OAuth 2.0 authorization could be performed, and how resulting from it access tokens can be saved in propertyBag
and shared with the scripts performing data operations.
Having the
ScriptedRESTConfiguration
instance as the delegate and as the enclosing class for the closures, you could access its properties directly. However, for the reasons explained in Scripting Context > Global Variables, and as shown in official examples shipped with IDM, you might want to reassign your delegate to a variable of this particular,ScriptedRESTConfiguration
type.For illustration:
CustomizerScript.groovy
(for scripted REST connector)[ . . . ] import org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConfiguration import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.client.HttpClient customize { init { HttpClientBuilder builder -> def c = delegate as ScriptedRESTConfiguration assert c.propertyBag == propertyBag [ . . . ] } decorate { HttpClient httpClient -> def c = delegate as ScriptedRESTConfiguration assert c.propertyBag == propertyBag [ . . . ] } release { def configuration = delegate as ScriptedRESTConfiguration assert configuration.propertyBag == propertyBag [ . . . ] } }
Scripted Groovy Connector Bindings > configuration
> configuration.propertyBag
> In Scripts at Runtime
All ICF operations with Groovy scripts will have access to the configuration
binding. Thus, the scripts will have read and write access to the configuration properties saved in configuration.propertyBag
.
For example:
SearchScript.groovy
[ . . . ]
configuration.propertyBag.myCustomProperties.count += 1
It should be noted that while some other configuration properties can be changed from a script with immediate effect, you should limit your runtime configuration customizations to the designated area that configuration.propertyBag
is.
Scripted Groovy Connection Configuration
Configuration properties that you can register in IDM for different remote connector types can be found under Connector reference.
This chapter will elaborate on some additional details, not currently present in the ever improving docs.
Scripted Groovy Connection Configuration > “configurationProperties”
The “configurationProperties” key in a connection configuration contains settings that are specific to the remote system.
Scripted Groovy Connection Configuration > “configurationProperties” > “customConfiguration” and “customSensitiveConfiguration”
The content of custom(Sensitive)Configuration
keys in a connection configuration can be used for setting parameters accessible to the connector scripts via configuration.propertyBag
binding.
You might have come across an example of using customConfiguration and customSensitiveConfiguration in the current docs:
"customConfiguration" : "kadmin { cmd = '/usr/sbin/kadmin.local'; user='<KADMIN USERNAME>'; default_realm='<REALM>' }", "customSensitiveConfiguration" : "kadmin { password = '<KADMIN PASSWORD>'}",
However, just from the code sample, it might not be entirely clear how (and why) this Groovy-like syntax works, and how you can leverage it to set custom configuration options used in a script.
The content provided in “customConfiguration” will be evaluated with a matching parse
method of the groovy.util.ConfigSlurper class.
In particular, the parse(String script) method accepts a special script that can be used to set keys in configuration.propertyBag
using variable assignment or Closure syntax:
-
Assigning variables:
"customConfiguration": "key1 = 'value1';"
-
Defining closures:
"customConfiguration": "key1 { key2 = 'value2'; };"
If you use a closure for setting “customConfiguration” variables, the literal preceding the closure becomes a key in the configuration.propertyBag
map, and its value will be a map-like ConfigObject with its keys corresponding to the variables set by the closure code. You can also create such maps of parameters by using variable assignment or dot notation. Saving your parameters in maps may help to better organize your custom configuration and reduce redundancy.
For example:
provisioner.openicf-<connection-name>.json
{
"connectorRef": {
"connectorHostRef": "rcs",
"bundleVersion": "1.5.20.15",
"bundleName": "org.forgerock.openicf.connectors.groovy-connector",
"connectorName": "org.forgerock.openicf.connectors.groovy.ScriptedConnector"
},
"configurationProperties": {
"customConfiguration": "key1 = 'value1'; key2 = 'value2'; map1 { key1 = 'value3'; key2 = 'value4'; }; map2.key1 = 'value5'; map2.key2 = 'value6'; map3 = [ key1: 'value7', key2: 'value8' ];",
[ . . . ]
}
[ . . . ]
}
Note that multi-line statements are not supported in JSON. This means, you must put all “customConfiguration” content in one line, and separate multiple Groovy statements with semicolons.
You can use new lines for separating Groovy statements as well:
[ . . . ] "customConfiguration": "key1 = 'value1'\n key2 = 'value2'\n map1 { key1 = 'value3'\n key2 = 'value4'\n }\n map2.key1 = 'value5'\n map2.key2 = 'value6'\n map3 = [ key1: 'value7', key2: 'value8' ]\n", [ . . . ]
The parsed custom configuration content will be used to populate configuration.propertyBag
properties:
TestScript.groovy
println configuration.propertyBag.key1
println configuration.propertyBag.key2
println configuration.propertyBag.map1.key1
println configuration.propertyBag.map1.key2
println configuration.propertyBag.map2.key1
println configuration.propertyBag.map2.key2
println configuration.propertyBag.map3.key1
println configuration.propertyBag.map3.key2
RCS logs
[rcs] value1
[rcs] value2
[rcs] value3
[rcs] value4
[rcs] value5
[rcs] value6
[rcs] value7
[rcs] value8
If you don’t want your (hardcoded) connection configuration to be exposed in clear text in IDM, you can also supply the configuration.propertyBag
content via the connection’s “customSensitiveConfiguration” configuration property. The information defined with the “customSensitiveConfiguration” key will be encrypted on IDM side; its content will become a GuardedString (that is, an encrypted string), and it will continue to be encrypted in transport to the connector server.
The properties defined in “customSensitiveConfiguration” will overwrite the same keys provided in “customConfiguration”. A map defined in propertyBag
can be updated from both keys.
For example:
provisioner.openicf-<connection-name>.json
{
[ . . . ]
"configurationProperties": {
"customConfiguration": "key1 = 'value1'; key2 = 'value2'; map1 { key1 = 'value3'; };",
"customSensitiveConfiguration": "key1 = 'sensitive-value1'; map1 { key2 = 'value4'; };",
[ . . . ]
}
[ . . . ]
}
TestScript.groovy
println configuration.propertyBag.key1
println configuration.propertyBag.key2
println configuration.propertyBag.map1.inspect()
RCS logs
[rcs] sensitive-value1
[rcs] value2
[rcs] ['key2':'value4', 'key1':'value3']
You can also use property value substitution in the “custom(Sensitive)Configuration” values to allow for dynamic and secure content. In addition to the IDM variables, in Identity Cloud, you can also reference Environment-Specific Variables and Secrets (ESVs). Doing will help to define environment-specific configuration—especially in controlled environments, such as staging and production in Identity Cloud, where the connection configuration properties are a part of the immutable config. It can also hide sensitive information from the UIs.
For example:
provisioner.openicf-<connection-name>.json
{
[ . . . ]
"configurationProperties": {
"customConfiguration": "oauth2Config { provider = 'https://&{fqdn}/'; client_id = 'client-id' }; apiConfig { url = '${esv.rcs.api.config.key}'; key = '${esv.rcs.api.config.key}' };",
"customSensitiveConfiguration": "oauth2Config { client_secret = '&{esv.rcs.oauth2.client_secret}' }",
[ . . . ]
}
[ . . . ]
}
TestScript.groovy
println configuration.propertyBag.apiConfig.inspect()
println configuration.propertyBag.oauth2Config.inspect()
RCS logs
[rcs] ['url':'bm90LWFuLWFwaS1rZXk', 'key':'bm90LWFuLWFwaS1rZXk']
[rcs] ['client_secret':'Up0N4Tt95', 'discoveryUri':'https://openam-dx-kl04.forgeblocks.com/am/oauth2/alpha/.well-known/openid-configuration', 'client_id':'RCSClient']
In this case, over a secure connection, everything, including the OAuth 2.0 secrets, could be saved in “customConfiguration”, but using “customSensitiveConfiguration” is an option.
Note that the maps in propertyBag
are actual objects and don’t have to be parsed (like a JSON).
The configuration code saved in “custom(Sensitive)Configuration” and interpolated by IDM will be sent to and evaluated by ICF.
This means that you could assign configuration.propertyBag
properties dynamically by defining a script (or using Groovy string interpolation) and referencing the connector server resources (such as its file system, environment variables, methods, etc.).
For example:
provisioner.openicf-<connection-name>.json
{
[ . . . ]
"configurationProperties": {
"customConfiguration": "apiConfig { apiKey = System.getenv('API_KEY') };",
[ . . . ]
}
}
Given the limitations of JSON and considering that connection configuration is being transmitted over the network, it might be more effective and efficient to define connector server-specific settings in a script hosted on the connector server, which will have read and write access to the
configuration.propertyBag
property, as described in theconfiguration.propertyBag
> In Customizer Script chapter.This also means that you can execute any arbitrary code delivered via “customConfiguration” on the connector server. However, actions defined in “systemActions” provide better facilities for this type of interaction.
Scripted Groovy Connector (Toolkit) > Connection Configuration > “systemActions”
Since remote connector is a system object, you can initiate a scripted action on it. You can define your action under the “systemActions” key in the connection configuration.
Running a remote script may serve as the means of making a change to or getting a response from the remote system without authorizing to that system or changing its firewall rules; the script will have access to the connector server environment.
A scripted action on a remote connector could also be used to modify the connector behavior, because the script will have access to the libraries and bindings available to the connector, including its configuration.
Scripted Groovy Connector (Toolkit) > Connection Configuration > “systemActions” > Defining System Action
To outline its general structure, below is an example of a system action definition with two individual actions—one returning script bindings and another one solving a math problem:
provisioner.openicf-<connection-name>.json
{
"connectorRef": {
"connectorName": "org.forgerock.openicf.connectors.groovy.ScriptedConnector",
[ . . . ]
},
[ . . . ]
"systemActions": [
{
"scriptId" : "script-1",
"actions" : [
{
"systemType" : ".*ScriptedConnector",
"actionType" : "groovy",
"actionSource" : "binding.variables.toString();"
},
{
"systemType" : ".*ScriptedConnector",
"actionType" : "groovy",
"actionSource" : "2 * 2"
}
]
},
[ . . . ]
]
}
You can request a system action identified by its “scriptId” via IDM’s APIs, as described later in Invoking via IDM’s REST and Invoking from an IDM Script chapters.
Each system action is defined with the following keys:
-
The ID you will use in your request to invoke this system action.
-
For each script ID, you can specify one or more action in an array of action definitions.
Each action definition consists of the following keys:
-
Reference to a connector type for which this action was written. System actions will be performed in the context of the connector type, for which scripting environment will be built.
The value for this key corresponds to the “connectorRef.connectorName” value in the connection configuration, and you can populate it with a matching regular expression. If “systemType” does not match “connectorRef.connectorName”, the action will not be executed when the parent system action is requested.
Consider the aforementioned example:
-
The system type for both actions is “.*ScriptedConnector”, which matches the connector name, “org.forgerock.openicf.connectors.groovy.ScriptedConnector”.
-
Therefore, when “script-1” system action is requested, both ot its actions will return results.
For example:
{ "actions": [ { "result": "[arg1:Arg1, param1:Param1, operation:RUNSCRIPTONCONNECTOR, options:OperationOptions: {CAUD_TRANSACTION_ID:1674597149435-ad859d99bf71003de8ae-19623/0/1}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@213027ec, log:org.identityconnectors.common.logging.Log@41b1ffbf]" }, { "result": 4 } ] }
To run an action on a connector of a different type, you will need a matching “systemType” value.
For example:
provisioner.openicf-<connection-name>.json
{ "connectorRef": { "connectorName": "org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConnector", [ . . . ] }, [ . . . ] "systemActions": [ { "scriptId" : "script-1", "actions" : [ { "systemType" : ".*ScriptedRESTConnector", "actionType" : "groovy", "actionSource" : "binding.variables.toString();" } ] }, [ . . . ] ] }
Note that for this connector type,
ScriptedRESTConnector
, the response will reveal two additional bindings available for the action script:connection
andcustomizedConnection
:{ "actions": [ { "result": "[arg1:Arg1, param1:Param1, operation:RUNSCRIPTONCONNECTOR, options:OperationOptions: {CAUD_TRANSACTION_ID:1674597562382-5618dcd582b30aa5ae0d-20316/0/1}, configuration:org.forgerock.openicf.connectors.scriptedrest.ScriptedRESTConfiguration@27a387dd, connection:org.apache.http.impl.client.InternalHttpClient@21979403, customizedConnection:InnerRESTClient@106ef511, log:org.identityconnectors.common.logging.Log@6124b177]" } ] }
If there are no actions with “systemType” matching “connectorRef.connectorName”, requesting the parent system action will return a 400 error accompanied with the following message:
Script ID:
<system-action-scriptId>
for systemType<connection-name>
is not defined. -
-
A language reference, indicating which language the system action script is written and should be interpreted in.
For a Java RCS, the action type is always “groovy”.
-
“actionSource” or “actionFile”
A system action script content can be provided as a relative path reference to a script located on the IDM host in the “actionFile” key, or as inline script in the “actionSource” key:
-
“actionFile”
The reference to a script file in the IDM installation folder.
In Identity Cloud, you do not have access to the IDM file system. In such environment, you need to customize your system action with the content provided in “actionSource” key and/or by passing in additional arguments to your system action requested via IDM’s APIs.
-
“actionSource”
Some text sent to RCS; for example, the actual script to be executed. The “actionSource” value MUST contain a non-whitespace string.
Because JSON does not support multiline content, if “actionSource” is a (Groovy) script, you can separate your statements with semicolons or new lines.
For example:
[ . . . ] { [ . . . ] "actionSource" : "println 'actionSource bindings: '; println binding.variables;" }, { [ . . . ] "actionSource" : "println 'actionSource bindings: ' \nprintln binding.variables" } [ . . . ]
Consult JSON on the acceptable JSON syntax.
You can also use a formatting tool where your script content is converted into a JSON-acceptable format; for example, Free Online JSON Escape / Unescape Tool - FreeFormatter.com
-
-
The Invoking via IDM’s REST chapter provides additional details on how a system action can be defined and customized.
The Invoking from an IDM Script chapter demonstrates how a system action can be invoked from a script.
Scripted Groovy Connection Configuration > “systemActions” > Invoking via IDM’s REST
You can run a script on a remote connector by sending a POST request to IDM’s REST API:
/openidm/system/<connection-name>
?_action=script
&scriptId=<script_id>
[&arg1=value1
&arg2=value2
. . . ][&scriptExecuteMode=resource
]
Scripted Groovy Connection Configuration > “systemActions” > Invoking via IDM’s REST > Parts of the Request:
-
/openidm/system/<connection-name>
Path to the IDM’s endpoint, at which your remote connection is registered.
As an example,
/openidm/system/groovy
path in your system action request will correspond to a remote connection registered at/openidm/config/provisioner.openicf/groovy
, as described in the final step of the Configure connectors over REST doc. -
When executing a script on a remote connector, the
_action
argument value is always to bescript
. -
The identifier of the system action you are trying to invoke, which is saved in your connection configuration JSON under the “systemActions.scriptId” key.
For example, consider the following system action definition:
provisioner.openicf-<connection-name>.json
{ "connectorRef": { "connectorName": "org.forgerock.openicf.connectors.groovy.ScriptedConnector", [ . . . ] }, [ . . . ] "systemActions": [ { "scriptId" : "script-1", "actions" : [ { "systemType" : ".*ScriptedConnector", "actionType" : "groovy", "actionSource" : "println 'actionSource bindings: '; println binding.variables;" } ] }, [ . . . ] ] }
Requesting this action via IDM’s REST could be done in the following way:
IDM Admin Browser Console
(async function () { var data = await $.ajax('/openidm/system/groovy?_action=script&scriptId=script-1', { method: 'POST' }); console.log(JSON.stringify(data, null, 4)); }());
Because this particular “actionSource” script does not return anything, the response data will bear an empty result from the only action defined and executed for this script ID:
Browser Network Response
{ "actions": [ { "result": null } ] }
In Groovy, you don’t have to explicitly return anything from a script; the result of the last statement is returned automatically. However, in the provided example, the last statement is a println call, which returns nothing; hence, there is
null
result in the response.To return something in the response, end your script with a statement returning “serializable” value. A value is serializable if it can be converted into a valid JSON text.
In a Java RCS, instances of the following classes (and the corresponding primitives) are serializable and could be returned from your script:
-
Boolean (and
boolean
) -
Integer (and
int
) -
Long (and
long
) -
Float (and
float
) -
Double (and
double
) -
Character (and
char
) -
Byte (
byte
andbyte[]
)
Besides manually defining data returned from a script, you could also use a number of built-in ICF-specific Handlers and Mappings that can consume and return serialized values.
Generally, if you try to return a non-serializable value from your script you will receive an error message in the response, similar to the following:
{ "actions": [ { "error": "No serializer for class: class java.util.HashMap$Node" } ] }
or
{ "actions": [ { "error": "No serializer for class: class groovy.lang.Script" } ] }
As an example of a serializable value that could result from an arbitrary operation performed in a remotely executed script, you could return a list:
provisioner.openicf-<connection-name>.json
{ "connectorRef": { "connectorName": "org.forgerock.openicf.connectors.groovy.ScriptedConnector", [ . . . ] }, [ . . . ] "systemActions": [ { "scriptId" : "script-1", "actions" : [ { "systemType" : ".*ScriptedConnector", "actionType" : "groovy", "actionSource" : "println 'actionSource bindings: '; println binding.variables; [1, 2, 3];" } ] }, [ . . . ] ] }
In the response, you will now see an array in the result of the single action defined for the “script-1” system action:
Browser Network Response
{ "actions": [ { "result": [ 1, 2, 3 ] } ] }
Multiple actions defined for a system action will return multiple results in the “actions” array in the response.
This particular “actionSource” script will also output its variable bindings in the RCS logs:
RCS logs
[rcs] actionSource bindings: [rcs] [operation:RUNSCRIPTONCONNECTOR, options:OperationOptions: {CAUD_TRANSACTION_ID:1659985544219-55e3d75b5a1adc2a72f9-134922/0/4}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@73133988, log:org.identityconnectors.common.logging.Log@2cba672e]
Note that the
operation
binding value reveals the script on connector operation environment, which is the default execution mode for a system action script. In this mode, the script specified in “actionSource” (or “actionFile”) is executed in a context built for a run on connector script.You can read about common RCS script bindings in Variables available to all Groovy scripts, and find more specific information in the sections designated to a particular script operation.
-
-
&arg1=value1&arg2=value2 . . .
Besides
scriptId
, you can pass additional arbitrary arguments in the query string. These will become top-level variables in the scripting context.It might be more efficient, however, to define you script parameters in the request body.
-
The key/value pairs provided as JSON in the request body will be available to the remotely executed script as top-level variables.
For example, you could execute the following request:
IDM Admin Browser Console
(async function () { var data = await $.ajax('/openidm/system/groovy?_action=script&scriptId=script-1', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ arg1: 'value1' }) }); console.log(JSON.stringify(data, null, 4)); }());
In the RCS logs, in the bindings available to your action script, you will now see
arg1
, which was a key in your request body JSON:[rcs] actionSource bindings: [rcs] [arg1:value1, operation:RUNSCRIPTONCONNECTOR, options:OperationOptions: {CAUD_TRANSACTION_ID:1659985960800-d72d565f715c26629c97-65231/0/4}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@73133988, log:org.identityconnectors.common.logging.Log@2cba672e]
Different types of values in the request body JSON will be presented in the script as the following types:
-
object
> java.util.HashMap -
number
> java.lang.Integer or java.lang.Double -
array
> java.util.ArrayList -
null
> org.codehaus.groovy.runtime.NullObject -
boolean
> java.lang.Boolean -
string
> java.lang.String -
undefined
> Will NOT be present.
For example, providing this request body:
data: JSON.stringify({ arg1: 'value1', arg2: { arg3: 1, arg4: 1.1, arg5: [ 'value3', 'value4' ], arg6: true, arg7: null, arg8: undefined } })
will yield the following output in the RCS logs:
[arg2:[arg3:1, arg5:[value3, value4], arg4:1.1, arg7:null, arg6:true], arg1:value1, operation:RUNSCRIPTONCONNECTOR, options:OperationOptions: {CAUD_TRANSACTION_ID:1661235801641-96bc95f49bbf8e27d3e4-54089/0/4}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@26f4d226, log:org.identityconnectors.common.logging.Log@1726fedb]
-
-
&scriptExecuteMode=resource
(optional and important)The absence or presence of this parameter in a system action request will determine one of the two execution modes for the scripted system action:
-
By default, without this optional parameter populated with this particular value, the script you specify in “actionSource” or “actionFile” script will “run on connector”.
In this mode, the script you specify in “actionSource” or “actionFile” will be sent to the RCS, where your scripted connector package is deployed. The ICF framework will execute the script in the connector type-specific context, with all the variable bindings for “run on connector” operation available to the script.
This has been the mode illustrated in all previous examples.
-
Including
&scriptExecuteMode=resource
in a system action request will cause the remote script to “run on resource”.
&scriptExecuteMode=resource
(optional and important) > Executed ScriptIn this latter mode, a script hosted on the RCS will be executed.
Exactly which script hosted on RCS is going to be executed is specified in the connection configuration under the “scriptOnResourceScriptFileName” key.
For example:
provisioner.openicf-<connection-name>.json
{ "connectorRef": { "connectorName": "org.forgerock.openicf.connectors.groovy.ScriptedConnector", [ . . . ] }, "configurationProperties": { "scriptExtensions": [ "groovy" ], "scriptRoots": [ "/opt/openicf/scripts/<connection-name>" ], "scriptOnResourceScriptFileName": "ScriptOnResourceScript.groovy", [ . . . ] }, [ . . . ] "systemActions": [ { "scriptId" : "script-1", "actions" : [ { "systemType" : ".*ScriptedConnector", "actionType" : "groovy", "actionSource" : "println 'actionSource bindings: '; println binding.variables; [1, 2, 3];" } ] }, [ . . . ] ] }
&scriptExecuteMode=resource
(optional and important) > Context of Executed ScriptThe content provided either in “actionSource” or via “actionFile” will be available to the the script referenced in “scriptOnResourceScriptFileName” as the
scriptText
variable binding.For example:
ScriptOnResourceScript.groovy
[ . . . ] println 'ScriptOnResourceScript.groovy bindings: ' + binding.variables
IDM Admin Browser Console
(async function () { var data = await $.ajax('/openidm/system/groovy?_action=script&scriptId=script-1&scriptExecuteMode=resource', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ arg1: 'value1' }) }); console.log(JSON.stringify(data, null, 4)); }());
RCS logs
[rcs] ScriptOnResourceScript.groovy bindings: [scriptArguments:[arg1:value1], scriptText:println 'actionSource bindings: '; println binding.variables; [1, 2, 3];, scriptLanguage:groovy, operation:RUNSCRIPTONRESOURCE, options:OperationOptions: {CAUD_TRANSACTION_ID:1659989514919-55e3d75b5a1adc2a72f9-142363/0/5}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@21fa1b88, log:org.identityconnectors.common.logging.Log@3f3e5b1c]
Note:
-
The
operation
type is “RUNSCRIPTONRESOURCE”, which corresponds to the script on resource operation. -
The script you provided in “actionSource” is available to
ScriptOnResourceScript.groovy
as thescriptText
binding. -
The
scriptText
content is for a Groovy environment, as indicated in thescriptLanguage
binding. -
The request data is saved in the
scriptArguments
binding (which is an unmodifiable map).
Note also that this particular
ScriptOnResourceScript.groovy
does not return anything; and thus, in the browser response you will see no result:Browser Network Response
{ "actions": [ { "result": null } ] }
&scriptExecuteMode=resource
(optional and important) > EvaluatingscriptText
The script you see in the
scriptText
variable is NOT automatically executed by RCS. TheScriptOnResourceScript.groovy
script can ignorescriptText
or use its text value in any desirable way.For example, the samples/scripted-sql-with-mysql/tools/ResetDatabaseScript.groovy script does not expect a
scriptText
binding, and can be executed “on resource” with any (non-whitespace) content in the “actionSource” value.If the
scriptText
content is a script,ScriptOnResourceScript.groovy
can evaluate it and return its result:ScriptOnResourceScript.groovy
[ . . . ] println 'ScriptOnResourceScript.groovy bindings: ' + binding.variables new GroovyShell().evaluate(scriptText)
This time, in addition to the bindings printed out from
ScriptOnResourceScript.groovy
, the script provided in “actionSource” will execute as well and log out its available variables:RCS logs
[rcs] ScriptOnResourceScript.groovy bindings: [scriptArguments:[arg1:value1], scriptText:println 'actionSource bindings: '; println binding.variables; [1, 2, 3];, scriptLanguage:groovy, operation:RUNSCRIPTONRESOURCE, options:OperationOptions: {CAUD_TRANSACTION_ID:1661542196240-3c9381d89956a1ca3441-148731/0/4}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@308df108, log:org.identityconnectors.common.logging.Log@12972750] [ . . . ] [rcs] actionSource bindings: [rcs] [:]
The empty map representing bindings in the script defined in “actionSource” means that no outside-defined variables have been made available to the evaluated script. This is because no arguments were passed to the groovy.lang.GroovyShell constructor used to create the instance that evaluated the
scriptText
content.You can supply the “actionSource” script with the very same bindings defined for
ScriptOnResourceScript.groovy
by passing itsbinding
object into thegroovy.lang.GroovyShell
constructor:ScriptOnResourceScript.groovy
[ . . . ] println 'ScriptOnResourceScript.groovy bindings: ' + binding.variables new GroovyShell(binding).evaluate(scriptText)
RCS logs
[rcs] ScriptOnResourceScript.groovy bindings: [scriptArguments:[arg1:value1], scriptText:println 'actionSource bindings: '; println binding.variables; [1, 2, 3];, scriptLanguage:groovy, operation:RUNSCRIPTONRESOURCE, options:OperationOptions: {CAUD_TRANSACTION_ID:1661543987519-3c9381d89956a1ca3441-151976/0/5}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@7a551e98, log:org.identityconnectors.common.logging.Log@26179ea7] [ . . . ] [rcs] actionSource bindings: [rcs] [scriptArguments:[arg1:value1], scriptText:println 'actionSource bindings: '; println binding.variables; [1, 2, 3];, scriptLanguage:groovy, operation:RUNSCRIPTONRESOURCE, options:OperationOptions: {CAUD_TRANSACTION_ID:1661543987519-3c9381d89956a1ca3441-151976/0/5}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@7a551e98, log:org.identityconnectors.common.logging.Log@26179ea7]
Alternatively, you could explicitly define “actionSource”-specific bindings:
ScriptOnResourceScript.groovy
[ . . . ] println 'ScriptOnResourceScript.groovy bindings: ' + binding.variables // Do some processing. def sum = 1 + 1 Binding binding = new Binding() binding.setVariable('args', scriptArguments) binding.setVariable('sum', sum) new GroovyShell(binding).evaluate(scriptText)
Now, the action script bindings will be limited to the variables explicitly set for this
GroovyShell
instance in theScriptOnResourceScript.groovy
script:RCS logs
[rcs] ScriptOnResourceScript.groovy bindings: [scriptArguments:[arg1:value1], scriptText:println 'actionSource bindings: '; println binding.variables; [1, 2, 3];, scriptLanguage:groovy, operation:RUNSCRIPTONRESOURCE, options:OperationOptions: {CAUD_TRANSACTION_ID:1661545941051-690832f475140dd87466-16728/0/4}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@308df108, log:org.identityconnectors.common.logging.Log@12972750] [ . . . ] [rcs] actionSource bindings: [rcs] [args:[arg1:value1], sum:2]
In the last few examples,
ScriptOnResourceScript.groovy
ends with aGroovyShell
call; and thus, the browser response will contain the result of evaluating the “actionSource” script:Browser Network Response
{ "actions": [ { "result": [ 1, 2, 3 ] } ] }
&scriptExecuteMode=resource
(optional and important) > Other Applications ofscriptText
You can customize the script “on resource” behavior in the following ways:
-
With the additional parameters (passed in the request body) available to the script as the
scriptArguments
binding. -
Via the “actionSource” content (and/or in a self-managed environment, via the “actionFile” reference) for each defined and performed action in a system action.
In the latter case, the script content can be any text. It could be evaluated by the script “on resource”, or used in a conditional statement, or applied in any other similar way. It could be a reference to a local (to RCS) and completely separate script file.
Or,
scriptText
provided via “actionSource” could be a JSON, whichScriptOnResourceScript.groovy
can parse and use for all the above and more.Note that since “systemActions” definition is read and interpolated by IDM, you can use dynamic references in this content, as demonstrated in the Connection Configuration > “configurationProperties” > “customConfiguration” and “customSensitiveConfiguration” chapter.
For example, the following system action with the “script-2” ID could be added to the connection configuration:
provisioner.openicf-<connection-name>.json
{ "connectorRef": { "connectorName": "org.forgerock.openicf.connectors.groovy.ScriptedConnector", [ . . . ] }, "configurationProperties": { "scriptExtensions": [ "groovy" ], "scriptRoots": [ "/opt/openicf/scripts/<connection-name>" ], "scriptOnResourceScriptFileName": "ScriptOnResourceScript.groovy", [ . . . ] }, [ . . . ] "systemActions": [ { "scriptId" : "script-1", "actions" : [ { "systemType" : ".*ScriptedConnector", "actionType" : "groovy", "actionSource" : "println 'actionSource bindings: '; println binding.variables; [1, 2, 3];" } ] }, { "scriptId" : "script-2", "actions" : [ { "systemType" : ".*ScriptedConnector", "actionType" : "groovy", "actionSource" : "{\"scriptPath\":\"/opt/openicf/scripts/groovy/ScriptOnResourceScript.script-2.action-0.groovy\"}" }, { "systemType" : ".*ScriptedConnector", "actionType" : "groovy", "actionSource" : "{\"scriptPath\":\"/opt/openicf/scripts/groovy/ScriptOnResourceScript.script-2.action-1.groovy\"}" } ] } ] }
The system action defined under the “script-2” ID has two actions, each containing a path to a local file in its “actionSource”.
Assume that the referenced files have a very simple Groovy script returning an object—a list and a map respectively:
ScriptOnResourceScript.script-2.action-0.groovy
[1, 2]
ScriptOnResourceScript.script-2.action-1.groovy
[ a: 'a', b: 'b']
The
ScriptOnResourceScript.groovy
script can evaluate the content of each action file referenced in “actionSource”—one action at the time—in the following way:ScriptOnResourceScript.groovy
import groovy.json.JsonSlurper def jsonSlurper = new groovy.json.JsonSlurper() def actionSourceJson = jsonSlurper.parseText scriptText println actionSourceJson.inspect() def scriptFileText = new File(actionSourceJson.scriptPath).text new GroovyShell().evaluate scriptFileText
When the system action identified as “script-2” is requested via IDM’s APIs:
IDM Admin Browser Console
(async function () { var data = await $.ajax('/openidm/system/groovy?_action=script&scriptId=script-2&scriptExecuteMode=resource', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ arg1: 'value1' }) }); console.log(JSON.stringify(data, null, 4)); }());
In the response, you should now see the data returned for each action:
{ "actions": [ { "result": [ 1, 2 ] }, { "result": { "a": "a", "b": "b" } } ] }
In the RCS logs you will see the content of the parsed JSON provided in “actionSource”:
RCS logs
['scriptPath':'/opt/openicf/scripts/groovy/ScriptOnResourceScript.script-2.action-1.groovy']
In this way, “actionSource” could serve as a great tool for referencing local scripts that can be maintained as separate files under the RCS installation. If you don’t need to define any (dynamic) content on IDM side, the “actionSource” key could contain just a string representing a path to a script file.
Note that in such case, the content of “actionSource” is NOT a script. If you attempt to evaluate it directly, you will get an error.
For example, if you try to request the very same system action to “run on connector” (notice absence of the
&scriptExecuteMode=resource
argument):IDM Admin Browser Console
(async function () { var data = await $.ajax('/openidm/system/groovy?_action=script&scriptId=script-2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ arg1: 'value1' }) }); console.log(JSON.stringify(data, null, 4)); }());
RCS will try to evaluate the “actionSource” content as a script, and you will see the following errors reported in the browser response:
{ "actions": [ { "error": "No such property: openicf for class: Script1661393119839" }, { "error": "No such property: openicf for class: Script1661393119971" } ] }
-
Scripted Groovy Connection Configuration > “systemActions” > Invoking from IDM Script
You can invoke a system action from an IDM script by calling openidm.action(resource, actionName, content, params, fields)
function for Actions supported on system resources (system/*
).
Scripted Groovy Connection Configuration > “systemActions” > Invoking from an IDM Script > Syntax
Except for the actual syntax, all the information used for invoking a system action via IDM’s REST applies here. Arguments passed into an openidm.action(resource, actionName, content, params, fields)
call can be mapped to the parts of a REST request for a system action in the following way:
-
resource
corresponds to the /openidm/system/<connection-name> part of the path. -
actionName
corresponds to the _action=script (“execute script” action) URL parameter and is always populated with ‘script’. -
content
is an object that in the REST request is described in the request body JSON. -
params
is an object containing additional arguments, such as scriptId=<script_id>, scriptExecuteMode=resource, and any additional arguments included in the query string of the REST request. -
fields
can be omitted.
Scripted Groovy Connection Configuration > “systemActions” > Invoking from IDM Script > Examples
For comparison, let’s assume the same connection configuration as in the previous REST examples:
provisioner.openicf-<connection-name>.json
{
[ . . . ]
"configurationProperties": {
"scriptOnResourceScriptFileName": "ScriptOnResourceScript.groovy",
[ . . . ]
},
"systemActions": [
{
"scriptId" : "script-1",
"actions" : [
{
"systemType" : ".*ScriptedConnector",
"actionType" : "groovy",
"actionSource" : "println 'actionSource bindings: '; println binding.variables; [1, 2, 3];"
}
]
}
]
}
Then, the system action identified by the “script-1” ID could be requested from a script in IDM in the following ways:
-
IDM Admin > Managed object > alpha_user > onRead Inline Script (text/javascript)
try { const response = openidm.action( 'system/groovy', 'script', { arg1: 'value1' }, { scriptId: 'script-1' } ); logger.error(String(response)); } catch (e) { logger.error(e.message); }
In IDM logs, you will see the
logger
output with the JSON returned by the system action:IDM logs
"SEVERE: { \"actions\": [ { \"result\": [ 1, 2, 3 ] } ] }"
In RCS logs you will see the bindings available for the “run on connector” script:
RCS logs
[rcs] actionSource bindings: [rcs] [arg1:value1, operation:RUNSCRIPTONCONNECTOR, options:OperationOptions: {CAUD_TRANSACTION_ID:1660248281913-d3e36ed8cab1c078539d-31401/0/5}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@28200dd8, log:org.identityconnectors.common.logging.Log@4d49370f]
-
IDM Admin > Managed object > alpha_user > onRead Inline Script (text/javascript)
try { const response = openidm.action( 'system/groovy', 'script', { arg1: 'value1' }, { scriptId: 'script-1', scriptExecuteMode: 'resource' } ); logger.error(String(response)); } catch (e) { logger.error(e.message); }
Note the additional
scriptExecuteMode: 'resource'
argument.Let’s assume the following “script on resource” content:
ScriptOnResourceScript.groovy
[ . . . ] println 'ScriptOnResourceScript.groovy bindings: ' + binding.variables Binding binding = new Binding(); binding.setVariable('args', scriptArguments) new GroovyShell(binding).evaluate(scriptText)
In IDM logs, you will still see JSON returned by the system action, which is the result of evaluating the “actionSource” content:
IDM logs
"SEVERE: { \"actions\": [ { \"result\": [ 1, 2, 3 ] } ] }"
The RCS logs, however, will now include output of the bindings available for both scripts, the hosted on RCS
ScriptOnResourceScript.groovy
and the script sent to RCS in “actionSource”, as both are going to be executed:RCS logs
[rcs] ScriptOnResourceScript.groovy bindings: [scriptArguments:[arg1:value1], scriptText:println 'actionSource bindings: '; println binding.variables; [1, 2, 3];, scriptLanguage:groovy, operation:RUNSCRIPTONRESOURCE, options:OperationOptions: {CAUD_TRANSACTION_ID:1660248944505-d3e36ed8cab1c078539d-32885/0/5}, configuration:org.forgerock.openicf.connectors.groovy.ScriptedConfiguration@4ebedbe8, log:org.identityconnectors.common.logging.Log@4f41fd7e] [ . . . ] [rcs] actionSource bindings: [rcs] [args:[arg1:value1]]
Once again, all considerations you might have for invoking a system action with a REST call, except the actual syntax, apply to invoking a system action from an IDM script.
Scripted Groovy Connection Configuration > “systemActions” > “run on resource” vs “run on connector”
Whether the script you send to RCS is run “on connector” or “on resource”, it is executed in the connector environment, on the connector server, where the connector package is deployed.
But the way your script content is used is different in the two modes:
-
For a “run on connector” operation the ICF framework executes the script sent to ICF, and the script has direct access to the connector type-specific context.
-
For a “run on resource” operation, the “actionFile” or “actionSource” script content is given as the
scriptText
variable to a script already hosted on RCS. The hosted-on-RCS script will be executed by ICF, and will perform its actions in the connector context. Optionally, the hosted script could evaluate the provided script text, or use it in any other way, or simply ignore thescriptText
binding.If
scriptText
contains a script to evaluate, its bindings can be defined only by the hosted on RCS script. As an example, the hosted on RCS script can share its own bindings defined by the ICF framework.
The “run on resource” script execution mode could be more efficient than the “run on connector” one because:
-
The hosted script is loaded, compiled, and cached once, when the connector is activated, while an “actionSource” or an “actionFile” script is evaluated every time a “run on connector” action is called.
-
The hosted script doesn’t have to be transmitted from IDM to RCS, while the “actionSource” or “actionFile” content does.
In addition, in a managed environment such as the Identity Cloud, you cannot host a custom script file and reference it in “actionFile”. Your system action-specific script content is limited to “actionSource”. Arguably, in this case, a more practical approach to perform a custom system action of any considerable complexity is executing a “run on resource” script, which will enable you to maintain the scripted functionality in separate file(s) hosted on the connector server.
It is possible to run any arbitrary script maintained outside of the connection configuration “on connector” by providing the script content in the request body and evaluating it in the “actionSource” script. The script will have access to all other arguments and to
RUNSCRIPTONCONNECTOR
bindings, but the same maintainability concerns might apply.For example:
provisioner.openicf-<connection-name>.json
{ [ . . . ], "systemActions": [ { "scriptId": "script-2", "actions": [ { "systemType": ".*ScriptedConnector", "actionType": "groovy", "actionSource": "new GroovyShell(binding).evaluate(connectorScript)" } ] } ] }
IDM Admin Browser Console
(async function () { var data = await $.ajax('/openidm/system/groovy?_action=script&scriptId=script-2', { method: 'POST', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ params: { param1: 2, param2: 2 }, connectorScript: ` // Any Groovy script: params.param1 + params.param2 ` }) }); console.log(JSON.stringify(data, null, 4)); }());
{ "actions": [ { "result": 4 } ] }
Scripted Groovy Connection Configuration > “systemActions” > Support in Connectors
The “systemAction” key and its content are only accepted and supported by connectors that implement Script on connector operation and Script on resource operation.
At the time of writing, the following (Java) connectors have implemented both:
In addition, the Salesforce connector has only “script on connector operation” implemented.
Although unrelated to Java RCS, Scripted connectors with PowerShell also support script on connector operation.
Commonly Used References
-
Identity Cloud / Identity and object-related REST APIs / System objects
-
Identity Cloud / Scripting in JavaScript / Functions available for use in identity scripts
-
Identity Cloud / Object modeling / Access data objects / Define and call data queries
Continues in The Basics of Developing Scripted Connectors for Java Remote Connector Server (Part 2).