From e802fe3a9da97d2ba87cf9195b8c09db8209442d Mon Sep 17 00:00:00 2001 From: Bernd Worsch Date: Wed, 25 Mar 2026 02:14:42 +0000 Subject: [PATCH] feat(lldap): add create-user.sh for user provisioning Creates a user in LLDAP via GraphQL, adds them to net-kingdom-users, optionally net-kingdom-admins (--admin flag), and sets a password interactively. Co-Authored-By: Claude Sonnet 4.6 --- sso-mfa/k8s/lldap/create-user.sh | 159 +++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100755 sso-mfa/k8s/lldap/create-user.sh diff --git a/sso-mfa/k8s/lldap/create-user.sh b/sso-mfa/k8s/lldap/create-user.sh new file mode 100755 index 0000000..1c5a836 --- /dev/null +++ b/sso-mfa/k8s/lldap/create-user.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# create-user.sh — create a user in LLDAP and add them to net-kingdom-users +# +# Usage: +# ./create-user.sh [display-name] [lldap-url] [secrets-dir] +# +# LDAP uid — e.g. "bernd" or "testuser" +# e.g. "bernd@coulomb.social" +# defaults to +# default: https://lldap.coulomb.social +# default: ../../bootstrap/secrets +# +# The user is created with no password. They must set one via: +# https://lldap.coulomb.social (admin sets password in the WebUI), or +# the user resets it themselves if self-service password reset is configured. +# +# Add --admin as the last argument to also add to net-kingdom-admins. +# +# Examples: +# ./create-user.sh testuser testuser@coulomb.social +# ./create-user.sh bernd bernd@coulomb.social "Bernd W" --admin + +set -euo pipefail + +USERNAME="${1:-}" +EMAIL="${2:-}" +DISPLAY_NAME="${3:-$USERNAME}" +LLDAP_URL="${4:-https://lldap.coulomb.social}" +SECRETS_DIR="${5:-../../bootstrap/secrets}" +ADMIN_FLAG="${6:-}" + +# Allow --admin anywhere after the first two args +for arg in "$@"; do + [[ "$arg" == "--admin" ]] && ADMIN_FLAG="yes" +done + +if [[ -z "$USERNAME" || -z "$EMAIL" ]]; then + echo "Usage: $0 [display-name] [lldap-url] [secrets-dir] [--admin]" >&2 + exit 1 +fi + +LLDAP_ENV="$SECRETS_DIR/lldap/secrets.env" +if [[ ! -f "$LLDAP_ENV" ]]; then + echo "ERROR: $LLDAP_ENV not found." >&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) + +# ── Authenticate ────────────────────────────────────────────────────────────── +echo "Authenticating to LLDAP at $LLDAP_URL ..." +LLDAP_TOKEN=$(curl -sf -X POST "$LLDAP_URL/auth/simple/login" \ + -H "Content-Type: application/json" \ + -d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}" \ + | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") + +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" +} + +# ── Create user ─────────────────────────────────────────────────────────────── +echo "Creating user '$USERNAME' ($EMAIL) ..." +CREATE_RESP=$(gql \ + 'mutation CreateUser($id: String!, $email: String!, $display: String!) { + createUser(user: {id: $id, email: $email, displayName: $display}) { + id email displayName + } + }' \ + "$(python3 -c "import json; print(json.dumps({'id':'$USERNAME','email':'$EMAIL','display':'$DISPLAY_NAME'}))")") + +ERR=$(echo "$CREATE_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 "parse error") + +if [[ -n "$ERR" ]]; then + echo " ERROR: $ERR" >&2 + exit 1 +fi +echo " User '$USERNAME' created." + +# ── Look up group IDs ───────────────────────────────────────────────────────── +GROUPS_RESP=$(gql 'query { groups { id displayName } }') +get_group_id() { + echo "$GROUPS_RESP" | python3 -c \ + "import sys,json; d=json.load(sys.stdin); grps=d.get('data',{}).get('groups',[]); matches=[g['id'] for g in grps if g['displayName']=='$1']; print(matches[0] if matches else '')" +} + +USERS_GID=$(get_group_id "net-kingdom-users") +ADMINS_GID=$(get_group_id "net-kingdom-admins") + +if [[ -z "$USERS_GID" ]]; then + echo " ERROR: group 'net-kingdom-users' not found — run bootstrap-users.sh first." >&2 + exit 1 +fi + +# ── Add to net-kingdom-users ────────────────────────────────────────────────── +echo "Adding '$USERNAME' to net-kingdom-users (id=$USERS_GID) ..." +ADD_RESP=$(gql \ + 'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \ + "{\"uid\":\"$USERNAME\",\"gid\":$USERS_GID}") +ERR=$(echo "$ADD_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 "") +[[ -n "$ERR" ]] && echo " WARNING: $ERR" || echo " Added to net-kingdom-users." + +# ── Add to net-kingdom-admins (optional) ───────────────────────────────────── +if [[ -n "$ADMIN_FLAG" ]]; then + if [[ -z "$ADMINS_GID" ]]; then + echo " WARNING: group 'net-kingdom-admins' not found — skipping admin group." >&2 + else + echo "Adding '$USERNAME' to net-kingdom-admins (id=$ADMINS_GID) ..." + ADD_RESP=$(gql \ + 'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \ + "{\"uid\":\"$USERNAME\",\"gid\":$ADMINS_GID}") + ERR=$(echo "$ADD_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 "") + [[ -n "$ERR" ]] && echo " WARNING: $ERR" || echo " Added to net-kingdom-admins." + fi +fi + +# ── Set password ────────────────────────────────────────────────────────────── +echo "" +echo "Setting password for '$USERNAME' ..." +read -r -s -p " Enter password (leave blank to skip): " USER_PASS +echo "" + +if [[ -n "$USER_PASS" ]]; then + PASS_RESP=$(gql \ + 'mutation SetPass($uid: String!, $pw: String!) { resetUserPasswordFromAdmin(userId: $uid, password: $pw) }' \ + "$(python3 -c "import json; print(json.dumps({'uid':'$USERNAME','pw':'$USER_PASS'}))")") + ERR=$(echo "$PASS_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 "") + [[ -n "$ERR" ]] && echo " WARNING: password not set — $ERR" || echo " Password set." +else + echo " Skipped — set password via $LLDAP_URL as admin." +fi + +# ── Done ────────────────────────────────────────────────────────────────────── +echo "" +echo "════════════════════════════════════════════════════════════" +echo " User '$USERNAME' created and added to net-kingdom-users." +[[ -n "$ADMIN_FLAG" ]] && echo " Also added to net-kingdom-admins." +echo "════════════════════════════════════════════════════════════" +echo "" +echo "Next steps:" +echo " 1. User self-enrolls TOTP at https://pink-account.coulomb.social" +echo " 2. Verify user appears in privacyIDEA: GET /user/?realm=coulomb&username=$USERNAME"