andrewmusselman opened a new pull request, #1207: URL: https://github.com/apache/tooling-trusted-releases/pull/1207
## Add explicit authentication level decorators for API endpoints Closes #1169. Every `@api.typed` API route now declares its authentication level via exactly one of four decorators, and forgetting the decorator fails at import time rather than silently defaulting to public access. ``` @api.auth.public # no authentication @api.auth.bearer # ATR-issued JWT in Authorization header @api.auth.body_oidc # Trusted Publisher OIDC token in request body @api.auth.pat # Personal Access Token (jwt_create only) ``` The `typed()` decorator inspects the wrapped function for an `_api_auth_level` marker and raises `TypeError` if it's missing. Adding a new route without an auth decorator means the server will not start. ### Three commits Each commit is independently reviewable; the split mirrors how I built it. **1. Scaffolding (no behavior change.)** Adds `atr/blueprints/api_auth.py` with the four decorators (all markers at this stage), wires `auth` onto the existing `api` namespace, adds the import-time enforcer in `typed()`, and marks all 46 existing API routes with their correct level. `@jwtoken.require` and the existing inline body-token validation stay where they are. The only new failure mode is "route registered without auth decorator → `TypeError`". **2. Bearer consolidation.** `@api.auth.bearer` now wraps `jwtoken.require` and applies the `BearerAuth` OpenAPI security scheme itself, so every bearer endpoint can drop two redundant decorators. 36 lines deleted across 18 endpoints. No behavior change — same auth check, one decorator instead of three. Also drops the now-unused `import atr.jwtoken` from `atr/api/__init__.py`. **3. body_oidc validation, registry, tests.** - `@api.auth.body_oidc` pre-validates the Trusted Publisher OIDC token and exposes the verified state via `api.auth.trusted_publisher_context()` (a frozen `TrustedPublisherContext` dataclass on `quart.g`). All 7 body_oidc handlers migrated off direct JWT re-verification — zero calls to `interaction.trusted_jwt*` remain in the API module. - New `interaction.trusted_project_for_payload()` and `trusted_release_for_payload()` helpers take an already-verified payload and do only the phase/project lookup. Existing `trusted_jwt()` and `trusted_jwt_for_dist()` are preserved and now share a `_trusted_dist_lookup()` helper. - `tests/unit/api_auth_registry.yaml` records the canonical URL → auth level mapping for all 46 endpoints; a drift test asserts the live Quart route map matches. - `tests/unit/test_api_auth_decorators.py` covers: marker correctness, import-time enforcement, stacking rejection, the registry drift, behavioral 401/200 per level, the `quart.g.tp_context` handoff, and the new `interaction` helpers. - `docs/authentication-security.html` updated. ### Endpoint audit Every API route is marked. Final distribution: | Level | Count | Notes | |---|---|---| | `public` | 20 | No authentication | | `bearer` | 18 | All previously `@jwtoken.require` | | `body_oidc` | 7 | Trusted Publisher OIDC handlers | | `pat` | 1 | `jwt_create` (PAT exchange) | `@api.auth.pat` is intentionally a pure marker. The only consumer, `jwt_create`, *is* the PAT exchange endpoint, so wrapping its own logic in a decorator would just duplicate storage-layer code. ### Scope API routes only, per discussion on the issue. Admin routes are admin-gated at the blueprint level, which is the right place for them. GET/POST web routes could plausibly benefit from a similar pattern in the future, but that's out of scope here. ### Edge cases worth knowing about - `publisher_distribution_record` still tries `TrustedProjectPhase.FINISH` and falls back to `COMPOSE`. That fallback is handler logic, not auth, so it stays in the handler. - `update_distribution_task_status` requires `ctx.asf_uid is None` (ATR-driven workflow only). Also handler logic. If this pattern spreads, a `body_oidc_atr_only` variant would be reasonable. - `distribute_ssh_register` and `distribution_record_from_workflow` use `trusted_release_for_payload()` because they take `asf_uid`/`project_key`/`version_key` from the body and need the dist-style lookup. - Rate limiting stays orthogonal to auth. - `/api/openapi.json` doesn't go through `@api.typed` and is filtered explicitly in the coverage test. ### Verification - `make unit`, `make e2e`, `make check` - Rebased on `main` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
