85 lines
2.9 KiB
JavaScript
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
|
|
};
|