#!/usr/bin/env bash # Exercises the plugin's nginx config generation against a fake dokku layout, # asserts the important directives are present, and (if docker is available) # validates the result with a real nginx binary. set -eo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" WORK="$(mktemp -d)" trap 'rm -rf "$WORK"' EXIT # Fake dokku host layout. export DOKKU_ROOT="$WORK/dokku-root" export DOKKU_LIB_ROOT="$WORK/dokku-lib" export PLUGIN_CORE_AVAILABLE_PATH="$WORK/nonexistent" # force built-in fallbacks APP="myapp" mkdir -p "$DOKKU_ROOT/$APP" "$DOKKU_LIB_ROOT/data/google-auth/global" # A minimal nginx.conf as dokku's template would generate it. cat >"$DOKKU_ROOT/$APP/nginx.conf" <<'EOF' upstream myapp-5000 { server 172.17.0.3:5000; } server { listen 80; server_name myapp.example.com; } EOF # shellcheck disable=SC1091 source "$ROOT/functions" fail() { echo "FAIL: $*" 1>&2 exit 1 } # --- pattern validation --- fn-ga-validate-pattern "/api/webhooks" || fail "prefix pattern should be valid" fn-ga-validate-pattern "re:^/v[0-9]+/public/" || fail "regex pattern should be valid" fn-ga-validate-pattern "api/webhooks" && fail "pattern without leading / should be invalid" fn-ga-validate-pattern "/x; }" && fail "pattern with injection chars should be invalid" fn-ga-validate-pattern '/x{2}' && fail "pattern with braces should be invalid" fn-ga-validate-pattern "" && fail "empty pattern should be invalid" echo "ok: pattern validation" # --- conf generation --- fn-ga-app-set-enabled "$APP" true fn-ga-exclude-add "$APP" "/api/webhooks" fn-ga-exclude-add "$APP" "re:^/healthz$" CONF="$WORK/google-auth.conf" fn-ga-generate-conf "$APP" >"$CONF" || fail "conf generation failed" grep -q 'proxy_pass http://myapp-5000;' "$CONF" || fail "conf should proxy to the app upstream" grep -q 'auth_request /_google-auth/verify;' "$CONF" || fail "conf should gate with auth_request" grep -q 'location ^~ /api/webhooks {' "$CONF" || fail "conf should contain prefix exclusion" grep -q 'location ~ ^/healthz$ {' "$CONF" || fail "conf should contain regex exclusion" grep -q 'error_page 401 = @google_auth_signin;' "$CONF" || fail "conf should redirect 401s to signin" grep -q 'proxy_set_header X-Forwarded-Email \$google_auth_email;' "$CONF" || fail "conf should forward the email header" grep -q 'proxy_set_header X-Forwarded-Email "";' "$CONF" || fail "excluded paths should strip identity headers" echo "ok: conf contents" # --- the app name must be stamped on every request that reaches the service --- # Checking the invariant rather than a fixed list of locations: any location # that proxies to the auth service must set X-Google-Auth-App to this app, or # the service would fall back to the global lists (silently widening access for # an app whose own allow list is narrower). Any location that proxies to the # app must blank it, so clients cannot pass one through. awk -v app="$APP" ' /^location/ { block = $0; inside = 1; to_service = 0; to_app = 0; stamped = 0; blanked = 0; next } inside && /^}/ { if (to_service && !stamped) { printf "location reaching the auth service without the app header: %s\n", block; bad = 1 } if (to_app && !blanked) { printf "location reaching the app without blanking the app header: %s\n", block; bad = 1 } inside = 0; next } inside { if ($0 ~ /proxy_pass http:\/\/127\.0\.0\.1:/) to_service = 1 if ($0 ~ /proxy_pass http:\/\/myapp-5000;/) to_app = 1 if ($0 == sprintf(" proxy_set_header X-Google-Auth-App \"%s\";", app)) stamped = 1 if ($0 == " proxy_set_header X-Google-Auth-App \"\";") blanked = 1 } END { exit bad } ' "$CONF" || fail "X-Google-Auth-App is not handled consistently across locations" # Guard the guard: the awk above must actually see both kinds of location. [[ "$(grep -c 'proxy_set_header X-Google-Auth-App "myapp";' "$CONF")" -eq 3 ]] || fail "expected the app header on the three auth-service locations" echo "ok: app name stamped for the auth service, blanked for the app" # --- write/remove behavior --- [[ "$(fn-ga-write-conf "$APP")" == "changed" ]] || fail "first write should report changed" [[ "$(fn-ga-write-conf "$APP")" == "unchanged" ]] || fail "second write should report unchanged" [[ -f "$DOKKU_ROOT/$APP/nginx.conf.d/google-auth.conf" ]] || fail "conf file should exist" fn-ga-app-set-enabled "$APP" false [[ "$(fn-ga-write-conf "$APP")" == "changed" ]] || fail "disable should remove the conf" [[ ! -f "$DOKKU_ROOT/$APP/nginx.conf.d/google-auth.conf" ]] || fail "conf file should be gone" fn-ga-app-set-enabled "$APP" true fn-ga-write-conf "$APP" >/dev/null echo "ok: write/remove behavior" # --- undeployed app is skipped --- mkdir -p "$DOKKU_ROOT/fresh-app" fn-ga-app-set-enabled "fresh-app" true [[ "$(fn-ga-write-conf "fresh-app")" == "skipped" ]] || fail "app without nginx.conf should be skipped" echo "ok: undeployed app skipped" # --- validate with real nginx if docker is around --- if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then cat >"$WORK/nginx-test.conf" <"$WORK/nginx-runtime.conf" </dev/null && break i=$((i + 1)); sleep 0.1 done echo "--- plain ---" wget -q -O - http://127.0.0.1:8080/some/page 2>&1 echo "--- spoofed ---" wget -q -O - --header "X-Google-Auth-App: spoofed-app" http://127.0.0.1:8080/some/page 2>&1 ' 2>/dev/null)" || fail "could not run the live nginx spoofing check: $runtime_out" if [[ "$(grep -c 'forwarded-email=app-seen=myapp' <<<"$runtime_out")" -ne 2 ]]; then fail "nginx should have told the auth service the app name on both requests, got: $runtime_out" fi if grep -q "spoofed-app" <<<"$runtime_out"; then fail "a client-supplied X-Google-Auth-App reached the auth service: $runtime_out" fi echo "ok: live nginx sends the real app name and discards a spoofed one" else echo "skip: docker unavailable, skipped real nginx validation" fi echo "ALL NGINX CONF TESTS PASSED"