sc-idp/lib/ldap/settings.js
2026-06-18 17:22:33 -05:00

131 lines
4.8 KiB
JavaScript

// Settings for the process-global LDAPS listener (enable / bind host / port).
//
// There is ONE listener per Saltcorn instance, shared by all tenants (see
// lib/ldap/tenant.js), so its settings are INSTANCE-global, not per-tenant: they
// live in the ROOT (public) tenant's _sc_config and are edited from the admin
// panel on the public site. The SALTCORN_IDP_LDAP_PORT / SALTCORN_IDP_LDAP_HOST
// environment variables, WHEN SET, override the stored value (an ops/container
// escape-hatch and backward-compat), but they are never required: a fresh
// instance is configured entirely from the panel.
//
// resolveRuntime() is what the bind path (server.js) and the self-heal watchdog
// (index.js) consult. getApplied()/setApplied() record what the running listener
// actually bound with, so the panel can show a "restart to apply" reminder.
const db = require("@saltcorn/data/db");
const constants = require("../constants");
const { readKey, writeKey } = require("../configStore");
const rootSchema = () => String((db.connectObj && db.connectObj.default_schema) || "public").toLowerCase();
// Run fn in the ROOT/public tenant context so listener config is global to the
// instance regardless of which tenant's request/onLoad we are serving. A
// single-tenant (SQLite) deployment has only one context, so run directly.
const inRoot = (fn) => {
if (db.is_it_multi_tenant && db.is_it_multi_tenant()) {
return db.runWithTenant(rootSchema(), fn);
}
return fn();
};
const parseEnvPort = (raw) => {
if (raw === undefined || raw === null || String(raw).trim() === "") {
return null;
}
const n = parseInt(String(raw), 10);
return Number.isFinite(n) ? n : null;
};
const isLoopbackHost = (host) => {
const h = String(host || "").trim().toLowerCase();
return h === "127.0.0.1" || h === "::1" || h === "localhost";
};
const validatePort = (n) => {
return Number.isInteger(n) && n >= constants.LDAP_PORT_MIN && n <= constants.LDAP_PORT_MAX;
};
// The values stored in the public-site config (no env applied).
const getStoredConfig = async () => {
return inRoot(async () => {
const enabled = await readKey(constants.CFG_LDAP_ENABLED);
const host = await readKey(constants.CFG_LDAP_HOST);
const port = await readKey(constants.CFG_LDAP_PORT);
return {
enabled: enabled === true,
host: typeof host === "string" ? host : "",
port: Number.isFinite(port) ? port : null
};
});
};
const setStoredConfig = async (cfg) => {
await inRoot(async () => {
await writeKey(constants.CFG_LDAP_ENABLED, cfg.enabled === true);
await writeKey(constants.CFG_LDAP_HOST, typeof cfg.host === "string" ? cfg.host.trim() : "");
await writeKey(constants.CFG_LDAP_PORT, Number.isFinite(cfg.port) ? cfg.port : null);
});
};
// Effective runtime settings: env overrides stored config PER SETTING (set =>
// wins), but env is never required. The listener runs iff enabled with a valid port.
const resolveRuntime = async () => {
const stored = await getStoredConfig();
const envPort = parseEnvPort(process.env[constants.LDAP_PORT_ENV]);
const envHost = (process.env[constants.LDAP_HOST_ENV] || "").trim();
const port = envPort !== null ? envPort : stored.port;
const host = envHost || stored.host || constants.LDAP_DEFAULT_HOST;
// Presence of an env port preserves the legacy "env enables LDAP" behaviour.
const enabled = envPort !== null ? true : stored.enabled;
return {
enabled: enabled && Number.isFinite(port),
host: host,
port: Number.isFinite(port) ? port : null,
portFromEnv: envPort !== null,
hostFromEnv: !!envHost
};
};
// What the running listener actually bound with (or { enabled: false } when off).
// The admin panel compares this against resolveRuntime() to decide whether a
// settings change still needs a restart to take effect.
const getApplied = async () => {
const a = await inRoot(() => readKey(constants.CFG_LDAP_APPLIED));
return a && typeof a === "object" ? a : null;
};
const setApplied = async (applied) => {
const norm = {
enabled: applied.enabled === true,
host: applied.enabled === true ? applied.host : null,
port: applied.enabled === true ? applied.port : null
};
// Avoid write churn (every onLoad/worker would otherwise rewrite it).
const cur = await getApplied();
if (cur && cur.enabled === norm.enabled && cur.host === norm.host && cur.port === norm.port) {
return;
}
await inRoot(() => writeKey(constants.CFG_LDAP_APPLIED, norm));
};
module.exports = {
getStoredConfig,
setStoredConfig,
resolveRuntime,
getApplied,
setApplied,
isLoopbackHost,
validatePort
};