feat: N-Way Multi-Master LDAP replication and Location property
This commit is contained in:
@@ -176,6 +176,8 @@ docker compose exec sso-manager ldapsearch -x -H ldap://localhost:389 \
|
|||||||
| `PORT` | `3001` | host port mapped to the UI |
|
| `PORT` | `3001` | host port mapped to the UI |
|
||||||
| `LDAPS_PORT` | `636` | host port mapped to LDAPS |
|
| `LDAPS_PORT` | `636` | host port mapped to LDAPS |
|
||||||
| `LDAP_PORT` | `389` | uncomment the host mapping in compose to expose plain LDAP (not recommended) |
|
| `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
|
Any `app_*` var may also be set directly to override any config value (see the
|
||||||
table at the top).
|
table at the top).
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ RUN apk add --no-cache \
|
|||||||
openldap-overlay-ppolicy \
|
openldap-overlay-ppolicy \
|
||||||
openldap-overlay-memberof \
|
openldap-overlay-memberof \
|
||||||
openldap-overlay-refint \
|
openldap-overlay-refint \
|
||||||
|
openldap-overlay-syncprov \
|
||||||
openldap-passwd-sha2 \
|
openldap-passwd-sha2 \
|
||||||
dumb-init \
|
dumb-init \
|
||||||
bash \
|
bash \
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -125,6 +125,8 @@ include /etc/openldap/schema/theta42.schema
|
|||||||
include /etc/openldap/schema/sudo.schema
|
include /etc/openldap/schema/sudo.schema
|
||||||
include /etc/openldap/schema/openssh-lpk.schema
|
include /etc/openldap/schema/openssh-lpk.schema
|
||||||
|
|
||||||
|
SERVER_ID_PLACEHOLDER
|
||||||
|
|
||||||
# Module loading (pw-sha2 provides {SSHA512} used by the app for user passwords;
|
# 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+
|
# ppolicy/memberof/refint are the overlays the app depends on). On OpenLDAP 2.5+
|
||||||
# the ppolicy schema (pwdPolicy, pwdAccountLockedTime, ...) is built into
|
# the ppolicy schema (pwdPolicy, pwdAccountLockedTime, ...) is built into
|
||||||
@@ -137,6 +139,7 @@ moduleload pw-sha2
|
|||||||
moduleload ppolicy
|
moduleload ppolicy
|
||||||
moduleload memberof
|
moduleload memberof
|
||||||
moduleload refint
|
moduleload refint
|
||||||
|
SYNCPROV_MODULE_PLACEHOLDER
|
||||||
|
|
||||||
# TLS (LDAPS on 636 + StartTLS on 389). Cert/key paths are fixed; the files are
|
# 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
|
# generated/mounted above. We accept clients without their own cert (the common
|
||||||
@@ -184,6 +187,8 @@ memberof-memberof-ad memberOf
|
|||||||
overlay refint
|
overlay refint
|
||||||
refint_attributes memberOf member manager owner
|
refint_attributes memberOf member manager owner
|
||||||
|
|
||||||
|
REPLICATION_BLOCK_PLACEHOLDER
|
||||||
|
|
||||||
# Access controls
|
# Access controls
|
||||||
access to attrs=userPassword
|
access to attrs=userPassword
|
||||||
by dn="BIND_DN_PLACEHOLDER" write
|
by dn="BIND_DN_PLACEHOLDER" write
|
||||||
@@ -211,6 +216,30 @@ else
|
|||||||
sed -i "/^SLAPMODULEPATH$/d" /etc/openldap/slapd.conf
|
sed -i "/^SLAPMODULEPATH$/d" /etc/openldap/slapd.conf
|
||||||
fi
|
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 ldap:ldap /etc/openldap/slapd.conf 2>/dev/null || true
|
||||||
chown -R ldap:ldap /var/lib/ldap 2>/dev/null || true
|
chown -R ldap:ldap /var/lib/ldap 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ async function addPosixAccount(client, data){
|
|||||||
entry.dateOfBirth = data.dob;
|
entry.dateOfBirth = data.dob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.location) {
|
||||||
|
entry.l = data.location;
|
||||||
|
}
|
||||||
|
|
||||||
// userPassword is optional -- a service account with no password set
|
// userPassword is optional -- a service account with no password set
|
||||||
// simply can't bind (no special enforcement needed, that's the default
|
// simply can't bind (no special enforcement needed, that's the default
|
||||||
// LDAP simple-bind behavior for an entry lacking the attribute).
|
// 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.username = data[conf.userNameAttribute]
|
||||||
data.userPassword = undefined;
|
data.userPassword = undefined;
|
||||||
}
|
}
|
||||||
|
data.location = data.l ? String(data.l) : '';
|
||||||
// Use truthy strings so jq-repeat section blocks ({{#isActive}}) fire correctly
|
// Use truthy strings so jq-repeat section blocks ({{#isActive}}) fire correctly
|
||||||
data.isActive = data.pwdAccountLockedTime ? '' : 'active';
|
data.isActive = data.pwdAccountLockedTime ? '' : 'active';
|
||||||
data.isInactive = data.pwdAccountLockedTime ? 'inactive' : '';
|
data.isInactive = data.pwdAccountLockedTime ? 'inactive' : '';
|
||||||
@@ -519,6 +524,16 @@ User.update = async function(data){
|
|||||||
this.dateOfBirth = data.dateOfBirth;
|
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){
|
if(data.manager !== undefined){
|
||||||
// Client sends uids; resolve each to a DN before writing --
|
// Client sends uids; resolve each to a DN before writing --
|
||||||
// manager (COSINE, SUP distinguishedName) stores DNs, not uids.
|
// manager (COSINE, SUP distinguishedName) stores DNs, not uids.
|
||||||
|
|||||||
@@ -240,6 +240,7 @@
|
|||||||
<i>Phone:</i> <b>{{mobile}}</b>
|
<i>Phone:</i> <b>{{mobile}}</b>
|
||||||
{{#phoneVerified}}<span class="badge bg-success ms-1"><i class="fa-solid fa-circle-check"></i> Verified</span>{{/phoneVerified}}
|
{{#phoneVerified}}<span class="badge bg-success ms-1"><i class="fa-solid fa-circle-check"></i> Verified</span>{{/phoneVerified}}
|
||||||
<br />
|
<br />
|
||||||
|
<i>Location (Site):</i> <b>{{location}} </b><br />
|
||||||
<i>LDAP DN:</i> <b>{{dn}} </b><br />
|
<i>LDAP DN:</i> <b>{{dn}} </b><br />
|
||||||
<i>Home Directory:</i> <b>{{homeDirectory}} </b><br />
|
<i>Home Directory:</i> <b>{{homeDirectory}} </b><br />
|
||||||
<i>Login Shell:</i> <b>{{loginShell}} </b><br />
|
<i>Login Shell:</i> <b>{{loginShell}} </b><br />
|
||||||
@@ -323,6 +324,10 @@
|
|||||||
<label class="form-label">Mobile Phone</label>
|
<label class="form-label">Mobile Phone</label>
|
||||||
<input type="text" class="form-control" name="mobile" placeholder="9175551234" value="{{mobile}}" />
|
<input type="text" class="form-control" name="mobile" placeholder="9175551234" value="{{mobile}}" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Location (Site)</label>
|
||||||
|
<input type="text" class="form-control" name="location" placeholder="Site One" value="{{location}}" />
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Home Directory</label>
|
<label class="form-label">Home Directory</label>
|
||||||
<input type="text" class="form-control" name="homeDirectory" placeholder="/home/jsmith" value="{{homeDirectory}}" />
|
<input type="text" class="form-control" name="homeDirectory" placeholder="/home/jsmith" value="{{homeDirectory}}" />
|
||||||
|
|||||||
@@ -141,6 +141,11 @@ async function fetchUsernameSuggestions() {
|
|||||||
<input type="text" class="form-control shadow" name="mobile" placeholder="+14155551234" />
|
<input type="text" class="form-control shadow" name="mobile" placeholder="+14155551234" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Location (Site) <small class="text-muted">(optional)</small></label>
|
||||||
|
<input type="text" class="form-control shadow" name="location" placeholder="Site One" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Password</label>
|
<label class="form-label">Password</label>
|
||||||
<input type="password" class="form-control shadow" name="userPassword" placeholder="Atleast 5 char. long" validate="password:5"/>
|
<input type="password" class="form-control shadow" name="userPassword" placeholder="Atleast 5 char. long" validate="password:5"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user