Create Custom Endpoints to Launch Scripts

Custom endpoints let you run arbitrary scripts through the REST API.

A custom endpoint configuration includes an inline script, in either JavaScript or Groovy. The script provides the endpoint functionality.

{
    "type" : "groovy",
    "source" : "import org.forgerock.json.resource.ActionRequest import org.forgerock.json.resource.CreateRequest import org.forgerock.json.resource.DeleteRequest import org.forgerock.json.resource.NotSupportedException import org.forgerock.json.resource.PatchRequest import org.forgerock.json.resource.QueryRequest import org.forgerock.json.resource.ReadRequest import org.forgerock.json.resource.UpdateRequest if (request instanceof CreateRequest) { return [ method: \"create\", resourceName: request.resourcePath, newResourceId: request.newResourceId, parameters: request.additionalParameters, content: request.content.getObject(), context: context.toJsonValue().getObject() ] } else if (request instanceof ReadRequest) { return [ method: \"read\", resourceName: request.resourcePath, parameters: request.additionalParameters, context: context.toJsonValue().getObject() ] } else if (request instanceof UpdateRequest) { return [ method: \"update\", resourceName: request.resourcePath, revision: request.revision, parameters: request.additionalParameters, content: request.content.getObject(), context: context.toJsonValue().getObject() ] } else if (request instanceof PatchRequest) { return [ method: \"patch\", resourceName: request.resourcePath, revision: request.revision, patch: request.patchOperations, parameters: request.additionalParameters, context: context.toJsonValue().getObject() ] } else if (request instanceof QueryRequest) { // query results must be returned as a list of maps return [ [ method: \"query\", resourceName: request.resourcePath, pagedResultsCookie: request.pagedResultsCookie, pagedResultsOffset: request.pagedResultsOffset, pageSize: request.pageSize, queryId: request.queryId, queryFilter: request.queryFilter.toString(), parameters: request.additionalParameters, context: context.toJsonValue().getObject() ] ] } else if (request instanceof DeleteRequest) { return [ method: \"delete\", resourceName: request.resourcePath, revision: request.revision, parameters: request.additionalParameters, context: context.toJsonValue().getObject() ] } else if (request instanceof ActionRequest) { return [ method: \"action\", action: request.action, content: request.content.getObject(), parameters: request.additionalParameters, context: context.toJsonValue().getObject() ] } else { throw new NotSupportedException(request.getClass().getName()); }",
    "apiDescription" : {
        "title" : "Echo",
        "description" : "Service that echo's your HTTP requests.",
        "mvccSupported" : false,
        "create" : {
            "description" : "Echo a CREATE request.",
            "mode" : "ID_FROM_SERVER",
            "singleton" : false
        },
        "read" : { "description" : "Echo a READ request." },
        "update" : { "description" : "Echo an UPDATE request." },
        "delete" : { "description" : "Echo a DELETE request." },
        "patch" : {
            "description" : "Echo a PATCH request.",
            "operations" : [ "ADD", "REMOVE", "REPLACE", "INCREMENT", "COPY", "MOVE", "TRANSFORM" ]
        },
        "actions" : [
            {
                "description" : "Echo an ACTION request.",
                "name" : "echo",
                "request" : { "type" : "object" },
                "response" : {
                    "title" : "Echo action response",
                    "type" : "object",
                    "properties" : {
                        "method" : {
                            "type" : "string",
                            "enum" : [ "action" ]
                        },
                        "action" : { "type" : "string" },
                        "content" : { "type" : "object" },
                        "parameters" : { "type" : "object" },
                        "context" : { "type" : "object" }
                    }
                }
            }
        ],
        "queries" : [
            {
                "description" : "Echo a query-filter request.",
                "type" : "FILTER",
                "queryableFields" : [ "*" ]
            },
            {
                "description" : "Echo a query-all request.",
                "type" : "ID",
                "queryId" : "query-all"
            },
            {
                "description" : "Echo a query-all-ids request.",
                "type" : "ID",
                "queryId" : "query-all-ids"
            }
        ],
        "resourceSchema" : {
            "title" : "Echo resource",
            "type" : "object",
            "properties" : {
                "method" : {
                    "title" : "CREST method",
                    "type" : "string"
                },
                "resourceName" : { "type" : "string" },
                "parameters" : { "type" : "object" },
                "context" : { "type" : "object" }
            }
        }
    }
}

Custom Endpoint Configuration

A custom endpoint configuration has the following structure:

{
    "context" : "context path",
    "type" : "script language",
    "source" : "script source" | "file" : "script file",
    "apiDescription" : "API descriptor object"
}
context

string, optional

The root URL path for the endpoint, in other words, the route to the endpoint. An endpoint with the context endpoint/test is addressable over REST at the URL http://localhost:8080/openidm/endpoint/test or by using a script such as openidm.read("endpoint/test").

Endpoint contexts support wild cards, as shown in the preceding example. The endpoint/linkedview/* route matches the following patterns:

endpoint/linkedView/managed/user/bjensen
endpoint/linkedView/system/ldap/account/bjensen
endpoint/linkedView/
endpoint/linkedView

The context parameter is not mandatory in the endpoint configuration file. If you do not include a context, the route to the endpoint is identified by the name of the file. For example, in the sample endpoint configuration provided in openidm/samples/example-configurations/custom-endpoint/conf/endpoint-echo.json, the route to the endpoint is endpoint/echo.

type

string, required

The type of script to be executed, either text/javascript or groovy.

file or source

The path to the script file, or the script itself, inline.

For example:

"file" : "workflow/gettasksview.js"

or

"source" : "require('linkedView').fetch(request.resourcePath);"
apiDescription

JSON object, optional

Describes the custom endpoint and includes its documentation in the REST API Explorer.

Custom Endpoint Scripts

The custom endpoint script files in the samples/example-configurations/custom-endpoint/script directory demonstrate all the HTTP operations that can be called by a script. Each HTTP operation is associated with a method - create, read, update, delete, patch, action, or query. Requests sent to the custom endpoint return a list of the variables available to each method.

All scripts are invoked with a global request variable in their scope. This request structure carries all the information about the request.

Warning

Read requests on custom endpoints must not modify the state of the resource, either on the client or the server, as this can make them susceptible to CSRF exploits.

The standard READ endpoints are safe from Cross Site Request Forgery (CSRF) exploits because they are inherently read-only. That is consistent with the Guidelines for Implementation of REST, from the US National Security Agency, as "... CSRF protections need only be applied to endpoints that will modify information in some way."

Custom endpoint scripts must return a JSON object. The structure of the return object depends on the method in the request.

The following example shows the create method in the echo.js file:

if (request.method === "create") {
    return {
        method: "create",
        resourceName: request.resourcePath,
        newResourceId: request.newResourceId,
        parameters: request.additionalParameters,
        content: request.content,
        context: context.current
    }
}

The following example shows the query method in the echo.groovy file:

else if (request instanceof QueryRequest) {
    // query results must be returned as a list of maps
    return [
        [
            method: "query",
            resourceName: request.resourcePath,
            pagedResultsCookie: request.pagedResultsCookie,
            pagedResultsOffset: request.pagedResultsOffset,
            pageSize: request.pageSize,
            queryId: request.queryId,
            queryFilter: request.queryFilter.toString(),
            parameters: request.additionalParameters,
            context: context.toJsonValue().getObject()
        ]
    ]
}

Depending on the method, the variables available to the script can include the following:

resourceName

The name of the resource, without the endpoint/ prefix, such as echo.

newResourceId

The identifier of the new object, available as the results of a create request.

revision

The revision of the object.

parameters

Any additional parameters provided in the request. The sample code returns request parameters from an HTTP GET with ?param=x, as "parameters":{"param":"x"}.

content

Content based on the latest revision of the object, using getObject.

context

The context of the request, including headers and security. For more information, see Request Context Chain.

Paging parameters

The pagedResultsCookie, pagedResultsOffset, and pageSize parameters are specific to query methods. For more information see "Page Query Results".

Query parameters

The queryId and queryFilter parameters are specific to query methods. For more information see "Construct Queries".

Script Exceptions

Some custom endpoint scripts require exception-handling logic. To return meaningful messages in REST responses and in logs, you must comply with the language-specific method of throwing errors.

A script written in JavaScript should comply with the following exception format:

throw {
    "code": 400, // any valid HTTP error code
    "message": "custom error message",
    "detail" : {
        "var": parameter1,
        "complexDetailObject" : [
            "detail1",
            "detail2"
        ]
    }
}

Any exceptions will include the specified HTTP error code, the corresponding HTTP error message, such as Bad Request, a custom error message that can help you diagnose the error, and any additional detail that you think might be helpful.

A script written in Groovy should comply with the following exception format:

import org.forgerock.json.resource.ResourceException
import org.forgerock.json.JsonValue

throw new ResourceException(404, "Your error message").setDetail(new JsonValue([
    "var": "parameter1",
    "complexDetailObject" : [
        "detail1",
        "detail2"
    ]
]))

Write an API Descriptor for a Custom Endpoint

Most Identity Cloud endpoints are described in the REST API Explorer. Documentation is not generated automatically for custom endpoints.

To generate the documentation for your custom endpoint in the API Explorer, add an apiDescription object to your custom endpoint configuration file. The apiDescription object includes the following properties:

title

The endpoint name, which expresses its purpose. For example, Audit, or Authentication.

description

A description of the endpoint.

mvccSupported

Boolean, true or false.

Indicates whether object versioning is supported. If you want to use If-None-Match or If-Match headers in read, delete, and patch requests, you must set "mvccSupported" : true.

Operations

An object that describes each operation supported on that endpoint (create, read, update, delete, patch, actions, and queries).

resourceSchema

The schema for the objects at this endpoint.

To see examples of the API descriptors included in Identity Cloud, log in to the Admin UI then point your browser to http://localhost:8080/openidm?_crestapi.

Compare the descriptors at that URL with what you see in the API Explorer.

In addition, the sample configuration file (openidm/samples/example-configurations/custom-endpoint/conf/endpoint-echo.json) shows how API descriptors must be constructed:

{
    "apiDescription" : {
        "title" : "Echo",
        "description" : "Service that echo's your HTTP requests.",
        "mvccSupported" : false,
        "create" : {
            "description" : "Echo a CREATE request.",
            "mode" : "ID_FROM_SERVER",
            "singleton" : false
        },
        "read" : { "description" : "Echo a READ request." },
        "update" : { "description" : "Echo an UPDATE request." },
        "delete" : { "description" : "Echo a DELETE request." },
        "patch" : {
            "description" : "Echo a PATCH request.",
            "operations" : [ "ADD", "REMOVE", "REPLACE", "INCREMENT", "COPY", "MOVE", "TRANSFORM" ]
        },
        "actions" : [
            {
                "description" : "Echo an ACTION request.",
                "name" : "echo",
                "request" : { "type" : "object" },
                "response" : {
                    "title" : "Echo action response",
                    "type" : "object",
                    "properties" : {
                        "method" : {
                            "type" : "string",
                            "enum" : [ "action" ]
                        },
                        "action" : { "type" : "string" },
                        "content" : { "type" : "object" },
                        "parameters" : { "type" : "object" },
                        "context" : { "type" : "object" }
                    }
                }
            }
        ],
        "queries" : [
            {
                "description" : "Echo a query-filter request.",
                "type" : "FILTER",
                "queryableFields" : [ "*" ]
            },
            {
                "description" : "Echo a query-all request.",
                "type" : "ID",
                "queryId" : "query-all"
            },
            {
                "description" : "Echo a query-all-ids request.",
                "type" : "ID",
                "queryId" : "query-all-ids"
            }
        ],
        "resourceSchema" : {
            "title" : "Echo resource",
            "type" : "object",
            "properties" : {
                "method" : {
                    "title" : "CREST method",
                    "type" : "string"
                },
                "resourceName" : { "type" : "string" },
                "parameters" : { "type" : "object" },
                "context" : { "type" : "object" }
            }
        }
    }
}

This object generates API documentation in the API explorer that looks like this:

API documentation for the sample "Echo" custom endpoint
Read a different version of :