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:
High level architecture
As of version 1.0.14
, the bridge agent supports two different types of attributes sources:
- Primary LDAP
- 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:
- LDAP
- Oracle Database
- PostgreSQL
- 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
Term | Alternate Names | Definition |
---|---|---|
Onprem Agent | Bridge Agent , ldap agent , agent , identity agent | The agent that runs onprem and interfaces with one or more onprem attribute sources to perform user authentication and attribute retrieval. |
Bridge Service | agentbridgesvc , Agent Bridge Service | ISV microservice responsible for handling internal workload requests and the responses received from the onprem agent . |
Support Service | agentbridgesupsvc , Agent Bridge Support Service | ISV microservice responsible for managing agent configurations. |
Data binding | javascript binding | A connection to an attribute source established via golang utilities and exposed via javascript functions for processing by a javascript plugin . |
Javascript plugin | jsplugin | A 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 configuration | jsconfig | Configuration required by the onprem agent to establish a data binding with an attribute source on behalf of a javascript plugin . |
Primary LDAP | auth LDAP | The primary LDAP directory against which username and password authentication is performed. Can be overriden via authenticationSource property. |
Attribute source | data source , attribute store , data store , database | A data source against which authentication can be performed and from which user attributes are retrieved. This can include: LDAP , Db2 , PostgreSQL and OracleDB . |
Authentication source | auth source | An 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:
Directory | Description | File Type |
---|---|---|
/go/src/jsconfig | This 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/jsplugins | This directory contains the jsplugins , small JavaScript programs that will handle authentication and retrieval of user attributes | .js |
/cert | Used 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
Property | Definition |
---|---|
pluginName | The name of the plugin. The Bridge Agent will search /go/src/jsplugins/<pluginName>.js for the matching plugin. |
pluginType | The binding type. Must be one of ldap , db2 , postgres or oracle . |
executionOrder | The order in which the plugin should be executed relative to other plugins. Executed lowest to highest, i.e. 2 is executed before 3 . |
hardFail | Set to true if you want the bridge to return if the plugin fails to execute or returns anything other than SUCCESS . |
isAuthenticationSource | Set 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. |
disablePrimaryLdapLookup | Disables the primary LDAP. This should be set when isAuthenticationSource is true and you wish to also disable attribute lookup from the primary LDAP. |
custom | This 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
).
Property | LDAP or DB | Definition | Example |
---|---|---|---|
connectionString | DB | The 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. |
maxPoolSize | BOTH | Maximum number of connections in connection pool. | 50 |
minPoolSize | BOTH | Minimum number of connections in connection pool. | 10 |
agedTimeout | BOTH | Time in seconds before a connection is discarded. | 60 |
maxIdleTime | BOTH | Maximum amount of time a connection can be idle before it's discarded. | 10 |
bindDn | LDAP | The bind DN used to establish an LDAP binding. | cn=admin,dc=ibm,dc=com |
bindPassword | LDAP | The bind password used to establish an LDAP binding. | pTms!REJECs@hzdNEGZ8 |
uris | LDAP | An 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 |
filter | LDAP | The filter to use during LDAP lookup. | (|(|(objectclass=ePerson)(objectclass=person))(objectclass=User)) |
userObjectClasses | LDAP | The user object classes. | top,Person,organizationalPerson,inetOrgPerson |
selector | LDAP | Selectors used during the LDAP lookup. | objectClass,cn,sn,givenName,userPassword,streetAddress,seeAlso,mobile |
baseDn | LDAP | The base DN used during a standard LDAP lookup. Plugin logic can override this if needed. | dc=ibm,dc=com |
caCert | LDAP | The CA cert used during TLS to verify the authenticity of the certificate presented by the LDAP server during TLS. | -----BEGIN CERTIFICATE-----\nMIIDazCCAlOgA... |
insecureSkipVerify | LDAP | Skip TLS certificate verification. N.B. should only be used in testing environments and not in production. | false |
tlsMinVersion | LDAP | Set the minimum TLS version allowed. See minimum tls version. | 771 |
clientCertLabel | LDAP | Used 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.pem | ldap.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 Version | Integer |
---|---|
v1.0 | 769 |
v1.1 | 770 |
v1.2 (Default) | 771 |
v1.3 | 772 |
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:
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 Object | Description |
---|---|
requestParameters | The request received from ISV. |
workingObject | Parameters already retrieved from the Primary LDAP or previous plugins in the execution order. |
pluginConfig | The 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:
Property | Description |
---|---|
addressedTo | The name of the module, this will always be ldapauth . |
enqueuedTime | The time the operation was enqueued. |
operation | The operation to be performed by the plugin. Can be either password-verify or password-change . |
parameters | A 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:
Property | Description |
---|---|
groups | An array of the ISV groups to which the user belong. |
user | An 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
:
- Database binding functions (db2, postgres, oracle)
- LDAP binding functions
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:
Function | Description |
---|---|
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 valueserverName
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
Database | Parameter 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:
Result Object
Property | Description |
---|---|
rowsAffected | Number of rows changed by operation. |
rows | An 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.
Property | Description |
---|---|
error | The 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);
- Pass in the
serverName
parameter as with database.query()
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:
Class | Description |
---|---|
ldapUserLookupHelper | Lookup attributes and perform user authentication associated with a user ID. |
ldapAttributeUtil | Create, 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:
Class | Overview |
---|---|
UserLookupHelper | Provides functions for the lookup of a user. |
User | Represents 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
Method | Return Type | Description |
---|---|---|
isReady() | boolean or Error | true when ready. Error object on error. |
createUser(userName, dn, password, firstName, LastName) | User | Creates an LDAP user. Returns a User object. |
getUser(userName) | User | Returns a User object with the corresponding User object. |
getUserByNativeId(nativeId) | User | Returns a User object. nativeId represents a full dn (distinguished name). |
deleteUser(userName) | boolean, null | Deletes 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
Method | Return Types | Description |
---|---|---|
hasError() | boolean | Returns 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() | string | Returns the value of the Error object (the error message). |
authenticate(password) | bool, null | Returns true when password string is correct for user. |
getId() | string, null | Returns the userId/username of the user. |
getNativeId() | string, null | Return full distinguished name (dn) of the user. |
getAttributeNames() | Array | Returns array of the attribute names for this user. Note: this does not return the values of these attributes. |
getAttribute(attrName) | string, null | Returns 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, null | Returns 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) | boolean | Returns 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:
Class | Overview |
---|---|
LdapAttributeUtil | Used for LDAP attribute creation and retrieval. |
Attribute | Represents an array of attribute values corresponding to an attribute ID. |
Attributes | Represents an array of Attribute objects. |
SearchResult | Represents a search result entry. |
NamingEnumeration | Class to enumerate over a provided type . |
LdapAttributeResult | Represents 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
Method | Return Types | Description |
---|---|---|
init() | LdapAttributeResult | Performs 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() | boolean | Performs a Healthcheck against LDAP attribute source. Returns true when healthy. |
addAttributeValue(dn, attributeName, attributeValue) | LdapAttributeResult | Adds an LDAP attribute attributeName with value attributeValue to dn . |
createSubContext(dn, attributes) | LdapAttributeResult | Creates the subcontext dn with the attributes attributes . For information about how to specify input attributes, see attributes format. |
getAttributeValue(dn, attributeNames) | LdapAttributeResult | Retrieves 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) | LdapAttributeResult | Removes attribute attributeName with value attributeValue from dn . |
search(dn, filter) | LdapAttributeResult | Search for entries at dn with filter filter . For example: (objectclass=*) . |
setAttributeValue(dn, attributeName, attributeValue) | LdapAttributeResult | Sets the attributeName attribute of the dn to the value attributeValue . |
attributes
format
attributes
formatSpecify 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.
Property | Type | Description |
---|---|---|
id | string | The attribute ID |
attr | Array | An array of attribute values |
Method | Return Types | Description |
---|---|---|
constructor(id, attr) | Attribute | Creates an Attribute object with the ID id and an array of attribute values attr . |
isOrdered() | boolean | Always returns true . |
getID() | string | Return the id property of the attribute. |
get(idx) | string | Returns the attribute value indexed by idx . |
getAll() | NamingEnumeration of attribute values | Returns a NamingEnumeration of the attribute values that this object represents. |
size() | int | Returns the number of values this object represents. |
contains(attrVal) | boolean | Returns true when this attribute contains the value attrVal . |
Attributes reference
The Attributes object represents an array of Attribute objects.
Property | Type | Description |
---|---|---|
attrs | Object | Object with properties that correspond to Attribute objects. |
Method | Return Types | Description |
---|---|---|
constructor(attrs) | Attributes | Takes an array of attr JSON objects and returns an Attributes object. |
isCaseIgnored() | boolean | Always returns false . |
size() | int | Returns the number of attributes represented by the object. |
get(attrID) | Attribute | Returns a specific Attribute with the attribute ID attrID . |
getIDs() | NamingEnumeration of string | Returns the attribute IDs represented by this object. |
getAll() | NamingEnumeration of Attribute | Returns the values of the attributes represented by this object. |
attr
JSON object format
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.
Property | Type | Description |
---|---|---|
type | The type that the result of next() is wrapped in. | |
wrapObj | boolean | When true the result of next() wrapped in type . |
results | Array | An array of results to be enumerated. |
curIdx | int | The current enumeration index. |
Method | Return Types | Description |
---|---|---|
constructor(results, wrapObj, type) | NamingEnumeration | Accepts an array of results . Set wrapObj to true if results must be wrapped in type before it is returned. |
hasMore() | boolean | Returns true when more elements exist in the enumeration. |
next() | SearchResult, Attribute, string, Object | Returns the next element of the enumeration. It is of the expected type. |
SearchResult reference
Represents a search result entry.
Property | Type | Description |
---|---|---|
entry | string | Search result entry. |
entry.attributes | Attributes | Returned attributes. |
entry.name | string | Returned attribute entries. |
Method | Return Types | Description |
---|---|---|
getAttributes() | Attributes | Returns an Attributes object that contains the attributes that the search returns. |
getName() | string | Returns the name of the search result entry. |
LdapAttributeResult reference
The LdapAttributeResult
class represents an attribute operation response. Properties defined on this class:
Property | Type | Description |
---|---|---|
cfgName | string | Name of the plugin configuration. It is in the form <pluginName>_config . |
result | Object | The result of the operation. |
namingEum | NamingEnumeration | Enumeration of search results as SearchResult objects. |
attrs | Attributes | Attributes from SearchResult. |
Methods defined on the LdapAttributeResult
class:
Method | Return Types | Description |
---|---|---|
getAttributes() | Attributes | Returns the attributes that LdapAttributeUtil.getAttributeValue() requests. |
getNamingEnumeration() | NamingEnumeration of SearchResult | Returns the NamingEnumeration result of the LdapAttributeUtil.search() operation. |
isSuccessful() | boolean | Returns true if the operation is completed with an error. |
hasError() | boolean | Returns true if operation completed with an error. |
getError() | Error | Returns the error that occurred. |
getNamingException() | Error | Alias 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:
Property | Definition |
---|---|
status.result | The result code. |
status.message | An optional status message. |
parameters.groups | An array of ISV group objects to which a user should belong. |
user | An 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
Property | Description |
---|---|
name | The displayName of the ISV group. |
id | The 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.
- Minimal Scaffold
- LDAP Simple Lookup Example
- LDAP Simple Authentication Example
- LDAP Attribute Util Example
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;
Updated 5 months ago