diff --git a/sso-mfa/k8s/lldap/create-user.sh b/sso-mfa/k8s/lldap/create-user.sh index 1c5a836..d3c7af2 100755 --- a/sso-mfa/k8s/lldap/create-user.sh +++ b/sso-mfa/k8s/lldap/create-user.sh @@ -2,22 +2,17 @@ # 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] +# ./create-user.sh [display-name] [--admin] [lldap-url] [secrets-dir] # # LDAP uid — e.g. "bernd" or "testuser" # e.g. "bernd@coulomb.social" # defaults to +# --admin also add to net-kingdom-admins # 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 testuser test.user@coulomb.social "Test User" # ./create-user.sh bernd bernd@coulomb.social "Bernd W" --admin set -euo pipefail @@ -25,17 +20,19 @@ 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:-}" +LLDAP_URL="https://lldap.coulomb.social" +SECRETS_DIR="../../bootstrap/secrets" +ADMIN_FLAG="" -# Allow --admin anywhere after the first two args for arg in "$@"; do [[ "$arg" == "--admin" ]] && ADMIN_FLAG="yes" done +# Allow lldap-url and secrets-dir as positional 4/5 if not --admin +[[ "${4:-}" != "--admin" && -n "${4:-}" ]] && LLDAP_URL="${4}" +[[ "${5:-}" != "--admin" && -n "${5:-}" ]] && SECRETS_DIR="${5}" if [[ -z "$USERNAME" || -z "$EMAIL" ]]; then - echo "Usage: $0 [display-name] [lldap-url] [secrets-dir] [--admin]" >&2 + echo "Usage: $0 [display-name] [--admin]" >&2 exit 1 fi @@ -55,28 +52,54 @@ LLDAP_TOKEN=$(curl -sf -X POST "$LLDAP_URL/auth/simple/login" \ -d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])") +# gql — execute a GraphQL query against LLDAP. +# Passes query and variables via environment to avoid shell quoting issues +# with special characters (spaces, quotes) in display names or emails. gql() { - local query="$1"; local vars="${2:-{}}" + 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") + body=$(GQL_QUERY="$query" GQL_VARS="$vars" python3 -c " +import json, os +print(json.dumps({ + 'query': os.environ['GQL_QUERY'], + 'variables': json.loads(os.environ['GQL_VARS']) +})) +") curl -sf -X POST "$LLDAP_URL/api/graphql" \ -H "Authorization: Bearer $LLDAP_TOKEN" \ -H "Content-Type: application/json" \ -d "$body" } +# Build variables JSON safely via env vars +make_vars() { + python3 -c " +import json, os +d = {} +for k in os.environ.get('VAR_KEYS','').split(','): + if k: + d[k] = os.environ.get('VAR_' + k, '') +print(json.dumps(d)) +" +} + # ── Create user ─────────────────────────────────────────────────────────────── -echo "Creating user '$USERNAME' ($EMAIL) ..." +echo "Creating user '$USERNAME' ($EMAIL, display='$DISPLAY_NAME') ..." + +VARS=$(VAR_KEYS="id,email,display" \ + VAR_id="$USERNAME" \ + VAR_email="$EMAIL" \ + VAR_display="$DISPLAY_NAME" \ + make_vars) + 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'}))")") + "$VARS") 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 '')" \ @@ -91,8 +114,14 @@ 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 '')" + local name="$1" + echo "$GROUPS_RESP" | GRP_NAME="$name" python3 -c " +import sys,json,os +d=json.load(sys.stdin) +grps=d.get('data',{}).get('groups',[]) +matches=[str(g['id']) for g in grps if g['displayName']==os.environ['GRP_NAME']] +print(matches[0] if matches else '') +" } USERS_GID=$(get_group_id "net-kingdom-users") @@ -105,9 +134,10 @@ fi # ── Add to net-kingdom-users ────────────────────────────────────────────────── echo "Adding '$USERNAME' to net-kingdom-users (id=$USERS_GID) ..." +VARS=$(VAR_KEYS="uid,gid" VAR_uid="$USERNAME" VAR_gid="$USERS_GID" make_vars) ADD_RESP=$(gql \ 'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \ - "{\"uid\":\"$USERNAME\",\"gid\":$USERS_GID}") + "$VARS") 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 "") @@ -116,12 +146,13 @@ ERR=$(echo "$ADD_RESP" | python3 -c \ # ── 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 + echo " WARNING: group 'net-kingdom-admins' not found — skipping." >&2 else echo "Adding '$USERNAME' to net-kingdom-admins (id=$ADMINS_GID) ..." + VARS=$(VAR_KEYS="uid,gid" VAR_uid="$USERNAME" VAR_gid="$ADMINS_GID" make_vars) ADD_RESP=$(gql \ 'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \ - "{\"uid\":\"$USERNAME\",\"gid\":$ADMINS_GID}") + "$VARS") 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 "") @@ -136,9 +167,10 @@ read -r -s -p " Enter password (leave blank to skip): " USER_PASS echo "" if [[ -n "$USER_PASS" ]]; then + VARS=$(VAR_KEYS="uid,pw" VAR_uid="$USERNAME" VAR_pw="$USER_PASS" make_vars) 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'}))")") + "$VARS") 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 "") @@ -150,10 +182,10 @@ 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 " User '$USERNAME' ready." +[[ -n "$ADMIN_FLAG" ]] && echo " Groups: net-kingdom-users, net-kingdom-admins" || echo " Group: net-kingdom-users" 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" +echo " 2. Verify in privacyIDEA: GET /user/?realm=coulomb&username=$USERNAME"