From 1b18a0fb25ac117a3d8bbd18688e35bdde1bd181 Mon Sep 17 00:00:00 2001 From: User Date: Fri, 21 Aug 2026 03:12:01 +0200 Subject: [PATCH] test: gate DWH authentication integration --- .github/workflows/deployment.yml | 25 ++ scripts/test-dwh-auth-nginx-contract.sh | 219 ++++++++++ scripts/test-dwh-auth-nginx-integration.sh | 457 +++++++++++++++++++++ 3 files changed, 701 insertions(+) create mode 100755 scripts/test-dwh-auth-nginx-contract.sh create mode 100755 scripts/test-dwh-auth-nginx-integration.sh diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 200fa58b..dc994ac2 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -106,6 +106,31 @@ jobs: - name: Run authentication and authenticated F1 browser smoke run: bash scripts/authentication-smoke.sh + dwh-auth-linux: + name: DWH authentication Nginx gate + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Check out source + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: "1.26.5" + cache-dependency-path: tools/dwh-auth/go.mod + - name: Install Nginx + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends nginx-light + - name: Run DWH authentication gates + run: | + (cd tools/dwh-auth && go test -race ./... -count=1 && go vet ./...) + bash scripts/test-dwh-auth-build-contract.sh + bash scripts/test-dwh-auth-nginx-contract.sh + bash scripts/test-dwh-auth-nginx-integration.sh + linux-docker: name: Linux Docker deployment and rollback runs-on: ubuntu-24.04 diff --git a/scripts/test-dwh-auth-nginx-contract.sh b/scripts/test-dwh-auth-nginx-contract.sh new file mode 100755 index 00000000..e939b47b --- /dev/null +++ b/scripts/test-dwh-auth-nginx-contract.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "$0")/.." && pwd -P) +http_template=${DWH_AUTH_NGINX_HTTP:-"$repo_root/deploy/dwh-auth/nginx-http.conf.example"} +location_template=${DWH_AUTH_NGINX_LOCATION:-"$repo_root/deploy/dwh-auth/nginx-dwh-location.conf.example"} +temp_root= + +report_pass() { + printf 'case=%s status=PASS\n' "$1" +} + +report_fail() { + printf 'case=%s status=FAIL\n' "$1" >&2 + exit 1 +} + +cleanup() { + if [[ "$temp_root" == /tmp/thothii-dwh-auth-nginx-contract.* && -d "$temp_root" ]]; then + rm -rf -- "$temp_root" + fi +} +trap cleanup EXIT + +effective_lines() { + awk ' + { + line = $0 + sub(/[[:space:]]*#.*/, "", line) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) + gsub(/[[:space:]]+/, " ", line) + if (line != "") print line + } + ' "$1" +} + +location_block() { + local file=$1 + local location=$2 + awk -v expected="location $location {" ' + function normalize(line) { + sub(/[[:space:]]*#.*/, "", line) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) + gsub(/[[:space:]]+/, " ", line) + return line + } + { + line = normalize($0) + if (!inside && line == expected) inside = 1 + if (inside) { + if (line != "") print line + if (line == "}") exit + } + } + ' "$file" +} + +contains_exactly_once() { + local haystack=$1 + local needle=$2 + [[ $(grep -Fxc -- "$needle" <<<"$haystack" || true) -eq 1 ]] +} + +contains_line() { + local haystack=$1 + local needle=$2 + grep -Fqx -- "$needle" <<<"$haystack" +} + +check_templates() { + local http=$1 + local location=$2 + local http_lines auth_lines unavailable_lines dwh_lines + + [[ -f "$http" && -f "$location" ]] || return 1 + http_lines=$(effective_lines "$http") + auth_lines=$(location_block "$location" '= /_check_dwh_key') + unavailable_lines=$(location_block "$location" '@dwh_auth_unavailable') + dwh_lines=$(location_block "$location" '/dwh/') + + contains_exactly_once "$auth_lines" 'internal;' || return 1 + contains_exactly_once "$auth_lines" 'proxy_method GET;' || return 1 + contains_exactly_once "$auth_lines" 'proxy_pass http://unix:/run/dwh-auth/verify.sock:/verify;' || return 1 + contains_exactly_once "$auth_lines" 'proxy_pass_request_body off;' || return 1 + contains_exactly_once "$auth_lines" 'proxy_pass_request_headers off;' || return 1 + contains_exactly_once "$auth_lines" 'proxy_set_header Content-Length "";' || return 1 + contains_exactly_once "$auth_lines" 'proxy_set_header X-API-Key $http_x_api_key;' || return 1 + contains_exactly_once "$unavailable_lines" 'return 503;' || return 1 + + contains_line "$dwh_lines" 'location /dwh/ {' || return 1 + contains_exactly_once "$dwh_lines" 'limit_req zone=dwh_auth burst=100 nodelay;' || return 1 + contains_exactly_once "$dwh_lines" 'auth_request /_check_dwh_key;' || return 1 + contains_exactly_once "$dwh_lines" 'auth_request_set $dwh_key_id $upstream_http_x_dwh_key_id;' || return 1 + contains_exactly_once "$dwh_lines" 'error_page 500 =503 @dwh_auth_unavailable;' || return 1 + contains_exactly_once "$dwh_lines" 'proxy_set_header X-API-Key "";' || return 1 + contains_exactly_once "$dwh_lines" 'proxy_set_header X-DWH-Key-ID "";' || return 1 + contains_exactly_once "$dwh_lines" 'proxy_set_header Host $host;' || return 1 + contains_exactly_once "$dwh_lines" 'proxy_pass http://127.0.0.1:3001;' || return 1 + + contains_exactly_once "$http_lines" 'map $http_x_api_key $dwh_public_key_class {' || return 1 + contains_line "$http_lines" 'default opaque;' || return 1 + contains_line "$http_lines" '"~^thtdwh_v1\.([A-Za-z0-9_-]{16})\.[A-Za-z0-9_-]{43}$" v1:$1;' || return 1 + contains_exactly_once "$http_lines" 'map "$remote_addr:$dwh_public_key_class" $dwh_auth_rate_key {' || return 1 + contains_exactly_once "$http_lines" 'limit_req_zone $dwh_auth_rate_key zone=dwh_auth:10m rate=20r/s;' || return 1 + + ! grep -Eq 'limit_req_zone.*\$(http_x_api_key|dwh_key_secret|request)' <<<"$http_lines" || return 1 + ! grep -Eq 'map .*\$http_x_api_key .*\$dwh_auth_rate_key' <<<"$http_lines" || return 1 +} + +replace_effective_line() { + local file=$1 + local needle=$2 + local replacement=$3 + local output="$file.replaced" + + awk -v needle="$needle" -v replacement="$replacement" ' + function normalize(line) { + sub(/[[:space:]]*#.*/, "", line) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", line) + gsub(/[[:space:]]+/, " ", line) + return line + } + { + if (normalize($0) == needle) { + print replacement + replaced++ + next + } + print + } + END { if (replaced != 1) exit 1 } + ' "$file" >"$output" || return 1 + mv -- "$output" "$file" +} + +expect_location_rejected() { + local name=$1 + local needle=$2 + local replacement=$3 + local fixture="$temp_root/$name" + mkdir -- "$fixture" + cp -- "$http_template" "$fixture/http.conf" + cp -- "$location_template" "$fixture/location.conf" + replace_effective_line "$fixture/location.conf" "$needle" "$replacement" || return 1 + ! check_templates "$fixture/http.conf" "$fixture/location.conf" +} + +expect_http_rejected() { + local name=$1 + local needle=$2 + local replacement=$3 + local fixture="$temp_root/$name" + mkdir -- "$fixture" + cp -- "$http_template" "$fixture/http.conf" + cp -- "$location_template" "$fixture/location.conf" + replace_effective_line "$fixture/http.conf" "$needle" "$replacement" || return 1 + ! check_templates "$fixture/http.conf" "$fixture/location.conf" +} + +[[ -f "$http_template" ]] || report_fail source_http_exists +[[ -f "$location_template" ]] || report_fail source_location_exists +check_templates "$http_template" "$location_template" || report_fail source_contract +report_pass source_contract + +temp_root=$(mktemp -d /tmp/thothii-dwh-auth-nginx-contract.XXXXXXXX) || report_fail fixture_root + +expect_location_rejected missing_auth_request 'auth_request /_check_dwh_key;' '# removed auth request' \ + || report_fail negative_missing_auth_request +report_pass negative_missing_auth_request + +expect_location_rejected missing_proxy_method 'proxy_method GET;' '# removed proxy method' \ + || report_fail negative_missing_proxy_method +report_pass negative_missing_proxy_method + +expect_location_rejected missing_proxy_body 'proxy_pass_request_body off;' '# removed proxy body suppression' \ + || report_fail negative_missing_proxy_body +report_pass negative_missing_proxy_body + +expect_location_rejected missing_proxy_header_isolation 'proxy_pass_request_headers off;' '# removed proxy header isolation' \ + || report_fail negative_missing_proxy_header_isolation +report_pass negative_missing_proxy_header_isolation + +expect_location_rejected missing_content_length_clear 'proxy_set_header Content-Length "";' '# removed content length clear' \ + || report_fail negative_missing_content_length_clear +report_pass negative_missing_content_length_clear + +expect_location_rejected missing_verifier_key_forward 'proxy_set_header X-API-Key $http_x_api_key;' '# removed verifier key forwarding' \ + || report_fail negative_missing_verifier_key_forward +report_pass negative_missing_verifier_key_forward + +expect_location_rejected missing_upstream_key_clear 'proxy_set_header X-API-Key "";' '# removed upstream key clear' \ + || report_fail negative_missing_upstream_key_clear +report_pass negative_missing_upstream_key_clear + +expect_location_rejected missing_failure_mapping 'error_page 500 =503 @dwh_auth_unavailable;' '# removed failure mapping' \ + || report_fail negative_missing_failure_mapping +report_pass negative_missing_failure_mapping + +expect_location_rejected public_verifier 'internal;' '# verifier became public' \ + || report_fail negative_public_verifier +report_pass negative_public_verifier + +expect_location_rejected tcp_authenticator 'proxy_pass http://unix:/run/dwh-auth/verify.sock:/verify;' 'proxy_pass http://127.0.0.1:19091/verify;' \ + || report_fail negative_tcp_authenticator +report_pass negative_tcp_authenticator + +expect_location_rejected postgrest_bypass 'auth_request /_check_dwh_key;' '# direct PostgREST bypass' \ + || report_fail negative_postgrest_bypass +report_pass negative_postgrest_bypass + +expect_location_rejected failure_mapped_to_success 'error_page 500 =503 @dwh_auth_unavailable;' 'error_page 500 =200 @dwh_auth_unavailable;' \ + || report_fail negative_failure_mapped_to_success +report_pass negative_failure_mapped_to_success + +expect_http_rejected full_secret_rate_key 'map "$remote_addr:$dwh_public_key_class" $dwh_auth_rate_key {' 'map "$remote_addr:$http_x_api_key" $dwh_auth_rate_key {' \ + || report_fail negative_full_secret_rate_key +report_pass negative_full_secret_rate_key + +report_pass summary diff --git a/scripts/test-dwh-auth-nginx-integration.sh b/scripts/test-dwh-auth-nginx-integration.sh new file mode 100755 index 00000000..33c9f1f5 --- /dev/null +++ b/scripts/test-dwh-auth-nginx-integration.sh @@ -0,0 +1,457 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root=$(cd "$(dirname "$0")/.." && pwd -P) +go_image='golang:1.26.5-bookworm@sha256:1ecb7edf62a0408027bd5729dfd6b1b8766e578e8df93995b225dfd0944eb651' +temp_root= +current_case=setup +failure_reported=false +registered_pids=() + +report_pass() { + printf 'case=%s status=PASS\n' "$1" +} + +fail_case() { + current_case=$1 + failure_reported=true + printf 'case=%s status=FAIL\n' "$current_case" >&2 + exit 1 +} + +register_pid() { + registered_pids+=("$1") +} + +cleanup() { + local pid + set +e + for pid in "${registered_pids[@]}"; do + if [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null; then + kill -TERM "$pid" 2>/dev/null + fi + done + for pid in "${registered_pids[@]}"; do + if [[ "$pid" =~ ^[0-9]+$ ]]; then + wait "$pid" 2>/dev/null + fi + done + if [[ "$temp_root" == /tmp/thothii-dwh-auth-nginx-integration.* && -d "$temp_root" ]]; then + rm -rf -- "$temp_root" + fi +} + +on_exit() { + local exit_code=$? + cleanup + if ((exit_code != 0)) && [[ "$failure_reported" != true ]]; then + printf 'case=%s status=FAIL\n' "$current_case" >&2 + fi + exit "$exit_code" +} +trap on_exit EXIT +trap 'exit 130' INT TERM + +wait_for_socket() { + local socket=$1 + local attempt + for attempt in $(seq 1 100); do + [[ -S "$socket" ]] && return 0 + sleep 0.05 + done + return 1 +} + +wait_for_file() { + local file=$1 + local attempt + for attempt in $(seq 1 100); do + [[ -s "$file" ]] && return 0 + sleep 0.05 + done + return 1 +} + +build_dwh_auth() { + local output=$1 + if command -v go >/dev/null 2>&1; then + ( + cd "$repo_root/tools/dwh-auth" + go build -o "$output" ./cmd/dwh-auth + ) + return + fi + command -v docker >/dev/null 2>&1 || return 1 + docker run --rm --network none --user "$(id -u):$(id -g)" \ + --volume "$repo_root:/work:ro" \ + --volume "$temp_root:/out" \ + --workdir /work/tools/dwh-auth \ + "$go_image" \ + /bin/sh -ec 'GOCACHE=/out/go-cache CGO_ENABLED=0 go build -o /out/dwh-auth ./cmd/dwh-auth' +} + +v1_key_id() { + local value=$1 + local remainder=${value#thtdwh_v1.} + printf '%s' "${remainder%%.*}" +} + +request_status() { + local body=$1 + shift + curl --silent --show-error --noproxy '*' \ + --unix-socket "$nginx_socket" \ + --output "$body" \ + --write-out '%{http_code}' \ + "$@" 2>"$temp_root/curl.stderr" +} + +expect_status() { + local name=$1 + local expected=$2 + local body=$3 + shift 3 + local status + current_case=$name + if ! status=$(request_status "$body" "$@"); then + fail_case "$name" + fi + [[ "$status" == "$expected" ]] || fail_case "$name" +} + +stop_registered_pid() { + local pid=$1 + [[ "$pid" =~ ^[0-9]+$ ]] || return 1 + if kill -0 "$pid" 2>/dev/null; then + kill -TERM "$pid" + wait "$pid" + fi +} + +for command in nginx curl python3 ss date; do + command -v "$command" >/dev/null 2>&1 || fail_case "missing_${command}" +done + +nginx_version=$(nginx -v 2>&1) +[[ "$nginx_version" == *nginx/1.24.* ]] || fail_case nginx_version +report_pass nginx_1_24 +temp_root=$(mktemp -d /tmp/thothii-dwh-auth-nginx-integration.XXXXXXXX) || fail_case fixture_root +chmod 0700 "$temp_root" || fail_case fixture_root +umask 077 + +dwh_auth="$temp_root/dwh-auth" +current_case=build_dwh_auth +build_dwh_auth "$dwh_auth" >"$temp_root/build.log" 2>&1 || fail_case build_dwh_auth +[[ -x "$dwh_auth" ]] || fail_case build_dwh_auth +report_pass build_dwh_auth + +registry_root="$temp_root/registry" +socket_parent="$temp_root/socket" +service_socket="$socket_parent/verify.sock" +auth_proxy_socket="$socket_parent/nginx-auth.sock" +nginx_prefix="$temp_root/nginx" +nginx_socket="$nginx_prefix/listener.sock" +nginx_config="$nginx_prefix/nginx.conf" +runtime_http="$nginx_prefix/http.conf" +runtime_location="$nginx_prefix/location.conf" +marker_port_file="$temp_root/marker-port" +marker_observations="$temp_root/marker-observations.jsonl" +auth_observations="$temp_root/auth-observations.jsonl" + +mkdir "$registry_root" "$socket_parent" "$nginx_prefix" || fail_case fixture_directories +chmod 0750 "$registry_root" +chmod 0700 "$socket_parent" "$nginx_prefix" + +v1_file="$temp_root/v1.key" +legacy_file="$temp_root/legacy.key" +revoked_file="$temp_root/revoked.key" +expired_file="$temp_root/expired.key" + +current_case=registry_setup +"$dwh_auth" --registry-root "$registry_root" key create --installation-id synthetic-primary --output "$v1_file" \ + >"$temp_root/cli.log" 2>&1 || fail_case registry_setup +printf '%s' 'synthetic-legacy-ordinary' >"$legacy_file" +chmod 0600 "$legacy_file" +"$dwh_auth" --registry-root "$registry_root" key import --legacy-raw --installation-id legacy-shared --from-file "$legacy_file" \ + >"$temp_root/cli.log" 2>&1 || fail_case registry_setup +"$dwh_auth" --registry-root "$registry_root" key create --installation-id synthetic-revoked --output "$revoked_file" \ + >"$temp_root/cli.log" 2>&1 || fail_case registry_setup +revoked_key=$(<"$revoked_file") +revoked_id=$(v1_key_id "$revoked_key") +"$dwh_auth" --registry-root "$registry_root" key revoke --key-id "$revoked_id" --reason synthetic \ + >"$temp_root/cli.log" 2>&1 || fail_case registry_setup +expires_at=$(date --utc --date='2 seconds' '+%Y-%m-%dT%H:%M:%SZ') +"$dwh_auth" --registry-root "$registry_root" key create --installation-id synthetic-expired --expires-at "$expires_at" --output "$expired_file" \ + >"$temp_root/cli.log" 2>&1 || fail_case registry_setup +sleep 3 +v1_key=$(<"$v1_file") +legacy_key=$(<"$legacy_file") +expired_key=$(<"$expired_file") +report_pass registry_setup + +current_case=verifier_start +"$dwh_auth" serve --registry-root "$registry_root" --socket "$service_socket" \ + >"$temp_root/verifier.log" 2>&1 & +verifier_pid=$! +register_pid "$verifier_pid" +wait_for_socket "$service_socket" || fail_case verifier_start +report_pass verifier_start + +current_case=synthetic_upstreams +AUTH_PROXY_SOCKET="$auth_proxy_socket" \ +VERIFIER_SOCKET="$service_socket" \ +MARKER_PORT_FILE="$marker_port_file" \ +MARKER_OBSERVATIONS="$marker_observations" \ +AUTH_OBSERVATIONS="$auth_observations" \ +python3 - >"$temp_root/upstreams.log" 2>&1 <<'PY' & +import http.client +import http.server +import json +import os +import signal +import socket +import socketserver +import sys +import threading + +proxy_socket = os.environ["AUTH_PROXY_SOCKET"] +verifier_socket = os.environ["VERIFIER_SOCKET"] +marker_port_file = os.environ["MARKER_PORT_FILE"] +marker_observations = os.environ["MARKER_OBSERVATIONS"] +auth_observations = os.environ["AUTH_OBSERVATIONS"] + + +def append_json(path, value): + with open(path, "a", encoding="utf-8") as handle: + handle.write(json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n") + + +class UnixHTTPConnection(http.client.HTTPConnection): + def __init__(self, path): + super().__init__("localhost") + self.path = path + + def connect(self): + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect(self.path) + + +class AuthProxy(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def log_message(self, _format, *_args): + pass + + def do_GET(self): + names = sorted( + name.lower() + for name in self.headers.keys() + if name.lower() not in {"host", "connection"} + ) + append_json( + auth_observations, + { + "client_header_names": names, + "has_authorization": "authorization" in self.headers, + "has_cookie": "cookie" in self.headers, + "has_dwh_key_id": "x-dwh-key-id" in self.headers, + "method": self.command, + "path": self.path, + }, + ) + try: + connection = UnixHTTPConnection(verifier_socket) + connection.request(self.command, self.path, headers=dict(self.headers.items())) + response = connection.getresponse() + payload = response.read() + self.send_response(response.status) + for name, value in response.getheaders(): + if name.lower() not in {"connection", "transfer-encoding", "content-length"}: + self.send_header(name, value) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + connection.close() + except OSError: + self.send_response(500) + self.send_header("Content-Length", "0") + self.end_headers() + + +class UnixHTTPServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer): + daemon_threads = True + + +class Marker(http.server.BaseHTTPRequestHandler): + def log_message(self, _format, *_args): + pass + + def do_GET(self): + append_json( + marker_observations, + { + "has_api_key": "x-api-key" in self.headers, + "has_dwh_key_id": "x-dwh-key-id" in self.headers, + "path": self.path, + }, + ) + payload = b"postgrest-marker\n" + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + +for path in (proxy_socket,): + try: + os.unlink(path) + except FileNotFoundError: + pass + +marker = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Marker) +auth_proxy = UnixHTTPServer(proxy_socket, AuthProxy) +os.chmod(proxy_socket, 0o600) +with open(marker_port_file, "w", encoding="ascii") as handle: + handle.write(str(marker.server_address[1])) + +for server in (marker, auth_proxy): + threading.Thread(target=server.serve_forever, daemon=True).start() + +signal.pause() +PY +upstreams_pid=$! +register_pid "$upstreams_pid" +wait_for_socket "$auth_proxy_socket" || fail_case synthetic_upstreams +wait_for_file "$marker_port_file" || fail_case synthetic_upstreams +marker_port=$(<"$marker_port_file") +[[ "$marker_port" =~ ^[0-9]+$ ]] || fail_case synthetic_upstreams +report_pass synthetic_upstreams + +current_case=render_nginx +cp -- "$repo_root/deploy/dwh-auth/nginx-http.conf.example" "$runtime_http" +sed \ + -e "s|http://unix:/run/dwh-auth/verify.sock:/verify|http://unix:$auth_proxy_socket:/verify|" \ + -e "s|http://127.0.0.1:3001|http://127.0.0.1:$marker_port|" \ + "$repo_root/deploy/dwh-auth/nginx-dwh-location.conf.example" >"$runtime_location" +cat >"$nginx_config" <"$temp_root/nginx-test.log" 2>&1 || fail_case render_nginx +report_pass composite_nginx_config + +current_case=nginx_start +nginx -p "$nginx_prefix" -c "$nginx_config" >"$temp_root/nginx-start.log" 2>&1 || fail_case nginx_start +wait_for_file "$nginx_prefix/nginx.pid" || fail_case nginx_start +nginx_pid=$(<"$nginx_prefix/nginx.pid") +[[ "$nginx_pid" =~ ^[0-9]+$ ]] || fail_case nginx_start +register_pid "$nginx_pid" +wait_for_socket "$nginx_socket" || fail_case nginx_start +report_pass nginx_start + +current_case=auth_socket_unix_only +[[ -S "$service_socket" ]] || fail_case auth_socket_unix_only +ss -xl >"$temp_root/ss-unix.log" 2>&1 || fail_case auth_socket_unix_only +grep -Fq -- "$service_socket" "$temp_root/ss-unix.log" || fail_case auth_socket_unix_only +report_pass auth_socket_unix_only + +probe_body="$temp_root/probe.body" +expect_status verifier_not_public 404 "$probe_body" 'http://synthetic/_check_dwh_key' +report_pass verifier_not_public + +route_url='http://synthetic/dwh/?keep=exact&second=two' +expect_status valid_v1 200 "$probe_body" \ + -H "X-API-Key: $v1_key" \ + -H 'Cookie: synthetic-session=one' \ + -H 'Authorization: Bearer synthetic' \ + -H 'X-DWH-Key-ID: client-spoof' \ + "$route_url" +[[ $(<"$probe_body") == 'postgrest-marker' ]] || fail_case valid_v1 +report_pass valid_v1 + +expect_status valid_legacy 200 "$probe_body" -H "X-API-Key: $legacy_key" 'http://synthetic/dwh/?legacy=one' +[[ $(<"$probe_body") == 'postgrest-marker' ]] || fail_case valid_legacy +report_pass valid_legacy + +expect_status invalid_key 401 "$probe_body" \ + -H 'X-API-Key: thtdwh_v1.AAAAAAAAAAAAAAAA.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ + 'http://synthetic/dwh/?invalid=one' +report_pass invalid_key + +expect_status revoked_key 401 "$probe_body" -H "X-API-Key: $revoked_key" 'http://synthetic/dwh/?revoked=one' +report_pass revoked_key + +expect_status expired_key 401 "$probe_body" -H "X-API-Key: $expired_key" 'http://synthetic/dwh/?expired=one' +report_pass expired_key + +expect_status duplicate_v1 401 "$probe_body" \ + -H "X-API-Key: $v1_key" \ + -H "X-API-Key: $v1_key" \ + 'http://synthetic/dwh/?duplicate=v1' +report_pass duplicate_v1 + +expect_status duplicate_legacy 401 "$probe_body" \ + -H "X-API-Key: $legacy_key" \ + -H "X-API-Key: $legacy_key" \ + 'http://synthetic/dwh/?duplicate=legacy' +report_pass duplicate_legacy + +current_case=stopped_verifier +stop_registered_pid "$verifier_pid" || fail_case stopped_verifier +expect_status stopped_verifier 503 "$probe_body" -H "X-API-Key: $v1_key" 'http://synthetic/dwh/?unavailable=one' +report_pass stopped_verifier + +current_case=header_and_path_isolation +AUTH_OBSERVATIONS="$auth_observations" MARKER_OBSERVATIONS="$marker_observations" python3 - >"$temp_root/assertions.log" 2>&1 <<'PY' || fail_case header_and_path_isolation +import json +import os + + +def load(path): + with open(path, encoding="utf-8") as handle: + return [json.loads(line) for line in handle if line.strip()] + + +auth = load(os.environ["AUTH_OBSERVATIONS"]) +marker = load(os.environ["MARKER_OBSERVATIONS"]) +assert len(auth) == 8 +for request in auth: + assert request["method"] == "GET" + assert request["path"] == "/verify" + assert request["client_header_names"] == ["x-api-key"] + assert not request["has_authorization"] + assert not request["has_cookie"] + assert not request["has_dwh_key_id"] + +assert len(marker) == 2 +assert marker[0] == { + "has_api_key": False, + "has_dwh_key_id": False, + "path": "/dwh/?keep=exact&second=two", +} +assert marker[1] == { + "has_api_key": False, + "has_dwh_key_id": False, + "path": "/dwh/?legacy=one", +} +PY +report_pass header_and_path_isolation + +report_pass summary