Some checks failed
CI / changes (push) Successful in 7s
CI / backend-tests (push) Has been skipped
CI / frontend-tests (push) Has been skipped
CI / changes (pull_request) Successful in 6s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
Deploy / build-backend (push) Blocked by required conditions
Deploy / build-worker (push) Blocked by required conditions
Deploy / build-frontend (push) Blocked by required conditions
Deploy / deploy (push) Blocked by required conditions
Deploy / changes (push) Has been cancelled
#71 (CRITICAL): backup.sh committed 100644 → git reset --hard on deploy re-asserts non-exec mode → raw-path cron fails Permission denied (last good dump 2026-05-27, no S3). Commit 100755 + chmod ops/*.sh in deploy.yml + size sanity-check (never prune good dumps for a truncated one) + keep-N retention + optional S3 (redacted /etc/default template). Modeled on the working backup-tradein-db.sh. #427: widen basic_auth username regex ^[a-z][a-z0-9_.-]{1,62}$ + literal-escape dotted names in grep probes. #429: replace list_users.sh false-positive grep with anchored awk over basic_auth block. #428: PILOT_ACCESS.md support email → pilot@gendsgn.ru. Closes #71 Closes #427 Closes #429 Closes #428
113 lines
4.7 KiB
Bash
113 lines
4.7 KiB
Bash
#!/usr/bin/env bash
|
||
# add_user.sh — добавить пользователя в caddy/users.caddy.snippet
|
||
# Usage: ./scripts/auth/add_user.sh <username> "<comment>"
|
||
#
|
||
# - Читает пароль из stdin (pipe) или генерирует 16-char random если интерактивен.
|
||
# - Bcrypt через `docker run --rm caddy:2 caddy hash-password`.
|
||
# - Печатает plain password на stdout ОДИН РАЗ.
|
||
# - НЕ коммитит изменения — это задача разработчика.
|
||
set -euo pipefail
|
||
|
||
# Скрипт лежит в `<repo>/scripts/auth/`; snippet — в `<repo>/caddy/`.
|
||
# Нужно 3× `dirname`: $0 (.../scripts/auth/add_user.sh) → .../scripts/auth → .../scripts → .../<repo>.
|
||
# Bug fix 2026-05-26 — было 2× dirname (см. [[Bug_AddUser_Sh_Snippet_Path_Fixed]]).
|
||
SCRIPT_DIR="$(cd "$(dirname "$(realpath "$0")")" && pwd)"
|
||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||
SNIPPET="$REPO_ROOT/caddy/users.caddy.snippet"
|
||
|
||
usage() {
|
||
echo "Usage: $0 <username> \"<comment>\"" >&2
|
||
exit 1
|
||
}
|
||
|
||
# ─── args ────────────────────────────────────────────────────────────────────
|
||
|
||
[[ $# -lt 2 ]] && usage
|
||
|
||
USERNAME="$1"
|
||
COMMENT="$2"
|
||
DATE="$(date -u +%Y-%m-%d)"
|
||
|
||
# Validate username: ^[a-z][a-z0-9_.-]{1,62}$
|
||
# Allows dot/underscore (john.doe, jane_smith) and 2-char names; starts with a
|
||
# letter. 63-char cap keeps it sane for a Caddyfile basic_auth entry (#427).
|
||
if ! [[ "$USERNAME" =~ ^[a-z][a-z0-9_.-]{1,62}$ ]]; then
|
||
echo "ERROR: invalid username '$USERNAME'. Must match ^[a-z][a-z0-9_.-]{1,62}$" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ─── idempotency guard ────────────────────────────────────────────────────────
|
||
|
||
# Escape regex metachars (notably the now-allowed '.') so the username is matched
|
||
# literally in the existence probe below (#427).
|
||
USERNAME_RE=$(printf '%s' "$USERNAME" | sed -E 's/[.[\^$*+?(){}|]/\\&/g')
|
||
|
||
if grep -qP "^\s+${USERNAME_RE}\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' </dev/urandom | head -c 16)"
|
||
else
|
||
# Piped input
|
||
read -r PLAIN_PASS
|
||
fi
|
||
|
||
if [[ -z "$PLAIN_PASS" ]]; then
|
||
echo "ERROR: empty password" >&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"
|