Bridge agent JavaScript Plugins

Authoring JavaScript Plugins for Bridge Agent

This documentation details the development of JavaScript plugins for the IBM Security Verify Bridge agent. JavaScript plugins are used to implement customized user authentication and attribute retrieval logic for attribute sources other than the primary LDAP.

High level procedure

Ensure you have access to an IBM Security Verify (ISV) tenant and follow the steps in the following sections:

  1. Install the bridge agent
  2. Define the plugin configuration
  3. View the Quick Start examples

High level architecture

909

As of version 1.0.14, the bridge agent supports two different types of attributes sources:

  1. Primary LDAP
  2. Non-primary LDAP

By default, the bridge agent performs user authentication using the primary LDAP. Authentication and attribute retrieval are inbuilt and require no JavaScript plugin.

For advanced use-cases, non-primary LDAP attribute sources may be desired. These allow for customized attribute retrieval and authentication logic to be implemented by means of the JavaScript plugin. The bridge agent supports the following non-primary LDAP attribute sources:

  1. LDAP
  2. Oracle Database
  3. PostgreSQL
  4. IBM Db2

Bindings to these attribute sources are created by means of a JavaScript configuration (jsconfig). The logic for handling attribute retrieval and user authentication must be implemented by means of JavaScript plugin (jsplugin).

This guide details the creation of a jsconfig for configuring a connection to your attribute source and developing a jsplugin for implementing the attribute retrieval and user authentication logic.

Terminology

TermAlternate NamesDefinition
Onprem AgentBridge Agent, ldap agent, agent, identity agentThe agent that runs onprem and interfaces with one or more onprem attribute sources to perform user authentication and attribute retrieval.
Bridge Serviceagentbridgesvc, Agent Bridge ServiceISV microservice responsible for handling internal workload requests and the responses received from the onprem agent.
Support Serviceagentbridgesupsvc, Agent Bridge Support ServiceISV microservice responsible for managing agent configurations.
Data bindingjavascript bindingA connection to an attribute source established via golang utilities and exposed via javascript functions for processing by a javascript plugin.
Javascript pluginjspluginA javascript program written by an IBM Security Verify Bridge consumer. It implements logic for attribute retrieval and user authentication on behalf of the onprem agent via various database specific data bindings.
Plugin configurationjsconfigConfiguration required by the onprem agent to establish a data binding with an attribute source on behalf of a javascript plugin.
Primary LDAPauth LDAPThe primary LDAP directory against which username and password authentication is performed. Can be overriden via authenticationSource property.
Attribute sourcedata source, attribute store, data store, databaseA data source against which authentication can be performed and from which user attributes are retrieved. This can include: LDAP, Db2, PostgreSQL and OracleDB.
Authentication sourceauth sourceAn attribute source against which authentication should be performed. Is the primary LDAP by default.

Installing the Bridge Agent

Plugins are a new feature of v1.0.14 of the Bridge Agent which is provided as a Docker container. See Installing and configuring the Verify Bridge on Docker.

Important directories

The Bridge Agent will search for configuration files and JavaScript plugins in the two following directories:

DirectoryDescriptionFile Type
/go/src/jsconfigThis directory contains the jsconfig files. These define the binding details and identify the jsplugin. N.B. The bridge can load a maximum of 10 JavaScript plugins. The first 10 configurations in this directory will be loaded in alphabetical order, others will be ignored..json
/go/src/jspluginsThis directory contains the jsplugins, small JavaScript programs that will handle authentication and retrieval of user attributes.js
/certUsed for MTLS with LDAP plugins. The bridge will look here in
/cert/<clientCertLabel>_cert.pem /cert/<clientCertLabel>_key.pem
.pem

Place your configuration and plugin files in their respective directories. Docker bind mounts can be used to simplify this process. Refer to the example docker-compose.yml for using bind mounts

# Example docker-compose.yml
version: "3"

services:
    verify-bridge:
         image: icr.io/isv-saas/verify-bridge:latest


         container_name: bridge_agent
         volumes:
                - ./jsconfig:/go/src/jsconfig:ro
                - ./jsplugins:/go/src/jsplugins:ro
                - ./cert/:/cert:ro

         environment:
                TRACE: "true"
                LICENSE_ACCEPT: "yes"
                TENANT_URI: "https://tenant.verify.ibm.com"
                CLIENT_ID: "12345678-1234-1234-1234-123456789012"
                CLIENT_SECRET: "ABCDEFGHIJ"
          
                
        restart: always

The bind mount section

- ./jsconfig:/go/src/jsconfig:ro
- ./jsplugins:/go/src/jsplugins:ro
- ./cert/:/cert:ro

Mounts the local directories ./jsconfig, ./jsplugins, ./cert to the relevant locations inside the container. The ro option mounts these directories as read only, this is a good security practice for bind mounts as it ensures that running containers cannot make changes to the host filesystem.

Defining Plugin Configurations

Plugins configurations must be placed in the /go/src/jsconfig directory and must be valid JSON documents. The following properties are available:

Standard Configurations

PropertyDefinition
pluginNameThe name of the plugin. The Bridge Agent will search /go/src/jsplugins/<pluginName>.js for the matching plugin.
pluginTypeThe binding type. Must be one of ldap, db2, postgres or oracle.
executionOrderThe order in which the plugin should be executed relative to other plugins. Executed lowest to highest, i.e. 2 is executed before 3.
hardFailSet to true if you want the bridge to return if the plugin fails to execute or returns anything other than SUCCESS.
isAuthenticationSourceSet to true if this plugin is intended to be the authentication source. I.e. username/password authentication is performed against this attribute source. Setting this to true will automatically cause the plugin to behave as though hardFail were also true. When true, authentication will NOT be performed against the Primary LDAP, however attribute lookup will still be performed. To disable primary LDAP lookup entirely (including attribute lookup) you must set disablePrimaryLdapLookup to true. NB Only ONE (1) plugin can be an authentication source.
disablePrimaryLdapLookupDisables the primary LDAP. This should be set when isAuthenticationSource is true and you wish to also disable attribute lookup from the primary LDAP.
customThis is an optional section and can include custom properties to be passed into the jsplugins at execution time.

Binding Configurations

Binding configurations are used to establish a binding to the attribute source and define the binding's connection details. The column LDAP or DB denotes whether or not the property is expected for either LDAP or DB plugins (db2, postgres, oracle).

PropertyLDAP or DBDefinitionExample
connectionStringDBThe connection string used for establishing a binding to that particular data source. Only used for db2, postgres, and oracle. See connection strings.See connection strings.
maxPoolSizeBOTHMaximum number of connections in connection pool.50
minPoolSizeBOTHMinimum number of connections in connection pool.10
agedTimeoutBOTHTime in seconds before a connection is discarded.60
maxIdleTimeBOTHMaximum amount of time a connection can be idle before it's discarded.10
bindDnLDAPThe bind DN used to establish an LDAP binding.cn=admin,dc=ibm,dc=com
bindPasswordLDAPThe bind password used to establish an LDAP binding.pTms!REJECs@hzdNEGZ8
urisLDAPAn array of LDAP server URIs. This is used for failover, if the first server in the array fails the next will be tried, etc. N.B. Protocol is important here. If you wish to use TLS you must use the protocol ldaps://. Other TLS settings will be ignored if this is not set.ldaps://localhost:636
filterLDAPThe filter to use during LDAP lookup.(|(|(objectclass=ePerson)(objectclass=person))(objectclass=User))
userObjectClassesLDAPThe user object classes.top,Person,organizationalPerson,inetOrgPerson
selectorLDAPSelectors used during the LDAP lookup.objectClass,cn,sn,givenName,userPassword,streetAddress,seeAlso,mobile
baseDnLDAPThe base DN used during a standard LDAP lookup. Plugin logic can override this if needed.dc=ibm,dc=com
caCertLDAPThe CA cert used during TLS to verify the authenticity of the certificate presented by the LDAP server during TLS.-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgA...
insecureSkipVerifyLDAPSkip TLS certificate verification. N.B. should only be used in testing environments and not in production.false
tlsMinVersionLDAPSet the minimum TLS version allowed. See minimum tls version.771
clientCertLabelLDAPUsed during MTLS. Here you should define a label for the client certificate and key to be presented to the LDAP server. The Bridge Agent will look for this at /cert/<clientCertLabel>_cert.pem /cert/<clientCertLabel>_key.pemldap.client

Connection strings

Connection strings apply to database attribute sources (db2, oracle and postgres). Connection strings each have their own format and can be used to specify connection details such as TLS, timeouts and other database specific settings. Please review the documentation for your datasource for a full list of connection string options available. The follow are example connection strings:

Oracle Database

oracle://system:[email protected]:1521/XE?CONNECTION TIMEOUT=5

Postgres Database

host=host.docker.internal port=8788 dbname=postgres user=postgres password=postgrespassword connect_timeout=5

Db2 Database

HOSTNAME=host.docker.internal;PORT=50000;UID=db2inst1;PWD=db2_password;DATABASE=usersdb

Minimum TLS Version

The tlsMinVersion property defines the minimum allowed TLS version. This needs to be an integer. The following table is a mapping of these integers to their TLS version.

TLS VersionInteger
v1.0769
v1.1770
v1.2 (Default)771
v1.3772

Use the integer when setting the minimum TLS version, e.g. "tlsMinVersion": 770. Omitting this property or setting a bad value will cause it to default to 771 (TLS version 1.2).

Example LDAP configuration

The following is an example LDAP jsconfig that makes use of MTLS.

{
  "pluginName": "ldap",
  "pluginType": "ldap",
  "executionOrder": 2,
  "hardFail": false,
  "authenticationSource": {
    "isAuthenticationSource": false,
    "disablePrimaryLDAPLookup": false
  },
  "bindingConfig": {
    "bindDn": "cn=admin,dc=ibm,dc=com",
    "bindPassword": "pass",
    "uris": ["ldaps://host.docker.internal:8636", "http://localhost:8389"],
    "maxPoolSize": 50,
    "agedTimeout": 60,
    "connectTimeout": 5,
    "filter": "(|(|(objectclass=ePerson)(objectclass=person))(objectclass=User))",
    "userObjectClasses": "top,Person,organizationalPerson,inetOrgPerson",
    "selector": "objectClass,cn,sn,givenName,userPassword,streetAddress,seeAlso,mobile",
    "baseDn": "dc=ibm,dc=com"
    "tlsConfig": {
      "caCert": "-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIB...\n-----END CERTIFICATE-----\n",
      "insecureSkipVerify": false,
      "tlsMinVersion": 771,
      "clientCertLabel": "ldap.client"
    }
  }
}

Example Database Configuration

The following is an example of a PostgreSQL jsconfig.

{
  "pluginName": "PgPlug",
  "pluginType": "postgres",
  "executionOrder": 1,
  "hardFail": true,
  "authenticationSource": {
    "isAuthenticationSource": true,
    "disablePrimaryLDAPLookup": true
  },
  "bindingConfig": {
    "connectionString": "host=host.docker.internal port=8788 dbname=postgres user=postgres password=postgrespassword connect_timeout=5",
    "maxPoolSize": 50,
    "minPoolSize": 10,
    "agedTimeout": 60,
    "maxIdleTime": 10
  },
  "custom": {
    "table": "users"
  }
}

Developing JavaScript Plugins

JavaScript plugins consist of three important concepts:

  1. Input Objects
  2. Binding Functions
  3. outputData Object

Input objects provide instruction to the plugin, the operational workload to process, the current working object (results of previous operations) and a copy of the plugin configuration. These contain all the data necessary to perform your plugin's processing.

Binding function binding functions are the functions and classes made available for interacting with the data binding. These can be used for performing queries and retrieving attributes from the data source.

outputData Object
The output object is special object that must be created by your script. The output object must conform to a specific structure.

Input Objects

When the script is executed three input objects are passed into script:

Input ObjectDescription
requestParametersThe request received from ISV.
workingObjectParameters already retrieved from the Primary LDAP or previous plugins in the execution order.
pluginConfigThe plugin configuration as exists in the ./jsconfig directory. Plus the addition of the userAttributes object.

N.B these input objects are passed into the plugin script as strings, they must be parsed into JSON before being used:

let rp = JSON.parse(requestParameters);
let wo = JSON.parse(workingObject);
let pc = JSON.parse(pluginConfig);
requestParameters

This is the request payload at retrieved from ISV. It has the following properties:
The properties of importance are:

PropertyDescription
addressedToThe name of the module, this will always be ldapauth.
enqueuedTimeThe time the operation was enqueued.
operationThe operation to be performed by the plugin. Can be either password-verify or password-change.
parametersA parameter set corresponding to the specific operation.
password-verify example
{
  "addressedTo": "ldapauth",
  "enqueuedTime": "2023-10-15 05:15:36.769",
  "id": "jobUuId",
  "operation": "password-verify",
  "parameters": { 
       "password": "Passw0rd", 
       "username": "Scott" 
   }
}
password-change example
{
  "addressedTo": "ldapauth",
  "enqueuedTime": "2023-10-15 05:15:36.769",
  "id": "jobUuId",
  "operation": "password-change",
  "parameters": { 
       "oldpassword": "OldPassw0rd", 
       "newpassword": "NewPassw0rd", 
       "username": "Scott" 
   }
}

N.B You should only process password-change for your authentication source.

workingObject

The workingObject contains attributes that have already been retrieved from previous lookups in the lookup chain. It has the following structure:

PropertyDescription
groupsAn array of the ISV groups to which the user belong.
userAn object whose child objects correspond to the returned attributes for the user.
workingObject Example
{
  "groups": [],
  "user": {
    "dn": ["uid=scott,ou=Verify,dc=ibm,dc=com"],
    "mail": ["[email protected]"],
    "mobile": ["+61 2345 6789"],
    "sn": ["Administrator"],
    "uid": ["scott"]
  }
}

N.B. The value for the user attribute must be enclosed inside an array as in the above example.

pluginConfig

The pluginConfig object simply contains an exact copy of the plugin configuration for the plugin that is executing plus the userAttributes.

The userAttributes are the attributes that this plugin will be asked to retrieve. These are injected into the configuration when the script is called.

pluginConfig Example
// pluginConfig value that will be passed to the PgPlug.js plugin
{
  "pluginName": "PgPlug",
  "pluginType": "postgres",
  "executionOrder": 1,
  "hardFail": true,
  "authenticationSource": {
    "isAuthenticationSource": true,
    "disablePrimaryLDAPLookup": true
  },
  "bindingConfig": {
    "connectionString": "host=host.docker.internal port=8788 dbname=postgres user=postgres password=postgrespassword connect_timeout=5",
    "maxPoolSize": 50,
    "minPoolSize": 10,
    "agedTimeout": 60,
    "maxIdleTime": 10
  },
  "userAttributes": [ // These attributes sourced from ISV
       "phone",
       "address",
       "sn" 
  ]
  "custom": {
    "table": "users"
  }
}
pluginConfig userAttributes

userAttributes is the string array representation of the user attributes requested by ISV.

let pc = JSON.parse(pluginConfig);
let requestedAttributes = pc.userAttributes; // Array of attributes requested for this user

Binding functions

Binding functions are functions/classes supplied to your script that enable you to interface with your attribute source via the established connection binding.

There are different bindings functions for databases and ldap:

Logger

A logger binding function is provided. To log, add the following to the top of your Plugin script

importClass(logger);

The logger class provides the following logging functions:

FunctionDescription
trace(message)Log with level trace.
debug(message)Log with level debug.
info(message)Log with level info.
warn(message)Log with level warn.
error(message)Log with level error.
Logger Example
importClass(logger); // Import logger at the top of your plugin script

// Log with logging level info
logger.info(`Plugin script logging with level info`)
Logging level

Logging level is for your reference and will appear in the bridge agent's standard output.

time="2023-10-24T11:15:47Z" level=<log level> msg="log message" func=pkg.binding.logger.log

Database binding functions

To access the database binding functions place the following import at the top of your plugin script.

// Place this at the top of your Plugin.js file
importClass(database);

This will import the javascript object database which has the following functions defined on it:

database.query()

database.query() accepts

database.query(serverName, query); 

Parameters:

  • serverName (String): Specifies the internal server name of the database against which the query will be performed. This can be set to the value serverName which is a preset variable containing the correct value.
  • query (Object): An object of the form:
{
       "sql": "query string", 
       "args": [
              ["argument 1", "argument 2"]
       ]
}

The arguments will be substituted for parameter markers.

The following is an example of a complete call to database.query:

// Db2 style substitution using the `?` symbol
result = database.query(serverName, {
       "sql": "SELECT * FROM USERS WHERE ID = ? AND PASSWORD = ?",
       "args": [
              ["scott", "Password"]
       ]
}); 

The ? symbol above is a parameter marker is an substituted for the values in the args property.

N.B. the different database types use different parameter markers:

Parameter markers
DatabaseParameter Marker
Db2?
Oracle Database:1
PostgreSQL$1

The following is the same example but using PostgreSQL style parameter markers:

// Postgres style substitution using the `$1` symbol
result = database.query(serverName, {
       "sql": "SELECT * FROM USERS WHERE ID = $1 AND PASSWORD = $1",
       "args": [
              ["scott", "Password"]
       ]
}); 

Result Object

When the query returns it will return one of the following:

  1. Result Object
  2. Error Object
Result Object
PropertyDescription
rowsAffectedNumber of rows changed by operation.
rowsAn array of Object. Each object represents a row returned by the query. Each property of the object corresponds to a column and its value.
{
  "rowsAffected": 0, // Rows changed
  "rows": [          // Array of objects representing each row. Each property represents a column and its value
    {
      "CREATED_ON": "2023-11-02T00:00:00Z",
      "EMAIL": "[email protected]",
      "ISLOCKED": false,
      "LAST_LOGIN": "2023-10-24T00:00:00Z",
      "PASSWORD": "scottpass",
      "PGATTR": "scottpsec1234",
      "USERNAME": "Scott",
      "USER_ID": 2
    }
  ]
}

When no rows are returned, null will be returned for rows.

Example:

{"rowsAffected":0,"rows":null}
Error Object

An error object is returned when database.query fails.

PropertyDescription
errorThe error string.

Error result object example:

{ "error": "the error string" }

An error object will be returned for situations in which the query failed to execute properly.

database.execute()

Executes a parameterized SQL with optional arguments and returns the number of rows affected. This function works similarly to the database.query function but is used for INSERT, UPDATE, DELETE, DROP.

result = database.execute(serverName, query)

See database query parameters for a description of these parameters.

See result object for a description of the result and error objects.

Example
result = database.execute(serverName,
  {
    "sql": "DELETE FROM TEST_DB_BINDING WHERE ID = ?",
    "args": [
      ["test_key001"],
      ["test_key002"]
    ]
  }
);

Like with database.query, substitutions can be used.

database.ping()

The ping function is a simple query to check if the database is alive.

result = database.ping(serverName);

A healthy result will return the following:

{"rowsAffected":0,"rows":null}

Otherwise an error object will be returned.

LDAP binding functions

LDAP binding functions are provided via two helper classes:

ClassDescription
ldapUserLookupHelperLookup attributes and perform user authentication associated with a user ID.
ldapAttributeUtilCreate, modify, delete, and search for attributes according to their DNs.

See the quickstart section for examples of how these classes can be utilized by jsplugins.

User Lookup Helper

To access the LDAP binding functions place the following import at the top of your Plugin script

importClass(ldapUserLookupHelper);

This import exposes the following classes:

ClassOverview
UserLookupHelperProvides functions for the lookup of a user.
UserRepresents a user and provides functions for user operations.
UserLookupHelper class reference

The UserLookupHelper provides classes to assist with authentication and user attribute retrieval. Quickstart examples are provided.

Initialize UserLookupHelper with:

// Initialize new user lookup helper
let pc = JSON.parse(pluginConfig);
let configName = `${pc.pluginName}_config`
let userLookupHelper = new UserLookupHelper(configName);

The configName parameter is a string that must be supplied in the form <pluginName>_config. The above code sample extracts the pluginName from the pluginConfig object and formats it appropriately.

UserLookupHelper methods
MethodReturn TypeDescription
isReady()boolean or Errortrue when ready. Error object on error.
createUser(userName, dn, password, firstName, LastName)UserCreates an LDAP user. Returns a User object.
getUser(userName)UserReturns a User object with the corresponding User object.
getUserByNativeId(nativeId)UserReturns a User object. nativeId represents a full dn (distinguished name).
deleteUser(userName)boolean, nullDeletes user with username userName. Returns true if deleted successfully. May return null on error.

All of the above lookups (excluding createUser and getUserByNativeId) will be performed using the LDAP lookup configuration defined in the LDAP binding configuration (userIdentifier, baseDn, etc).

getUserByNativeId uses a a full DN to look for the user and therefore bypasses userIdentifier.

User object reference

The user object represents a user. It provides methods for user operations such as password authentication and attribute retrieval.

Initialization

This object should not be initialized manually, rather it should be returned via a UserLookupHelper function.

let ulh = new UserLookupHelper(configName);
user = ulh.getUser(`Scott`) // returned user is a User object for the user with id `Scott`
User methods
MethodReturn TypesDescription
hasError()booleanReturns true if the user object has an error. This error can be set during user lookup function (getUser, getUserByNativeId, createUser). N.B. You should check this value before calling any other method on a User object.
getError()stringReturns the value of the Error object (the error message).
authenticate(password)bool, nullReturns true when password string is correct for user.
getId()string, nullReturns the userId/username of the user.
getNativeId()string, nullReturn full distinguished name (dn) of the user.
getAttributeNames()ArrayReturns array of the attribute names for this user. Note: this does not return the values of these attributes.
getAttribute(attrName)string, nullReturns the value for the attribute attrName. null if no corresponding attribute found. Note: if the attribute attrName returns an array, this function will return the first element of that array. If you wish to return this array use the getAttributes method.
getAttributes(attrName)Array, nullReturns an array of values corresponding to the attribute name attrName. If the value of attrName isn't an array it will be returned as a single member array containing that value.
attributeExists(attrName)booleanReturns true if the user attribute attrName exists.
AttributeUtil class reference

The ldapAttributeUtil classes can be used to perform custom attribute lookup and manipulation operations. A quickstart example is provided.

Import the helper class with:

// Add import to the top of your javascript plugin
importClass(ldapAttributeUtil);

This import exposes the following classes:

ClassOverview
LdapAttributeUtilUsed for LDAP attribute creation and retrieval.
AttributeRepresents an array of attribute values corresponding to an attribute ID.
AttributesRepresents an array of Attribute objects.
SearchResultRepresents a search result entry.
NamingEnumerationClass to enumerate over a provided type.
LdapAttributeResultRepresents the response to the attribute util operation.
LdapAttributeUtil reference

The LdapAttributeUtil is used for LDAP attribute creation and retrieval. Initialize LdapAttributeUtil by using the plugin configuration name:

// Initialize attribute util
let pc = JSON.parse(pluginConfig);
let configName = `${pc.pluginName}_config`
let attrUtil = new LdapAttributeUtil(configName);
LdapAttributeUtil methods
MethodReturn TypesDescription
init()LdapAttributeResultPerforms a Healthcheck against the LDAP attribute source and returns an LdapAttributeResult object. On Healthcheck failure, the result property of this object is an Error object.
isReady()booleanPerforms a Healthcheck against LDAP attribute source. Returns true when healthy.
addAttributeValue(dn, attributeName, attributeValue)LdapAttributeResultAdds an LDAP attribute attributeName with value attributeValue to dn.
createSubContext(dn, attributes)LdapAttributeResultCreates the subcontext dn with the attributes attributes. For information about how to specify input attributes, see attributes format.
getAttributeValue(dn, attributeNames)LdapAttributeResultRetrieves the attribute values of dn. The input parameter attributeNames must be a JSON encoded array of attribute names to be retrieved. For example `['apples', 'banana'].
removeAttribute(dn, attributeName, attributeValue)LdapAttributeResultRemoves attribute attributeName with value attributeValue from dn.
search(dn, filter)LdapAttributeResultSearch for entries at dn with filter filter. For example: (objectclass=*).
setAttributeValue(dn, attributeName, attributeValue)LdapAttributeResultSets the attributeName attribute of the dn to the value attributeValue.
attributes format

Specify the attributes as a JSON object that maps the attribute name to an array of its values. Example:

// createSubContext(dn, attributes)
attrUtil.createSubContext("ou=newOu1", {"objectClass": ["top", "organizationalUnit"]}
Attribute reference

The Attribute object represents an array of attribute values.

PropertyTypeDescription
idstringThe attribute ID
attrArrayAn array of attribute values
MethodReturn TypesDescription
constructor(id, attr)AttributeCreates an Attribute object with the ID id and an array of attribute values attr.
isOrdered()booleanAlways returns true.
getID()stringReturn the id property of the attribute.
get(idx)stringReturns the attribute value indexed by idx.
getAll()NamingEnumeration of attribute valuesReturns a NamingEnumeration of the attribute values that this object represents.
size()intReturns the number of values this object represents.
contains(attrVal)booleanReturns true when this attribute contains the value attrVal.
Attributes reference

The Attributes object represents an array of Attribute objects.

PropertyTypeDescription
attrsObjectObject with properties that correspond to Attribute objects.
MethodReturn TypesDescription
constructor(attrs)AttributesTakes an array of attr JSON objects and returns an Attributes object.
isCaseIgnored()booleanAlways returns false.
size()intReturns the number of attributes represented by the object.
get(attrID)AttributeReturns a specific Attribute with the attribute ID attrID.
getIDs()NamingEnumeration of stringReturns the attribute IDs represented by this object.
getAll()NamingEnumeration of AttributeReturns the values of the attributes represented by this object.
attr JSON object format
{
  "name": "attributeName", // The attribute name
  "attrs": [] // Array of attribute values
}
NamingEnumeration reference

The NamingEnumeration provides functions to count through an array of objects. The underlying array can be accessed directly through the results property. next() returns the next item in the enumeration.

PropertyTypeDescription
typeThe type that the result of next() is wrapped in.
wrapObjbooleanWhen true the result of next() wrapped in type.
resultsArrayAn array of results to be enumerated.
curIdxintThe current enumeration index.
MethodReturn TypesDescription
constructor(results, wrapObj, type)NamingEnumerationAccepts an array of results. Set wrapObj to true if results must be wrapped in type before it is returned.
hasMore()booleanReturns true when more elements exist in the enumeration.
next()SearchResult, Attribute, string, ObjectReturns the next element of the enumeration. It is of the expected type.
SearchResult reference

Represents a search result entry.

PropertyTypeDescription
entrystringSearch result entry.
entry.attributesAttributesReturned attributes.
entry.namestringReturned attribute entries.
MethodReturn TypesDescription
getAttributes()AttributesReturns an Attributes object that contains the attributes that the search returns.
getName()stringReturns the name of the search result entry.
LdapAttributeResult reference

The LdapAttributeResult class represents an attribute operation response. Properties defined on this class:

PropertyTypeDescription
cfgNamestringName of the plugin configuration. It is in the form <pluginName>_config.
resultObjectThe result of the operation.
namingEumNamingEnumerationEnumeration of search results as SearchResult objects.
attrsAttributesAttributes from SearchResult.

Methods defined on the LdapAttributeResult class:

MethodReturn TypesDescription
getAttributes()AttributesReturns the attributes that LdapAttributeUtil.getAttributeValue() requests.
getNamingEnumeration()NamingEnumeration of SearchResultReturns the NamingEnumeration result of the LdapAttributeUtil.search() operation.
isSuccessful()booleanReturns true if the operation is completed with an error.
hasError()booleanReturns true if operation completed with an error.
getError()ErrorReturns the error that occurred.
getNamingException()ErrorAlias for getError(). Returns the error that occurred.

outputData Object reference

Your plugin script must end by defining an object called outputData. The outputData object represents a response payload to be returned to ISV. It has the following properties:

PropertyDefinition
status.resultThe result code.
status.messageAn optional status message.
parameters.groupsAn array of ISV group objects to which a user should belong.
userAn object whose child objects represent the parameters returned from this operation. Note: the value of each attribute must be an array.
Example
// Define this object at the end of your plugin script
outputData = {
  status: {
    result: "Success",
    message: "",
  },
  parameters: {
    groups: [
      {
        name: "AGroup1",
        id: "AGroup1Id1",
      },
      {
        name: "AGroup2",
        id: "AGroup2Id2",
      },
    ],
    user: {
      attribute1: ["value for attribute 1"],
      attribute2: ["value for attribute 2"],
    },
  },
};
Group Object
PropertyDescription
nameThe displayName of the ISV group.
idThe id of the ISV group.

You can obtain group ID's via the /v2.0/Groups ISV endpoint.

Example:

curl --request GET \
  --url https://tenant.verify.ibm.com/v2.0/Groups \
  --header 'authorization: Bearer <token>'
// Response from /v2.0/Groups
{
  "displayName": "developer",
  "meta": {
    "created": "2023-03-15T04:52:32Z",
    "lastModified": "2023-03-15T05:06:57Z"
  },
  "id": "60500081MT",
  "urn:ietf:params:scim:schemas:extension:ibm:2.0:Group": {
    "totalMembers": 1,
    "groupType": "reserved",
    "description": ""
  }
}

Setting group objects will JIT provision the returned user into the relevant ISV group.

Quick Start

This section contains simplistic/minimal example plugins scripts. To use these examples you must first define a plugin configuration to point to the script and correctly establish the binding.

LDAP Minimal Scaffold

The following scaffold is an incomplete LDAP plugin. Logic for authentication and attribute retrieval can be added in the TODO section.

// LDAP minimal scaffold

// Import binding functions
importClass(logger);
importClass(ldapUserLookupHelper);

// Parse Input Objects
let rp = JSON.parse(requestParameters);
let wo = JSON.parse(workingObject);
let pc = JSON.parse(pluginConfig);

// Initialize the UserLookupHelper
let configName = `${pc.pluginName}_config`;
let userLookupHelper = new UserLookupHelper(configName);

// Define the output object to be returned
let output = {
  status: {
    result: "",
    message: "",
  },
  parameters: {
    groups: [],
    user: {},
  },
};

// Inserting main logic inside a function allows for `return` to be used in order to break execution
let process = () => {
  // TODO: Insert plugin logic here
};
process();

// End script by defining the outputData object
let outputData = output;

LDAP Simple Lookup Example

// simpleLdapLookup.js - Simple 

// Import binding functions
importClass(logger);
importClass(ldapUserLookupHelper);

// Parse Input Objects
let rp = JSON.parse(requestParameters);
let wo = JSON.parse(workingObject);
let pc = JSON.parse(pluginConfig);

// Initialize the UserLookupHelper
let configName = `${pc.pluginName}_config`;
let userLookupHelper = new UserLookupHelper(configName);

// Define the output object to be returned
let output = {
  status: {
    result: "",
    message: "",
  },
  parameters: {
    groups: [],
    user: {},
  },
};

// Inserting main logic inside a function allows for `return` to be used in order to break execution
let process = () => {
  // Get the requested user
  let userName = rp.parameters.username;

  // Get the requested attributes
  let requestedAttributes = pc.userAttributes;
  if (!Array.isArray(requestedAttributes)) {
    requestedAttributes = Array.from([requestedAttributes]);
  }

  // Get the user
  let user = userLookupHelper.getUser(userName);

  // Check if user has error
  if (user.hasError()) {
    // Get the error object
    let errorString = user.getError();

    // Log error
    logger.info(`Unable to get user: ${errorString}`);

    // Update the output object with the error
    output.status.result = "ERROR";
    output.status.message = errorString;

    // Return will break execution and cause the output object to return with the above error
    return;
  }

  // Get the requested attributes
  requestedAttributes.forEach((attrName) => {
    // Retrieve this attribute from the user
    output.parameters.user[attrName] = user.getAttribute(attrName);
  });

  // Set the status to successful
  output.status.result = "SUCCESS";
};
process();

// End script by defining the outputData object
let outputData = output;

LDAP Simple Authentication Example

This example adds authentication to the LDAP Simple Lookup Example.

Make sure to identify this plugin as an authentication source in the plugin configuration:

// simpleLdapAuth_config.json
"authenticationSource": {
  "isAuthenticationSource": true,
  "disablePrimaryLDAPLookup": true
},
// simpleLdapAuth.js

// Import binding functions
importClass(logger);
importClass(ldapUserLookupHelper);

// Parse Input Objects
let rp = JSON.parse(requestParameters);
let wo = JSON.parse(workingObject);
let pc = JSON.parse(pluginConfig);

// Initialize the UserLookupHelper
let configName = `${pc.pluginName}_config`;
let userLookupHelper = new UserLookupHelper(configName);

// Define the output object to be returned
let output = {
  status: {
    result: "",
    message: "",
  },
  parameters: {
    groups: [],
    user: {},
  },
};

// Inserting main logic inside a function allows for `return` to be used in order to break execution
let process = () => {
  // Get the requested user
  let userName = rp.parameters.username;
  let password = rp.parameters.password;

  // Get the requested attributes
  let requestedAttributes = pc.userAttributes;
  if (!Array.isArray(requestedAttributes)) {
    requestedAttributes = Array.from([requestedAttributes]);
  }

  // Get the user
  let user = userLookupHelper.getUser(userName);

  // Check if user has error
  if (user.hasError()) {
    // Get the error object
    let errorString = user.getError();

    // Log error
    logger.info(`Unable to get user: ${errorString}`);

    // Update the output object with the error
    output.status.result = "ERROR";
    output.status.message = errorString;

    // Return will break execution and cause the output object to return with the above error
    return;
  }

  // Perform authentication
  if (user.authenticate(password) !== true) {
    output.status.result = "PASSWORD_NOT_VALID";
    output.status.message = "Invalid user password";
    return;
  }

  // Get the requested attributes
  requestedAttributes.forEach((attrName) => {
    // Retrieve this attribute from the user
    output.parameters.user[attrName] = user.getAttribute(attrName);
  });

  // Set the status to successful
  output.status.result = "SUCCESS";
};
process();

// End script by defining the outputData object
let outputData = output;

Notice that this example only differs from the simple LDAP lookup example in that it adds authentication:

// Perform authentication
if (user.authenticate(password) !== true) {
  output.status.result = "PASSWORD_NOT_VALID";
  output.status.message = "Invalid user password";
  return;
}

LDAP Attribute Util Example

This example demonstrates how to use the LdapAttributeUtil class to set, retrieve, and search for LDAP attributes. Output is given through the logger. The outputData object does not contain any attributes.

// LdapAttributeUtil Example

// Import binding functions
importClass(logger);
importClass(ldapUserLookupHelper);
importClass(ldapAttributeUtil);


// Parse Input Objects
let rp = JSON.parse(requestParameters);
let wo = JSON.parse(workingObject);
let pc = JSON.parse(pluginConfig);

// Initialize the UserLookupHelper
let configName = `${pc.pluginName}_config`;
let userLookupHelper = new UserLookupHelper(configName);
let attrUtil = new LdapAttributeUtil(configName)


// Define the output object to be returned
let output = {
  status: {
    result: "",
    message: "",
  },
  parameters: {
    groups: [],
    user: {},
  },
};

// Inserting main logic inside a function allows for `return` to be used in order to break execution
let process = () => {

  if (!attrUtil.isReady()) {
    output.status.result = "ERROR";
    output.status.message = "Attribute Utility failed healthcheck";
    return;
  }

  // Random integer for sub context
  let rand = Math.floor(Math.random() * 100);
  let newOu = `ou=newOu${rand}`;

  // Create a new sub context
  let resultCreateSubContextErr = attrUtil.createSubContext(newOu, {"objectClass": ["top", "organizationalUnit"]});
  if (resultCreateSubContextErr.hasError()) {
    let error = resultCreateSubContextErr.getError();
    output.status.result = "ERROR";
    output.status.message = error;
    return;
  }
  logger.info(`Created sub context entry ${newOu}`);

  // Create an attribute
  let attributeName = "description";
  let attributeValue = "1234";
  let attributeValue2 = "4567";
  let resultAddAttributeValue = attrUtil.addAttributeValue(newOu, attributeName, attributeValue);
  if (resultAddAttributeValue.hasError()) {
    let error = resultAddAttributeValue.getError();
    output.status.result = "ERROR";
    output.status.message = error;
    return;
  }
  logger.info(`Added attribute ${attributeName} with value ${attributeValue} to the entry ${newOu}`);

  // Validate that the entry has been written successfully by retrieving the value
  let resultGetAttributeValue = attrUtil.getAttributeValue(newOu, ['description']);
  if (resultGetAttributeValue.hasError()) {
    let error = resultGetAttributeValue.getError();
    output.status.result = "ERROR";
    output.status.message = error;
    return;
  }

  // Check value is correct
  let retrievedAttr = resultGetAttributeValue.getAttributes().get(attributeName).get(0); 
  if (retrievedAttr !== attributeValue) {
    let error = `Retrieved attribute ${retrievedAttr}, expected ${attributeValue}`;
    logger.info(error);
    output.status.result = "ERROR";
    output.status.message = error;
    return;
  }
  logger.info(`Retrieved attribute ${retrievedAttr}, matches expected ${attributeValue}`);

  // Modify the new entries description value
  let resultSetAttributeValue = attrUtil.setAttributeValue(newOu, attributeName, attributeValue2);
  if (resultSetAttributeValue.hasError()) {
    let error = resultSetAttributeValue.getError();
    output.status.result = "ERROR";
    output.status.message = error;
    return;
  }
  logger.info(`Set entry ${newOu} attribute ${attributeName} to value ${attributeValue2}`);

  // Remove the description value
  let resultRemoveAttribute = attrUtil.removeAttribute(newOu, attributeName, attributeValue2);
  if (resultRemoveAttribute.hasError()) {
    let error = resultRemoveAttribute.getError();
    output.status.result = "ERROR";
    output.status.message = error;
    return;
  }
  logger.info(`Removed attribute ${attributeName} with value ${attributeValue2} from entry ${newOu}`);

  // Perform a search
  let resultSearch = attrUtil.search(newOu, "(objectclass=top)");
  if (resultSearch.hasError()) {
    let error = resultSearch.getError();
    output.status.result = "ERROR";
    output.status.message = error;
    return;
  }
  logger.info(`Removed attribute ${attributeName} with value ${attributeValue2} from entry ${newOu}`);

  // Enumerate through the search results
  let searchEnumerator = resultSearch.getNamingEnumeration();
  let i = 0;
  while (searchEnumerator.hasMore()) {
    let searchResult = searchEnumerator.next();
    logger.info(`Search results [${i}]: ${searchResult.getName()}`);
  }


  // Set the status to successful
  output.status.result = "SUCCESS";
};
process();

// End script by defining the outputData object
let outputData = output;