#!/usr/bin/env bash # # Sprint 22: создание anonymized stage-dump'a из прод-БД. # # Алгоритм: # 1. pg_dump прода в кастомном формате (-Fc) # 2. pg_restore во временную staging-БД (`food_market_anon`) # 3. UPDATE PII-полей в staging: # - email → user{N}@example.kz # - phone → +7700111{N:04} # - passwordHash → один общий тестовый hash для пароля "Test12345!" # - IIN / БИН → синтетические но валидные # - имена/адреса → "Test Tester {N}" / "Тестовый адрес {N}" # 4. pg_dump anonymized → .sql.gz файл # 5. Удалить staging-БД # # Результат используется на dev-vm для воспроизведения багов на реальном # объёме данных без утечки persistent PII. # # Usage: # deploy/anonymize-prod.sh [--source ] [--target ] # [--out ] [--dry-run] # # Default source: ssh prod docker exec food-market-postgres pg_dump # Default target: local postgres@14 with TEMP DB food_market_anon # Default out: /home/nns/food-market-anon-YYYYMMDD.sql.gz set -uo pipefail SOURCE_HOST="${FM_PROD_HOST:-nns@admin.food-market.kz}" SOURCE_CONTAINER="${FM_PROD_CONT:-food-market-postgres}" SOURCE_DB="${FM_PG_DB:-food_market}" SOURCE_USER="${FM_PG_USER:-food_market}" LOCAL_USER="${PGUSER:-nns}" LOCAL_HOST="${PGHOST:-localhost}" LOCAL_PORT="${PGPORT:-5432}" TARGET_DB="food_market_anon_$$" OUT="${FM_ANON_OUT:-$HOME/food-market-anon-$(date +%Y%m%d-%H%M%S).sql.gz}" DRY_RUN=0 while [[ $# -gt 0 ]]; do case "$1" in --source-host) SOURCE_HOST="$2"; shift 2 ;; --source-container) SOURCE_CONTAINER="$2"; shift 2 ;; --out) OUT="$2"; shift 2 ;; --dry-run) DRY_RUN=1; shift ;; --help|-h) grep -E '^#( |$)' "$0" | sed 's/^# \?//'; exit 0 ;; *) echo "Unknown: $1" >&2; exit 2 ;; esac done log() { echo "[$(date -Iseconds)] $*"; } run() { if [[ $DRY_RUN -eq 1 ]]; then echo "[dry-run] $*"; else echo "[exec] $*"; "$@"; fi } cleanup() { log "cleanup: drop $TARGET_DB" if [[ $DRY_RUN -eq 0 ]]; then psql -h "$LOCAL_HOST" -p "$LOCAL_PORT" -U "$LOCAL_USER" -d postgres \ -c "DROP DATABASE IF EXISTS $TARGET_DB;" 2>/dev/null || true fi } trap cleanup EXIT # ── 1. pg_dump из прода ────────────────────────────────────────────── DUMP="/tmp/food-market-prod-$$.dump" log "Step 1/5: pg_dump from $SOURCE_HOST → $DUMP (Fc format)" if [[ $DRY_RUN -eq 1 ]]; then log "[dry-run] ssh $SOURCE_HOST docker exec $SOURCE_CONTAINER pg_dump -Fc -U $SOURCE_USER -d $SOURCE_DB > $DUMP" else ssh -o ConnectTimeout=10 "$SOURCE_HOST" \ "docker exec $SOURCE_CONTAINER pg_dump -Fc --no-owner --no-privileges -U $SOURCE_USER -d $SOURCE_DB" \ > "$DUMP" || { log "FAIL pg_dump"; exit 1; } log "dump size: $(du -h "$DUMP" | cut -f1)" fi # ── 2. Создать temp-БД и restore ───────────────────────────────────── log "Step 2/5: create $TARGET_DB + pg_restore" if [[ $DRY_RUN -eq 0 ]]; then psql -h "$LOCAL_HOST" -p "$LOCAL_PORT" -U "$LOCAL_USER" -d postgres \ -c "CREATE DATABASE $TARGET_DB;" || { log "FAIL create db"; exit 1; } pg_restore -h "$LOCAL_HOST" -p "$LOCAL_PORT" -U "$LOCAL_USER" -d "$TARGET_DB" \ --no-owner --no-privileges "$DUMP" 2>/dev/null || \ log "(некритично) pg_restore warnings — продолжаем" fi # ── 3. Anonymize PII ───────────────────────────────────────────────── # Hash для пароля "Test12345!" — генерируется через идентичный # алгоритм ASP.NET Identity (PBKDF2 SHA256, 10000 iter). Чтобы не # вводить криптографию в скрипт — берём заранее известный hash. # Сгенерировать новый можно через `dotnet run --project dev-tools/hash-pass.cs`. TEST_PASS_HASH='AQAAAAIAAYagAAAAEHJsxbHF3MoBGSe+1bktB4O9aERPI4j5Jt6w0iN4dCqU/5jL+i5xT8E+UEqcVf0Vqg==' log "Step 3/5: anonymize PII in $TARGET_DB" if [[ $DRY_RUN -eq 0 ]]; then psql -h "$LOCAL_HOST" -p "$LOCAL_PORT" -U "$LOCAL_USER" -d "$TARGET_DB" -v ON_ERROR_STOP=1 < "$OUT" log "anonymized dump: $(du -h "$OUT" | cut -f1) → $OUT" fi # ── 5. Cleanup (через trap) ────────────────────────────────────────── log "Step 5/5: cleanup" rm -f "$DUMP" 2>/dev/null || true log "✓ Готово: $OUT" log "Восстановить можно: gunzip -c $OUT | psql -d food_market_dev" exit 0