test: gate DWH authentication integration
This commit is contained in:
@@ -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
|
||||
|
||||
Executable
+219
@@ -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
|
||||
Executable
+457
@@ -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" <<EOF
|
||||
worker_processes 1;
|
||||
pid $nginx_prefix/nginx.pid;
|
||||
error_log $nginx_prefix/error.log crit;
|
||||
|
||||
events {
|
||||
worker_connections 16;
|
||||
}
|
||||
|
||||
http {
|
||||
access_log $nginx_prefix/access.log;
|
||||
include $runtime_http;
|
||||
|
||||
server {
|
||||
listen unix:$nginx_socket;
|
||||
server_name synthetic;
|
||||
include $runtime_location;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
nginx -t -p "$nginx_prefix" -c "$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
|
||||
Reference in New Issue
Block a user