feat(multi-site): auto-assign LDAP ServerID + replication hosts at join time
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).
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
'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 };
|
||||
Reference in New Issue
Block a user