generated from coulomb/repo-seed
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>
This commit is contained in:
@@ -2,22 +2,17 @@
|
|||||||
# create-user.sh — create a user in LLDAP and add them to net-kingdom-users
|
# create-user.sh — create a user in LLDAP and add them to net-kingdom-users
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./create-user.sh <username> <email> [display-name] [lldap-url] [secrets-dir]
|
# ./create-user.sh <username> <email> [display-name] [--admin] [lldap-url] [secrets-dir]
|
||||||
#
|
#
|
||||||
# <username> LDAP uid — e.g. "bernd" or "testuser"
|
# <username> LDAP uid — e.g. "bernd" or "testuser"
|
||||||
# <email> e.g. "bernd@coulomb.social"
|
# <email> e.g. "bernd@coulomb.social"
|
||||||
# <display-name> defaults to <username>
|
# <display-name> defaults to <username>
|
||||||
|
# --admin also add to net-kingdom-admins
|
||||||
# <lldap-url> default: https://lldap.coulomb.social
|
# <lldap-url> default: https://lldap.coulomb.social
|
||||||
# <secrets-dir> default: ../../bootstrap/secrets
|
# <secrets-dir> 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:
|
# 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
|
# ./create-user.sh bernd bernd@coulomb.social "Bernd W" --admin
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -25,17 +20,19 @@ set -euo pipefail
|
|||||||
USERNAME="${1:-}"
|
USERNAME="${1:-}"
|
||||||
EMAIL="${2:-}"
|
EMAIL="${2:-}"
|
||||||
DISPLAY_NAME="${3:-$USERNAME}"
|
DISPLAY_NAME="${3:-$USERNAME}"
|
||||||
LLDAP_URL="${4:-https://lldap.coulomb.social}"
|
LLDAP_URL="https://lldap.coulomb.social"
|
||||||
SECRETS_DIR="${5:-../../bootstrap/secrets}"
|
SECRETS_DIR="../../bootstrap/secrets"
|
||||||
ADMIN_FLAG="${6:-}"
|
ADMIN_FLAG=""
|
||||||
|
|
||||||
# Allow --admin anywhere after the first two args
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
[[ "$arg" == "--admin" ]] && ADMIN_FLAG="yes"
|
[[ "$arg" == "--admin" ]] && ADMIN_FLAG="yes"
|
||||||
done
|
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
|
if [[ -z "$USERNAME" || -z "$EMAIL" ]]; then
|
||||||
echo "Usage: $0 <username> <email> [display-name] [lldap-url] [secrets-dir] [--admin]" >&2
|
echo "Usage: $0 <username> <email> [display-name] [--admin]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -55,28 +52,54 @@ LLDAP_TOKEN=$(curl -sf -X POST "$LLDAP_URL/auth/simple/login" \
|
|||||||
-d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}" \
|
-d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}" \
|
||||||
| python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
|
| 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() {
|
gql() {
|
||||||
local query="$1"; local vars="${2:-{}}"
|
local query="$1"
|
||||||
|
local vars="${2:-{}}"
|
||||||
local body
|
local body
|
||||||
body=$(python3 -c "
|
body=$(GQL_QUERY="$query" GQL_VARS="$vars" python3 -c "
|
||||||
import json, sys
|
import json, os
|
||||||
print(json.dumps({'query': sys.argv[1], 'variables': json.loads(sys.argv[2])}))
|
print(json.dumps({
|
||||||
" "$query" "$vars")
|
'query': os.environ['GQL_QUERY'],
|
||||||
|
'variables': json.loads(os.environ['GQL_VARS'])
|
||||||
|
}))
|
||||||
|
")
|
||||||
curl -sf -X POST "$LLDAP_URL/api/graphql" \
|
curl -sf -X POST "$LLDAP_URL/api/graphql" \
|
||||||
-H "Authorization: Bearer $LLDAP_TOKEN" \
|
-H "Authorization: Bearer $LLDAP_TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "$body"
|
-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 ───────────────────────────────────────────────────────────────
|
# ── 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 \
|
CREATE_RESP=$(gql \
|
||||||
'mutation CreateUser($id: String!, $email: String!, $display: String!) {
|
'mutation CreateUser($id: String!, $email: String!, $display: String!) {
|
||||||
createUser(user: {id: $id, email: $email, displayName: $display}) {
|
createUser(user: {id: $id, email: $email, displayName: $display}) {
|
||||||
id email displayName
|
id email displayName
|
||||||
}
|
}
|
||||||
}' \
|
}' \
|
||||||
"$(python3 -c "import json; print(json.dumps({'id':'$USERNAME','email':'$EMAIL','display':'$DISPLAY_NAME'}))")")
|
"$VARS")
|
||||||
|
|
||||||
ERR=$(echo "$CREATE_RESP" | python3 -c \
|
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 '')" \
|
"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 ─────────────────────────────────────────────────────────
|
# ── Look up group IDs ─────────────────────────────────────────────────────────
|
||||||
GROUPS_RESP=$(gql 'query { groups { id displayName } }')
|
GROUPS_RESP=$(gql 'query { groups { id displayName } }')
|
||||||
get_group_id() {
|
get_group_id() {
|
||||||
echo "$GROUPS_RESP" | python3 -c \
|
local name="$1"
|
||||||
"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 '')"
|
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")
|
USERS_GID=$(get_group_id "net-kingdom-users")
|
||||||
@@ -105,9 +134,10 @@ fi
|
|||||||
|
|
||||||
# ── Add to net-kingdom-users ──────────────────────────────────────────────────
|
# ── Add to net-kingdom-users ──────────────────────────────────────────────────
|
||||||
echo "Adding '$USERNAME' to net-kingdom-users (id=$USERS_GID) ..."
|
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 \
|
ADD_RESP=$(gql \
|
||||||
'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \
|
'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \
|
||||||
"{\"uid\":\"$USERNAME\",\"gid\":$USERS_GID}")
|
"$VARS")
|
||||||
ERR=$(echo "$ADD_RESP" | python3 -c \
|
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 '')" \
|
"import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \
|
||||||
2>/dev/null || echo "")
|
2>/dev/null || echo "")
|
||||||
@@ -116,12 +146,13 @@ ERR=$(echo "$ADD_RESP" | python3 -c \
|
|||||||
# ── Add to net-kingdom-admins (optional) ─────────────────────────────────────
|
# ── Add to net-kingdom-admins (optional) ─────────────────────────────────────
|
||||||
if [[ -n "$ADMIN_FLAG" ]]; then
|
if [[ -n "$ADMIN_FLAG" ]]; then
|
||||||
if [[ -z "$ADMINS_GID" ]]; 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
|
else
|
||||||
echo "Adding '$USERNAME' to net-kingdom-admins (id=$ADMINS_GID) ..."
|
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 \
|
ADD_RESP=$(gql \
|
||||||
'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \
|
'mutation AddToGroup($uid: String!, $gid: Int!) { addUserToGroup(userId: $uid, groupId: $gid) { ok } }' \
|
||||||
"{\"uid\":\"$USERNAME\",\"gid\":$ADMINS_GID}")
|
"$VARS")
|
||||||
ERR=$(echo "$ADD_RESP" | python3 -c \
|
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 '')" \
|
"import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \
|
||||||
2>/dev/null || echo "")
|
2>/dev/null || echo "")
|
||||||
@@ -136,9 +167,10 @@ read -r -s -p " Enter password (leave blank to skip): " USER_PASS
|
|||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [[ -n "$USER_PASS" ]]; then
|
if [[ -n "$USER_PASS" ]]; then
|
||||||
|
VARS=$(VAR_KEYS="uid,pw" VAR_uid="$USERNAME" VAR_pw="$USER_PASS" make_vars)
|
||||||
PASS_RESP=$(gql \
|
PASS_RESP=$(gql \
|
||||||
'mutation SetPass($uid: String!, $pw: String!) { resetUserPasswordFromAdmin(userId: $uid, password: $pw) }' \
|
'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 \
|
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 '')" \
|
"import sys,json; d=json.load(sys.stdin); errs=d.get('errors',[]); print(errs[0]['message'] if errs else '')" \
|
||||||
2>/dev/null || echo "")
|
2>/dev/null || echo "")
|
||||||
@@ -150,10 +182,10 @@ fi
|
|||||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════"
|
||||||
echo " User '$USERNAME' created and added to net-kingdom-users."
|
echo " User '$USERNAME' ready."
|
||||||
[[ -n "$ADMIN_FLAG" ]] && echo " Also added to net-kingdom-admins."
|
[[ -n "$ADMIN_FLAG" ]] && echo " Groups: net-kingdom-users, net-kingdom-admins" || echo " Group: net-kingdom-users"
|
||||||
echo "════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo " 1. User self-enrolls TOTP at https://pink-account.coulomb.social"
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user