|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +LDAP_URI="ldap://localhost:3389" |
| 6 | +DM_DN="cn=Directory Manager" |
| 7 | +DM_PW="admin1234" |
| 8 | +SUFFIX="dc=example,dc=org" |
| 9 | +CONTAINER="389ds-ppolicy" |
| 10 | + |
| 11 | +wait_for_ldap() { |
| 12 | + echo "Waiting for 389ds to be ready..." |
| 13 | + for i in $(seq 1 30); do |
| 14 | + if ldapsearch -x -H "$LDAP_URI" -D "$DM_DN" -w "$DM_PW" -b "" -s base vendorVersion 2>/dev/null | grep -q vendorVersion; then |
| 15 | + echo "389ds is ready." |
| 16 | + return 0 |
| 17 | + fi |
| 18 | + sleep 1 |
| 19 | + done |
| 20 | + echo "389ds did not become ready in time." |
| 21 | + exit 1 |
| 22 | +} |
| 23 | + |
| 24 | +create_ou_people() { |
| 25 | + echo "Creating ou=people..." |
| 26 | + # The Dockerfile creates the suffix entry (dc=example,dc=org) via |
| 27 | + # dscreate, but we still need the ou=people container for users. |
| 28 | + ldapadd -x -H "$LDAP_URI" -D "$DM_DN" -w "$DM_PW" <<'EOF' |
| 29 | +dn: ou=people,dc=example,dc=org |
| 30 | +objectClass: organizationalUnit |
| 31 | +ou: people |
| 32 | +EOF |
| 33 | +} |
| 34 | + |
| 35 | +configure_password_policy() { |
| 36 | + echo "Configuring password policy..." |
| 37 | + # passwordSendExpiringTime makes 389ds always include timeBeforeExpiration |
| 38 | + # in the ppolicy response, regardless of the warning period. |
| 39 | + # passwordWarning is set to passwordMaxAge + 1 so the timeBeforeExpiration |
| 40 | + # warning appears immediately after a password change. |
| 41 | + # nsslapd-require-secure-binds is disabled so password operations work |
| 42 | + # over plain LDAP (this is a test setup, not production). |
| 43 | + ldapmodify -x -H "$LDAP_URI" -D "$DM_DN" -w "$DM_PW" <<'EOF' |
| 44 | +dn: cn=config |
| 45 | +changetype: modify |
| 46 | +replace: nsslapd-require-secure-binds |
| 47 | +nsslapd-require-secure-binds: off |
| 48 | +- |
| 49 | +replace: passwordMustChange |
| 50 | +passwordMustChange: on |
| 51 | +- |
| 52 | +replace: passwordExp |
| 53 | +passwordExp: on |
| 54 | +- |
| 55 | +replace: passwordMaxAge |
| 56 | +passwordMaxAge: 86400 |
| 57 | +- |
| 58 | +replace: passwordWarning |
| 59 | +passwordWarning: 86401 |
| 60 | +- |
| 61 | +replace: passwordSendExpiringTime |
| 62 | +passwordSendExpiringTime: on |
| 63 | +EOF |
| 64 | +} |
| 65 | + |
| 66 | +create_manager() { |
| 67 | + echo "Creating manager account..." |
| 68 | + # The manager account is used as Keycloak's bind DN. Using Directory |
| 69 | + # Manager as a Keycloak bind DN is bad practice — it has unrestricted |
| 70 | + # access to the entire directory. |
| 71 | + ldapadd -x -H "$LDAP_URI" -D "$DM_DN" -w "$DM_PW" <<'EOF' |
| 72 | +dn: uid=manager,ou=people,dc=example,dc=org |
| 73 | +objectClass: inetOrgPerson |
| 74 | +uid: manager |
| 75 | +cn: LDAP Manager |
| 76 | +sn: Manager |
| 77 | +userPassword: manager |
| 78 | +EOF |
| 79 | +} |
| 80 | + |
| 81 | +add_manager_aci() { |
| 82 | + echo "Granting manager read/write access..." |
| 83 | + # ACIs grant the manager write access to userPassword and read access |
| 84 | + # to the rest of the tree. |
| 85 | + ldapmodify -x -H "$LDAP_URI" -D "$DM_DN" -w "$DM_PW" <<'EOF' |
| 86 | +dn: dc=example,dc=org |
| 87 | +changetype: modify |
| 88 | +add: aci |
| 89 | +aci: (targetattr="userPassword")(version 3.0; acl "manager write userPassword"; allow (write,search) userdn="ldap:///uid=manager,ou=people,dc=example,dc=org";) |
| 90 | +- |
| 91 | +add: aci |
| 92 | +aci: (targetattr="*")(version 3.0; acl "manager read all"; allow (read,search,compare) userdn="ldap:///uid=manager,ou=people,dc=example,dc=org";) |
| 93 | +EOF |
| 94 | +} |
| 95 | + |
| 96 | +create_test_user() { |
| 97 | + echo "Creating test user..." |
| 98 | + ldapadd -x -H "$LDAP_URI" -D "$DM_DN" -w "$DM_PW" <<'EOF' |
| 99 | +dn: uid=testuser,ou=people,dc=example,dc=org |
| 100 | +objectClass: inetOrgPerson |
| 101 | +uid: testuser |
| 102 | +cn: Test User |
| 103 | +sn: User |
| 104 | +mail: testuser@example.org |
| 105 | +EOF |
| 106 | +} |
| 107 | + |
| 108 | +reset_test_user_password() { |
| 109 | + echo "Setting testuser password (triggers passwordMustChange)..." |
| 110 | + # Directory Manager password resets trigger passwordMustChange |
| 111 | + # automatically — no need to manually set pwdReset: TRUE. |
| 112 | + # Use ldapmodify instead of ldappasswd — 389ds may reject the LDAP |
| 113 | + # Password Modify Extended Operation over plain LDAP. |
| 114 | + ldapmodify -x -H "$LDAP_URI" -D "$DM_DN" -w "$DM_PW" <<'EOF' |
| 115 | +dn: uid=testuser,ou=people,dc=example,dc=org |
| 116 | +changetype: modify |
| 117 | +replace: userPassword |
| 118 | +userPassword: changeme |
| 119 | +EOF |
| 120 | +} |
| 121 | + |
| 122 | +verify() { |
| 123 | + echo "" |
| 124 | + echo "=== Verification ===" |
| 125 | + # Expect both ppolicy response elements: |
| 126 | + # warning [0]: timeBeforeExpiration (password expires in ~86400s) |
| 127 | + # error [1]: changeAfterReset(2) ("Password must be changed") |
| 128 | + echo "" |
| 129 | + ldapwhoami -x -H "$LDAP_URI" \ |
| 130 | + -D "uid=testuser,ou=people,$SUFFIX" -w "changeme" \ |
| 131 | + -e ppolicy -v 2>&1 || true |
| 132 | + echo "" |
| 133 | +} |
| 134 | + |
| 135 | +wait_for_ldap |
| 136 | +create_ou_people |
| 137 | +create_manager |
| 138 | +add_manager_aci |
| 139 | +configure_password_policy |
| 140 | +create_test_user |
| 141 | +reset_test_user_password |
| 142 | +verify |
| 143 | + |
| 144 | +echo "" |
| 145 | +echo "Done. 389ds is running on $LDAP_URI" |
| 146 | +echo " Directory Manager: $DM_DN (password: $DM_PW)" |
| 147 | +echo " Manager DN: uid=manager,ou=people,$SUFFIX (password: manager) — use as Keycloak bind DN" |
| 148 | +echo " Test user: uid=testuser,ou=people,$SUFFIX" |
| 149 | +echo " Test pass: changeme (must be changed on next login)" |
0 commit comments