|
@@ -0,0 +1,205 @@
|
|
|
|
|
+#!/usr/bin/env bash
|
|
|
|
|
+# m13a_smoke.sh — End-to-end smoke for the M13a auth gate.
|
|
|
|
|
+#
|
|
|
|
|
+# Walks through:
|
|
|
|
|
+# 1. authd /health and /metrics
|
|
|
|
|
+# 2. login (super_admin) → access + refresh
|
|
|
|
|
+# 3. /v1/users/me with Bearer → user info
|
|
|
|
|
+# 4. refresh → new pair (rotated)
|
|
|
|
|
+# 5. re-use OLD refresh → session_killed
|
|
|
|
|
+# 6. invite → magic-link token (will not email; we just verify
|
|
|
|
|
+# the route is wired)
|
|
|
|
|
+# 7. admind /v1/dlq (gated): no token → 401, valid token → 200
|
|
|
|
|
+# 8. ingestd /v1/admin/ingest (gated): no token → 401, valid
|
|
|
|
|
+# token → 200 (we don't check the full ingest pipeline here,
|
|
|
|
|
+# just that the gate lets the request through)
|
|
|
|
|
+#
|
|
|
|
|
+# Requires:
|
|
|
|
|
+# - authd running on $BA_AUTHD_HTTP (default http://127.0.0.1:8804)
|
|
|
|
|
+# - admind running on $BA_ADMIND_HTTP (default http://127.0.0.1:8803)
|
|
|
|
|
+# - ingestd running on $BA_INGESTD_HTTP (default http://127.0.0.1:8800)
|
|
|
|
|
+# - $BA_AUTHD_JWT_SECRET set
|
|
|
|
|
+# - super_admin user in Postgres with a known password
|
|
|
|
|
+# (created by scripts/bootstrap-super-admin.sh)
|
|
|
|
|
+#
|
|
|
|
|
+# Run:
|
|
|
|
|
+# bash scripts/m13a_smoke.sh
|
|
|
|
|
+#
|
|
|
|
|
+# Exits 0 if all steps pass, non-zero with a summary table on failure.
|
|
|
|
|
+
|
|
|
|
|
+set -euo pipefail
|
|
|
|
|
+cd "$(dirname "$0")/.."
|
|
|
|
|
+
|
|
|
|
|
+AUTHD="${BA_AUTHD_HTTP:-http://127.0.0.1:8804}"
|
|
|
|
|
+ADMIND="${BA_ADMIND_HTTP:-http://127.0.0.1:8803}"
|
|
|
|
|
+INGESTD="${BA_INGESTD_HTTP:-http://127.0.0.1:8800}"
|
|
|
|
|
+SUPER_EMAIL="${BA_SMOKE_SUPER_EMAIL:-super@broad-announce.test}"
|
|
|
|
|
+SUPER_PASSWORD="${BA_SMOKE_SUPER_PASSWORD:-test-password-123}"
|
|
|
|
|
+TENANT_SLUG="${BA_SMOKE_TENANT_SLUG:-acme}"
|
|
|
|
|
+INVITE_EMAIL="${BA_SMOKE_INVITE_EMAIL:-invite-$(date +%s)@acme.test}"
|
|
|
|
|
+
|
|
|
|
|
+PASS=0
|
|
|
|
|
+FAIL=0
|
|
|
|
|
+RESULTS=()
|
|
|
|
|
+
|
|
|
|
|
+check() {
|
|
|
|
|
+ local name="$1"
|
|
|
|
|
+ local actual="$2"
|
|
|
|
|
+ local want="$3"
|
|
|
|
|
+ if [[ "$actual" == "$want" ]]; then
|
|
|
|
|
+ PASS=$((PASS+1))
|
|
|
|
|
+ RESULTS+=("OK $name")
|
|
|
|
|
+ else
|
|
|
|
|
+ FAIL=$((FAIL+1))
|
|
|
|
|
+ RESULTS+=("FAIL $name (got $actual, want $want)")
|
|
|
|
|
+ fi
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# 1. authd /health
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+status=$(curl -s -o /dev/null -w "%{http_code}" "$AUTHD/health")
|
|
|
|
|
+check "1. authd /health" "$status" "200"
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# 2. login
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+login_resp=$(curl -s -X POST "$AUTHD/v1/auth/login" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
|
|
|
|
|
+login_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/login" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
|
|
|
|
|
+check "2. login (super_admin)" "$login_code" "200"
|
|
|
|
|
+
|
|
|
|
|
+ACCESS=$(echo "$login_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))")
|
|
|
|
|
+REFRESH=$(echo "$login_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('refresh_token',''))")
|
|
|
|
|
+if [[ -z "$ACCESS" || -z "$REFRESH" ]]; then
|
|
|
|
|
+ echo "FATAL: login response missing tokens" >&2
|
|
|
|
|
+ echo "$login_resp" >&2
|
|
|
|
|
+ exit 1
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# 3. /v1/users/me with Bearer
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+me_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $ACCESS" "$AUTHD/v1/users/me")
|
|
|
|
|
+check "3. /v1/users/me (with Bearer)" "$me_code" "200"
|
|
|
|
|
+
|
|
|
|
|
+me_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" "$AUTHD/v1/users/me")
|
|
|
|
|
+check "3a. /v1/users/me (no Bearer)" "$me_no_auth_code" "401"
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# 4. refresh → new pair
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+refresh_resp=$(curl -s -X POST "$AUTHD/v1/auth/refresh" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"refresh_token\":\"$REFRESH\"}")
|
|
|
|
|
+refresh_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/refresh" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"refresh_token\":\"$REFRESH\"}")
|
|
|
|
|
+check "4. refresh" "$refresh_code" "200"
|
|
|
|
|
+
|
|
|
|
|
+NEW_ACCESS=$(echo "$refresh_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))")
|
|
|
|
|
+NEW_REFRESH=$(echo "$refresh_resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('refresh_token',''))")
|
|
|
|
|
+if [[ "$NEW_ACCESS" == "$ACCESS" || "$NEW_REFRESH" == "$REFRESH" ]]; then
|
|
|
|
|
+ FAIL=$((FAIL+1))
|
|
|
|
|
+ RESULTS+=("FAIL 4a. refresh rotated (got same tokens)")
|
|
|
|
|
+else
|
|
|
|
|
+ PASS=$((PASS+1))
|
|
|
|
|
+ RESULTS+=("OK 4a. refresh rotated (new JTI + new refresh)")
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# 5. re-use OLD refresh → session_killed
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+reuse_resp=$(curl -s -X POST "$AUTHD/v1/auth/refresh" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"refresh_token\":\"$REFRESH\"}")
|
|
|
|
|
+reuse_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/refresh" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"refresh_token\":\"$REFRESH\"}")
|
|
|
|
|
+check "5. re-use old refresh" "$reuse_code" "401"
|
|
|
|
|
+if echo "$reuse_resp" | grep -q "session_killed"; then
|
|
|
|
|
+ PASS=$((PASS+1))
|
|
|
|
|
+ RESULTS+=("OK 5a. reuse → session_killed (family killed)")
|
|
|
|
|
+else
|
|
|
|
|
+ FAIL=$((FAIL+1))
|
|
|
|
|
+ RESULTS+=("FAIL 5a. reuse → expected 'session_killed', got: $reuse_resp")
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+# After kill, NEW_REFRESH is also revoked
|
|
|
|
|
+post_kill_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/auth/refresh" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"refresh_token\":\"$NEW_REFRESH\"}")
|
|
|
|
|
+check "5b. new refresh after family kill" "$post_kill_code" "401"
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# 6. invite
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+invite_resp=$(curl -s -X POST "$AUTHD/v1/users/invite" \
|
|
|
|
|
+ -H "Authorization: Bearer $ACCESS" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"tenant_slug\":\"$TENANT_SLUG\",\"email\":\"$INVITE_EMAIL\",\"role\":\"tenant_admin\"}")
|
|
|
|
|
+invite_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/users/invite" \
|
|
|
|
|
+ -H "Authorization: Bearer $ACCESS" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"tenant_slug\":\"$TENANT_SLUG\",\"email\":\"another-$(date +%s)@acme.test\",\"role\":\"viewer\"}")
|
|
|
|
|
+check "6. invite (super_admin)" "$invite_code" "200"
|
|
|
|
|
+
|
|
|
|
|
+invite_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$AUTHD/v1/users/invite" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d '{"email":"x@y.com","role":"viewer"}')
|
|
|
|
|
+check "6a. invite (no Bearer)" "$invite_no_auth_code" "401"
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# 7. admind /v1/dlq (gated)
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+dlq_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" "$ADMIND/v1/dlq")
|
|
|
|
|
+check "7. admind /v1/dlq (no auth)" "$dlq_no_auth_code" "401"
|
|
|
|
|
+
|
|
|
|
|
+# Need a fresh token (the family was killed above)
|
|
|
|
|
+login_resp=$(curl -s -X POST "$AUTHD/v1/auth/login" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d "{\"email\":\"$SUPER_EMAIL\",\"password\":\"$SUPER_PASSWORD\"}")
|
|
|
|
|
+ACCESS=$(echo "$login_resp" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
|
|
|
|
|
+
|
|
|
|
|
+dlq_code=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $ACCESS" "$ADMIND/v1/dlq")
|
|
|
|
|
+check "7a. admind /v1/dlq (super_admin Bearer)" "$dlq_code" "200"
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# 8. ingestd /v1/admin/ingest (gated)
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+ingest_no_auth_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$INGESTD/v1/admin/ingest")
|
|
|
|
|
+check "8. ingestd /v1/admin/ingest (no auth)" "$ingest_no_auth_code" "401"
|
|
|
|
|
+
|
|
|
|
|
+ingest_auth_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$INGESTD/v1/admin/ingest" \
|
|
|
|
|
+ -H "Authorization: Bearer $ACCESS" \
|
|
|
|
|
+ -H 'Content-Type: application/json' \
|
|
|
|
|
+ -d '{}')
|
|
|
|
|
+# We expect 4xx (invalid alert) or 5xx (deps nil), not 401. The point is
|
|
|
|
|
+# the gate let us through.
|
|
|
|
|
+if [[ "$ingest_auth_code" == "401" ]]; then
|
|
|
|
|
+ FAIL=$((FAIL+1))
|
|
|
|
|
+ RESULTS+=("FAIL 8a. ingestd /v1/admin/ingest (auth Bearer) — gate rejected the token")
|
|
|
|
|
+else
|
|
|
|
|
+ PASS=$((PASS+1))
|
|
|
|
|
+ RESULTS+=("OK 8a. ingestd /v1/admin/ingest (auth Bearer) — gate passed ($ingest_auth_code)")
|
|
|
|
|
+fi
|
|
|
|
|
+
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+# Summary
|
|
|
|
|
+# ---------------------------------------------------------------------------
|
|
|
|
|
+echo
|
|
|
|
|
+echo "=== M13a smoke summary ==="
|
|
|
|
|
+for r in "${RESULTS[@]}"; do
|
|
|
|
|
+ echo " $r"
|
|
|
|
|
+done
|
|
|
|
|
+echo
|
|
|
|
|
+echo " $PASS passed, $FAIL failed"
|
|
|
|
|
+echo
|
|
|
|
|
+
|
|
|
|
|
+if [[ $FAIL -gt 0 ]]; then
|
|
|
|
|
+ exit 1
|
|
|
|
|
+fi
|
|
|
|
|
+echo "all M13a smoke checks passed"
|