BoxLang 🚀 A New JVM Dynamic Language Learn More...
|:------------------------------------------------------: |
| ⚡︎ B o x L a n g ⚡︎
| Dynamic : Modular : Productive
|:------------------------------------------------------: |
🔐 A comprehensive LDAP module for BoxLang that brings full-featured LDAP directory access to your applications!
This module provides powerful LDAP (Lightweight Directory Access Protocol) capabilities to the BoxLang language, making it easy to query, modify, and manage directory services with minimal code.
If you are using CommandBox for your web applications, simply run:
box install bx-ldap@ortus
If you are using the BoxLang OS Binary, simply run:
install-bx-module bx-ldap@ortus
The module will automatically register and be available as
bxldap in your BoxLang applications.
Here's how to query an LDAP directory in just a few lines:
bx:ldap
action="query"
server="ldap.example.com"
port="389"
result="users"
start="dc=example,dc=org"
filter="(objectClass=person)";
println( "Found #users.recordCount# users" );
That's it! 🎉 You now have LDAP query results in a BoxLang Query object.
The module supports seven core operations:
Search the directory with filters, scopes, and attribute selection.
recordCount=0 if no entries match (no exception thrown)Create new entries in the directory.
true on successUpdate existing directory entries.
true on successRemove entries from the directory.
true on successRename entries or move them within the directory tree.
true on successCreate and store a named connection for reuse across multiple operations.
connection
(connection name), server, plus authentication detailsresult
attribute is specified, otherwise ignoredClose and release a named connection.
connection
(connection name to close)true on success<bx:ldap> ComponentThe main component for all LDAP operations.
| Attribute | Type | Required | Default | Description |
|---|---|---|---|---|
action
| string | ✅ Yes | - | LDAP operation: "query", "add", "modify", "delete", "modifydn", "open", "close" |
server
| string | ⚠️ Conditional | - | LDAP
server hostname or IP address. Required if
connection is not provided. |
connection
| string | ⚠️ Conditional | - | Named
connection to an LDAP server (previously created). Required if
server is not provided. Allows reusing connection
credentials across multiple operations. |
port
| number | - | 389 (636 for SSL) | LDAP server port |
result
| string | ⚠️ Query | - | Variable name to store query results (primary attribute for result handling) |
name
| string | - | - | Deprecated:
Use result instead. Maintained for backwards
compatibility with query actions only |
start
| string | ✅ Yes | - | Starting DN (Distinguished Name) for search or operation |
| Attribute | Type | Default | Description |
|---|---|---|---|
username
| string | "" (anonymous) | Bind DN for authentication (e.g., "cn=admin,dc=example,dc=org") |
password
| string | "" (anonymous) | Password for authentication |
secure
| boolean/string | false | Security mode:
false (no SSL), true (SSL),
"CFSSL_BASIC" (server auth only),
"CFSSL_CLIENT_AUTH" (mutual TLS) |
useTls
| boolean | false | Whether to use StartTLS extension to initiate SSL/TLS over normal port |
clientCert
| string | - | Full path to keystore file containing client certificate |
clientCertPassword
| string | - | Password for client certificate keystore |
timeout
| integer | 60000 | Operation timeout in milliseconds |
| Attribute | Type | Default | Description |
|---|---|---|---|
filter
| string | "(objectClass=*)" | LDAP search filter (e.g., "(uid=jdoe)") |
scope
| string | "onelevel" | Search scope: "base", "onelevel", "subtree" |
attributes
| string | "*" | Comma-separated list of attributes to return |
sort
| string | - | Attribute name to sort results by |
sortDirection
| string | "asc" | Sort direction: "asc" or "desc" |
maxrows
| number | - | Maximum number of results to return |
startRow
| number | 1 | Starting row for pagination |
returnFormat
| string | "query" | Result format: "query" (Query object) or "array" (Array of structs) |
| Attribute | Type | Required | Description |
|---|---|---|---|
dn
| string | ✅ Yes | Distinguished Name of entry to modify |
attributes
| struct | ✅ Yes | Struct of attributes to modify (key=attribute, value=new value) |
modifyType
| string | "replace" | Modification type: "replace", "add", "delete" |
| Attribute | Type | Required | Description |
|---|---|---|---|
dn
| string | ✅ Yes | Distinguished Name for new entry |
attributes
| struct | ✅ Yes | Struct of attributes for new entry |
| Attribute | Type | Required | Description |
|---|---|---|---|
dn
| string | ✅ Yes | Distinguished Name of entry to delete |
| Attribute | Type | Required | Description |
|---|---|---|---|
dn
| string | ✅ Yes | Current Distinguished Name |
attributes
| struct | ✅ Yes | Struct with "newRDN" and optionally "newParentDN" |
Find all users in a directory:
bx:ldap
action="query"
server="ldap.example.com"
port="389"
result="users"
start="ou=users,dc=example,dc=org"
filter="(objectClass=person)";
println( "Found #users.recordCount# users" );
💡 Use Case: Quick directory lookup to list all users.
Search for a specific user:
bx:ldap
action="query"
server="ldap.example.com"
port="389"
result="user"
start="dc=example,dc=org"
scope="subtree"
filter="(uid=jdoe)"
attributes="cn,mail,telephoneNumber";
if( user.recordCount > 0 ){
println( "Name: #user.cn#, Email: #user.mail#" );
} else {
println( "User not found" );
}
💡 Use Case: User lookup with specific attributes for profile display.
Create a new directory entry:
newUser = {
"objectClass": ["inetOrgPerson", "organizationalPerson", "person", "top"],
"cn": "John Doe",
"sn": "Doe",
"uid": "jdoe",
"mail": "[email protected]",
"userPassword": "SecurePassword123"
};
bx:ldap
action="add"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
dn="uid=jdoe,ou=users,dc=example,dc=org"
attributes=newUser;
println( "User created successfully!" );
💡 Use Case: User registration or bulk user import.
Update an existing entry:
updates = {
"mail": "[email protected]",
"telephoneNumber": "+1-555-0123"
};
bx:ldap
action="modify"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
dn="uid=jdoe,ou=users,dc=example,dc=org"
modifyType="replace"
attributes=updates;
println( "User updated successfully!" );
💡 Use Case: Profile updates, contact information changes.
Remove an entry from the directory:
bx:ldap
action="delete"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
dn="uid=jdoe,ou=users,dc=example,dc=org";
println( "User deleted successfully!" );
💡 Use Case: Account deactivation, cleanup of obsolete entries.
Change an entry's RDN (Relative Distinguished Name):
renameOp = {
"newRDN": "uid=johnd"
};
bx:ldap
action="modifydn"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
dn="uid=jdoe,ou=users,dc=example,dc=org"
attributes=renameOp;
println( "User renamed from jdoe to johnd!" );
💡 Use Case: Username changes, standardizing naming conventions.
Create a named connection once and reuse it across multiple operations:
// Define a connection once with all credentials
bx:ldap
action="query"
connection="myLdapConn"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
start="dc=example,dc=org"
filter="(objectClass=person)"
result="users";
println( "Initial query found #users.recordCount# users" );
// Reuse the same named connection for subsequent operations
// No need to pass server, username, password again
bx:ldap
action="query"
connection="myLdapConn"
start="ou=groups,dc=example,dc=org"
filter="(objectClass=groupOfNames)"
result="groups";
println( "Found #groups.recordCount# groups" );
// Use the connection for add operations
newGroup = {
"objectClass": ["groupOfNames"],
"cn": "developers",
"member": "uid=jdoe,ou=users,dc=example,dc=org"
};
bx:ldap
action="add"
connection="myLdapConn"
dn="cn=developers,ou=groups,dc=example,dc=org"
attributes=newGroup;
println( "Group created successfully using reused connection" );
💡 Use Case: Simplifies code when performing multiple LDAP operations. Credentials are passed once, then the connection is reused by its name.
Establish a connection with the open action for later reuse:
// Explicitly open a named connection
bx:ldap
action="open"
connection="prodLdap"
server="ldap.production.com"
port="389"
username="cn=service,dc=prod,dc=org"
password="servicepass"
timeout="30000";
println( "Connection 'prodLdap' opened and ready for reuse" );
// Now use the connection for various operations
// No need to pass credentials again
// Query operation
bx:ldap
action="query"
connection="prodLdap"
start="ou=users,dc=prod,dc=org"
filter="(department=IT)"
result="itUsers";
// Add operation
bx:ldap
action="add"
connection="prodLdap"
dn="uid=newuser,ou=users,dc=prod,dc=org"
attributes={
"objectClass": ["inetOrgPerson", "person"],
"uid": "newuser",
"cn": "New User",
"sn": "User"
};
// Modify operation
bx:ldap
action="modify"
connection="prodLdap"
dn="uid=newuser,ou=users,dc=prod,dc=org"
attributes={"mail": "[email protected]"}
modifyType="replace";
💡 Use Case: Use open when you need
explicit connection lifecycle management. Useful in microservices or
long-running applications.
Explicitly close a connection to free resources:
// Close a named connection
bx:ldap
action="close"
connection="prodLdap";
println( "Connection 'prodLdap' closed and resources released" );
💡 Use Case: Important for resource management in applications that maintain many connections or run for extended periods.
Use advanced LDAP filter syntax:
// Find active users in IT department created after a date
bx:ldap
action="query"
server="ldap.example.com"
port="389"
result="itUsers"
start="dc=example,dc=org"
scope="subtree"
filter="(&(objectClass=person)(department=IT)(!(accountStatus=disabled))(createTimestamp>=20240101000000Z))"
sort="cn"
sortDirection="asc";
println( "Found #itUsers.recordCount# active IT users" );
💡 Use Case: Department reporting, audit queries, compliance checks.
Filter Operators:
& - AND (all conditions must match)| - OR (any condition matches)! - NOT (negation)= - Equals>= - Greater than or equal<= - Less than or equal=* - Presence check (attribute exists)=value* - Starts with=*value - Ends with=*value* - ContainsHandle large result sets efficiently:
// Get 50 users at a time
pageSize = 50;
currentPage = 1;
startRow = ((currentPage - 1) * pageSize) + 1;
bx:ldap
action="query"
server="ldap.example.com"
port="389"
result="pagedUsers"
start="ou=users,dc=example,dc=org"
filter="(objectClass=person)"
maxrows="#pageSize#"
startRow="#startRow#"
sort="cn";
println( "Showing #pagedUsers.recordCount# users (Page #currentPage#)" );
💡 Use Case: User management interfaces, large directory browsing.
Return results as an array of structs instead of a Query object:
// Query returning array format
bx:ldap
action="query"
server="ldap.example.com"
port="389"
result="userArray"
start="ou=users,dc=example,dc=org"
filter="(objectClass=person)"
returnFormat="array"
sort="cn";
// Iterate over array results
userArray.each( user () => {
println( "User: #user.cn# (Email: #user.mail#)" );
} );
// Or serialize to JSON for API responses
apiResponse = {
"success": true,
"users": userArray,
"count": userArray.len()
};
println( jsonSerialize( apiResponse ) );
💡 Use Case: JSON API responses, data transformation, modern data handling patterns.
Supported Formats:
"query" - Returns BoxLang Query object
(default) - good for compatibility with traditional CFML patterns"array" - Returns Array of Structs - better
for JSON APIs and modern applicationsConnect securely with SSL:
bx:ldap
action="query"
server="ldaps.example.com"
port="636"
secure="false"
result="secureUsers"
start="dc=example,dc=org"
filter="(objectClass=person)";
println( "Secure query returned #secureUsers.recordCount# users" );
💡 Use Case: Production environments, sensitive data access, compliance requirements.
Use client certificates for authentication:
bx:ldap
action="query"
server="ldaps.example.com"
port="636"
secure="true"
username="cn=app,dc=example,dc=org"
password="apppass"
result="users"
start="dc=example,dc=org";
println( "Authenticated with client certificate" );
💡 Use Case: High-security environments, API integrations, service accounts.
Create an entry with multi-valued attributes:
newGroup = {
"objectClass": ["groupOfNames", "top"],
"cn": "Developers",
"member": [
"uid=jdoe,ou=users,dc=example,dc=org",
"uid=jsmith,ou=users,dc=example,dc=org",
"uid=alee,ou=users,dc=example,dc=org"
],
"description": "Development Team"
};
bx:ldap
action="add"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
dn="cn=Developers,ou=groups,dc=example,dc=org"
attributes=newGroup;
println( "Group created with multiple members!" );
💡 Use Case: Group management, access control lists, distribution lists.
Add values to existing multi-valued attributes:
// Add new members to existing group
newMembers = {
"member": [
"uid=bmiller,ou=users,dc=example,dc=org",
"uid=kchen,ou=users,dc=example,dc=org"
]
};
bx:ldap
action="modify"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
dn="cn=Developers,ou=groups,dc=example,dc=org"
modifyType="add"
attributes=newMembers;
println( "New members added to group!" );
💡 Use Case: Group membership management, role assignments.
Remove specific values from multi-valued attributes:
// Remove a member from group
removeMember = {
"member": "uid=jsmith,ou=users,dc=example,dc=org"
};
bx:ldap
action="modify"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
dn="cn=Developers,ou=groups,dc=example,dc=org"
modifyType="delete"
attributes=removeMember;
println( "Member removed from group!" );
💡 Use Case: Membership revocation, access control updates.
Move an entry to a different organizational unit:
moveOp = {
"newRDN": "uid=jdoe",
"newParentDN": "ou=contractors,dc=example,dc=org"
};
bx:ldap
action="modifydn"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
dn="uid=jdoe,ou=employees,dc=example,dc=org"
attributes=moveOp;
println( "User moved from employees to contractors!" );
💡 Use Case: Organizational restructuring, employee status changes.
Important: LDAP queries return empty Query objects (recordCount=0) instead of throwing exceptions when:
bx:ldap
action="query"
server="ldap.example.com"
port="389"
result="user"
start="dc=example,dc=org"
filter="(uid=nonexistent)";
// Check for empty results
if( user.recordCount == 0 ){
println( "User not found" );
} else {
println( "Found user: #user.cn#" );
}
Handle connection and operation errors:
try {
ldap
action="modify"
server="ldap.example.com"
port="389"
username="cn=admin,dc=example,dc=org"
password="adminpass"
dn="uid=jdoe,ou=users,dc=example,dc=org"
attributes=updates;
println( "Operation successful!" );
} catch( any e ) {
println( "Error: #e.message#" );
println( "Detail: #e.detail#" );
}
| Error | Cause | Solution |
|---|---|---|
| Connection timeout | Server unreachable | Check server hostname, port, firewall |
| Invalid credentials | Wrong username/password | Verify bind DN and password |
| Entry already exists | Duplicate DN in add | Use unique DN or modify existing entry |
| No such object | DN doesn't exist | Verify DN syntax and entry existence |
| Insufficient access | Permission denied | Check ACLs and bind user permissions |
| Invalid DN syntax | Malformed DN | Validate DN
format: uid=user,ou=org,dc=example,dc=org
|
secure="true")maxrows
and startRow for large result sets(uid=jdoe) faster than (uid=*jdoe*)
(cn=John*) faster than (cn=*John)
(&(attr1=val1)(attr2=val2)) not multiple queriesThe LDAP module announces events at key points in the connection lifecycle, allowing you to monitor, audit, and react to connection operations. These events are dispatched through BoxLang's interception system.
onLDAPConnectionOpen
Announced when an LDAP connection is successfully opened (either new or from pool).
Event Payload:
{
"context": context, // BoxLang context
"connection": connection, // The opened LDAP connection object
"result": connectionName, // Named connection reference (if used)
"attributes": attributes // Original component attributes
}
Example Interceptor:
class {
function onLDAPConnectionOpen( struct eventData ) {
var connectionName = eventData.result;
if ( connectionName.len() ) {
logger.info( "Named LDAP connection opened: #connectionName#" );
} else {
logger.info( "Anonymous LDAP connection opened to #eventData.attributes.server#" );
}
}
}
onLDAPConnectionClose
Announced when an LDAP connection is closed or removed from the pool.
Event Payload:
{
"context": context, // BoxLang context
"result": connectionName, // Named connection being closed
"returnValue": true, // Success flag
"attributes": attributes // Original component attributes
}
Example Interceptor:
class {
function onLDAPConnectionClose( struct eventData ) {
if ( eventData.returnValue ) {
writeLog( text:"LDAP connection closed: #eventData.result#" , log: "ldap");
} else {
writeLog( text:"Failed to close LDAP connection: #eventData.result#" , log: "ldap");
}
}
}
Create an interceptor to monitor all connection lifecycle events:
// Interceptor.bx
class {
function onLDAPConnectionOpen( struct eventData ) {
var conn = eventData.result ?: "default";
writeLog( text:"LDAP Connection opened: #conn#" , log: "ldap");
}
function onLDAPConnectionClose( struct eventData ) {
var conn = eventData.result;
var status = eventData.returnValue ? "success" : "failed";
writeLog( text:"LDAP Connection closed (#status#): #conn#" , log: "ldap");
}
}
Problem: Cannot connect to LDAP server.
Solutions:
telnet ldap.example.com 389
Problem: Invalid credentials or bind failure.
Solutions:
cn=admin,dc=example,dc=org
[email protected] or
DOMAIN\user formatProblem: Query completes but returns 0 results.
Solutions:
start DN exists in directoryProblem: SSL handshake failure or certificate errors.
Solutions:
Problem: Write operations fail or return errors.
Solutions:
Problem: Queries are slow or timeout.
Solutions:
attributes parametertimeout value for complex queriesWe ❤️ contributions! This project is open source and welcomes your help to make it even better.
If you discover a bug, please:
We'd love to hear your ideas! Please:
Excellent! Here's how to get started:
Clone the Repository:
git clone https://github.com/ortus-solutions-private/bx-ldap.git
cd bx-ldap
Build the Project:
# Compile Java code
./gradlew compileJava
# Run tests (starts Docker LDAP container)
./gradlew test
# Full build
./gradlew build
Test the Module:
The test suite uses Testcontainers with OpenLDAP in Docker for integration testing:
# Run all tests
./gradlew test
# Run specific test class
./gradlew test --tests "ortus.boxlang.ldap.components.LDAPTest"
Code Formatting:
# Auto-format code to Ortus standards
./gradlew spotlessApply
# Check formatting
./gradlew spotlessCheck
development branch (NOT master)spotlessApply)Documentation improvements are always welcome:
This project is licensed under the Apache License 2.0.
Copyright 2025 Ortus Solutions, Corp
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
See LICENSE file for full details.
You can support BoxLang and all Ortus Solutions open source projects:
Patrons get exclusive benefits like:
If you discover a security vulnerability:
#security channel on Box Team SlackAll vulnerabilities will be promptly addressed.
"I am the way, and the truth, and the life; no one comes to the Father, but by me (JESUS)" Jn 14:1-12
Copyright Since 2025 by Ortus Solutions, Corp
www.boxlang.io | www.ortussolutions.com
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
bx-plus dependency for extended functionalitysourceSets for easier VSCode Testing
$
box install bx-ldap