generated from coulomb/repo-seed
264 lines
11 KiB
Bash
Executable File
264 lines
11 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] [--test] [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
|
|
# --test set password automatically as <DisplayName-Pwd> (spaces→hyphens)
|
|
# e.g. display "Test User" → password "Test-User-Pwd"
|
|
# <lldap-url> default: https://lldap.coulomb.social
|
|
# <secrets-dir> default: ../../bootstrap/secrets
|
|
#
|
|
# Examples:
|
|
# ./create-user.sh testuser test.user@coulomb.social "Test User" --test
|
|
# ./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=""
|
|
TEST_FLAG=""
|
|
|
|
for arg in "$@"; do
|
|
[[ "$arg" == "--admin" ]] && ADMIN_FLAG="yes"
|
|
[[ "$arg" == "--test" ]] && TEST_FLAG="yes"
|
|
done
|
|
# Allow lldap-url and secrets-dir as positional 4/5 if not a flag
|
|
for pos in 4 5; do
|
|
val="${!pos:-}"
|
|
[[ "$val" == "--admin" || "$val" == "--test" || -z "$val" ]] && continue
|
|
[[ $pos -eq 4 ]] && LLDAP_URL="$val"
|
|
[[ $pos -eq 5 ]] && SECRETS_DIR="$val"
|
|
done
|
|
|
|
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"
|
|
LLDAP_ADMIN_PASS="${LLDAP_ADMIN_PASS:-}"
|
|
|
|
if [[ -z "$LLDAP_ADMIN_PASS" ]]; then
|
|
if [[ -f "$LLDAP_ENV" ]]; then
|
|
read_env() { bash -c "source '$1' 2>/dev/null; echo \${$2}"; }
|
|
LLDAP_ADMIN_PASS=$(read_env "$LLDAP_ENV" LLDAP_LDAP_USER_PASS)
|
|
else
|
|
# Safer fallback for dry-runs / automation (NET-WP-0019-T02): pull directly from k8s secret
|
|
# without requiring (or writing) a local secrets.env file on disk.
|
|
if command -v kubectl >/dev/null 2>&1 || [[ -n "${KUBECTL:-}" ]]; then
|
|
KUBECTL_BIN="${KUBECTL:-kubectl}"
|
|
LLDAP_ADMIN_PASS="$($KUBECTL_BIN get secret -n sso lldap-secrets -o jsonpath='{.data.LLDAP_LDAP_USER_PASS}' 2>/dev/null | base64 -d 2>/dev/null || true)"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$LLDAP_ADMIN_PASS" ]]; then
|
|
echo "ERROR: Could not obtain LLDAP admin password (no $LLDAP_ENV and no k8s fallback succeeded)." >&2
|
|
echo " For dry-runs prefer setting LLDAP_ADMIN_PASS=... or ensuring kubectl can reach the sso/lldap-secrets secret." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ── 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 _empty="{}"
|
|
local vars="${2:-$_empty}"
|
|
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.
|
|
# Set VAR_INT_KEYS to a comma-separated list of keys that should be JSON integers.
|
|
make_vars() {
|
|
python3 -c "
|
|
import json, os
|
|
d = {}
|
|
int_keys = set(k for k in os.environ.get('VAR_INT_KEYS','').split(',') if k)
|
|
for k in os.environ.get('VAR_KEYS','').split(','):
|
|
if k:
|
|
v = os.environ.get('VAR_' + k, '')
|
|
d[k] = int(v) if k in int_keys else v
|
|
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_INT_KEYS="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_INT_KEYS="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' ..."
|
|
if [[ -n "$TEST_FLAG" ]]; then
|
|
# Derive password from display name: "Test User" → "Test-User-Pwd"
|
|
USER_PASS=$(echo "$DISPLAY_NAME" | tr ' ' '-')-Pwd
|
|
echo " [--test] Using derived password: $USER_PASS"
|
|
else
|
|
read -r -s -p " Enter password (leave blank to skip): " USER_PASS
|
|
echo ""
|
|
fi
|
|
|
|
if [[ -n "$USER_PASS" ]]; then
|
|
# LLDAP has no GraphQL mutation for password setting.
|
|
# Use RFC 3062 LDAP Password Modify extended operation via a kubectl
|
|
# port-forward to the in-cluster LLDAP LDAP port (ClusterIP only).
|
|
LLDAP_BIND_DN="uid=admin,ou=people,dc=netkingdom,dc=local"
|
|
TARGET_DN="uid=$USERNAME,ou=people,dc=netkingdom,dc=local"
|
|
LOCAL_PORT=13891 # avoid conflict with other forwarders
|
|
|
|
echo " Opening kubectl port-forward to lldap:3890 ..."
|
|
KUBECONFIG="${KUBECONFIG:-$HOME/.kube/config-railiance01}" \
|
|
kubectl port-forward svc/lldap "$LOCAL_PORT:3890" -n sso &>/dev/null &
|
|
PF_PID=$!
|
|
# Wait for the forwarder to be ready (up to 10s)
|
|
for i in $(seq 1 10); do
|
|
nc -z 127.0.0.1 "$LOCAL_PORT" 2>/dev/null && break
|
|
sleep 1
|
|
done
|
|
|
|
PASS_ERR=$(LLDAP_USER_PASS="$LLDAP_ADMIN_PASS" \
|
|
LLDAP_BIND_DN_VAR="$LLDAP_BIND_DN" \
|
|
TARGET_DN_VAR="$TARGET_DN" \
|
|
NEW_PASS="$USER_PASS" \
|
|
LOCAL_PORT_VAR="$LOCAL_PORT" \
|
|
python3 -c "
|
|
import os, sys
|
|
try:
|
|
from ldap3 import Server, Connection
|
|
s = Server('127.0.0.1', port=int(os.environ['LOCAL_PORT_VAR']))
|
|
c = Connection(s, user=os.environ['LLDAP_BIND_DN_VAR'], password=os.environ['LLDAP_USER_PASS'])
|
|
c.bind()
|
|
c.extend.standard.modify_password(
|
|
user=os.environ['TARGET_DN_VAR'],
|
|
new_password=os.environ['NEW_PASS'])
|
|
if c.result['result'] != 0:
|
|
print(c.result['description'], file=sys.stderr)
|
|
sys.exit(1)
|
|
c.unbind()
|
|
except ImportError:
|
|
print('ldap3 not installed — install with: pip install ldap3 --break-system-packages', file=sys.stderr)
|
|
sys.exit(1)
|
|
" 2>&1)
|
|
kill "$PF_PID" 2>/dev/null; wait "$PF_PID" 2>/dev/null || true
|
|
|
|
if [[ -n "$PASS_ERR" ]]; then
|
|
echo " WARNING: password not set — $PASS_ERR" >&2
|
|
echo " Set manually via the LLDAP WebUI at $LLDAP_URL"
|
|
else
|
|
echo " Password set."
|
|
fi
|
|
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"
|