diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index ef58cd6..2777e00 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -176,6 +176,8 @@ docker compose exec sso-manager ldapsearch -x -H ldap://localhost:389 \ | `PORT` | `3001` | host port mapped to the UI | | `LDAPS_PORT` | `636` | host port mapped to LDAPS | | `LDAP_PORT` | `389` | uncomment the host mapping in compose to expose plain LDAP (not recommended) | +| `LDAP_SERVER_ID` | empty | Unique integer ID (e.g. 1, 2) required to enable Multi-Master replication | +| `LDAP_REPLICATION_HOSTS` | empty | Space-separated list of other sites' LDAP URLs for replication (e.g. `ldaps://site2:636`) | Any `app_*` var may also be set directly to override any config value (see the table at the top). diff --git a/Dockerfile.openldap b/Dockerfile.openldap index 10e90ad..b8520b3 100644 --- a/Dockerfile.openldap +++ b/Dockerfile.openldap @@ -55,6 +55,7 @@ RUN apk add --no-cache \ openldap-overlay-ppolicy \ openldap-overlay-memberof \ openldap-overlay-refint \ + openldap-overlay-syncprov \ openldap-passwd-sha2 \ dumb-init \ bash \ diff --git a/docker-compose.repl-test.yml b/docker-compose.repl-test.yml new file mode 100644 index 0000000..1b8a833 --- /dev/null +++ b/docker-compose.repl-test.yml @@ -0,0 +1,36 @@ +services: + site1: + build: . + container_name: sso_site1 + environment: + - LDAP_SERVER_ID=1 + - LDAP_REPLICATION_HOSTS=ldap://site2:389 + - LDAP_BASE_DN=dc=test,dc=local + - LDAP_ADMIN_PASS=secret + ports: + - "3001:3001" + - "10389:389" + volumes: + - site1-ldap:/var/lib/ldap + - site1-redis:/data + + site2: + build: . + container_name: sso_site2 + environment: + - LDAP_SERVER_ID=2 + - LDAP_REPLICATION_HOSTS=ldap://site1:389 + - LDAP_BASE_DN=dc=test,dc=local + - LDAP_ADMIN_PASS=secret + ports: + - "3002:3001" + - "20389:389" + volumes: + - site2-ldap:/var/lib/ldap + - site2-redis:/data + +volumes: + site1-ldap: + site1-redis: + site2-ldap: + site2-redis: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index c95d6af..c3aa61c 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -125,6 +125,8 @@ include /etc/openldap/schema/theta42.schema include /etc/openldap/schema/sudo.schema include /etc/openldap/schema/openssh-lpk.schema +SERVER_ID_PLACEHOLDER + # Module loading (pw-sha2 provides {SSHA512} used by the app for user passwords; # ppolicy/memberof/refint are the overlays the app depends on). On OpenLDAP 2.5+ # the ppolicy schema (pwdPolicy, pwdAccountLockedTime, ...) is built into @@ -137,6 +139,7 @@ moduleload pw-sha2 moduleload ppolicy moduleload memberof moduleload refint +SYNCPROV_MODULE_PLACEHOLDER # TLS (LDAPS on 636 + StartTLS on 389). Cert/key paths are fixed; the files are # generated/mounted above. We accept clients without their own cert (the common @@ -184,6 +187,8 @@ memberof-memberof-ad memberOf overlay refint refint_attributes memberOf member manager owner +REPLICATION_BLOCK_PLACEHOLDER + # Access controls access to attrs=userPassword by dn="BIND_DN_PLACEHOLDER" write @@ -211,6 +216,30 @@ else sed -i "/^SLAPMODULEPATH$/d" /etc/openldap/slapd.conf fi +# ── Multi-Master Replication Configuration ── +if [[ -n "${LDAP_SERVER_ID:-}" && -n "${LDAP_REPLICATION_HOSTS:-}" ]]; then + info "Configuring Multi-Master replication (Server ID: ${LDAP_SERVER_ID})" + sed -i "s|^SERVER_ID_PLACEHOLDER|ServerID ${LDAP_SERVER_ID}|" /etc/openldap/slapd.conf + sed -i "s|^SYNCPROV_MODULE_PLACEHOLDER|moduleload syncprov|" /etc/openldap/slapd.conf + + # Generate syncrepl blocks + REPL_BLOCK="overlay syncprov\nsyncprov-checkpoint 100 10\nsyncprov-sessionlog 100\n\n" + RID=100 + for HOST in ${LDAP_REPLICATION_HOSTS}; do + RID=$((RID + 1)) + REPL_BLOCK="${REPL_BLOCK}syncrepl rid=${RID}\n provider=${HOST}\n type=refreshAndPersist\n retry=\"60 +\"\n searchbase=\"${LDAP_BASE_DN}\"\n bindmethod=simple\n binddn=\"${LDAP_BIND_DN}\"\n credentials=\"${LDAP_ADMIN_PASS}\"\n\n" + done + REPL_BLOCK="${REPL_BLOCK}mirrormode on\n" + + # Replace placeholder (awk is safer for multiline replacements than sed) + awk -v repl="$(printf '%b' "$REPL_BLOCK")" '{gsub(/REPLICATION_BLOCK_PLACEHOLDER/, repl)}1' /etc/openldap/slapd.conf > /etc/openldap/slapd.conf.tmp + mv /etc/openldap/slapd.conf.tmp /etc/openldap/slapd.conf +else + sed -i "/^SERVER_ID_PLACEHOLDER/d" /etc/openldap/slapd.conf + sed -i "/^SYNCPROV_MODULE_PLACEHOLDER/d" /etc/openldap/slapd.conf + sed -i "/^REPLICATION_BLOCK_PLACEHOLDER/d" /etc/openldap/slapd.conf +fi + chown ldap:ldap /etc/openldap/slapd.conf 2>/dev/null || true chown -R ldap:ldap /var/lib/ldap 2>/dev/null || true diff --git a/nodejs/models/user_ldap.js b/nodejs/models/user_ldap.js index 30fe669..11fd5cf 100644 --- a/nodejs/models/user_ldap.js +++ b/nodejs/models/user_ldap.js @@ -146,6 +146,10 @@ async function addPosixAccount(client, data){ entry.dateOfBirth = data.dob; } + if (data.location) { + entry.l = data.location; + } + // userPassword is optional -- a service account with no password set // simply can't bind (no special enforcement needed, that's the default // LDAP simple-bind behavior for an entry lacking the attribute). @@ -221,6 +225,7 @@ const user_parse = function(data){ data.username = data[conf.userNameAttribute] data.userPassword = undefined; } + data.location = data.l ? String(data.l) : ''; // Use truthy strings so jq-repeat section blocks ({{#isActive}}) fire correctly data.isActive = data.pwdAccountLockedTime ? '' : 'active'; data.isInactive = data.pwdAccountLockedTime ? 'inactive' : ''; @@ -519,6 +524,16 @@ User.update = async function(data){ this.dateOfBirth = data.dateOfBirth; } + if(data.location !== undefined){ + await client.modify(this.dn, [ + new Change({ + operation: 'replace', + modification: new Attribute({ type: 'l', values: [data.location] }), + }), + ]); + this.location = data.location; + } + if(data.manager !== undefined){ // Client sends uids; resolve each to a DN before writing -- // manager (COSINE, SUP distinguishedName) stores DNs, not uids. diff --git a/nodejs/views/profile.ejs b/nodejs/views/profile.ejs index 08b33b4..dc97b4d 100644 --- a/nodejs/views/profile.ejs +++ b/nodejs/views/profile.ejs @@ -240,6 +240,7 @@ Phone: {{mobile}} {{#phoneVerified}} Verified{{/phoneVerified}}
+ Location (Site): {{location}}
LDAP DN: {{dn}}
Home Directory: {{homeDirectory}}
Login Shell: {{loginShell}}
@@ -323,6 +324,10 @@ +
+ + +
diff --git a/nodejs/views/user_form.ejs b/nodejs/views/user_form.ejs index 876e51c..6f78163 100644 --- a/nodejs/views/user_form.ejs +++ b/nodejs/views/user_form.ejs @@ -141,6 +141,11 @@ async function fetchUsernameSuggestions() {
+
+ + +
+