Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 92 additions & 3 deletions npm/src/LdapEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,11 @@ class LdapEngine extends EventEmitter {
* @private
*/
_setupSearchHandlers() {
// Authorization middleware (if enabled)
// RootDSE handler - handles queries to empty base DN ("") per RFC 4512 section 5.1
// This must be registered as a separate route handler
this.server.search('', (req, res, next) => this._handleRootDSE(req, res, next));

// Authorization middleware (if enabled) for normal searches
const authorizeSearch = (req, res, next) => {
if (!this.config.requireAuthForSearch) {
return next();
Expand All @@ -243,7 +247,7 @@ class LdapEngine extends EventEmitter {
return next();
};

// Search handler with authorization middleware
// Search handler with authorization middleware for normal directory searches
this.server.search(this.config.baseDn, authorizeSearch, async (req, res, next) => {
const filterStr = req.filter.toString();
this.logger.debug(`LDAP Search - Filter: ${filterStr}, Attributes: ${req.attributes}`);
Expand Down Expand Up @@ -417,6 +421,91 @@ class LdapEngine extends EventEmitter {
const { extractCredentials } = require('./utils/filterUtils');
return extractCredentials(req);
}

_handleRootDSE(req, res, next) {
const filterStr = req.filter.toString();
const scope = req.scope;
const requestedAttrs = req.attributes || [];
this.logger.debug(`RootDSE Search - Filter: ${filterStr}, Scope: ${scope}, Attributes: ${JSON.stringify(requestedAttrs)}`);

try {
// Check scope - ldapjs uses numeric constants: 0='base', 1='one', 2='sub'
// We check both forms for compatibility with different ldapjs versions
if (scope === 'base' || scope === 0) {
this.emit('rootDSERequest', { filter: filterStr, attributes: requestedAttrs });

// Determine which attributes to return based on request
// RootDSE attribute filtering rules (per RFC 4512):
// - No attributes = all attributes (both user and operational)
// - '*' with '+' = all user and operational attributes
// - '+' only = operational attributes only (namingContexts, supportedLDAPVersion) + objectClass
// - '*' only or '*' with specific names = user attributes + any specifically requested operational attributes
// - Specific names only = only those attributes + objectClass (which is always returned)
const hasWildcard = requestedAttrs.includes('*');
const hasPlus = requestedAttrs.includes('+');
const noAttrsRequested = requestedAttrs.length === 0;

// Build the entry attributes
const attributes = {
objectClass: ['top'] // objectClass is always returned
};

// Determine what to include
if (hasWildcard && !hasPlus) {
// Specific attributes requested (no wildcards)
requestedAttrs.forEach(attr => {
const attrLower = attr.toLowerCase();
if (attrLower === 'namingcontexts') {
attributes.namingContexts = [this.config.baseDn];
} else if (attrLower === 'supportedldapversion') {
attributes.supportedLDAPVersion = ['3'];
}
});
} else {
attributes.namingContexts = [this.config.baseDn];
attributes.supportedLDAPVersion = ['3'];
}

const rootDSEEntry = {
dn: '',
attributes
};

// Work around ldapjs attribute filtering:
// ldapjs filters attributes based on the requested attributes list.
// When '+' is requested, we need to replace it with actual operational attribute names.
// When specific attributes are requested, ensure they're in the list (in lowercase).
if (hasPlus && !hasWildcard) {
// Replace '+' with actual operational attribute names (lowercase for ldapjs matching)
const idx = res.attributes.indexOf('+');
if (idx !== -1) {
res.attributes.splice(idx, 1, 'namingcontexts', 'supportedldapversion');
}
} else if (requestedAttrs.length > 0 && !hasWildcard) {
// For specific attribute requests, add them to res.attributes in lowercase
requestedAttrs.forEach(attr => {
const attrLower = attr.toLowerCase();
if (attrLower !== '+' && attrLower !== '*' && res.attributes.indexOf(attrLower) === -1) {
res.attributes.push(attrLower);
}
});
}

res.send(rootDSEEntry);
this.logger.debug('RootDSE entry sent');
this.emit('rootDSEResponse', { entry: rootDSEEntry });
}

res.end();
return next();
} catch (error) {
this.logger.error("RootDSE search error", { error, filter: filterStr });
const { normalizeSearchError } = require('./utils/errorUtils');
const normalizedError = normalizeSearchError(error);
this.emit('rootDSEError', { error: normalizedError });
return next(normalizedError);
}
}
}

module.exports = LdapEngine;
module.exports = LdapEngine;
21 changes: 0 additions & 21 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading