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

59 lines
2.1 KiB
JavaScript

// Multi-tenant LDAP helpers. The LDAP listener is process-level and runs in the
// process's default tenant context, so each bind/search must establish its own
// tenant context. The tenant is encoded as an extra dc component in the DN
// (uid=<email>,ou=people,dc=<tenant>,dc=saltcorn,dc=local); the bare base
// (dc=saltcorn,dc=local) means the default tenant (no wrap). resolveTenant
// validates the token against the live tenant set and DENIES unknown tenants so
// a crafted DN cannot reach another tenant's schema.
const db = require("@saltcorn/data/db");
// Capture the dc=<tenant> immediately preceding the dc=saltcorn,dc=local base.
const TENANT_RE = /dc=([^,]+),\s*dc=saltcorn,\s*dc=local\s*$/i;
const tenantFromDn = (dn) => {
const s = typeof dn === "string" ? dn : (dn ? dn.toString() : "");
const m = s.match(TENANT_RE);
return m ? m[1].trim().toLowerCase() : null;
};
// Resolve a parsed tenant token to a context decision:
// { tenant: null } -> run in the default context (no wrap)
// { tenant: "t1" } -> run inside runWithTenant("t1")
// { deny: true } -> reject (unknown tenant / illegal on single-tenant)
const resolveTenant = (token) => {
const def = String((db.connectObj && db.connectObj.default_schema) || "public").toLowerCase();
if (!token || token === def) {
return { tenant: null };
}
if (!db.is_it_multi_tenant || !db.is_it_multi_tenant()) {
return { deny: true };
}
let known;
try {
// getAllTenants is a MODULE-level export (returns the live {subdomain:State}
// map), NOT a method on the State instance.
const { getAllTenants } = require("@saltcorn/data/db/state");
known = new Set(Object.keys(getAllTenants() || {}).map((t) => String(t).toLowerCase()));
} catch (e) {
known = new Set();
}
if (known.has(token)) {
return { tenant: token };
}
return { deny: true };
};
const withTenant = (tenant, fn) => {
return tenant ? db.runWithTenant(tenant, fn) : fn();
};
module.exports = {
tenantFromDn,
resolveTenant,
withTenant
};