How To Display LDAP User Details with VCF Orchestrator and DataGrid

Back in the early ’90s, a frustrated grad student at the University of Michigan decided he’d had enough. The main system for storing and finding user information — called X.500 — was so complex it required a full OSI network stack just to look up someone’s email. So Tim Howes built a faster, simpler workaround: the Lightweight Directory Access Protocol, or LDAP.

What started as a side project quickly became the backbone of enterprise identity management. Decades later, LDAP still runs quietly behind the scenes — powering logins, user directories, and authentication systems across countless organizations.

And while the protocol itself is old-school, the ways we interact with it keep evolving. Following my previous pst, In this one, I’ll show how you can display LDAP user details using VMware Cloud Foundation (VCF) Orchestrator and a DataGrid, turning a traditional directory lookup into a modern, dynamic user interface.

The requirements

The workflow had an input called “user”. Previously, the user was a text field. The operator would type a username, and of course, in many cases, typos and copy-paste issues went unavoidable. The workflow would start and then fail, of course. To enhance user experience, I wanted to eliminate this problem by validating that the user exists and has been typed correctly. But that time not by using the external validation.

In large organizations, there’s a possibility of having multiple users with the same or similar names. Therefore, my solution should support this option as well.

Therefore, my idea was to provide the operator a way to get details of the user (if existing). Using those details, the operator could pick up the proper user from the list.

And of course, I will try to implement some best practices.

The solution

I will not cover the basics of the Datagrid. If needed, please take a look on my previous post.

The userDetails DataGrid is connected to the external action getLdapUserDetails and to the text field called Username.

💡

The trick here is that the Username is just a canvas object, which is not bound to any variable or input. It’s used to help the operator provide a place to put a user name. Its value is passed to the action element as an input.

Main execution

There are tree main steps I need to do:

  1. Get LDAP configuration
  2. Create LDAP client connection
  3. Perform LDAP search

Each on of them are a function/s I will cover down below.

// Main execution
var ldapClient = null;
var config = null;
var results = [];
// Step 1: Get LDAP configuration
try {
    config = getLdapConfig();
}
catch (error) {
    System.error('[LDAP] Failed to retrieve LDAP configuration: ' + error);
    throw error;
}
// Step 2: Create LDAP client connection
try {
    ldapClient = createLdapClient(config);
}
catch (error) {
    System.error('[LDAP] Failed to create LDAP client: ' + error);
    throw error;
}
// Step 3: Perform LDAP search
try {
    results = searchLdap(ldapClient, user);
    return results;
}
catch (error) {
    System.error('[LDAP] Failed to search LDAP directory: ' + error);
    throw error;
}
finally {
    // Always close the LDAP connection
    if (ldapClient !== null) {
        try {
            ldapClient.close();
            System.log('[LDAP] Client connection closed');
        }
        catch (closeError) {
            System.warn('[LDAP] Error closing LDAP client: ' + closeError);
        }
    }
}

💡

Best practice #1:
– Split/extract your code into as many small functions as you can. That will make the code more readable and testable (Hello, Uncle Bob).
– Always close client connection.

Preparation

The Datagrid object is loaded immediately as the workflow starts. Because the Datagrid is using an external action, the action will be triggered immediately as well.

As a result, an obvious error will be popped up claiming the necessary value is missing (because no one has provided a username yet).

To avoid it, I added this at the very first line of the action element to solve that problem.

if (!user || typeof user !== 'string' || user.trim() === '')
    return [];

💡

Best practice #2: always use that pattern to avoid unnecessary errors, improve user experience, and “exit early” best practice when an error occurs.

The next step will be configuration variables. The better approach will be to configure all of that in the Configuration Element, but because this was already covered many times, to simplify that long story a bit, I will store it in an action element, which is fine in my case.

💡

The reason for a best practice to manage configuration variables externally is because there could be a separate department, which is responsible for that. It can be done manually, via GIT or any other way. And the workflow developers may not have an access to update those values.

var CONFIG = {
    baseDN: 'cn=Users,dc=example,dc=com',
    scope: LdapSearchScope.SUB,
    policy: LdapDereferencePolicy.NEVER,
    configName: 'ldap_prod',
    configPath: 'my_org',
    timeLimit: 5000,
    sizeLimit: 100,
    attributes: ['cn', 'mail', 'uid', 'givenName', 'sn']
};

LDAP Search Scope

Defines how deep an LDAP search should go within the directory information tree (DIT) — that is, whether the search includes just the base entry, its immediate children, or the entire subtree.

It controls the scope of an LDAP query relative to the base DN (Distinguished Name) used in the search.

Here are the common values:

  • BASE: Searches only the exact entry specified by the base DN.
  • ONE: Searches only the immediate children (one level below) of the base DN, excluding the base itself.
  • SUB: Searches the base DN and all its descendants (any depth).
  • SUBORDINATE_SUBTREE: Searches all descendants (any depth) below the base DN, but not the base DN itself.

LDAP Dereference Policy

Defines how an LDAP search handles alias entries (references or pointers to other entries) during a directory lookup.

In LDAP, an alias is like a symbolic link — it points to another entry in the directory. The dereference policy tells the LDAP server or client whether and when to follow those aliases while searching.

Here are the standard options:

  • NEVER: Never dereference aliases. Treat aliases as normal entries; do not follow them.
  • SEARCHING: Dereference aliases only while searching (i.e., follow aliases found below the base DN during the search).
  • FINDING: Dereference aliases only when locating the base DN (before the search starts).
  • ALWAYS: Dereference aliases both when finding the base DN and while searching below it.

Get LDAP configuration

In my case, the LDAP details are already stored in the Configuration Element, so I just need to fetch them

/**
 * Retrieves LDAP configuration from vRO configuration element
 * @returns {Object} Configuration object with host, port, username, password
 * @throws {Error} If configuration element not found
 */
function getLdapConfig() {
    var config = {};
    var category = Server.getConfigurationElementCategoryWithPath(CONFIG.configPath);
    if (!category || !category.configurationElements) {
        throw new Error('LDAP configuration category "' + CONFIG.configPath + '" not found');
    }
    var elements = category.configurationElements;
    for (var i = 0; i < elements.length; i++) {
        var element = elements[i];
        if (element.name === CONFIG.configName) {
            config.hostname = element.getAttributeWithKey('hostname').value;
            config.username = element.getAttributeWithKey('username').value;
            config.password = element.getAttributeWithKey('password').value;
            config.port = element.getAttributeWithKey('port').value;
            return config;
        }
    }
    throw new Error('LDAP configuration element "' + CONFIG.configName + '" not found');
}

getLdapConfig function

Create LDAP client

Using an LDAP client in Orchestrator is quite straightforward. I’ll create a new LDAP client using LdapClientFactory.newLdapClient() and provide the necessary values, such as the hostname, credentials, and port.

/**
 * Creates LDAP client connection
 * @param {Object} config - LDAP configuration
 * @returns {LdapClient} Connected LDAP client
 * @throws {Error} If client creation fails
 */
function createLdapClient(config) {
    if (!config.hostname || !config.port || !config.username || !config.password) {
        throw new Error('Invalid LDAP configuration: missing required parameters');
    }
    try {
        var client = LdapClientFactory.newLdapClient(config.hostname, config.port, config.username, config.password, false);
        System.log('[LDAP] Client created successfully for host: ' + config.hostname);
        return client;
    }
    catch (error) {
        throw new Error('Failed to create LDAP client: ' + error);
    }
}

createLdapClient function

Search LDAP

This function is the heart of our LDAP user lookup system. It searches an LDAP directory for users matching a search term and returns a sorted list of results.

How It Works

  1. Builds a Smart Filter – Creates an LDAP query that searches across multiple user attributes (username, display name, common name, surname) to cast a wide net
  2. Executes the Search – Queries the LDAP directory with configurable limits (max 100 results, 5-second timeout) to prevent runaway searches
  3. Validates Results – Checks if anything was found and returns an empty array if not, avoiding null reference errors
  4. Processes Entries – Extracts key user details (name, email, username) from each LDAP entry into clean JavaScript objects
  5. Sorts Alphabetically – Orders results by common name using locale-aware string comparison for proper Unicode handling
  6. Logs Everything – Provides detailed logging at each step for debugging and audit trails

What You Get Back

An array of user objects sorted A-Z, each containing:

  • Common name (full name)
  • Username (uid)
  • Email address
  • First name
  • Last name
/**
 * Performs LDAP search and processes results
 * @param {LdapClient} client - Connected LDAP client
 * @param {string} searchValue - User search term
 * @returns {Array/CompositeType(cn:string,uid:string,mail:string):ldapUserDetails} Array of user detail objects
 */
function searchLdap(client, searchValue) {
    var filter = buildLdapFilter(searchValue);
    System.log('[LDAP] Search filter: ' + filter);
    System.log('[LDAP] Searching for user: "' + searchValue + '" in ' + CONFIG.baseDN);
    var searchResult = client.search(CONFIG.baseDN, CONFIG.scope, CONFIG.policy, CONFIG.sizeLimit, CONFIG.timeLimit, filter, CONFIG.attributes);
    if (!searchResult) {
        System.warn('[LDAP] No results found for user: "' + searchValue + '"');
        return [];
    }
    // getSearchEntries() returns an array of entries
    var entries = searchResult.getSearchEntries();
    if (!entries || entries.length === 0) {
        System.warn('[LDAP] Search returned no entries for user: "' + searchValue + '"');
        return [];
    }
    System.log('[LDAP] Found ' + entries.length + ' matching entries');
    // Process all entries and return array of user details
    var results = [];
    for (var i = 0; i < entries.length; i++) {
        var userDetails = extractUserDetails(entries[i]);
        results.push(userDetails);
    }
    // Sort results alphabetically by cn (common name) using localeCompare
    results.sort(function (a, b) {
        var nameA = a.cn || '';
        var nameB = b.cn || '';
        return nameA.localeCompare(nameB, 'en', { sensitivity: 'base' });
    });
    // Log sorted results
    for (var j = 0; j < results.length; j++) {
        System.log('[LDAP] Entry ' + (j + 1) + ': ' + results[j].mail + ' (' + results[j].cn + ')');
    }
    return results;
}

searchLdap function

Auxiliary function

There are two additional small functions that assist in constructing an LDAP search filter.

/**
 * Builds LDAP search filter with proper escaping
 * @param {string} searchValue - User input to search for
 * @returns {string} LDAP filter string
 */
function buildLdapFilter(searchValue) {
    if (!searchValue || searchValue.trim() === '') {
        throw new Error('Search value cannot be empty');
    }
    // Escape special LDAP characters
    var escaped = searchValue.replace(/[*()]/g, '$&');
    // Search across multiple attributes (OR logic)
    var filter = '(|' +
        '(uid=*' + escaped + '*)' +
        '(displayName=*' + escaped + '*)' +
        '(cn=*' + escaped + '*)' +
        '(sn=*' + escaped + '*)' +
        ')';
    return filter;
}

buildLdapFilter function

And extract user details from the search response.

/**
 * Extracts user details from LDAP search entry
 * @param {LdapSearchEntry} entry - LDAP search entry
 * @returns {Object} User details object
 */
function extractUserDetails(entry) {
    return {
        cn: entry.getAttributeValue('cn') || '',
        uid: entry.getAttributeValue('uid') || '',
        mail: entry.getAttributeValue('mail') || '',
        givenName: entry.getAttributeValue('givenName') || '',
        sn: entry.getAttributeValue('sn') || ''
    };
}

extractUserDetails function


Summary

Working with LDAP plugin is very essential knowledge, which may be relevant in many use cases.

💡

Would you consider referring this post to a friend if you enjoy my job? Your support helps me to grow and brings more aspiring mates into the community.
I also want to hear from you about how I can improve my topics! Please leave a comment with the following:
– Topics you’re interested in for future editions
– Your favorite part or take away from this one

I’ll do my best to read and respond to every single comment!

Similar Posts