fix: gate /kong, split /health public, enforce PDP token at router level (PER-15244)#317
Conversation
…vel (PER-15244) - Mount the enforcer router with router-level enforce_pdp_token and drop the per-route copies (notify_seen_sdk deps kept), closing the previously unauthenticated POST /kong decision endpoint. - Move GET /health to a dedicated public router so k8s/LB liveness probes keep working without the PDP token. - Default the Authorization header to None in enforce_pdp_token so a missing header returns 401 instead of 422. - Add auth regression tests for all enforcer routes, /health, and the Kong integration flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🔍 Vulnerabilities of
|
| digest | sha256:f58d60349ab33993fe834b0ac4d708b48588de02fadd85118cf19c7bd327d89a |
| vulnerabilities | |
| platform | linux/amd64 |
| size | 218 MB |
| packages | 250 |
📦 Base Image python:3.10-alpine3.22
| also known as |
|
| digest | sha256:c8f94b3bb77e6ea9015ccd091b7f8aec1b1fcbca95159675235d9a93788797cd |
| vulnerabilities |
Description
Description
Description
Description
Description
Description
Description
Description
Description
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
|
🔍 Vulnerabilities of
|
| digest | sha256:f58d60349ab33993fe834b0ac4d708b48588de02fadd85118cf19c7bd327d89a |
| vulnerabilities | |
| platform | linux/amd64 |
| size | 218 MB |
| packages | 250 |
📦 Base Image python:3.10-alpine3.22
| also known as |
|
| digest | sha256:c8f94b3bb77e6ea9015ccd091b7f8aec1b1fcbca95159675235d9a93788797cd |
| vulnerabilities |
Description
Description
Description
Description
Description
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
Description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Description
|
- Pin aiohttp<3.14 in the dev requirements: aioresponses 0.7.x cannot mock aiohttp>=3.14 (ClientResponse gained a required stream_writer argument), which broke every OPA-mocking test. Runtime pin is unchanged. - Pin k3d to v5.9.0 in the pdp-tester job: the k3d-action default (v5.4.6) predates release checksums.txt assets, which the k3d install script now requires, so cluster setup 404'd before any test ran. Both breakages pre-date this branch (main last ran CI green on 2026-05-13). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR closes an auth gap by enforcing PDP token authentication at the router level for enforcer endpoints (including /kong), while keeping /health publicly accessible for k8s/LB probes.
Changes:
- Split
/healthinto a dedicated public router and mount it without auth. - Apply
enforce_pdp_tokenas a router-level dependency for the enforcer router and remove per-route copies. - Add/expand tests to validate 401 behavior for missing/invalid tokens across enforcer endpoints and cover
/kongflows; pinaiohttp<3.14for test mocking compatibility and bump k3d version in CI.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| requirements-dev.txt | Pin aiohttp<3.14 in dev/test env to keep aioresponses compatible. |
| horizon/tests/test_enforcer_api.py | Add auth-sweep tests for protected routes; add /health public test and /kong auth + integration tests. |
| horizon/pdp.py | Mount new health router publicly; enforce PDP token at enforcer router include level. |
| horizon/enforcer/api.py | Split out init_enforcer_health_router() and remove per-route PDP-token dependencies from enforcer routes. |
| horizon/authentication.py | Make Authorization header optional so missing token yields 401 instead of 422. |
| .github/workflows/tests.yml | Pin k3d version to avoid upstream install/download issues in CI. |
Comments suppressed due to low confidence (1)
horizon/authentication.py:15
authorization.split(" ")will raiseValueErrorfor malformed Authorization headers (e.g. "Bearer", extra spaces, or no space), which will surface as a 500 instead of a 401. Sinceenforce_pdp_tokenis now applied router-wide, harden parsing to always return a controlled 401 on malformed headers.
def enforce_pdp_token(authorization: Annotated[str | None, Header()] = None):
if authorization is None:
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Missing Authorization header")
schema, token = authorization.split(" ")
if schema.strip().lower() != "bearer" or token.strip() != get_env_api_key():
raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Invalid PDP token")
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The k8s/k3d-based pdp-tester job timed out at 600s with no logs and no PDP pods created — the tester's k3d/Helm orchestration never started. The pdp-tester repo added a Docker runtime backend (k8s-free) for exactly this; mirror its own CI's `pdp-tester-docker` job. Install the tester with the [docker] extra and run it as a plain process against the runner's Docker daemon. LOCAL_IMAGE + LOCAL_TAGS make the runtime launch the PR-built permitio/pdp-v2:next directly (no registry pull — aiodocker only pulls on image-not-found, and we docker-load it first). Drops k3d, Helm, the tester image build, and the earlier k3d-version pin those steps needed. The tester attaches the PDP token on every call and probes /healthy for readiness, so this exercises the router-level auth change end-to-end (incl. the health_check case asserting /health -> 200). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes PER-15244
What
Closes the unauthenticated
POST /kongdecision endpoint and kills the per-route auth footgun class on the enforcer router:/healthsplit to a public router (done first, deliberately): moved out ofenforcer_routerinto a dedicatedinit_enforcer_health_router(), mounted without auth. k8s/LB liveness probes (Helm chart probesGET /healthwith no headers) are unaffected by the next step. Body unchanged.include_router(enforcer_router, dependencies=[Depends(enforce_pdp_token)])— matching every sibling router (local/proxy/facts/connectivity). This gates/kong: FastAPI runs dependencies before the handler, so auth now precedes theKONG_INTEGRATION503 check.enforce_pdp_tokencopies removed (8 routes). TheDepends(notify_seen_sdk)deps are kept where present; router-level deps run first, so effective order is preserved.enforce_pdp_tokenheader param defaults toNone: previously a missingAuthorizationheader was rejected by FastAPI param validation as 422 and the function'sis None -> 401branch was dead code. Now a missing header returns 401, per the issue's acceptance spec. Invalid-token 401 behavior unchanged. (Malformed-header 500 is intentionally untouched — that is PER-15245/PER-15250 scope.)Behavior changes
POST /kong, no/any-invalid tokenKONG_INTEGRATION=true)AuthorizationheaderGET /health, no tokenTests
/healthtokenless → 200./kong: valid token + integration disabled → 503; full enabled flow (routes table + mocked OPA) → tokenless 401, valid token 200{"result": true}./authorized_usersand/nginx_allowed(previously uncovered).horizon/tests/suite: 72 passed; ruff check + format clean at the pre-commit-pinned v0.11.6.route.dependant.dependencieson the live app): everyAPIRoutecarries the PDP-token/control-key dep except the intended public set (/health, OPAL's/,/healthcheck,/healthy,/ready) and OPAL routes with their own listener-JWT auth. The OPAL trigger routes stay open by design here — they are PER-15245/PER-15247 scope.⚠ Pre-merge check for reviewers
Confirm the Kong OPA plugin forwards
Authorization: Bearer <PDP_API_KEY>(precedent: the gated/nginx_allowedworks with its nginx caller). If Kong cannot send it,/kongneeds a dedicated credential — flag to the integrations owner.🤖 Generated with Claude Code