taler-monitoring v1.13.12
Multi-phase global SUMMARY, warn-filter dimmed context, SUMMARY chrome as meta; FP stage host-agent via francpaysan-stage-user (stagepaysan); monpages GOA + FP stage.
This commit is contained in:
commit
5a5431bfa9
88 changed files with 21996 additions and 0 deletions
704
check_auth401.sh
Executable file
704
check_auth401.sh
Executable file
|
|
@ -0,0 +1,704 @@
|
|||
#!/usr/bin/env bash
|
||||
# auth401 — merchant HTTP 401 / case-sensitivity matrix (WebUI-shaped flows)
|
||||
#
|
||||
# Exercises the same APIs the merchant SPA uses for:
|
||||
# • self-provision signup (POST /instances) — mixed-case id
|
||||
# • login / createAccessToken (POST …/private/token) — Basic user case
|
||||
# • password change (POST …/private/auth)
|
||||
# • admin-create (POST /management/instances) when admin bearer available
|
||||
# • access-token + private/ with Bearer
|
||||
#
|
||||
# Backend fact (#11340): Basic-auth *username* is case-sensitive; URL path is
|
||||
# usually not. SPA must always lowercase the username (createAccessToken, signup
|
||||
# id, admin-create, pw change re-login). These tests assert the *raw API*
|
||||
# behaviour so a missing SPA lowercasing shows up as 401 where 200 is wanted
|
||||
# only after lowercasing.
|
||||
#
|
||||
# ./taler-monitoring.sh -d stage.monnaie.lefrancpaysan.ch auth401
|
||||
# ./taler-monitoring.sh -d hacktivism.ch auth401
|
||||
#
|
||||
# Continue through all groups / collect every ERROR (optional):
|
||||
# AUTH401_CONTINUE=1 ./taler-monitoring.sh -d stage… auth401
|
||||
# CONTINUE_ON_ERROR=1 ./taler-monitoring.sh -d stage… auth401 # same
|
||||
#
|
||||
# Env: see secrets.env.example (AUTH401_*).
|
||||
#
|
||||
# NOTE: we intentionally do NOT use `set -e`. A single failed curl/grep under
|
||||
# set -e would abort with no ERROR summary — the opposite of what monitoring
|
||||
# needs. Optional CONTINUE_ON_ERROR still controls hard prereq early-exit.
|
||||
set -uo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
# shellcheck source=lib.sh
|
||||
source "$ROOT/lib.sh"
|
||||
load_monitoring_secrets_env 2>/dev/null || true
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
AUTH401_FINISHED=0
|
||||
_auth401_finish() {
|
||||
[ "${AUTH401_FINISHED}" = "1" ] && return 0
|
||||
AUTH401_FINISHED=1
|
||||
# summary() returns non-zero when FAIL_N>0 — never let that skip the print
|
||||
summary || true
|
||||
rm -rf "${tmp:-}"
|
||||
if [ "${FAIL_N:-0}" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
trap '_auth401_finish' EXIT
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parameters
|
||||
# ---------------------------------------------------------------------------
|
||||
: "${AUTH401_TIMEOUT:=${TIMEOUT:-20}}"
|
||||
: "${AUTH401_CREATE:=1}"
|
||||
: "${AUTH401_INSTANCE:=}"
|
||||
: "${AUTH401_PASSWORD:=${MERCHANT_INSTANCE_PASSWORD:-${AUTH401_PASS:-}}}"
|
||||
: "${AUTH401_ID_PREFIX:=mon401}"
|
||||
: "${AUTH401_SKIP_IF_MFA:=1}"
|
||||
: "${AUTH401_BEARER:=${E2E_MERCHANT_TOKEN:-${MERCHANT_TOKEN:-}}}"
|
||||
: "${AUTH401_CREATE_PASSWORD:=${AUTH401_PASSWORD:-Mon401-Test-Pass!}}"
|
||||
: "${AUTH401_DURABLE_ID:=mon401}"
|
||||
# Prefer durable alone (skip throwaway) — default 0 so full matrix always runs
|
||||
: "${AUTH401_PREFER_DURABLE:=0}"
|
||||
# Run expanded groups (signup/login/pwchange/admin/webui). Default 1.
|
||||
: "${AUTH401_FULL:=1}"
|
||||
# Admin bearer for POST /management/instances (optional)
|
||||
: "${AUTH401_ADMIN_TOKEN:=${MERCHANT_ADMIN_TOKEN:-}}"
|
||||
: "${AUTH401_ADMIN_PASSWORD:=}"
|
||||
: "${AUTH401_ADMIN_USER:=admin}"
|
||||
# 1 = do not abort on hard prereq fail; run remaining groups (default 0)
|
||||
# Alias: CONTINUE_ON_ERROR=1
|
||||
: "${AUTH401_CONTINUE:=${CONTINUE_ON_ERROR:-0}}"
|
||||
# Never rotate password on durable account (default 1 — keep mon401.password valid)
|
||||
: "${AUTH401_PWCHANGE_THROWAY_ONLY:=1}"
|
||||
|
||||
TIMEOUT="$AUTH401_TIMEOUT"
|
||||
export TIMEOUT
|
||||
|
||||
_auth401_abort_or_continue() {
|
||||
local why="${1:-prereq failed}"
|
||||
if [ "${AUTH401_CONTINUE}" = "1" ]; then
|
||||
warn "continue" "${why} (AUTH401_CONTINUE=1 / CONTINUE_ON_ERROR=1)"
|
||||
return 0
|
||||
fi
|
||||
# hard stop — EXIT trap prints summary + all ERRORS collected so far
|
||||
exit 1
|
||||
}
|
||||
|
||||
BASE="${MERCHANT_PUBLIC%/}"
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "MERCHANT_PUBLIC empty — set -d domain or --merchant" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
expect_http() {
|
||||
local label="$1" want="$2" got="$3" detail="${4:-}"
|
||||
case ",$want," in
|
||||
*",$got,"*) ok "$label" "HTTP $got${detail:+ · $detail}" ;;
|
||||
*) fail "$label" "HTTP $got (want $want)${detail:+ · $detail}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
to_lower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]'; }
|
||||
to_upper() { printf '%s' "$1" | tr '[:lower:]' '[:upper:]'; }
|
||||
|
||||
# First char upper, rest lower → synthetic MIX for Basic user (always ≠ low if len>0)
|
||||
synth_mix() {
|
||||
python3 -c "s='''$1'''; print((s[:1].upper()+s[1:].lower()) if s else s)"
|
||||
}
|
||||
|
||||
# First 3 upper (partial mix)
|
||||
synth_partmix() {
|
||||
python3 -c "s='''$1'''; print(s[:3].upper()+s[3:] if len(s)>=3 else s.upper())"
|
||||
}
|
||||
|
||||
# Normalize curl status: "200", "000", never "200000" / "000000"
|
||||
_http_norm() {
|
||||
local raw="$1" dig
|
||||
dig=$(printf '%s' "$raw" | tr -cd '0-9')
|
||||
if [ -z "$dig" ]; then
|
||||
printf '000'
|
||||
else
|
||||
# first 3 digits = real code (curl 200 then || echo 000 → 200000)
|
||||
printf '%s' "$dig" | head -c 3
|
||||
fi
|
||||
}
|
||||
|
||||
token_http() {
|
||||
local path_id="$1" user="$2" password="$3"
|
||||
local b64 raw
|
||||
b64=$(printf '%s' "${user}:${password}" | base64 -w0 2>/dev/null \
|
||||
|| printf '%s' "${user}:${password}" | base64)
|
||||
raw=$(curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/last_token.json" -w '%{http_code}' \
|
||||
-X POST "${BASE}/instances/${path_id}/private/token" \
|
||||
-H "Authorization: Basic ${b64}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"scope":"write","duration":{"d_us":3600000000},"refreshable":true}' \
|
||||
2>/dev/null || true)
|
||||
_http_norm "${raw:-}"
|
||||
}
|
||||
|
||||
json_code_hint() {
|
||||
python3 - "$tmp/last_token.json" <<'PY' 2>/dev/null || true
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
print("")
|
||||
raise SystemExit
|
||||
c = d.get("code", "")
|
||||
h = (d.get("hint") or d.get("detail") or "")[:70]
|
||||
print(f"code={c} {h}".strip())
|
||||
PY
|
||||
}
|
||||
|
||||
extract_token() {
|
||||
python3 - "$tmp/last_token.json" <<'PY' 2>/dev/null || true
|
||||
import json, sys
|
||||
try:
|
||||
d = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
print("")
|
||||
raise SystemExit(0)
|
||||
print(d.get("access_token") or d.get("token") or "")
|
||||
PY
|
||||
}
|
||||
|
||||
instance_body() {
|
||||
local id="$1" pass="$2" name="${3:-auth401 probe}"
|
||||
python3 - "$id" "$pass" "$name" <<'PY'
|
||||
import json, sys
|
||||
print(json.dumps({
|
||||
"address": {},
|
||||
"auth": {"method": "token", "password": sys.argv[2]},
|
||||
"default_pay_delay": {"d_us": 60000000},
|
||||
"default_wire_transfer_delay": {"d_us": 30000000},
|
||||
"default_refund_delay": {"d_us": 30000000},
|
||||
"id": sys.argv[1],
|
||||
"jurisdiction": {},
|
||||
"name": sys.argv[3],
|
||||
"use_stefan": True,
|
||||
}))
|
||||
PY
|
||||
}
|
||||
|
||||
auth_change_http() {
|
||||
local path_id="$1" bearer="$2" new_pass="$3"
|
||||
local tok="$bearer" body
|
||||
case "$tok" in secret-token:*) ;; *) tok="secret-token:${tok}" ;; esac
|
||||
body=$(NEW_PASS="$new_pass" python3 -c 'import json,os; print(json.dumps({"method":"token","password":os.environ["NEW_PASS"]}))')
|
||||
curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/auth_change.json" -w '%{http_code}' \
|
||||
-X POST "${BASE}/instances/${path_id}/private/auth" \
|
||||
-H "Authorization: Bearer ${tok}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$body" \
|
||||
2>/dev/null || echo 000
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage secrets (quiet)
|
||||
# ---------------------------------------------------------------------------
|
||||
_auth401_stage_secret_roots() {
|
||||
printf '%s\n' \
|
||||
"${FRANCPAYSAN_SECRETS:-}" \
|
||||
"${HOME}/francpaysan-secrets" \
|
||||
"${HOME}/src/francpaysan-secrets" \
|
||||
"${HOME}/taler/src/francpaysan-secrets"
|
||||
}
|
||||
|
||||
_auth401_resolve_secrets_quiet() {
|
||||
local root inst_id pass
|
||||
for root in $(_auth401_stage_secret_roots); do
|
||||
[ -n "$root" ] && [ -d "$root" ] || continue
|
||||
# durable ids: prefer mon401fix when mon401 was rotated by old tests
|
||||
if [ -z "${DURABLE_PASS:-}" ]; then
|
||||
for inst_id in "${AUTH401_DURABLE_ID}" mon401fix mon401 "${MERCHANT_INSTANCE:-}"; do
|
||||
[ -n "$inst_id" ] || continue
|
||||
pass=""
|
||||
if [ -f "$root/stage/instances/${inst_id}.password" ]; then
|
||||
pass=$(tr -d '\n\r' <"$root/stage/instances/${inst_id}.password")
|
||||
fi
|
||||
# skip empty password files
|
||||
if [ -n "$pass" ]; then
|
||||
# verify file is for this id (mon401.password may be stale copy of mon401fix)
|
||||
DURABLE_PASS="$pass"
|
||||
DURABLE_ID=$(to_lower "$inst_id")
|
||||
AUTH401_SECRET_NOTE="durable ${inst_id} from ${root}/stage/instances/"
|
||||
# only set AUTH401_PASSWORD if caller did not pin an instance
|
||||
if [ -z "${AUTH401_PASSWORD}" ] && [ -z "${AUTH401_INSTANCE}" ]; then
|
||||
AUTH401_PASSWORD="$pass"
|
||||
fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ -z "${AUTH401_BEARER}" ] && [ -f "$root/stage/default-instance-token.txt" ]; then
|
||||
AUTH401_BEARER=$(tr -d '\n\r' <"$root/stage/default-instance-token.txt")
|
||||
AUTH401_BEARER_NOTE="default-instance-token.txt"
|
||||
fi
|
||||
if [ -z "${AUTH401_ADMIN_TOKEN}" ] && [ -f "$root/stage/admin-instance-token.txt" ]; then
|
||||
AUTH401_ADMIN_TOKEN=$(tr -d '\n\r' <"$root/stage/admin-instance-token.txt")
|
||||
AUTH401_ADMIN_NOTE="admin-instance-token.txt"
|
||||
fi
|
||||
[ -n "${DURABLE_PASS:-}" ] && break
|
||||
done
|
||||
# also keep AUTH401_INSTANCE for durable-only mode
|
||||
if [ "${AUTH401_PREFER_DURABLE}" = "1" ] && [ -n "${DURABLE_ID:-}" ] && [ -n "${DURABLE_PASS:-}" ]; then
|
||||
AUTH401_INSTANCE="${AUTH401_INSTANCE:-$DURABLE_ID}"
|
||||
AUTH401_PASSWORD="${AUTH401_PASSWORD:-$DURABLE_PASS}"
|
||||
AUTH401_CREATE=0
|
||||
AUTH401_MODE_NOTE="PREFER_DURABLE=1 → only durable ${AUTH401_INSTANCE}"
|
||||
fi
|
||||
}
|
||||
|
||||
_auth401_resolve_secrets_quiet
|
||||
: "${AUTH401_CREATE_PASSWORD:=${AUTH401_PASSWORD:-Mon401-Test-Pass!}}"
|
||||
export AUTH401_INSTANCE AUTH401_PASSWORD AUTH401_BEARER AUTH401_CREATE AUTH401_ADMIN_TOKEN
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
set_area auth401
|
||||
section "auth401 · merchant 401 / case matrix (WebUI APIs) · ${BASE}"
|
||||
|
||||
if [ -n "${AUTH401_SECRET_NOTE:-}" ]; then info "stage secret" "$AUTH401_SECRET_NOTE"; fi
|
||||
if [ -n "${AUTH401_BEARER_NOTE:-}" ]; then info "stage secret" "AUTH401_BEARER from ${AUTH401_BEARER_NOTE}"; fi
|
||||
if [ -n "${AUTH401_ADMIN_NOTE:-}" ]; then info "stage secret" "AUTH401_ADMIN_TOKEN from ${AUTH401_ADMIN_NOTE}"; fi
|
||||
if [ -n "${AUTH401_MODE_NOTE:-}" ]; then info "auth401 mode" "$AUTH401_MODE_NOTE"; fi
|
||||
info "auth401 scope" "FULL=${AUTH401_FULL} CREATE=${AUTH401_CREATE} PREFER_DURABLE=${AUTH401_PREFER_DURABLE} CONTINUE=${AUTH401_CONTINUE}"
|
||||
if [ "${AUTH401_CONTINUE}" = "1" ]; then
|
||||
info "continue mode" "CONTINUE_ON_ERROR/AUTH401_CONTINUE=1 — hard prereqs soft; always full ERROR list via EXIT trap"
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
set_group config
|
||||
# ===========================================================================
|
||||
cfg_code=$(http_body "$BASE/config" "$tmp/mer-config.json")
|
||||
expect_http "merchant /config" 200 "$cfg_code" "$BASE/config"
|
||||
|
||||
HAVE_SP=0
|
||||
MANDATORY_TAN=0
|
||||
if [ "$cfg_code" = "200" ]; then
|
||||
eval "$(python3 - "$tmp/mer-config.json" <<'PY'
|
||||
import json, sys
|
||||
d = json.load(open(sys.argv[1]))
|
||||
sp = 1 if d.get("have_self_provisioning") else 0
|
||||
tans = d.get("mandatory_tan_channels") or []
|
||||
print(f"HAVE_SP={sp}")
|
||||
print(f"MANDATORY_TAN={1 if tans else 0}")
|
||||
print(f"TAN_CH={','.join(tans) if tans else ''}")
|
||||
print(f"API_VER={d.get('version','')}")
|
||||
PY
|
||||
)"
|
||||
info "self-provision" "have_self_provisioning=${HAVE_SP} mandatory_tan=${MANDATORY_TAN}${TAN_CH:+ ($TAN_CH)} api=${API_VER:-?}"
|
||||
else
|
||||
fail "merchant config required for auth401" "HTTP $cfg_code"
|
||||
_auth401_abort_or_continue "no /config"
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
set_group webui
|
||||
# SPA surface: /webui/ loads + version stamp; optional toLowerCase in bundle
|
||||
# ===========================================================================
|
||||
if [ "${AUTH401_FULL}" = "1" ]; then
|
||||
wcode=$(http_code "$BASE/webui/")
|
||||
case "$wcode" in
|
||||
200|301|302) ok "webui /" "HTTP $wcode" ;;
|
||||
*) warn "webui /" "HTTP $wcode" ;;
|
||||
esac
|
||||
vcode=$(http_body "$BASE/webui/version.txt" "$tmp/spa-ver.txt" 2>/dev/null || echo 000)
|
||||
if [ "$vcode" = "200" ] && [ -s "$tmp/spa-ver.txt" ]; then
|
||||
ok "webui version.txt" "$(tr -d '\n\r' <"$tmp/spa-ver.txt")"
|
||||
else
|
||||
warn "webui version.txt" "HTTP ${vcode:-000}"
|
||||
fi
|
||||
# Download a slice of index.js — look for lowercasing mitigation (#11340)
|
||||
jcode=$(curl -skS -m 60 -o "$tmp/spa-index.js" -w '%{http_code}' "$BASE/webui/index.js" 2>/dev/null || echo 000)
|
||||
jcode=$(printf '%s' "$jcode" | tr -cd '0-9' | head -c 3)
|
||||
if [ "$jcode" = "200" ] && [ -s "$tmp/spa-index.js" ]; then
|
||||
ok "webui index.js" "HTTP 200 · $(wc -c <"$tmp/spa-index.js" | tr -d ' ') bytes"
|
||||
if grep -q 'toLowerCase' "$tmp/spa-index.js" 2>/dev/null; then
|
||||
ok "webui bundle toLowerCase" "present (SPA may mitigate #11340)"
|
||||
else
|
||||
warn "webui bundle toLowerCase" "not found in index.js — login MIX may 401 without lowercasing"
|
||||
fi
|
||||
else
|
||||
warn "webui index.js" "HTTP ${jcode:-000}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
set_group signup
|
||||
# WebUI self-provision: POST /instances with mixed-case id (as typed)
|
||||
# ===========================================================================
|
||||
CREATED=0
|
||||
USE_ID=""
|
||||
USE_PASS=""
|
||||
ID_MIX=""
|
||||
ID_LOW=""
|
||||
CREATE_PASS="$AUTH401_CREATE_PASSWORD"
|
||||
stamp=$(date +%H%M%S)
|
||||
# Mixed-case id like the SPA would send before lowercasing (or if it forgot)
|
||||
ID_MIX="$(printf '%s' "${AUTH401_ID_PREFIX}" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')X${stamp}"
|
||||
ID_LOW=$(to_lower "$ID_MIX")
|
||||
|
||||
if [ "${AUTH401_FULL}" = "1" ] && [ "$AUTH401_CREATE" = "1" ] && [ "$HAVE_SP" = "1" ]; then
|
||||
if [ "$MANDATORY_TAN" = "1" ] && [ "$AUTH401_SKIP_IF_MFA" = "1" ]; then
|
||||
warn "signup skipped" "mandatory_tan_channels set — set AUTH401_INSTANCE+PASSWORD"
|
||||
else
|
||||
body=$(instance_body "$ID_MIX" "$CREATE_PASS" "auth401 signup MIX")
|
||||
ccode=$(_http_norm "$(curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/create.json" -w '%{http_code}' \
|
||||
-X POST "${BASE}/instances" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$body" 2>/dev/null || true)")
|
||||
case "$ccode" in
|
||||
200|204)
|
||||
ok "signup POST /instances id=MIX" "HTTP $ccode id=$ID_MIX (SPA self-provision)"
|
||||
CREATED=1
|
||||
USE_ID="$ID_MIX"
|
||||
USE_PASS="$CREATE_PASS"
|
||||
# Immediate login as SPA after signup: must use *lowercase* Basic user
|
||||
code=$(token_http "$ID_LOW" "$ID_LOW" "$CREATE_PASS")
|
||||
expect_http "signup→login SPA-sim path=low user=low → 200" 200 "$code" "$(json_code_hint)"
|
||||
# Bug path: SPA forgot toLowerCase on login after signup with MIX id typed
|
||||
code=$(token_http "$ID_LOW" "$ID_MIX" "$CREATE_PASS")
|
||||
expect_http "signup→login RAW user=MIX (no lower) → 401" 401 "$code" "$(json_code_hint)"
|
||||
;;
|
||||
202)
|
||||
warn "signup needs MFA" "HTTP 202 — use AUTH401_INSTANCE"
|
||||
;;
|
||||
*)
|
||||
fail "signup POST /instances" "HTTP $ccode $(head -c 120 "$tmp/create.json" 2>/dev/null | tr '\n' ' ')"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
elif [ -n "${AUTH401_INSTANCE:-}" ] && [ -n "${AUTH401_PASSWORD:-}" ]; then
|
||||
USE_ID=$(to_lower "$AUTH401_INSTANCE")
|
||||
USE_PASS="$AUTH401_PASSWORD"
|
||||
ID_LOW=$(to_lower "$USE_ID")
|
||||
ID_MIX=$(synth_mix "$ID_LOW")
|
||||
info "signup" "using existing AUTH401_INSTANCE=$USE_ID (no POST /instances)"
|
||||
ok "using existing instance" "$USE_ID"
|
||||
else
|
||||
warn "signup skipped" "CREATE=${AUTH401_CREATE} HAVE_SP=${HAVE_SP} (need instance for matrix)"
|
||||
fi
|
||||
|
||||
# Fallback durable
|
||||
if [ -z "$USE_ID" ] && [ -n "${DURABLE_ID:-}" ] && [ -n "${DURABLE_PASS:-}" ]; then
|
||||
USE_ID="$DURABLE_ID"
|
||||
USE_PASS="$DURABLE_PASS"
|
||||
ID_LOW=$(to_lower "$USE_ID")
|
||||
ID_MIX=$(synth_mix "$ID_LOW")
|
||||
info "fallback durable" "$USE_ID"
|
||||
ok "using durable instance" "$USE_ID"
|
||||
fi
|
||||
|
||||
if [ -z "$USE_ID" ]; then
|
||||
fail "no instance under test" "need signup OK or AUTH401_INSTANCE+PASSWORD or mon401.password"
|
||||
_auth401_abort_or_continue "no instance under test"
|
||||
# CONTINUE: synthesize dummy ids so later groups still run (expect many 404/401)
|
||||
ID_LOW_USE="auth401-missing-instance"
|
||||
ID_USER_MIX=$(synth_mix "$ID_LOW_USE")
|
||||
ID_PATH_MIX="$ID_USER_MIX"
|
||||
ID_UPPER=$(to_upper "$ID_LOW_USE")
|
||||
ID_PARTMIX=$(synth_partmix "$ID_LOW_USE")
|
||||
PASS_OK="invalid"
|
||||
PASS_BAD='auth401-wrong-password-NOT-VALID'
|
||||
USE_ID="$ID_LOW_USE"
|
||||
USE_PASS="$PASS_OK"
|
||||
fi
|
||||
|
||||
ID_LOW_USE=$(to_lower "$USE_ID")
|
||||
# Always synthetic case variants (even for mon401) so MIX ≠ low
|
||||
ID_USER_MIX=$(synth_mix "$ID_LOW_USE")
|
||||
ID_PATH_MIX="$ID_USER_MIX"
|
||||
ID_UPPER=$(to_upper "$ID_LOW_USE")
|
||||
ID_PARTMIX=$(synth_partmix "$ID_LOW_USE")
|
||||
PASS_OK="$USE_PASS"
|
||||
PASS_BAD='auth401-wrong-password-NOT-VALID'
|
||||
|
||||
info "probe ids" "low=${ID_LOW_USE} MIX_user=${ID_USER_MIX} UPPER=${ID_UPPER} created=${CREATED} pass_len=${#PASS_OK}"
|
||||
|
||||
# ===========================================================================
|
||||
set_group login
|
||||
# WebUI login = POST …/private/token with Basic (username, password)
|
||||
# ===========================================================================
|
||||
if [ "${AUTH401_FULL}" = "1" ]; then
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_LOW_USE" "$PASS_OK")
|
||||
expect_http "login SPA-sim user=low pwd=OK → 200" 200 "$code" "$(json_code_hint)"
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_USER_MIX" "$PASS_OK")
|
||||
expect_http "login RAW user=MIX pwd=OK → 401 (#11340)" 401 "$code" "$(json_code_hint)"
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_UPPER" "$PASS_OK")
|
||||
expect_http "login RAW user=UPPER pwd=OK → 401" 401 "$code" "$(json_code_hint)"
|
||||
code=$(token_http "$ID_PATH_MIX" "$ID_LOW_USE" "$PASS_OK")
|
||||
expect_http "login path=MIX user=low pwd=OK → 200 (path tolerant)" 200 "$code" "$(json_code_hint)"
|
||||
code=$(token_http "$ID_UPPER" "$ID_LOW_USE" "$PASS_OK")
|
||||
expect_http "login path=UPPER user=low pwd=OK → 200" 200 "$code" "$(json_code_hint)"
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
set_group case
|
||||
# Full Basic case matrix (always hard expects — synthetic MIX)
|
||||
# ===========================================================================
|
||||
code=$(token_http "$ID_PATH_MIX" "$ID_USER_MIX" "$PASS_OK")
|
||||
expect_http "path=MIX user=MIX pwd=OK → 401 (case)" 401 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "$ID_PATH_MIX" "$ID_LOW_USE" "$PASS_OK")
|
||||
expect_http "path=MIX user=low pwd=OK → 200" 200 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_USER_MIX" "$PASS_OK")
|
||||
expect_http "path=low user=MIX pwd=OK → 401 (case)" 401 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_LOW_USE" "$PASS_OK")
|
||||
expect_http "path=low user=low pwd=OK → 200" 200 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "$ID_UPPER" "$ID_LOW_USE" "$PASS_OK")
|
||||
expect_http "path=UPPER user=low pwd=OK → 200" 200 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_UPPER" "$PASS_OK")
|
||||
expect_http "path=low user=UPPER pwd=OK → 401 (case)" 401 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_PARTMIX" "$PASS_OK")
|
||||
expect_http "path=low user=MiX pwd=OK → 401 (case)" 401 "$code" "$(json_code_hint)"
|
||||
|
||||
# ===========================================================================
|
||||
set_group password
|
||||
# ===========================================================================
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_LOW_USE" "$PASS_BAD")
|
||||
expect_http "pwd=BAD → 401" 401 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_LOW_USE" "")
|
||||
expect_http "pwd=EMPTY → 401" 401 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "$ID_PATH_MIX" "$ID_USER_MIX" "$PASS_BAD")
|
||||
expect_http "path=MIX user=MIX pwd=BAD → 401" 401 "$code" "$(json_code_hint)"
|
||||
|
||||
# ===========================================================================
|
||||
set_group pwchange
|
||||
# WebUI password change: POST private/auth then re-login
|
||||
# Only on throwaway signup instances — never rotate durable mon401.password
|
||||
# ===========================================================================
|
||||
if [ "${AUTH401_FULL}" = "1" ]; then
|
||||
if [ "${AUTH401_PWCHANGE_THROWAY_ONLY}" = "1" ] && [ "${CREATED:-0}" != "1" ]; then
|
||||
info "pwchange skipped" "not a throwaway instance (CREATED=0) — refuse to rotate durable password"
|
||||
else
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_LOW_USE" "$PASS_OK")
|
||||
if [ "$code" != "200" ]; then
|
||||
fail "pwchange: login for token" "HTTP $code — skip pwchange"
|
||||
else
|
||||
TOK=$(extract_token)
|
||||
if [ -z "$TOK" ]; then
|
||||
fail "pwchange: parse token" "empty access_token"
|
||||
else
|
||||
ok "pwchange: got bearer" "len=${#TOK}"
|
||||
NEW_PASS="Auth401-New-${stamp}!"
|
||||
ac=$(_http_norm "$(auth_change_http "$ID_LOW_USE" "$TOK" "$NEW_PASS")")
|
||||
case "$ac" in
|
||||
200|204)
|
||||
ok "pwchange POST private/auth" "HTTP $ac (SPA password form)"
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_LOW_USE" "$PASS_OK")
|
||||
expect_http "pwchange: old pwd → 401" 401 "$code" "$(json_code_hint)"
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_LOW_USE" "$NEW_PASS")
|
||||
expect_http "pwchange: new pwd user=low → 200" 200 "$code" "$(json_code_hint)"
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_USER_MIX" "$NEW_PASS")
|
||||
expect_http "pwchange: new pwd user=MIX → 401" 401 "$code" "$(json_code_hint)"
|
||||
PASS_OK="$NEW_PASS"
|
||||
USE_PASS="$NEW_PASS"
|
||||
;;
|
||||
202)
|
||||
warn "pwchange needs MFA" "HTTP 202"
|
||||
;;
|
||||
*)
|
||||
fail "pwchange POST private/auth" "HTTP $ac $(head -c 100 "$tmp/auth_change.json" 2>/dev/null | tr '\n' ' ')"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
set_group admin
|
||||
# Admin UI: POST /management/instances (needs admin bearer)
|
||||
# ===========================================================================
|
||||
if [ "${AUTH401_FULL}" = "1" ]; then
|
||||
ADMIN_TOK="${AUTH401_ADMIN_TOKEN:-}"
|
||||
# Optional: login as admin user with password
|
||||
if [ -z "$ADMIN_TOK" ] && [ -n "${AUTH401_ADMIN_PASSWORD}" ]; then
|
||||
code=$(token_http "$AUTH401_ADMIN_USER" "$AUTH401_ADMIN_USER" "$AUTH401_ADMIN_PASSWORD")
|
||||
if [ "$code" = "200" ]; then
|
||||
ADMIN_TOK=$(extract_token)
|
||||
ok "admin login user=${AUTH401_ADMIN_USER}" "got token"
|
||||
else
|
||||
# try lower only
|
||||
au=$(to_lower "$AUTH401_ADMIN_USER")
|
||||
code=$(token_http "$au" "$au" "$AUTH401_ADMIN_PASSWORD")
|
||||
if [ "$code" = "200" ]; then
|
||||
ADMIN_TOK=$(extract_token)
|
||||
ok "admin login user=${au}" "got token"
|
||||
else
|
||||
warn "admin login" "HTTP $code — set AUTH401_ADMIN_TOKEN"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ADMIN_TOK" ]; then
|
||||
info "admin-create skipped" "no AUTH401_ADMIN_TOKEN / AUTH401_ADMIN_PASSWORD (stage default is not admin)"
|
||||
else
|
||||
case "$ADMIN_TOK" in secret-token:*) ;; *) ADMIN_TOK="secret-token:${ADMIN_TOK}" ;; esac
|
||||
AID_MIX="Adm401X${stamp}"
|
||||
AID_LOW=$(to_lower "$AID_MIX")
|
||||
APASS="Adm401-Pass-${stamp}!"
|
||||
body=$(instance_body "$AID_MIX" "$APASS" "auth401 admin-create MIX")
|
||||
acode=$(curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/admin_create.json" -w '%{http_code}' \
|
||||
-X POST "${BASE}/management/instances" \
|
||||
-H "Authorization: Bearer ${ADMIN_TOK}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$body" 2>/dev/null || echo 000)
|
||||
case "$acode" in
|
||||
200|204)
|
||||
ok "admin-create POST /management/instances id=MIX" "HTTP $acode id=$AID_MIX"
|
||||
code=$(token_http "$AID_LOW" "$AID_LOW" "$APASS")
|
||||
expect_http "admin-create→login user=low → 200" 200 "$code" "$(json_code_hint)"
|
||||
code=$(token_http "$AID_LOW" "$AID_MIX" "$APASS")
|
||||
expect_http "admin-create→login user=MIX → 401" 401 "$code" "$(json_code_hint)"
|
||||
# admin-set auth on new instance
|
||||
code=$(token_http "$AID_LOW" "$AID_LOW" "$APASS")
|
||||
if [ "$code" = "200" ]; then
|
||||
AT=$(extract_token)
|
||||
NEW_A="Adm401-New-${stamp}!"
|
||||
ch=$(auth_change_http "$AID_LOW" "$AT" "$NEW_A")
|
||||
case "$ch" in
|
||||
200|204)
|
||||
ok "admin-created instance pwchange" "HTTP $ch"
|
||||
code=$(token_http "$AID_LOW" "$AID_LOW" "$NEW_A")
|
||||
expect_http "admin-created re-login new pwd → 200" 200 "$code" "$(json_code_hint)"
|
||||
;;
|
||||
*) warn "admin-created pwchange" "HTTP $ch" ;;
|
||||
esac
|
||||
fi
|
||||
;;
|
||||
401|403)
|
||||
warn "admin-create unauthorized" "HTTP $acode — token not admin (expected on stage default)"
|
||||
;;
|
||||
202)
|
||||
warn "admin-create MFA" "HTTP 202"
|
||||
;;
|
||||
*)
|
||||
fail "admin-create" "HTTP $acode $(head -c 120 "$tmp/admin_create.json" 2>/dev/null | tr '\n' ' ')"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
set_group missing
|
||||
# ===========================================================================
|
||||
code=$(curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/last_token.json" -w '%{http_code}' \
|
||||
-X POST "${BASE}/instances/${ID_LOW_USE}/private/token" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"scope":"write","duration":{"d_us":3600000000},"refreshable":true}' \
|
||||
2>/dev/null || echo 000)
|
||||
expect_http "private/token NO_AUTH → 401" 401 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "$ID_LOW_USE" "auth401-not-a-real-user" "$PASS_OK")
|
||||
expect_http "user=OTHER pwd=OK → 401" 401 "$code" "$(json_code_hint)"
|
||||
|
||||
code=$(token_http "auth401-does-not-exist-${stamp}" "$ID_LOW_USE" "$PASS_OK")
|
||||
# Must not look like auth failure (401). Prefer 404; 502 from proxy is soft.
|
||||
case "$code" in
|
||||
404) ok "path=missing → 404 (not 401)" "HTTP 404 · $(json_code_hint)" ;;
|
||||
401) fail "path=missing must not be 401" "HTTP 401 · $(json_code_hint)" ;;
|
||||
502|503) warn "path=missing" "HTTP $code (want 404; proxy flake, not auth)" ;;
|
||||
*) fail "path=missing → 404 (not 401)" "HTTP $code (want 404) · $(json_code_hint)" ;;
|
||||
esac
|
||||
|
||||
# ===========================================================================
|
||||
set_group bearer
|
||||
# ===========================================================================
|
||||
code=$(token_http "$ID_LOW_USE" "$ID_LOW_USE" "$PASS_OK")
|
||||
if [ "$code" = "200" ]; then
|
||||
TOKEN=$(extract_token)
|
||||
if [ -n "$TOKEN" ]; then
|
||||
ok "got access_token" "len=${#TOKEN}"
|
||||
code=$(curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/priv.json" -w '%{http_code}' \
|
||||
"${BASE}/instances/${ID_LOW_USE}/private/" \
|
||||
-H "Authorization: Bearer ${TOKEN}" 2>/dev/null || echo 000)
|
||||
expect_http "GET private/ Bearer OK → 200" 200 "$code"
|
||||
|
||||
code=$(curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/priv.json" -w '%{http_code}' \
|
||||
"${BASE}/instances/${ID_LOW_USE}/private/" \
|
||||
-H "Authorization: Bearer secret-token:AUTH401INVALID000" 2>/dev/null || echo 000)
|
||||
expect_http "GET private/ Bearer BAD → 401" 401 "$code"
|
||||
|
||||
code=$(curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/priv.json" -w '%{http_code}' \
|
||||
"${BASE}/instances/${ID_LOW_USE}/private/" 2>/dev/null || echo 000)
|
||||
expect_http "GET private/ NO_AUTH → 401" 401 "$code"
|
||||
else
|
||||
fail "parse access_token" "HTTP $code but no token field"
|
||||
fi
|
||||
else
|
||||
fail "token for bearer tests" "HTTP $code"
|
||||
fi
|
||||
|
||||
code=$(curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/mgmt.json" -w '%{http_code}' \
|
||||
"${BASE}/management/instances" 2>/dev/null || echo 000)
|
||||
expect_http "GET /management/instances NO_AUTH → 401" 401 "$code"
|
||||
|
||||
if [ -n "${AUTH401_BEARER:-}" ]; then
|
||||
if [ -z "${AUTH401_BEARER_INSTANCE:-}" ]; then
|
||||
case "${AUTH401_BEARER_NOTE:-}" in
|
||||
*default-instance-token*) AUTH401_BEARER_INSTANCE=default ;;
|
||||
*) AUTH401_BEARER_INSTANCE="${DURABLE_ID:-default}" ;;
|
||||
esac
|
||||
fi
|
||||
inst=$(to_lower "$AUTH401_BEARER_INSTANCE")
|
||||
tok="$AUTH401_BEARER"
|
||||
case "$tok" in secret-token:*) ;; *) tok="secret-token:${tok}" ;; esac
|
||||
code=$(curl -skS --max-redirs 0 -m "${TIMEOUT}" \
|
||||
-o "$tmp/priv2.json" -w '%{http_code}' \
|
||||
"${BASE}/instances/${inst}/private/" \
|
||||
-H "Authorization: Bearer ${tok}" 2>/dev/null || echo 000)
|
||||
case "$code" in
|
||||
200) ok "GET private/ secrets Bearer" "HTTP 200 instance=$inst" ;;
|
||||
401) warn "secrets Bearer rejected" "HTTP 401 on $inst" ;;
|
||||
*) warn "secrets Bearer" "HTTP $code on $inst" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
set_group durable
|
||||
# Extra: durable mon401 still works after throwaway tests (if present)
|
||||
# ===========================================================================
|
||||
if [ -n "${DURABLE_ID:-}" ] && [ -n "${DURABLE_PASS:-}" ]; then
|
||||
dlow=$(to_lower "$DURABLE_ID")
|
||||
dmix=$(synth_mix "$dlow")
|
||||
code=$(token_http "$dlow" "$dlow" "$DURABLE_PASS")
|
||||
expect_http "durable ${dlow} login user=low → 200" 200 "$code" "$(json_code_hint)"
|
||||
code=$(token_http "$dlow" "$dmix" "$DURABLE_PASS")
|
||||
expect_http "durable ${dlow} login user=MIX → 401" 401 "$code" "$(json_code_hint)"
|
||||
else
|
||||
info "durable" "no mon401.password — skipped"
|
||||
fi
|
||||
|
||||
# ===========================================================================
|
||||
set_group report
|
||||
# ===========================================================================
|
||||
info "summary matrix" "401: Basic user case≠low | bad/empty pwd | no auth | bad bearer | wrong user · 200: user=low + path any case · 404: missing instance · SPA must toLowerCase username on login/signup/admin/pw-relogin"
|
||||
if [ "$CREATED" = "1" ]; then
|
||||
info "throwaway left" "id=${ID_MIX:-?} low=${ID_LOW_USE} (password rotated if pwchange ran)"
|
||||
fi
|
||||
|
||||
# fall through → EXIT trap runs _auth401_finish (summary + full ERROR list)
|
||||
:
|
||||
Loading…
Add table
Add a link
Reference in a new issue