#!/usr/bin/env bash
#MISE description="Проверка глазами сервиса: вход по AppRole, секреты из Vault, доступ к S3 и Postgres"
set -euo pipefail

KV=$VAULT_MOUNT
DB=$VAULT_DATABASE_MOUNT

step() { printf '\n== %s\n' "$*"; }
ok() { printf '   ok: %s\n' "$*"; }
fail() {
  printf '   ОШИБКА: %s\n' "$*" >&2
  exit 1
}

vault_api() {
  local method=$1 path=$2 token=${3:-} body=${4:-}
  curl -sS -X "$method" -w '\n%{http_code}' \
    ${token:+-H "X-Vault-Token: $token"} \
    ${body:+-d "$body"} \
    "$VAULT_ADDR/v1/$path"
}
code() { tail -n1 <<<"$1"; }
body() { sed '$d' <<<"$1"; }

psql_as() {
  docker compose exec -T -e PGPASSWORD="$2" postgres \
    psql -h postgres -U "$1" -d "$3" -Atc "select session_user || ' работает как ' || current_user" 2>&1 | tail -1 || true
}

mc_cfg=/tmp/app-check-$$
mc_app() { docker compose exec -T object-storage mc --config-dir "$mc_cfg" "$@"; }

token=""
revoke_token() {
  [[ -z "$token" ]] || vault_api POST auth/token/revoke-self "$token" >/dev/null 2>&1 || true
  token=""
}
cleanup() {
  revoke_token
  docker compose exec -T object-storage rm -rf "$mc_cfg" >/dev/null 2>&1 || true
}
trap cleanup EXIT

admin_token=$VAULT_TOKEN
CONFIG=$(echo 'jsonencode(yamldecode(file("config.yaml")))' | terraform console | jq -r .)
ROLES=$(terraform -chdir=vault output -json approle_role_ids)
mapfile -t ALL < <(jq -r '.services // {} | keys[]' <<<"$CONFIG")
mapfile -t ALL_BUCKETS < <(jq -r '.buckets // {} | keys[]' <<<"$CONFIG")
mapfile -t ALL_DBS < <(jq -r '.databases // {} | keys[]' <<<"$CONFIG")
if [[ $# -gt 0 ]]; then SERVICES=("$@"); else SERVICES=("${ALL[@]}"); fi

contains() {
  local needle=$1 item
  shift
  for item in "$@"; do [[ "$item" == "$needle" ]] && return 0; done
  return 1
}

for svc in "${SERVICES[@]}"; do
  printf '\n################ %s\n' "$svc"
  mapfile -t buckets < <(jq -r --arg s "$svc" '.services[$s].buckets // {} | keys[]' <<<"$CONFIG")
  mapfile -t ro_buckets < <(jq -r --arg s "$svc" '.services[$s].buckets // {} | to_entries[] | select(.value == "ro") | .key' <<<"$CONFIG")
  mapfile -t dbs < <(jq -r --arg s "$svc" '.services[$s].databases // [] | .[]' <<<"$CONFIG")

  role_id=$(jq -r --arg s "$svc" '.[$s] // empty' <<<"$ROLES")
  [[ -n "$role_id" ]] || fail "нет роли AppRole для $svc"
  secret_id=$(vault_api POST "auth/approle/role/$svc/secret-id" "$admin_token" '{"ttl": "5m", "num_uses": 1}' |
    sed '$d' | jq -r .data.secret_id)

  step "1. Вход через AppRole"
  r=$(vault_api POST auth/approle/login "" "$(jq -n --arg r "$role_id" --arg s "$secret_id" '{role_id: $r, secret_id: $s}')")
  [[ $(code "$r") == 200 ]] || fail "вход не удался: $(body "$r")"
  token=$(body "$r" | jq -r .auth.client_token)
  ok "токен получен, политики: $(body "$r" | jq -r '.auth.policies | join(", ")'), ttl: $(body "$r" | jq -r .auth.lease_duration)s"

  step "2. S3: $KV/$svc/s3"
  r=$(vault_api GET "$KV/data/$svc/s3" "$token")
  if [[ ${#buckets[@]} -eq 0 ]]; then
    [[ $(code "$r") == 404 ]] || fail "у сервиса нет бакетов, но секрет есть"
    ok "бакетов нет, секрета нет"
  else
    [[ $(code "$r") == 200 ]] || fail "секрет не читается: HTTP $(code "$r")"
    s3=$(body "$r" | jq .data.data)
    ok "поля: $(jq -r 'keys | join(", ")' <<<"$s3")"
    mc_app alias set app http://localhost:9000 "$(jq -r .access_key <<<"$s3")" "$(jq -r .secret_key <<<"$s3")" >/dev/null
    for b in "${buckets[@]}"; do
      mc_app ls "app/$b" >/dev/null 2>&1 || fail "свой бакет $b недоступен"
      if contains "$b" "${ro_buckets[@]}"; then
        out=$(echo check | mc_app pipe "app/$b/.app-check" 2>&1 || true)
        [[ "$out" == *"Access Denied"* || "$out" == *"Insufficient permissions"* ]] ||
          fail "запись в бакет $b с доступом ro прошла: $out"
        ok "бакет $b: чтение есть, запись запрещена"
      else
        ok "бакет $b доступен"
      fi
    done
    for b in "${ALL_BUCKETS[@]}"; do
      contains "$b" "${buckets[@]}" && continue
      out=$(mc_app ls "app/$b" 2>&1 || true)
      [[ "$out" == *"Access Denied"* ]] || fail "чужой бакет $b доступен: $out"
      ok "чужой бакет $b: Access Denied"
    done
  fi

  step "3. Postgres"
  dyn_users=()
  if [[ ${#dbs[@]} -eq 0 ]]; then
    ok "баз нет"
  fi
  for db in "${dbs[@]}"; do
    r=$(vault_api GET "$KV/data/$svc/postgres/$db" "$token")
    [[ $(code "$r") == 200 ]] || fail "секрет $KV/$svc/postgres/$db не читается: HTTP $(code "$r")"
    pg=$(body "$r" | jq .data.data)
    out=$(psql_as "$(jq -r .username <<<"$pg")" "$(jq -r .password <<<"$pg")" "$db")
    [[ "$out" == *"работает как ${db}_owner" ]] || fail "постоянная роль не подключилась к $db: $out"
    ok "$db, постоянная роль: $out"

    r=$(vault_api GET "$DB/creds/$svc-$db" "$token")
    [[ $(code "$r") == 200 ]] || fail "Vault не выдал временную роль для $db: $(body "$r")"
    dyn_user=$(body "$r" | jq -r .data.username)
    dyn_pass=$(body "$r" | jq -r .data.password)
    dyn_users+=("$dyn_user")
    out=$(psql_as "$dyn_user" "$dyn_pass" "$db")
    [[ "$out" == *"работает как ${db}_owner" ]] || fail "временная роль не подключилась к $db: $out"
    ok "$db, временная роль на $(body "$r" | jq -r .lease_duration)s: $out"

    for other in "${ALL_DBS[@]}"; do
      contains "$other" "${dbs[@]}" && continue
      out=$(psql_as "$dyn_user" "$dyn_pass" "$other")
      [[ "$out" == *"CONNECT privilege"* ]] || fail "роль попала в чужую базу $other: $out"
      ok "чужая база $other: нет права CONNECT"
    done
  done

  step "4. Чужие секреты"
  for db in "${ALL_DBS[@]}"; do
    contains "$db" "${dbs[@]}" && continue
    [[ $(code "$(vault_api GET "$DB/creds/$svc-$db" "$token")") == 403 ]] || fail "сервис получил роль для чужой базы $db"
    ok "$DB/creds/$svc-$db: 403"
  done
  for other in "${ALL[@]}"; do
    [[ "$other" == "$svc" ]] && continue
    paths=("$KV/data/$other/s3")
    while read -r db; do
      [[ -n "$db" ]] && paths+=("$KV/data/$other/postgres/$db" "$DB/creds/$other-$db")
    done < <(jq -r --arg s "$other" '.services[$s].databases // [] | .[]' <<<"$CONFIG")
    for path in "${paths[@]}"; do
      [[ $(code "$(vault_api GET "$path" "$token")") == 403 ]] || fail "сервис прочитал $path"
      ok "$path: 403"
    done
  done

  step "5. Отзыв токена"
  old_token=$token
  revoke_token
  [[ $(code "$(vault_api GET "$KV/data/$svc/s3" "$old_token")") == 403 ]] || fail "токен всё ещё работает"
  ok "токен больше не принимается"
  if [[ ${#dyn_users[@]} -gt 0 ]]; then
    sleep 2
    for dyn_user in "${dyn_users[@]}"; do
      left=$(docker compose exec -T postgres psql -U "$POSTGRES_USER" -d postgres -Atc \
        "select count(*) from pg_roles where rolname = '$dyn_user'")
      [[ "$left" == 0 ]] || fail "временная роль $dyn_user осталась в Postgres"
    done
    ok "временные роли удалены вместе с токеном"
  fi
done

printf '\nВсе проверки пройдены.\n'
