d486fb946b
OpenLDAP N-way multi-master replication (docs/replication.md) required an operator to hand-set LDAP_SERVER_ID (unique per site) and LDAP_REPLICATION_HOSTS (every OTHER site's LDAP URL, kept in sync by hand across every node) -- real coordination work, and easy to get wrong or let drift as sites are added. Automates the coordination the master is already in a position to do: - SiteSpoke gets ldapServerId, auto-assigned (next free from 2 upward, 1 reserved for the master) at registration and reused across re-registrations -- same pattern as jump-host's mesh index. - ldapHost is derived from each site's already-known HTTP(S) endpoint (same hostname, port 636) rather than a separately-configured field that could drift from it. - New utils/ldap_replication.js (nextFreeLdapServerId, ldapHostFor), shared between the spoke-facing GET /api/site/ldap-peers (Bearer site join key, returns this caller's own ID + every peer) and the master-local GET /directory-admin/ldap-replication-config (computes its own config directly from SiteSpoke, no HTTP round-trip needed). Verified against real running containers (docker-compose.multisite-e2e.yml): after a real join, the master's computed config correctly includes the spoke as a peer with an assigned ID, and the spoke's own fetched config matches that ID and correctly excludes itself from its own peer list. Known limitation, documented in docs/replication.md: the master's own LDAP_REPLICATION_HOSTS only gets recomputed when ITS setup.sh is re-run (or an admin re-applies it directly) -- there's no live push to an already-running master when a new spoke joins. A spoke's own config is re-checked on every setup.sh run, which is the common/recurring event; the master side is a documented manual step for now rather than a live hot-reload (which would need OpenLDAP's dynamic cn=config backend -- a bigger change, deliberately out of scope here to avoid risking a live directory's LDAP replication on undertested config).
45 lines
1.8 KiB
JavaScript
45 lines
1.8 KiB
JavaScript
'use strict';
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
module.exports = { MAX_LDAP_SERVER_ID, nextFreeLdapServerId, ldapHostFor };
|