#!/usr/bin/env bash # manage-group-members.sh — add/remove/list LLDAP group membership (NK-WP-0021) # # Usage: # ./manage-group-members.sh add [lldap-url] [secrets-dir] # ./manage-group-members.sh remove [lldap-url] [secrets-dir] # ./manage-group-members.sh list [lldap-url] [secrets-dir] # # Auth: LLDAP_LDAP_USER_PASS from secrets-dir/lldap/secrets.env, or env # LLDAP_ADMIN_PASS (e.g. from kubectl secret for live ops). # # Examples: # ./manage-group-members.sh add bernd activity-core-operators # ./manage-group-members.sh list activity-core-operators set -euo pipefail ACTION="${1:-}" shift || true LLDAP_URL="https://lldap.coulomb.social" SECRETS_DIR="../../bootstrap/secrets" usage() { sed -n '2,15p' "$0" | sed 's/^# \?//' exit 1 } case "$ACTION" in add|remove) USERNAME="${1:-}" GROUP_NAME="${2:-}" [[ -z "$USERNAME" || -z "$GROUP_NAME" ]] && usage shift 2 || true ;; list) GROUP_NAME="${1:-}" [[ -z "$GROUP_NAME" ]] && usage shift 1 || true USERNAME="" ;; *) usage ;; esac [[ -n "${1:-}" ]] && LLDAP_URL="$1" [[ -n "${2:-}" ]] && SECRETS_DIR="$2" LLDAP_ENV="$SECRETS_DIR/lldap/secrets.env" LLDAP_ADMIN_PASS="${LLDAP_ADMIN_PASS:-}" if [[ -z "$LLDAP_ADMIN_PASS" ]]; then if [[ ! -f "$LLDAP_ENV" ]]; then echo "ERROR: $LLDAP_ENV not found and LLDAP_ADMIN_PASS unset." >&2 exit 1 fi LLDAP_ADMIN_PASS=$(bash -c "source '$LLDAP_ENV' 2>/dev/null; echo \"\${LLDAP_LDAP_USER_PASS:-}\"") fi if [[ -z "$LLDAP_ADMIN_PASS" ]]; then echo "ERROR: empty LLDAP admin password" >&2 exit 1 fi echo "Authenticating to LLDAP at $LLDAP_URL ..." AUTH_RESP=$(curl -sS -X POST "$LLDAP_URL/auth/simple/login" \ -H "Content-Type: application/json" \ -d "{\"username\":\"admin\",\"password\":\"$LLDAP_ADMIN_PASS\"}") LLDAP_TOKEN=$(echo "$AUTH_RESP" | python3 -c \ "import sys,json; print(json.load(sys.stdin).get('token',''))" 2>/dev/null || echo "") if [[ -z "$LLDAP_TOKEN" ]]; then echo "ERROR: Authentication failed" >&2 exit 1 fi # Build GraphQL POST body without shell-expanding $variables inside the query. gql_post() { local query_file="$1" local vars_json="$2" python3 - "$LLDAP_URL" "$LLDAP_TOKEN" "$query_file" "$vars_json" <<'PY' import json, sys, urllib.request url, token, qpath, vars_s = sys.argv[1:5] query = open(qpath, encoding="utf-8").read() body = json.dumps({"query": query, "variables": json.loads(vars_s)}).encode() req = urllib.request.Request( url.rstrip("/") + "/api/graphql", data=body, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, method="POST", ) with urllib.request.urlopen(req, timeout=30) as resp: print(resp.read().decode()) PY } TMPDIR_GQL=$(mktemp -d) trap 'rm -rf "$TMPDIR_GQL"' EXIT cat >"$TMPDIR_GQL/list.gql" <<'EOF' query { groups { id displayName users { id displayName email } } } EOF GROUPS_JSON=$(gql_post "$TMPDIR_GQL/list.gql" '{}') GROUP_ID=$(echo "$GROUPS_JSON" | python3 -c " import sys, json name = sys.argv[1] d = json.load(sys.stdin) for g in d.get('data', {}).get('groups', []) or []: if g.get('displayName') == name: print(g['id']) break " "$GROUP_NAME") if [[ -z "$GROUP_ID" ]]; then echo "ERROR: group '$GROUP_NAME' not found — run bootstrap-users.sh first" >&2 exit 1 fi case "$ACTION" in list) echo "$GROUPS_JSON" | python3 -c " import sys, json name = sys.argv[1] d = json.load(sys.stdin) for g in d.get('data', {}).get('groups', []) or []: if g.get('displayName') == name: users = g.get('users') or [] print(f\"Group {name} (id={g.get('id')}): {len(users)} member(s)\") for u in users: print(f\" - {u.get('id')} {u.get('displayName') or ''} {u.get('email') or ''}\") break " "$GROUP_NAME" ;; add) cat >"$TMPDIR_GQL/add.gql" <<'EOF' mutation AddToGroup($userId: String!, $groupId: Int!) { addUserToGroup(userId: $userId, groupId: $groupId) { ok } } EOF VARS=$(python3 -c "import json,sys; print(json.dumps({'userId':sys.argv[1],'groupId':int(sys.argv[2])}))" "$USERNAME" "$GROUP_ID") RESP=$(gql_post "$TMPDIR_GQL/add.gql" "$VARS") echo "$RESP" | python3 -c " import sys, json d = json.load(sys.stdin) if d.get('errors'): msg = d['errors'][0].get('message', str(d['errors'])) if 'UNIQUE constraint' in msg or 'already' in msg.lower(): print('OK: already a member (no-op)') sys.exit(0) print('ERROR:', msg, file=sys.stderr) sys.exit(1) print('OK: added user to group') " echo " user=$USERNAME group=$GROUP_NAME (id=$GROUP_ID)" ;; remove) cat >"$TMPDIR_GQL/remove.gql" <<'EOF' mutation RemoveFromGroup($userId: String!, $groupId: Int!) { removeUserFromGroup(userId: $userId, groupId: $groupId) { ok } } EOF VARS=$(python3 -c "import json,sys; print(json.dumps({'userId':sys.argv[1],'groupId':int(sys.argv[2])}))" "$USERNAME" "$GROUP_ID") RESP=$(gql_post "$TMPDIR_GQL/remove.gql" "$VARS") echo "$RESP" | python3 -c " import sys, json d = json.load(sys.stdin) if d.get('errors'): print('ERROR:', d['errors'][0].get('message', d['errors']), file=sys.stderr) sys.exit(1) print('OK: removed user from group') " echo " user=$USERNAME group=$GROUP_NAME (id=$GROUP_ID)" ;; esac