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]

Reply via email to