Files
net-kingdom/sso-mfa/k8s/lldap/create-user.sh
Bernd Worsch ca69f6bb73 fix(lldap): use env vars in create-user.sh to avoid shell injection
Pass GraphQL query/variables and group names via environment variables
to python3 instead of shell argument interpolation. Prevents breakage
when display names, emails, or passwords contain quotes or spaces.

Also adds --admin flag support and interactive password prompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-25 11:49:26 +00:00

192 lines
8.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# create-user.sh — create a user in LLDAP and add them to net-kingdom-users
#
# Usage:
# ./create-user.sh <username> <email> [display-name] [--admin] [lldap-url] [secrets-dir]
#
# <username> LDAP uid — e.g. "bernd" or "testuser"
# <email> e.g. "bernd@coulomb.social"
# <display-name> defaults to <username>
# --admin also add to net-kingdom-admins
# <lldap-url> default: https://lldap.coulomb.social
# <secrets-dir> default: ../../bootstrap/secrets
#
# Examples:
# ./create-user.sh testuser test.user@coulomb.social "Test User"
# ./create-user.sh bernd bernd@coulomb.social "Bernd W" --admin
set -euo pipefail
USERNAME="${1:-}"
EMAIL="${2:-}"
DISPLAY_NAME="${3:-$USERNAME}"
LLDAP_URL="https://lldap.coulomb.social"
SECRETS_DIR="../../bootstrap/secrets"
ADMIN_FLAG=""
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 <username> <email> [display-name] [--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 — 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 body
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, 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
}
}' \
"$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 '')" \
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() {
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")
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) ..."
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 } }' \
"$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 "")
[[ -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." >&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 } }' \
"$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 "")
[[ -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
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) }' \
"$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 "")
[[ -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' 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 in privacyIDEA: GET /user/?realm=coulomb&username=$USERNAME"