#!/usr/bin/env bash # add_user.sh — добавить пользователя в caddy/users.caddy.snippet # Usage: ./scripts/auth/add_user.sh "" # # - Читает пароль из stdin (pipe) или генерирует 16-char random если интерактивен. # - Bcrypt через `docker run --rm caddy:2 caddy hash-password`. # - Печатает plain password на stdout ОДИН РАЗ. # - НЕ коммитит изменения — это задача разработчика. set -euo pipefail SNIPPET="$(dirname "$(dirname "$(realpath "$0")")")/caddy/users.caddy.snippet" usage() { echo "Usage: $0 \"\"" >&2 exit 1 } # ─── args ──────────────────────────────────────────────────────────────────── [[ $# -lt 2 ]] && usage USERNAME="$1" COMMENT="$2" DATE="$(date -u +%Y-%m-%d)" # Validate username: ^[a-z][a-z0-9-]{2,30}$ if ! [[ "$USERNAME" =~ ^[a-z][a-z0-9-]{2,30}$ ]]; then echo "ERROR: invalid username '$USERNAME'. Must match ^[a-z][a-z0-9-]{2,30}$" >&2 exit 1 fi # ─── idempotency guard ──────────────────────────────────────────────────────── if grep -qP "^\s+${USERNAME}\s" "$SNIPPET" 2>/dev/null; then echo "ERROR: user '$USERNAME' already exists in $SNIPPET. Remove first with remove_user.sh." >&2 exit 1 fi # ─── password ───────────────────────────────────────────────────────────────── if [ -t 0 ]; then # Interactive — generate random password PLAIN_PASS="$(LC_ALL=C tr -dc 'A-Za-z0-9' &2 exit 1 fi # ─── bcrypt via caddy container ─────────────────────────────────────────────── # Caddy 2.11+ requires base64-encoded bcrypt in Caddyfile basic_auth. # Password passed via stdin to avoid exposure in process list. HASH="$(printf '%s' "$PLAIN_PASS" | docker run --rm -i caddy:2 sh -c ' pass=$(cat) raw=$(caddy hash-password --algorithm bcrypt --plaintext "$pass") printf "%s" "$raw" | base64 | tr -d "\n" ')" if [[ -z "$HASH" ]]; then echo "ERROR: caddy hash-password returned empty hash" >&2 exit 1 fi # ─── insert into snippet ───────────────────────────────────────────────────── # Insert before the closing brace of basic_auth block if ! grep -q '^}' "$SNIPPET"; then echo "ERROR: cannot find closing brace in $SNIPPET" >&2 exit 1 fi # Use a temp file for safe in-place edit TMP="$(mktemp)" trap 'rm -f "$TMP"' EXIT # Insert new user line before the last closing `}` of the basic_auth block awk -v user="$USERNAME" -v hash="$HASH" -v comment="$COMMENT" -v date="$DATE" ' /^}/ && !done { printf " %-7s %s # %s (%s)\n", user, hash, comment, date done=1 } { print } ' "$SNIPPET" > "$TMP" mv "$TMP" "$SNIPPET" # ─── output ─────────────────────────────────────────────────────────────────── echo "Added user '$USERNAME' to $SNIPPET" echo "" echo "Plain password (save now — not stored anywhere):" echo "$PLAIN_PASS" echo "" echo "Next steps:" echo " 1. Commit caddy/users.caddy.snippet → PR → merge → GHA deploy" echo " 2. After deploy, Caddy auto-reloads (deploy.yml does caddy reload)" echo " 3. Send username + password to stakeholder via secure channel"