Skip to content

Stage 12A: Check Auth0 permissions on Simulation Entrypoint routes - #758

Draft
anth-volk wants to merge 7 commits into
mainfrom
feat/stage-12a-entrypoint-permissions
Draft

anth-volk wants to merge 7 commits into
mainfrom
feat/stage-12a-entrypoint-permissions

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #759

Same-Repository Draft PR Check

  • This PR is a draft.
  • This PR is from a branch in PolicyEngine/policyengine-sim-api, not a personal fork.

Summary

  • Handlers now receive the caller's identity. Protected routes get the validated CallerIdentity, which used to be parsed and discarded.
  • Auth0 permissions per route:
    • simulation:submit: POST /simulate/economy/comparison and POST /simulate/economy/budget-window.
    • simulation:read: GET /jobs/{job_id} and GET /budget-window-jobs/{batch_job_id}.
    • stage12:operate: both /internal/stage12/* routes.
  • How a permission is matched: exact matches only, from the RBAC permissions claim on any token. The space-delimited scope claim also counts, but only on client-credentials (machine) tokens, identified by gty (a string or a list) or an @clients subject; a gty of any other shape is ignored rather than rejecting the token. Claims are read only by their registered JWT names, so a grant_types or subject claim cannot stand in for gty or sub. Without RBAC, Auth0 copies requested API scopes into a user token's scope, so a user token's scope is ignored. Public routes are unchanged.
  • SIMULATION_ENTRYPOINT_ENFORCE_PERMISSIONS, strictly 0 or 1, defaulting to 0:
    • 0 (log-only): emits simulation_entry_permission_missing with the permission and azp client ID, never tokens, and lets the request through.
    • 1: answers 403.
    • Disabled auth, which is non-production only, is always allowed.
  • Deploy wiring: the deploy script and reusable workflow pass the flag from the GitHub variable SIMULATION_ENTRYPOINT_ENFORCE_PERMISSIONS; unset means 0. The deployed auth tests receive it too.
  • Deployed check: a new authenticated test runs only when enforcement is 1. It confirms the deployed test client, which must not hold stage12:operate, gets 403 from a Stage 12 route.
  • Docs: the README and DEPLOYMENT docs cover the permission table, the modes, the Auth0 prerequisites, and rollout and rollback. Beta never promotes its candidate, so missing API v1 grants can only be seen in prod's log-only events; the rollout steps say so.

Before setting enforcement to 1: on the shared entrypoint API in Auth0, enable RBAC and "Add Permissions in the Access Token". Then grant:

  • simulation:submit and simulation:read to the API v1 client and to the deployed test client (but not stage12:operate to the test client);
  • stage12:operate to the Stage 12 operator client.

Wait one access-token lifetime with no log-only events before flipping, because cached tokens don't carry new grants. The API v1 client uses a single GATEWAY_AUTH_AUDIENCE for both the old gateway and the entrypoint.

Testing

  • uv run pytest tests/ (entry project): 233 passed. Before this change there were 158. The new tests cover:
    • the per-route permission matrix;
    • permissions vs scope claims, including near misses, and scope being ignored on user tokens, including signed user tokens with string, list and malformed gty, and tokens whose grant_types or subject claims try to stand in for gty or sub;
    • log-only vs enforced;
    • disabled auth;
    • public routes staying public;
    • telemetry failure not changing the response.
  • ruff format --check src tests authenticated_tests is clean.
  • ruff check src was already failing on main with 4 findings and now has 11. The 7 new ones are all B008, the same Depends(...)-as-default pattern as the existing Depends(_bearer).
  • Pyright shows only the 2 errors already on main.
  • Deployed tests were not run.

Related:

🤖 Generated with Claude Code

anth-volk and others added 3 commits October 12, 2026 23:27
Protected handlers now receive the validated CallerIdentity through a
require_permission dependency instead of discarding it. Submission routes
require simulation:submit, polling routes require simulation:read, and the
temporary Stage 12 routes require stage12:operate. A permission may come from
the permissions claim or the space-delimited scope claim.

SIMULATION_ENTRYPOINT_ENFORCE_PERMISSIONS accepts exactly 0 or 1 and defaults
to 0. In log-only mode a missing permission emits
simulation_entry_permission_missing with the permission and the token's azp,
then allows the request. With 1 the request is rejected with 403. Disabled
authentication has no identity to check and stays allowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deployment workflow passes each environment's
SIMULATION_ENTRYPOINT_ENFORCE_PERMISSIONS variable to the entrypoint. An unset
or empty value deploys 0, and any value other than 0 or 1 fails before Cloud
Run changes.

Every authenticated deployment test run receives the same value. When it is
1, a new check confirms that the deployed test client's valid token is
rejected on a stage12:operate route; otherwise the check skips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the route permissions, the log-only and enforced modes, the Auth0
RBAC settings and client grants required before enforcement, and the rollout
and rollback steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 4 commits October 13, 2026 15:45
A route permission now comes from the RBAC permissions claim for every
token, but from the space-delimited scope claim only when the token was
issued to a machine client (gty is client-credentials or the subject ends in
@Clients). Without RBAC on the Auth0 API, a user token's scope carries any
API scope the user's application requested, so it is not proof of a grant.
This matters once a public sign-in application exists in the same tenant.

CallerIdentity now parses gty, and the verified-token tests use a realistic
client-credentials token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CallerIdentity typed gty as a string, so a token whose gty was a list
failed validation and was rejected with 403 even in log-only mode, where
main had ignored the claim. It is now read as a tuple of grant types: a
string or a list of strings, with any other shape ignored. The claim only
decides whether scope counts, so ignoring a malformed value can only narrow
what a token grants.

New tests send signed tokens through JWTDecoder: user tokens with scope but
no permissions are rejected under enforcement whatever their grant type,
list-valued and malformed gty claims still authenticate, and a listed
client-credentials grant still counts scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Beta never promotes its candidate revision, so a beta enforcement value
applies only to the candidate that the deployment tests call, and log-only
events from beta cannot reveal a missing API v1 grant. The rollout steps now
watch prod for those events, note that no deployment test sends API v1's own
token, and name the production-only workflow that can redeploy the
entrypoint for a faster rollback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CallerIdentity allowed population by field name, so a verified token
with no gty but a top-level grant_types claim counted as a
client-credentials token and had its scope trusted, and a subject claim
could stand in for sub (an @Clients subject marks a machine client).
Nothing builds CallerIdentity by field name, so drop populate_by_name:
sub, iss, aud, exp, iat, azp and gty are now read only by those claim
names.

Tests sign tokens that carry grant_types (with a user sub) or subject in
place of the registered claims, and check that neither grants scope
permissions, while a registered @Clients sub still does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stage 12A: Check Auth0 permissions on Simulation Entrypoint routes

1 participant