#!/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 the two required groups: net-kingdom-users, net-kingdom-admins. # 3. Prints a user onboarding checklist (groups-only; individual users are # added via the WebUI or by re-running this script with USER_EMAIL set). # # 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. # # Usage: # ./bootstrap-users.sh [lldap-url] [secrets-dir] # # default: https://lldap.coulomb.social # 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 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 ───────────────────────────────────────────────── create_group "net-kingdom-users" create_group "net-kingdom-admins" # ── 3. Verify ───────────────────────────────────────────────────────────────── echo "" echo "Verifying groups ..." LIST_RESP=$(gql 'query { groups { id displayName } }') if [[ "$LIST_RESP" != "CURL_FAILED" ]]; then for grp in "net-kingdom-users" "net-kingdom-admins"; 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 or LDAP provisioning." 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. User logs in to Authelia (auth.coulomb.social) to verify their password" echo " 4. User self-enrolls TOTP at pink-account.coulomb.social" echo " 5. 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