net-kingdom/sso-mfa/k8s/lldap/bootstrap-users.sh
tegwick a9aec541ec
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 4s
Implement NK-WP-0021 activity-core ops SSO least-privilege.
Seed LLDAP activity-core-operators, add membership runbook and helper,
restrict Authelia access on activity/temporal.coulomb.social to that
group (member one_factor + domain deny fallback), apply live, and verify
via Authelia check-policy plus unauthenticated edge redirects.
2026-07-22 15:47:26 +02:00

185 lines
7.4 KiB
Bash
Executable file

#!/usr/bin/env bash
# bootstrap-users.sh — seed required groups in LLDAP
#
# Run AFTER LLDAP is deployed and Running (T05a).
#
# What it does:
# 1. Authenticates to LLDAP via its GraphQL API.
# 2. Creates required platform + app-operator groups (idempotent).
# 3. Prints a user onboarding checklist (groups-only; individual users are
# added via the WebUI, create-user.sh, or manage-group-members.sh).
#
# Groups created:
# net-kingdom-users — standard users; all human accounts go here.
# net-kingdom-admins — privileged users; KeyCape policies can enforce
# MFA step-up or grant extra scopes to this group.
# activity-core-operators — app-scoped: browser access to activity-core
# ops console + Temporal UI (Authelia domain rules).
# Not platform admin — grant consciously (NK-WP-0021).
#
# Usage:
# ./bootstrap-users.sh [lldap-url] [secrets-dir]
#
# <lldap-url> default: https://lldap.coulomb.social
# <secrets-dir> default: ../../bootstrap/secrets
set -euo pipefail
LLDAP_URL="${1:-https://lldap.coulomb.social}"
SECRETS_DIR="${2:-../../bootstrap/secrets}"
LLDAP_ENV="$SECRETS_DIR/lldap/secrets.env"
PASS_COUNT=0
FAIL_COUNT=0
ok() { echo " [OK] $1"; ((PASS_COUNT++)); }
fail() { echo " [FAIL] $1"; ((FAIL_COUNT++)); }
info() { echo " [INFO] $1"; }
if [[ ! -f "$LLDAP_ENV" ]]; then
echo "ERROR: $LLDAP_ENV not found — run sso-mfa/bootstrap/gen-secrets.sh first." >&2
exit 1
fi
read_env() { bash -c "source '$1' 2>/dev/null; echo \${$2}"; }
LLDAP_ADMIN_PASS=$(read_env "$LLDAP_ENV" LLDAP_LDAP_USER_PASS)
if [[ -z "$LLDAP_ADMIN_PASS" ]]; then
echo "ERROR: LLDAP_LDAP_USER_PASS not found in $LLDAP_ENV" >&2
exit 1
fi
# ── 1. Authenticate ───────────────────────────────────────────────────────────
echo ""
echo "Authenticating to LLDAP at $LLDAP_URL ..."
AUTH_RESP=$(curl -sf -X POST "$LLDAP_URL/auth/simple/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}" \
2>/dev/null || echo "CURL_FAILED")
if [[ "$AUTH_RESP" == "CURL_FAILED" ]]; then
echo "ERROR: Could not reach $LLDAP_URL — is LLDAP deployed and ingress up?" >&2
exit 1
fi
LLDAP_TOKEN=$(echo "$AUTH_RESP" | python3 -c \
"import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null || echo "")
if [[ -z "$LLDAP_TOKEN" ]]; then
echo "ERROR: Authentication failed. Response: $AUTH_RESP" >&2
exit 1
fi
info "Authenticated as admin"
gql() {
# gql <query> <variables-json>
local query="$1"; local vars="${2:-{}}"
local body
body=$(python3 -c "
import json, sys
print(json.dumps({'query': sys.argv[1], 'variables': json.loads(sys.argv[2])}))
" "$query" "$vars")
curl -sf -X POST "$LLDAP_URL/api/graphql" \
-H "Authorization: Bearer $LLDAP_TOKEN" \
-H "Content-Type: application/json" \
-d "$body" 2>/dev/null || echo "CURL_FAILED"
}
create_group() {
local name="$1"
echo ""
echo "Creating group: $name ..."
# Check if group already exists
LIST_RESP=$(gql 'query { groups { id displayName } }')
if [[ "$LIST_RESP" != "CURL_FAILED" ]]; then
EXISTS=$(echo "$LIST_RESP" | python3 -c \
"import sys,json; d=json.load(sys.stdin); print('yes' if any(g['displayName']=='$name' for g in d.get('data',{}).get('groups',[])) else 'no')" \
2>/dev/null || echo "no")
if [[ "$EXISTS" == "yes" ]]; then
ok "Group '$name' already exists — skipping"
return 0
fi
fi
RESP=$(gql 'mutation CreateGroup($name: String!) { createGroup(name: $name) { id displayName } }' \
"{\"name\":\"$name\"}")
if [[ "$RESP" == "CURL_FAILED" ]]; then
fail "Group '$name' — curl request failed"
return 1
fi
ERR=$(echo "$RESP" | python3 -c \
"import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \
2>/dev/null || echo "")
if [[ -n "$ERR" ]]; then
fail "Group '$name' — $ERR"
return 1
fi
GID=$(echo "$RESP" | python3 -c \
"import sys,json; print(json.load(sys.stdin).get('data',{}).get('createGroup',{}).get('id','?'))" \
2>/dev/null || echo "?")
ok "Group '$name' created (id=$GID)"
}
# ── 2. Create required groups ─────────────────────────────────────────────────
REQUIRED_GROUPS=(
"net-kingdom-users"
"net-kingdom-admins"
"activity-core-operators"
)
for grp in "${REQUIRED_GROUPS[@]}"; do
create_group "$grp"
done
# ── 3. Verify ─────────────────────────────────────────────────────────────────
echo ""
echo "Verifying groups ..."
LIST_RESP=$(gql 'query { groups { id displayName } }')
if [[ "$LIST_RESP" != "CURL_FAILED" ]]; then
for grp in "${REQUIRED_GROUPS[@]}"; do
EXISTS=$(echo "$LIST_RESP" | python3 -c \
"import sys,json; d=json.load(sys.stdin); print('yes' if any(g['displayName']=='$grp' for g in d.get('data',{}).get('groups',[])) else 'no')" \
2>/dev/null || echo "no")
if [[ "$EXISTS" == "yes" ]]; then
ok "Group '$grp' confirmed"
else
fail "Group '$grp' not found after creation"
fi
done
else
fail "Could not retrieve group list from LLDAP"
fi
# ── Summary ───────────────────────────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════════════════════════"
echo " LLDAP group bootstrap: PASS=$PASS_COUNT FAIL=$FAIL_COUNT"
echo "════════════════════════════════════════════════════════════"
echo ""
echo "Next: add users via the LLDAP WebUI, create-user.sh, or manage-group-members.sh."
echo "App-operator membership: see OPERATOR-GROUPS.md (do not auto-grant all admins)."
echo ""
echo "User onboarding checklist:"
echo ""
echo " Per new user:"
echo " 1. Create account in LLDAP WebUI ($LLDAP_URL)"
echo " Fields: username (uid), display name, email"
echo " 2. Assign to net-kingdom-users group (mandatory)"
echo " Assign to net-kingdom-admins too if privileged access is needed"
echo " 3. For activity-core ops/Temporal UI: also add to activity-core-operators"
echo " (./manage-group-members.sh add <uid> activity-core-operators)"
echo " 4. User logs in to Authelia (auth.coulomb.social) to verify their password"
echo " 5. User self-enrolls TOTP at pink-account.coulomb.social"
echo " 6. User tests end-to-end login via an OIDC-protected application"
echo ""
echo " Break-glass account:"
echo " Run: sso-mfa/k8s/lldap/break-glass.sh"
echo " (Creates a pre-seeded local bypass user outside the normal MFA flow.)"
echo ""
if [[ "$FAIL_COUNT" -gt 0 ]]; then
exit 1
fi
exit 0