Implement NK-WP-0021 activity-core ops SSO least-privilege.
Seed LLDAP activity-core-operators, add membership runbook and helper, restrict Authelia access on activity/temporal.coulomb.social to that group (member one_factor + domain deny fallback), apply live, and verify via Authelia check-policy plus unauthenticated edge redirects.
This commit is contained in:
parent
3580e43e5f
commit
a9aec541ec
8 changed files with 384 additions and 33 deletions
179
sso-mfa/k8s/lldap/manage-group-members.sh
Executable file
179
sso-mfa/k8s/lldap/manage-group-members.sh
Executable file
|
|
@ -0,0 +1,179 @@
|
|||
#!/usr/bin/env bash
|
||||
# manage-group-members.sh — add/remove/list LLDAP group membership (NK-WP-0021)
|
||||
#
|
||||
# Usage:
|
||||
# ./manage-group-members.sh add <uid> <group> [lldap-url] [secrets-dir]
|
||||
# ./manage-group-members.sh remove <uid> <group> [lldap-url] [secrets-dir]
|
||||
# ./manage-group-members.sh list <group> [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
|
||||
Loading…
Add table
Add a link
Reference in a new issue