sc-idp/lib/ldap/dn.js
2026-06-01 16:40:54 -05:00

85 lines
2.9 KiB
JavaScript

// LDAP distinguished-name helpers. Users are uid=<email>,ou=people,<base>;
// groups are cn=<group>,ou=groups,<base>. In a multi-tenant deployment the
// tenant is an extra dc component: <base> becomes dc=<tenant>,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; `\<char>` -> <char>.
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 "\<x>" 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
};