chore: commit work in progress before registrar reconciliation
Refs CUST-WP-0068 Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
This commit is contained in:
parent
eebf5de371
commit
80c3a76431
1 changed files with 52 additions and 22 deletions
|
|
@ -8,16 +8,18 @@
|
||||||
# manifest.
|
# manifest.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./reconcile-lldap-resolver-live.sh --apply
|
# ./reconcile-lldap-resolver-live.sh --check # read-only proof
|
||||||
|
# ./reconcile-lldap-resolver-live.sh --apply # resolver update + proof
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
if [[ "${1:-}" != "--apply" || "${2:-}" != "" ]]; then
|
MODE="${1:-}"
|
||||||
echo "Usage: $0 --apply" >&2
|
if [[ "$MODE" != "--apply" && "$MODE" != "--check" || "${2:-}" != "" ]]; then
|
||||||
|
echo "Usage: $0 --check|--apply" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
if [[ ! -t 0 ]]; then
|
if [[ ! -t 0 ]]; then
|
||||||
echo "ERROR: --apply requires an interactive terminal." >&2
|
echo "ERROR: --check/--apply requires an interactive terminal." >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -34,7 +36,7 @@ KEYCAPE_DISCOVERY_URL="${KEYCAPE_DISCOVERY_URL:-https://kc.coulomb.social/.well-
|
||||||
tmp="$(mktemp -d)"
|
tmp="$(mktemp -d)"
|
||||||
chmod 700 "$tmp"
|
chmod 700 "$tmp"
|
||||||
cleanup() {
|
cleanup() {
|
||||||
for file in pi-admin lldap-new lldap-old otp; do
|
for file in pi-admin lldap-new lldap-old otp phase; do
|
||||||
if [[ -f "$tmp/$file" ]]; then
|
if [[ -f "$tmp/$file" ]]; then
|
||||||
shred -u "$tmp/$file" 2>/dev/null || rm -f "$tmp/$file"
|
shred -u "$tmp/$file" 2>/dev/null || rm -f "$tmp/$file"
|
||||||
fi
|
fi
|
||||||
|
|
@ -65,19 +67,23 @@ prompt_secret "one-time MFA code for $MFA_USER@$MFA_REALM" "$tmp/otp"
|
||||||
|
|
||||||
if python3 - "$tmp/pi-admin" "$tmp/lldap-new" "$tmp/lldap-old" "$tmp/otp" \
|
if python3 - "$tmp/pi-admin" "$tmp/lldap-new" "$tmp/lldap-old" "$tmp/otp" \
|
||||||
"$PI_URL" "$LLDAP_AUTH_URL" "$LLDAP_URL" "$LLDAP_BASE_DN" "$LLDAP_BIND_DN" \
|
"$PI_URL" "$LLDAP_AUTH_URL" "$LLDAP_URL" "$LLDAP_BASE_DN" "$LLDAP_BIND_DN" \
|
||||||
"$RESOLVER_NAME" "$MFA_USER" "$MFA_REALM" "$KEYCAPE_DISCOVERY_URL" <<'PY'
|
"$RESOLVER_NAME" "$MFA_USER" "$MFA_REALM" "$KEYCAPE_DISCOVERY_URL" "$MODE" "$tmp/phase" <<'PY'
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
(
|
(
|
||||||
pi_path, new_path, old_path, otp_path, pi_url, lldap_auth_url, lldap_url,
|
pi_path, new_path, old_path, otp_path, pi_url, lldap_auth_url, lldap_url,
|
||||||
base_dn, bind_dn, resolver_name, mfa_user, mfa_realm, discovery_url,
|
base_dn, bind_dn, resolver_name, mfa_user, mfa_realm, discovery_url, mode, phase_path,
|
||||||
) = sys.argv[1:]
|
) = sys.argv[1:]
|
||||||
|
|
||||||
|
def phase(name: str) -> None:
|
||||||
|
Path(phase_path).write_text(name, encoding="ascii")
|
||||||
|
|
||||||
def secret(path: str) -> str:
|
def secret(path: str) -> str:
|
||||||
value = Path(path).read_text(encoding="utf-8")
|
value = Path(path).read_text(encoding="utf-8")
|
||||||
if not value:
|
if not value:
|
||||||
|
|
@ -133,14 +139,27 @@ def lldap_login(password: str) -> tuple[int, bool]:
|
||||||
return status, status == 200 and isinstance(body, dict) and bool(body.get("token"))
|
return status, status == 200 and isinstance(body, dict) and bool(body.get("token"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
phase("preflight")
|
||||||
check_k8s_ready()
|
check_k8s_ready()
|
||||||
check_health()
|
check_health()
|
||||||
|
|
||||||
|
phase("privacyidea-auth")
|
||||||
status, auth = request(pi_url + "/auth", {"username": "pi-admin", "password": secret(pi_path)})
|
status, auth = request(pi_url + "/auth", {"username": "pi-admin", "password": secret(pi_path)})
|
||||||
pi_token = str((auth or {}).get("result", {}).get("value", {}).get("token", ""))
|
pi_token = str((auth or {}).get("result", {}).get("value", {}).get("token", ""))
|
||||||
if status != 200 or not pi_token:
|
if status != 200 or not pi_token:
|
||||||
raise RuntimeError("privacyIDEA authentication failed")
|
raise RuntimeError("privacyIDEA authentication failed")
|
||||||
|
|
||||||
|
# Prove the provider-approved replacement and predecessor disposition
|
||||||
|
# before any resolver mutation is attempted.
|
||||||
|
phase("replacement-lldap-auth")
|
||||||
|
new_status, new_authenticated = lldap_login(secret(new_path))
|
||||||
|
if new_status != 200 or not new_authenticated:
|
||||||
|
raise RuntimeError("replacement LLDAP authentication failed")
|
||||||
|
phase("predecessor-denial")
|
||||||
|
old_status, old_authenticated = lldap_login(secret(old_path))
|
||||||
|
if old_status not in (401, 403) or old_authenticated:
|
||||||
|
raise RuntimeError("predecessor LLDAP authentication was not denied")
|
||||||
|
|
||||||
resolver_body = {
|
resolver_body = {
|
||||||
"type": "ldapresolver", "LDAPURI": lldap_url, "BINDDN": bind_dn,
|
"type": "ldapresolver", "LDAPURI": lldap_url, "BINDDN": bind_dn,
|
||||||
"BINDPW": secret(new_path), "LDAPBASE": base_dn,
|
"BINDPW": secret(new_path), "LDAPBASE": base_dn,
|
||||||
|
|
@ -149,15 +168,28 @@ try:
|
||||||
"USERINFO": json.dumps({"username": "uid", "phone": "telephoneNumber", "mobile": "mobile", "email": "mail", "surname": "sn", "givenname": "givenName"}),
|
"USERINFO": json.dumps({"username": "uid", "phone": "telephoneNumber", "mobile": "mobile", "email": "mail", "surname": "sn", "givenname": "givenName"}),
|
||||||
"UIDTYPE": "uid", "NOREFERRALS": True, "NOSCHEMAS": True,
|
"UIDTYPE": "uid", "NOREFERRALS": True, "NOSCHEMAS": True,
|
||||||
}
|
}
|
||||||
status, result = request(pi_url + "/resolver/" + resolver_name, resolver_body, pi_token)
|
if mode == "--apply":
|
||||||
if status != 200 or (result or {}).get("result", {}).get("status") not in (True, "true", "True"):
|
phase("resolver-update")
|
||||||
raise RuntimeError("resolver update failed")
|
status, result = request(pi_url + "/resolver/" + resolver_name, resolver_body, pi_token)
|
||||||
|
if status != 200 or (result or {}).get("result", {}).get("status") not in (True, "true", "True"):
|
||||||
|
raise RuntimeError("resolver update failed")
|
||||||
|
else:
|
||||||
|
phase("resolver-read-only-check")
|
||||||
|
|
||||||
status, result = request(pi_url + f"/user/?realm={mfa_realm}&pagesize=1", token=pi_token)
|
phase("resolver-lookup")
|
||||||
|
status, result = request(
|
||||||
|
pi_url + f"/user/?realm={mfa_realm}&username={urllib.parse.quote(mfa_user)}",
|
||||||
|
token=pi_token,
|
||||||
|
)
|
||||||
users = (result or {}).get("result", {}).get("value", {}).get("users", [])
|
users = (result or {}).get("result", {}).get("value", {}).get("users", [])
|
||||||
if status != 200 or not users:
|
if status != 200:
|
||||||
|
phase(f"resolver-lookup-http-{status}")
|
||||||
|
raise RuntimeError("replacement resolver lookup failed")
|
||||||
|
if not users:
|
||||||
|
phase("resolver-lookup-empty")
|
||||||
raise RuntimeError("replacement resolver lookup failed")
|
raise RuntimeError("replacement resolver lookup failed")
|
||||||
|
|
||||||
|
phase("privacyidea-mfa")
|
||||||
status, result = request(
|
status, result = request(
|
||||||
pi_url + "/validate/check",
|
pi_url + "/validate/check",
|
||||||
{"user": mfa_user, "realm": mfa_realm, "pass": secret(otp_path)},
|
{"user": mfa_user, "realm": mfa_realm, "pass": secret(otp_path)},
|
||||||
|
|
@ -166,17 +198,10 @@ try:
|
||||||
if status != 200 or (result or {}).get("result", {}).get("value") is not True:
|
if status != 200 or (result or {}).get("result", {}).get("value") is not True:
|
||||||
raise RuntimeError("replacement MFA validation failed")
|
raise RuntimeError("replacement MFA validation failed")
|
||||||
|
|
||||||
new_status, new_authenticated = lldap_login(secret(new_path))
|
phase("postflight")
|
||||||
if new_status != 200 or not new_authenticated:
|
|
||||||
raise RuntimeError("replacement LLDAP authentication failed")
|
|
||||||
old_status, old_authenticated = lldap_login(secret(old_path))
|
|
||||||
if old_status not in (401, 403) or old_authenticated:
|
|
||||||
raise RuntimeError("predecessor LLDAP authentication was not denied")
|
|
||||||
|
|
||||||
check_k8s_ready()
|
check_k8s_ready()
|
||||||
check_health()
|
check_health()
|
||||||
except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
|
except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
|
||||||
print("NK-WP-0033 receipt FAIL: proof checks did not pass", file=sys.stderr)
|
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
PY
|
PY
|
||||||
then
|
then
|
||||||
|
|
@ -185,14 +210,19 @@ else
|
||||||
rc=$?
|
rc=$?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
phase_result="$(cat "$tmp/phase" 2>/dev/null || echo unknown)"
|
||||||
cleanup
|
cleanup
|
||||||
trap - EXIT INT TERM
|
trap - EXIT INT TERM
|
||||||
if [[ "$rc" -ne 0 ]]; then
|
if [[ "$rc" -ne 0 ]]; then
|
||||||
echo "NK-WP-0033 receipt FAIL: proof checks did not pass; cleanup=PASS" >&2
|
echo "NK-WP-0033 receipt FAIL: phase=$phase_result; cleanup=PASS" >&2
|
||||||
exit "$rc"
|
exit "$rc"
|
||||||
fi
|
fi
|
||||||
if [[ -d "$tmp" ]]; then
|
if [[ -d "$tmp" ]]; then
|
||||||
echo "NK-WP-0033 receipt FAIL: cleanup=FAIL" >&2
|
echo "NK-WP-0033 receipt FAIL: cleanup=FAIL" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "NK-WP-0033 receipt PASS: resolver lookup, privacyIDEA MFA, predecessor denial, readiness, health, cleanup=PASS"
|
if [[ "$MODE" == "--check" ]]; then
|
||||||
|
echo "NK-WP-0033 receipt PASS: read-only resolver lookup, privacyIDEA MFA, predecessor denial, readiness, health, cleanup=PASS"
|
||||||
|
else
|
||||||
|
echo "NK-WP-0033 receipt PASS: resolver lookup, privacyIDEA MFA, predecessor denial, readiness, health, cleanup=PASS"
|
||||||
|
fi
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue