4542c055bb
The modal previously showed zero LDAP replication status -- no ServerID, no MMR active/inactive indicator, nothing -- and only aggregate counts, never per-spoke detail (no-inbound flag, relay note, assigned ldapServerId). An operator had no way to tell whether replication was actually configured/working without SSHing in. Added utils/ldap_replication.js's currentSlapdServerId(), which reads the ACTUAL running ServerID straight from this node's own slapd.conf -- distinct from what GET /ldap-peers / /ldap-replication-config currently ADVERTISE for it, which can genuinely disagree right after a promotion or a new spoke joining (OpenLDAP's static config only reloads at process start). GET /directory-admin/site-status now surfaces both plus a `stale` flag, and a full spokes list (not just a count) with each one's endpoint, LDAP ServerID, and relay path. directory.ejs renders this as an "LDAP Replication (MMR)" status row (ServerID, peer count, a "needs setup.sh re-run" warning when stale) and a Registered Spokes table. Verified two ways: real running containers via docker-compose.multisite-e2e.yml (new site-status assertions), and an actual browser session against the promoted node -- screenshotted the rendered modal showing the real registered spoke with its assigned ldapServerId and the correctly-surfaced "not configured (standalone)" MMR state (this test node never ran site-ldap-register.js against it, so the mismatch itself is the expected, documented behavior).
69 lines
2.8 KiB
JavaScript
69 lines
2.8 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
|
|
// OpenLDAP multi-master replication (docs/replication.md) config derivation,
|
|
// shared between routes/api_site.js (the spoke-facing side: assigns a
|
|
// ServerID at registration, serves GET /api/site/ldap-peers) and
|
|
// routes/api_directory_admin.js (the master-local side: GET
|
|
// /directory-admin/ldap-replication-config computes the master's own
|
|
// replication config from the same SiteSpoke registry, no HTTP round-trip
|
|
// needed since it already has the data).
|
|
|
|
const { SiteSpoke } = require('../models/site_spoke');
|
|
|
|
// The master reserves ServerID 1 for itself; every spoke gets the lowest
|
|
// free ID from 2 upward, assigned once at registration and reused across
|
|
// re-registrations (SiteSpoke.ldapServerId is only ever set on first
|
|
// create). Small max, matching mesh_gateway.js's mesh index -- nothing in
|
|
// the OpenLDAP protocol requires a small ServerID, but this deployment's
|
|
// docs/examples always have.
|
|
const MAX_LDAP_SERVER_ID = 4094;
|
|
|
|
async function nextFreeLdapServerId() {
|
|
const spokes = await SiteSpoke.list();
|
|
const used = new Set(spokes.map((s) => s.ldapServerId).filter(Boolean));
|
|
for (let i = 2; i <= MAX_LDAP_SERVER_ID; i++) {
|
|
if (!used.has(i)) return i;
|
|
}
|
|
throw new Error(`LDAP server ID space exhausted (max ${MAX_LDAP_SERVER_ID} spokes)`);
|
|
}
|
|
|
|
// A site's LDAP replication URL, derived from its already-known HTTP(S)
|
|
// endpoint rather than requiring a separately-configured field: same
|
|
// hostname, LDAPS port 636 -- exactly the convention docs/replication.md's
|
|
// own worked examples already use (ldaps://sso.site2.com:636 alongside
|
|
// https://sso.site2.com). No new config an operator has to keep in sync.
|
|
function ldapHostFor(endpoint) {
|
|
try {
|
|
const host = new URL(endpoint).hostname;
|
|
return `ldaps://${host}:636`;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const SLAPD_CONF_PATH = process.env.SLAPD_CONF_PATH || '/etc/openldap/slapd.conf';
|
|
|
|
// The ServerID this node's OpenLDAP is ACTUALLY running with right now, read
|
|
// straight from slapd.conf (the same file docker-entrypoint.sh writes
|
|
// `ServerID <n>` into). This can genuinely differ from what
|
|
// GET /ldap-peers / /ldap-replication-config currently ADVERTISE for this
|
|
// node -- OpenLDAP's static slapd.conf is only read at process start, so a
|
|
// promotion or a new spoke joining doesn't retroactively change what's
|
|
// already running until `setup.sh` restarts the container. Surfaced on the
|
|
// Multi-Site modal so an operator can see "configured X, but slapd is still
|
|
// running Y" instead of assuming replication is live because the API says so.
|
|
function currentSlapdServerId() {
|
|
let contents;
|
|
try {
|
|
contents = fs.readFileSync(SLAPD_CONF_PATH, 'utf8');
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
const m = contents.match(/^ServerID\s+(\d+)/m);
|
|
return m ? Number(m[1]) : null;
|
|
}
|
|
|
|
module.exports = { MAX_LDAP_SERVER_ID, nextFreeLdapServerId, ldapHostFor, currentSlapdServerId };
|