131 lines
4.8 KiB
JavaScript
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
|
|
};
|