Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02929-244b-7391-b933-c04010e8eedb
125 lines
4.3 KiB
Bash
Executable file
125 lines
4.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# update-lldap-resolver-live.sh — attended, resolver-only privacyIDEA update.
|
|
#
|
|
# This does not repair realms or policies. It updates only the persisted
|
|
# lldap-coulomb resolver after the LLDAP bind credential has changed.
|
|
#
|
|
# Usage:
|
|
# ./update-lldap-resolver-live.sh --apply
|
|
#
|
|
# The operator supplies both passwords interactively. Values are kept in a
|
|
# mode-0700 temporary directory and mode-0600 files, passed to a child process
|
|
# by pathname, and never printed or placed in command arguments.
|
|
|
|
set -euo pipefail
|
|
|
|
if [[ "${1:-}" != "--apply" || "${2:-}" != "" ]]; then
|
|
echo "Usage: $0 --apply" >&2
|
|
exit 2
|
|
fi
|
|
if [[ ! -t 0 ]]; then
|
|
echo "ERROR: --apply requires an interactive terminal." >&2
|
|
exit 2
|
|
fi
|
|
|
|
PI_URL="${PI_URL:-https://pink.coulomb.social}"
|
|
RESOLVER_NAME="${RESOLVER_NAME:-lldap-coulomb}"
|
|
LLDAP_URL="${LLDAP_URL:-ldap://lldap.sso.svc.cluster.local:3890}"
|
|
LLDAP_BASE_DN="${LLDAP_BASE_DN:-dc=netkingdom,dc=local}"
|
|
LLDAP_BIND_DN="${LLDAP_BIND_DN:-uid=admin,ou=people,dc=netkingdom,dc=local}"
|
|
|
|
tmp="$(mktemp -d)"
|
|
chmod 700 "$tmp"
|
|
cleanup() {
|
|
if [[ -f "$tmp/pi-admin" ]]; then shred -u "$tmp/pi-admin" 2>/dev/null || rm -f "$tmp/pi-admin"; fi
|
|
if [[ -f "$tmp/lldap-bind" ]]; then shred -u "$tmp/lldap-bind" 2>/dev/null || rm -f "$tmp/lldap-bind"; fi
|
|
rmdir "$tmp" 2>/dev/null || true
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
printf 'privacyIDEA pi-admin password: ' >&2
|
|
IFS= read -r -s pi_admin_password
|
|
printf '\n' >&2
|
|
printf 'LLDAP bind/admin password: ' >&2
|
|
IFS= read -r -s lldap_bind_password
|
|
printf '\n' >&2
|
|
if [[ -z "$pi_admin_password" || -z "$lldap_bind_password" ]]; then
|
|
echo "ERROR: passwords must not be empty." >&2
|
|
exit 2
|
|
fi
|
|
printf '%s' "$pi_admin_password" > "$tmp/pi-admin"
|
|
printf '%s' "$lldap_bind_password" > "$tmp/lldap-bind"
|
|
unset pi_admin_password lldap_bind_password
|
|
chmod 600 "$tmp/pi-admin" "$tmp/lldap-bind"
|
|
|
|
python3 - "$tmp/pi-admin" "$tmp/lldap-bind" "$PI_URL" "$RESOLVER_NAME" \
|
|
"$LLDAP_URL" "$LLDAP_BASE_DN" "$LLDAP_BIND_DN" <<'PY'
|
|
import json
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
pi_path, ldap_path, base_url, resolver, ldap_url, base_dn, bind_dn = sys.argv[1:]
|
|
|
|
def read_secret(path: str) -> str:
|
|
value = Path(path).read_text(encoding="utf-8")
|
|
if not value:
|
|
raise RuntimeError("empty protected input")
|
|
return value
|
|
|
|
def post(path: str, payload: dict, token: str | None = None) -> dict:
|
|
headers = {"Content-Type": "application/json"}
|
|
if token is not None:
|
|
headers["Authorization"] = token
|
|
request = urllib.request.Request(
|
|
base_url.rstrip("/") + path,
|
|
data=json.dumps(payload).encode("utf-8"),
|
|
headers=headers,
|
|
method="POST",
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(request, timeout=20) as response:
|
|
return json.load(response)
|
|
except urllib.error.HTTPError as exc:
|
|
raise RuntimeError(f"HTTP status {exc.code}") from None
|
|
except (urllib.error.URLError, TimeoutError):
|
|
raise RuntimeError("request failed") from None
|
|
|
|
try:
|
|
auth = post("/auth", {"username": "pi-admin", "password": read_secret(pi_path)})
|
|
token = str(auth.get("result", {}).get("value", {}).get("token", ""))
|
|
if not token:
|
|
raise RuntimeError("privacyIDEA authentication failed")
|
|
|
|
resolver_body = {
|
|
"type": "ldapresolver",
|
|
"LDAPURI": ldap_url,
|
|
"BINDDN": bind_dn,
|
|
"BINDPW": read_secret(ldap_path),
|
|
"LDAPBASE": base_dn,
|
|
"LOGINNAMEATTRIBUTE": "uid",
|
|
"LDAPSEARCHFILTER": "(objectClass=inetOrgPerson)",
|
|
"LDAPFILTER": "(&(objectClass=inetOrgPerson)(uid=%s))",
|
|
"USERINFO": json.dumps({
|
|
"username": "uid",
|
|
"phone": "telephoneNumber",
|
|
"mobile": "mobile",
|
|
"email": "mail",
|
|
"surname": "sn",
|
|
"givenname": "givenName",
|
|
}),
|
|
"UIDTYPE": "uid",
|
|
"NOREFERRALS": True,
|
|
"NOSCHEMAS": True,
|
|
}
|
|
result = post(f"/resolver/{resolver}", resolver_body, token)
|
|
status = result.get("result", {}).get("status")
|
|
if status not in (True, "true", "True"):
|
|
raise RuntimeError("privacyIDEA resolver update was rejected")
|
|
except RuntimeError as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
print("privacyIDEA resolver update: PASS")
|
|
PY
|