#!/usr/bin/env bash # Author: Marcel Herrguth, Claude Code set -euo pipefail usage() { cat < [options] Actions: create-ro Create a read-only user (and a matching role) create-rw Create a read-write user (and a matching role) remove Remove a single named user (refuses default/reserved users) Options: -u Elasticsearch URL (default: https://127.0.0.1:9200) -U Admin username to authenticate with (default: elastic) -p Admin password (required, or set ES_PASSWORD env var) -k Allow insecure TLS (skip cert verification) -n Username (required for all actions) -w New user's password (required for create-ro / create-rw, or set ES_NEW_PASSWORD) -i Restrict access to indices matching "*" (default: "*", all indices) Examples: $0 create-ro -U elastic -p "\$ES_PASSWORD" -n zammad_reader -w "\$NEW_PW" -i zammad_production -k $0 create-rw -U elastic -p "\$ES_PASSWORD" -n zammad_writer -w "\$NEW_PW" -i zammad_production -k $0 remove -U elastic -p "\$ES_PASSWORD" -n zammad_reader -k EOF exit 1 } [[ $# -ge 1 ]] || usage action="$1"; shift url="https://127.0.0.1:9200" admin_user="elastic" admin_password="${ES_PASSWORD:-}" insecure=() new_user="" new_password="${ES_NEW_PASSWORD:-}" index_pattern="*" while getopts "u:U:p:kn:w:i:h" opt; do case "$opt" in u) url="$OPTARG" ;; U) admin_user="$OPTARG" ;; p) admin_password="$OPTARG" ;; k) insecure=(--insecure) ;; n) new_user="$OPTARG" ;; w) new_password="$OPTARG" ;; i) index_pattern="${OPTARG}*" ;; h|*) usage ;; esac done [[ -n "$admin_password" ]] || { echo "Admin password required (-p or ES_PASSWORD)"; usage; } CURL=(curl "${insecure[@]}" -s -u "${admin_user}:${admin_password}") create_user() { local role_name="$1" privileges_json="$2" [[ -n "$new_user" ]] || { echo "New username required (-n)"; usage; } [[ -n "$new_password" ]] || { echo "New user's password required (-w or ES_NEW_PASSWORD)"; usage; } role_body=$(jq -n --argjson privileges "$privileges_json" --arg pattern "$index_pattern" \ '{indices: [{names: [$pattern], privileges: $privileges}]}') role_resp=$("${CURL[@]}" -X PUT "${url}/_security/role/${role_name}" -H 'Content-Type: application/json' -d "$role_body") echo "role ${role_name}: $(echo "$role_resp" | jq -c .)" user_body=$(jq -n --arg password "$new_password" --arg role "$role_name" \ '{password: $password, roles: [$role]}') user_resp=$("${CURL[@]}" -X PUT "${url}/_security/user/${new_user}" -H 'Content-Type: application/json' -d "$user_body") echo "user ${new_user}: $(echo "$user_resp" | jq -c .)" } case "$action" in create-ro) create_user "${new_user}_ro" '["read","view_index_metadata"]' echo "Created read-only user '${new_user}', scoped to indices matching '${index_pattern}'." ;; create-rw) create_user "${new_user}_rw" '["read","write","create_index","view_index_metadata"]' echo "Created read-write user '${new_user}', scoped to indices matching '${index_pattern}'." ;; remove) [[ -n "$new_user" ]] || { echo "Username required (-n)"; usage; } lookup=$("${CURL[@]}" "${url}/_security/user/${new_user}") if echo "$lookup" | jq -e 'has("error")' >/dev/null; then echo "User '${new_user}' not found." exit 1 fi if echo "$lookup" | jq -e --arg u "$new_user" '.[$u].metadata._reserved == true' >/dev/null; then echo "'${new_user}' is a default/reserved user — refusing to delete." exit 1 fi roles=$(echo "$lookup" | jq -r --arg u "$new_user" --arg ro "${new_user}_ro" --arg rw "${new_user}_rw" \ '.[$u].roles[] | select(. == $ro or . == $rw)') read -rp "Delete user '${new_user}'? [y/N] " ok [[ "$ok" == "y" ]] || exit 1 "${CURL[@]}" -X DELETE "${url}/_security/user/${new_user}" | jq -c . for role in $roles; do "${CURL[@]}" -X DELETE "${url}/_security/role/${role}" | jq -c --arg role "$role" '{role: $role, result: .}' done ;; *) usage ;; esac