// LDAP distinguished-name helpers. Users are uid=,ou=people,; // groups are cn=,ou=groups,. In a multi-tenant deployment the // tenant is an extra dc component: becomes dc=,dc=saltcorn,dc=local. // A falsy tenant yields the bare default base, so single-tenant DNs are unchanged. const constants = require("../constants"); const baseFor = (tenant) => { return tenant ? ("dc=" + tenant + "," + constants.LDAP_BASE_DN) : constants.LDAP_BASE_DN; }; // RFC 4514 escaping for an RDN attribute value. A user email or group name // (admin-controlled free text) may contain DN special characters (comma, plus, // quote, ...); without escaping the concatenated DN is malformed and ldapjs's // DN.fromString() throws when res.send() builds the SearchEntry, aborting the // WHOLE search. We escape on output (the result/memberOf DNs we emit); inbound // bind DNs are formatted by the client, not by us. const escapeDnValue = (value) => { let s = String(value === null || value === undefined ? "" : value); // Characters that are special anywhere in the value. s = s.replace(/([\\",+;<>=])/g, "\\$1"); // A leading '#' or space, and a trailing space, are positionally special. s = s.replace(/^([ #])/, "\\$1").replace(/ $/, "\\ "); // NUL byte. s = s.replace(/\0/g, "\\00"); return s; }; const userDn = (email, tenant) => { return "uid=" + escapeDnValue(email) + ",ou=people," + baseFor(tenant); }; const groupDn = (group, tenant) => { return "cn=" + escapeDnValue(group) + ",ou=groups," + baseFor(tenant); }; // Reverse of escapeDnValue: turn RFC 4514 escapes back into the literal value. // `\XX` (two hex digits) -> that byte; `\` -> . const unescapeDnValue = (v) => { let out = ""; for (let i = 0; i < v.length; i++) { if (v[i] === "\\" && i + 1 < v.length) { const hex = v.slice(i + 1, i + 3); if (/^[0-9a-fA-F]{2}$/.test(hex)) { out += String.fromCharCode(parseInt(hex, 16)); i += 2; } else { out += v[i + 1]; i += 1; } } else { out += v[i]; } } return out; }; // Extract the uid (the user's email) from a bind DN (ldapjs DN object or string). // ldapjs re-escapes RFC 4514 special chars in toString() (e.g. a comma becomes // "\2c" or "\,"), so we capture the full RDN value -- up to the first UNESCAPED // comma, treating "\" as one unit -- then unescape it. A naive /uid=([^,]+)/ // truncated at escaped commas and returned the escaped form, so users whose email // contains a comma/plus/etc. could never bind. const uidFromDn = (dn) => { const s = typeof dn === "string" ? dn : (dn ? dn.toString() : ""); const m = s.match(/uid=((?:\\.|[^,\\])*)/i); if (!m) { return null; } const v = unescapeDnValue(m[1]).trim(); return v || null; }; module.exports = { userDn, groupDn, uidFromDn };