// 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 };