#!/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" } location_declarations() { effective_lines "$1" | awk "/^location / { print }" } 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 locations [[ -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/') locations=$(location_declarations "$location") [[ $(wc -l <<<"$locations") -eq 3 ]] || return 1 [[ $(grep -Fxc -- "location = /_check_dwh_key {" <<<"$locations" || true) -eq 1 ]] || return 1 [[ $(grep -Fxc -- "location @dwh_auth_unavailable {" <<<"$locations" || true) -eq 1 ]] || return 1 [[ $(grep -Fxc -- "location /dwh/ {" <<<"$locations" || true) -eq 1 ]] || return 1 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" } expect_postgrest_regex_bypass_rejected() { local fixture="$temp_root/postgrest_regex_bypass" mkdir -- "$fixture" cp -- "$http_template" "$fixture/http.conf" cp -- "$location_template" "$fixture/location.conf" printf "%s\n" "location ~ ^/dwh/ {" " proxy_pass http://127.0.0.1:3001/;" "}" >>"$fixture/location.conf" ! check_templates "$fixture/http.conf" "$fixture/location.conf" } expect_postgrest_duplicate_bypass_rejected() { local fixture="$temp_root/postgrest_duplicate_bypass" mkdir -- "$fixture" cp -- "$http_template" "$fixture/http.conf" cp -- "$location_template" "$fixture/location.conf" printf "%s\n" "location /dwh/ {" " proxy_pass http://127.0.0.1:3001/;" "}" >>"$fixture/location.conf" ! 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_postgrest_regex_bypass_rejected || report_fail negative_postgrest_regex_bypass report_pass negative_postgrest_regex_bypass expect_postgrest_duplicate_bypass_rejected || report_fail negative_postgrest_duplicate_bypass report_pass negative_postgrest_duplicate_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