Repository navigation
Stage 12A: Sign-in identity and v1-anchored user associations - #3891
Merged
Merged
Conversation
Add the user_identities table and reshape the four user association tables so each row can reference a v1 artifact, a v2 record, or both, with a per-user client_ref for idempotent imports. Simulation and report associations lose their cross-service foreign keys to v2 simulations and reports. One autogenerated v2 revision (fbb7488ebe8a) covers both parts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port the Simulation Entrypoint JWTDecoder (PyJWT PyJWKClient, RS256, iss/aud) into libs/user_auth.py with issuer normalization and client principal classification, plus an optional Auth0 Management API client for account deletion. Validate PUBLIC_API_AUTH_* and AUTH0_MANAGEMENT_* settings in the readiness gate, map authentication failures to the v2 error envelope with WWW-Authenticate, and expose that header via CORS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve (and on first use provision) the caller's v2 user from the bearer token through user_identities, with savepoint race recovery on UNIQUE(issuer, subject). Remove user_id from association requests and queries, scope every read and write to the owner (404 otherwise), and accept legacy v1 references checked through a batched, read-only v1 connector (422 when missing). Repointing re-resolves or clears the v2 reference, and rows owned by the Stage 10 mirror return 409. Add /v2/user-simulations, /v2/user-reports (with the v3 report fields), and /v2/me (GET, PATCH, DELETE, export, idempotent imports). The Stage 10 projection now records legacy_policy_id. require_user is a fail-closed NativeRouteDependencies field attached at router level. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add user-simulations, user-reports, and me path segments to the migration registry; give ContractRequest a required-headers field; record authenticated variants of the user-policy and user-household contracts and new simulation, report, and account contracts; guard that user-owned routes always require Authorization; and regenerate the contract artifacts. Add disposable-Postgres tests for concurrent first sign-in, concurrent idempotent imports, MATCH SIMPLE keys, and account deletion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Report the four v2 user-association row counts through SQLAlchemy in a read-only, rolled-back transaction, and stop when the Stage 12A revision is pending while user_simulation_associations or user_report_associations hold rows. The seed workflow runs it before the schema upgrade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…utes Pass PUBLIC_API_AUTH_* and the optional AUTH0_MANAGEMENT_* settings through the staging and production deploy jobs and the Cloud Run candidate script, validate them before deploying, and keep them out of the release test environment. Rewrite the live staging probe to sign in a dedicated staging Auth0 test user and exercise /v2/me, all four association resources, imports, export, and account deletion without direct database access. Both candidate smoke suites now assert that anonymous user-route requests return 401. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the Stage 12A migration guide (routes, provisioning, schema revision, settings, GitHub variables and secrets, Auth0 prerequisites, rollout, verification), cross-reference it from the Stage 10 and 11 guides, add Stage 12A test commands to the testing skill, record a changelog fragment, and add a doc-only, preview-first Cloud Armor proposal for signed-in user writes and imports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt test Fold the new auth unsets into the existing release-test command line so the workflow keeps its three-line inline limit, and move the Stage 11 household reassignment integration test to the signed-in contract, repointing through a mapped legacy household. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 12, 2026
Merged
… rows The Stage 12A revision dropped country and label from the dormant simulation and report association tables, but the previous Public API revision still maps them: deleting a user through its models raised UndefinedColumn during the deploy window or after an application rollback. Keep both columns as retired, nullable, unused fields and regenerate fbb7488ebe8a (same revision ID) so the upgrade drops nothing. Verified against disposable Postgres: master's delete_transition_user now deletes a user who owns Stage 12A rows, and fails with the previous generated revision. A lifecycle test now requires every parent-revision column to survive and no nullable column to become required. The Stage 12A doc records the exact rollback rules and the follow-up revision that drops the retired columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An import whose policy client_ref already existed fell back to a lookup that ignored country, and the report import let that in-request map override its country-filtered lookup, so a US report could link to the same user's UK saved policy. Imports now resolve existing client_refs only in the import country, skip a client_ref the user holds in another country with the new client_ref_conflict reason, and resolve report policy links with one country-filtered lookup that also sees policies inserted earlier in the same transaction. The four association services now share the insert-or-reuse import, the client_ref reads, the single-create clash check, and the v1 reference gate, replacing four copies of each and their per-resource import integrity errors. The dead, owner-unscoped read_complete_user_policy, read_user_policy_row, read_user_household_row, and household read_user helpers are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only POST /v2/me/imports had a body limit, so any signed-in user could persist multi-megabyte rows through single creates and patches. Every /v2/me* and /v2/user-* write now rejects a body over 256 KiB with 413 (imports keep 1 MiB) through one shared guard that runs after authentication. Caller inputs share field bounds on both paths: description and source_note at 10,000 characters, and the report validation snapshot at 500 calibration matches, 200 scorecard programs, and bounded strings. Persistence models stay unbounded so the Stage 10 mirror projection is unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UserReportItem published provisions and validation, and UserPolicyItem published provenance, as untyped JSON, so the OpenAPI lost the shared contract's shapes. Responses now use the same ReportProvision, ReportValidation, and PolicyProvenance models that validate requests. Nullable contract keys a client omitted come back as null, and the optional interval parameter is omitted rather than null, as the contract specifies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Request logging only treated policies, households, and their user routes as native Supabase resources, so /v2/user-simulations, /v2/user-reports, and /v2/me* were logged with the configured v1 database sources of the simulation, report, and user route groups. One segment set now covers every native v2 Supabase resource, with reads and writes labeled by method. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the Auth0 client - Every user-route service factory built the v1 Cloud SQL session factory up front, so a v1 misconfiguration failed reads, lists, and /v2/me too. The default checker now builds the v1 reference service only when a request actually checks a v1 reference. - user_resource_operation turned unexpected exceptions into 500s without any log. It now logs the error type and the innermost frame locations, never the message, which can carry SQL parameters or token claims. - Each account deletion built a new Auth0 Management client and fetched a new token. Deleters now share one client per settings, so the token is reused until shortly before it expires. - The user-route log fields were passed as plain logging extras, which the structured log sink drops; they now go through policyengine_attributes so the user UUID and error type reach the logs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The staging live test deletes its own account through DELETE /v2/me, so Management API settings in staging would also delete the CI user from Auth0, and only documentation prevented it. The staging deploy job no longer receives AUTH0_MANAGEMENT_*, and the deploy validation rejects them for any environment other than production. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The no-token test listed 11 of the 25 /v2/me* and /v2/user-* operations by hand. It now enumerates every operation from the built application's OpenAPI, so new user routes are covered automatically. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alembic tests run logging.config.fileConfig, which disables loggers that already exist, so the new failure-logging test passed alone but failed in the full suite. It now re-enables and records on the module logger directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python's JSON parser and Pydantic's float and JsonValue types accept NaN and Infinity, but Postgres JSONB rejects them, so one such value in a report became a 503 and failed a whole import, including its valid items. Every user-route input base now sets allow_inf_nan=False, and report provision values use a finite-only recursive JSON value type. A single write containing one returns 422 and an import item containing one is skipped as invalid. Tests cover the types, the route, SQLite imports, and Postgres JSONB storage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… shapes user_resource_operation mapped every SQLAlchemyError to 503 without a log line, hiding schema drift such as the original UndefinedColumn and rejected data. It now logs the error type and frame locations, never the message, as it does for unexpected 500s. Responses reused the strict input models for report provisions and validation and for policy provenance, so any later tightening of the input bounds would make existing rows fail to serialize and break a whole list or /v2/me/export. Responses now use read shapes that mirror the contract without the input bounds and ignore unknown keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cted token - DELETE /v2/me now reads the Management client secret and obtains a token before deleting any PolicyEngine data. A configuration, Secret Manager, or credential failure returns 503 with nothing deleted, instead of a 502 after the data is gone. - A 401 from Auth0 drops the cached token and retries once with a fresh one, so a revoked or rotated token no longer fails every deletion for up to its 24-hour lifetime. - The Management settings are read only for account deletion, not for every /v2/me request. - The Stage 12A doc lists the runtime service account's Secret Manager grant as a prerequisite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
#3891's previous-revision test picks a value for each required column through _test_value, whose type checks exceeded the complexity and return limits (C901, PLR0911). The type checks move to TYPED_TEST_VALUES, an ordered table that keeps Enum ahead of its String base, so every column gets the same value as before; the lifecycle tests pass against a disposable PostgreSQL 17 database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
#3891's live signed-in lifecycle and signed-out upgrade tests exceeded the statement limit (PLR0915). Each saved resource's round trip runs in _assert_saved_resource_lifecycle, which takes the resource, its v1 field and id as one _SavedResource; the linked reform, report and export checks run in _assert_export_links_the_saved_reform; and the signed-out saving checks run in _assert_signed_out_saving. The requests, their bodies and order, and the assertions are unchanged. (The stack's split of the earlier live policy lifecycle test no longer applies: #3891 replaced that test.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
#3891's test that signed-in user resources log Supabase sources took five parametrized arguments besides monkeypatch (PLR0913). Each case is now a _UserResourceRequest, and the test IDs are rebuilt to match the ones pytest generated for the five separate parameters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The /v2/me, user-report, user-simulation, user-household and user-policy routes that #3891 added or reshaped took 44 dependencies as parameter defaults. Their query dependencies become module-level Annotated aliases, as the other v2 routes do, and the request-size check and the upgrade route's unauthenticated-user header become Annotated parameters. Each handler's principal comes from require_user, the authenticator that NativeRouteDependencies injects per router. FastAPI resolves string annotations against module globals only: with `from __future__ import annotations`, Annotated[AccountPrincipal, Depends(require_user)] would silently become a required "principal" query parameter. These five modules therefore no longer use the future import, so their annotations are evaluated where the handlers are defined, and import the route dependency types and their default service factory at run time. Moving the default factory import out of _service_factory also clears its PLC0415 finding in the three new modules, as the other v2 resource routers already do; the dependencies module is already loaded wherever these routers are built. The OpenAPI documents, with the injected test authenticator and with the default one, are byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
Of the 18 PLC0415 findings left in the code #3891 added, the route dependencies' account-request authenticator import moves to the top level. Its module is already loaded wherever the dependencies module is, and it creates no cycle; the ASGI and Flask entry points import exactly the same modules as before. The other deferred imports keep an inline reason, worded as for the existing ones: the v1 session factory is looked up per call so tests can replace it; the v1 reference checks, v2 persistence, the user-account service and Auth0 Management load only when a default factory runs or a v1 reference is checked; readiness validation loads the user-auth and Auth0 settings only when it runs; and the Google Secret Manager client loads only when Auth0 Management reads a secret. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The user-service proof fixture's docstring puts the rule's verdict on its own line, as the validator fixtures do. The import side-effect test's subprocess script asserts the association qualification module is unloaded across three lines, and the identity persistence test's qualification test names the qualification module once, as qualification, so its pending checker fits on one line. The checks are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- FBT001: require_not_mirror_owned and require_owned_user_policy take their boolean flag by keyword, and their three callers pass it so. - B008: the default account authenticator declares its bearer and unauthenticated-user security dependencies with Annotated instead of as parameter defaults. - ARG001: DELETE /v2/me needs its country query only for validation, so the parameter is named _query, like the _size request-size check. The OpenAPI documents, with the injected test authenticator and with the default one, are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- PT018: split the 12 composite assertions, so a failure names the part that failed. - SIM117: the three pytest.raises blocks that open a transaction enter both contexts in one with statement, in the same order. - PT011, PT007: the update-input validation tests match the message each ValueError really carries, and the user-service guard's parametrize values are a list. - ARG: the qualification test and the signed-out header test request the sessions and configured_auth fixtures through pytest.mark.usefixtures; stand-in lambdas and result callbacks that are only called positionally prefix their unused parameters with an underscore, as does the Alembic test's unused loop name; and the deferred reference service test's stand-in keeps its keyword parameters with a reason. - FBT001: the signed-out header test takes auth_configured by keyword, which pytest already does. - SIM118, RUF005: drop a .keys() membership check and unpack a list instead of concatenating it. The collected test IDs are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The Management API client compares the token response with HTTPStatus.OK and retries a deletion after HTTPStatus.UNAUTHORIZED, as the gateway token provider does. HTTPStatus is an IntEnum, so the comparisons are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
user_resource_operation maps each typed service failure #3891 added to a status, message and headers, and logs persistence and unexpected failures by event name only. The new characterization test pins all eighteen cases, including subclasses that must match their base's clause and an IntegrityError that must win over the SQLAlchemyError clause, and passes on the current code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The user-report and user-simulation create routes call a module helper that checks the body and query countries agree and runs the creation, as the user-household and user-policy routes do. The /v2/me router builds its signed-in-only dependency through _signed_in_user_dependency and runs the upgrade through _upgrade_account. The three builders #3891 added stay within the complexity limit (C901); route registration, order, signatures and the OpenAPI documents are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
#3891 added include_compute to get_migration_context, giving it six arguments (PLR0913). Its db_entity and sim_flow overrides were never passed: every caller, including the request logging and the tests, lets the route group's configuration name them. The overrides go, so the context takes the entity and flow from the route group and, with include_compute=False, records no flow; every logged context is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
…value each The PLR0913 findings in the code #3891 added: - The user-household and user-policy services, their route Protocols and routes take an optional UserHouseholdFilter or UserPolicyFilter (the v2 and v1 ids to narrow a listing to) instead of two keyword arguments, as the metadata layers take a ParameterValueFilter. The routes build the filter from the same query fields. - read_user_report_rows and read_user_simulation_rows take a UserReportListing or UserSimulationListing, as the household and policy row readers take their listings. - create_user_identity takes the UserPrincipal whose issuer, subject and email it stores, instead of the three fields. The statements, their parameters and the OpenAPI documents are unchanged. The route and service tests, and the concurrent sign-in test's stand-in, use the new arguments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
#3891's user-route credential check took the contract workflow guard's _check_workflows past the complexity limit (C901). Each request's key now comes from _request_key and its problems from _request_violations, which runs the same checks in the same order, so the guard reports the same violations in the same order; the pinned workflow-guard test and the credential tests pass unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
#3891's previous-revision test picks a value for each required column through _test_value, whose type checks exceeded the complexity and return limits (C901, PLR0911). The type checks move to TYPED_TEST_VALUES, an ordered table that keeps Enum ahead of its String base, so every column gets the same value as before; the lifecycle tests pass against a disposable PostgreSQL 17 database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
#3891's live signed-in lifecycle and signed-out upgrade tests exceeded the statement limit (PLR0915). Each saved resource's round trip runs in _assert_saved_resource_lifecycle, which takes the resource, its v1 field and id as one _SavedResource; the linked reform, report and export checks run in _assert_export_links_the_saved_reform; and the signed-out saving checks run in _assert_signed_out_saving. The requests, their bodies and order, and the assertions are unchanged. (The stack's split of the earlier live policy lifecycle test no longer applies: #3891 replaced that test.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
#3891's test that signed-in user resources log Supabase sources took five parametrized arguments besides monkeypatch (PLR0913). Each case is now a _UserResourceRequest, and the test IDs are rebuilt to match the ones pytest generated for the five separate parameters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The /v2/me, user-report, user-simulation, user-household and user-policy routes that #3891 added or reshaped took 44 dependencies as parameter defaults. Their query dependencies become module-level Annotated aliases, as the other v2 routes do, and the request-size check and the upgrade route's unauthenticated-user header become Annotated parameters. Each handler's principal comes from require_user, the authenticator that NativeRouteDependencies injects per router. FastAPI resolves string annotations against module globals only: with `from __future__ import annotations`, Annotated[AccountPrincipal, Depends(require_user)] would silently become a required "principal" query parameter. These five modules therefore no longer use the future import, so their annotations are evaluated where the handlers are defined, and import the route dependency types and their default service factory at run time. Moving the default factory import out of _service_factory also clears its PLC0415 finding in the three new modules, as the other v2 resource routers already do; the dependencies module is already loaded wherever these routers are built. The OpenAPI documents, with the injected test authenticator and with the default one, are byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
Of the 18 PLC0415 findings left in the code #3891 added, the route dependencies' account-request authenticator import moves to the top level. Its module is already loaded wherever the dependencies module is, and it creates no cycle; the ASGI and Flask entry points import exactly the same modules as before. The other deferred imports keep an inline reason, worded as for the existing ones: the v1 session factory is looked up per call so tests can replace it; the v1 reference checks, v2 persistence, the user-account service and Auth0 Management load only when a default factory runs or a v1 reference is checked; readiness validation loads the user-auth and Auth0 settings only when it runs; and the Google Secret Manager client loads only when Auth0 Management reads a secret. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The user-service proof fixture's docstring puts the rule's verdict on its own line, as the validator fixtures do. The import side-effect test's subprocess script asserts the association qualification module is unloaded across three lines, and the identity persistence test's qualification test names the qualification module once, as qualification, so its pending checker fits on one line. The checks are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- FBT001: require_not_mirror_owned and require_owned_user_policy take their boolean flag by keyword, and their three callers pass it so. - B008: the default account authenticator declares its bearer and unauthenticated-user security dependencies with Annotated instead of as parameter defaults. - ARG001: DELETE /v2/me needs its country query only for validation, so the parameter is named _query, like the _size request-size check. The OpenAPI documents, with the injected test authenticator and with the default one, are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
- PT018: split the 12 composite assertions, so a failure names the part that failed. - SIM117: the three pytest.raises blocks that open a transaction enter both contexts in one with statement, in the same order. - PT011, PT007: the update-input validation tests match the message each ValueError really carries, and the user-service guard's parametrize values are a list. - ARG: the qualification test and the signed-out header test request the sessions and configured_auth fixtures through pytest.mark.usefixtures; stand-in lambdas and result callbacks that are only called positionally prefix their unused parameters with an underscore, as does the Alembic test's unused loop name; and the deferred reference service test's stand-in keeps its keyword parameters with a reason. - FBT001: the signed-out header test takes auth_configured by keyword, which pytest already does. - SIM118, RUF005: drop a .keys() membership check and unpack a list instead of concatenating it. The collected test IDs are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The Management API client compares the token response with HTTPStatus.OK and retries a deletion after HTTPStatus.UNAUTHORIZED, as the gateway token provider does. HTTPStatus is an IntEnum, so the comparisons are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
user_resource_operation maps each typed service failure #3891 added to a status, message and headers, and logs persistence and unexpected failures by event name only. The new characterization test pins all eighteen cases, including subclasses that must match their base's clause and an IntegrityError that must win over the SQLAlchemyError clause, and passes on the current code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The user-report and user-simulation create routes call a module helper that checks the body and query countries agree and runs the creation, as the user-household and user-policy routes do. The /v2/me router builds its signed-in-only dependency through _signed_in_user_dependency and runs the upgrade through _upgrade_account. The three builders #3891 added stay within the complexity limit (C901); route registration, order, signatures and the OpenAPI documents are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
#3891 added include_compute to get_migration_context, giving it six arguments (PLR0913). Its db_entity and sim_flow overrides were never passed: every caller, including the request logging and the tests, lets the route group's configuration name them. The overrides go, so the context takes the entity and flow from the route group and, with include_compute=False, records no flow; every logged context is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
…value each The PLR0913 findings in the code #3891 added: - The user-household and user-policy services, their route Protocols and routes take an optional UserHouseholdFilter or UserPolicyFilter (the v2 and v1 ids to narrow a listing to) instead of two keyword arguments, as the metadata layers take a ParameterValueFilter. The routes build the filter from the same query fields. - read_user_report_rows and read_user_simulation_rows take a UserReportListing or UserSimulationListing, as the household and policy row readers take their listings. - create_user_identity takes the UserPrincipal whose issuer, subject and email it stores, instead of the three fields. The statements, their parameters and the OpenAPI documents are unchanged. The route and service tests, and the concurrent sign-in test's stand-in, use the new arguments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The Management API client compares the token response with HTTPStatus.OK and retries a deletion after HTTPStatus.UNAUTHORIZED, as the gateway token provider does. HTTPStatus is an IntEnum, so the comparisons are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
user_resource_operation maps each typed service failure #3891 added to a status, message and headers, and logs persistence and unexpected failures by event name only. The new characterization test pins all eighteen cases, including subclasses that must match their base's clause and an IntegrityError that must win over the SQLAlchemyError clause, and passes on the current code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The user-report and user-simulation create routes call a module helper that checks the body and query countries agree and runs the creation, as the user-household and user-policy routes do. The /v2/me router builds its signed-in-only dependency through _signed_in_user_dependency and runs the upgrade through _upgrade_account. The three builders #3891 added stay within the complexity limit (C901); route registration, order, signatures and the OpenAPI documents are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3892
Summary
Stage 12A of the unified API migration, Public API side. It adds user identity and server-side user associations anchored to v1 artifacts, so app v3 saves every user's work through the Public API: signed in with Auth0 social sign-on, or signed out as the browser's unauthenticated user.
users.user_typeisstandard(signed in with Auth0) orunauthenticated(a browser's signed-out user, whose ID is a random UUID the browser made and every other user column null).require_user, before any 422):Authorization: Bearer …: Auth0 RS256 validation (libs/user_auth.py); 403 for machine clients, 503 when auth is unconfigured. A bad token is a 401 and never falls back to the header.PolicyEngine-Unauthenticated-User: <uuid>(lowercase, hyphenated): an unauthenticated principal. No Auth0 settings are needed. A UUID that names a standard user is a 401, so a signed-out browser can never act on an account.WWW-Authenticate: Bearer.StandardAccountProvisioning(theuser_identities(issuer, subject)→users.idmapping, race-safe, provisioning on any request) andUnauthenticatedAccountProvisioning(ausersrow with the browser's UUID, created only by a write: reads by a browser that has saved nothing return empty lists or 404 and create nothing), chosen by principal type.POST /v2/me/upgrade?country_id=takes both the bearer and the header. A first sign-in converts the unauthenticated user in place (adds the identity, setsstandardand the country), so every saved row stays; an existing account merges its rows in and the unauthenticated user is deleted; otherwise nothing happens (none). It never touches a standard user named in the header, and is safe under concurrent requests. Returns{item: MeItem, outcome}./v2/me:GET(MeItemnow hasuser_type, andprimary_countrymay be null),PATCH(403 for unauthenticated users),GET /v2/me/export(UK GDPR;useris null for a browser that has saved nothing) andDELETE /v2/me(an unauthenticated user's data is deleted without any Auth0 call). There is no import route: signed-out work is already in the database./v2/me*and/v2/user-*write rejects a body over 256 KiB with 413, checked after authentication (FastAPI has already parsed the body by then). NaN and Infinity are rejected with 422.descriptionandsource_noteare capped at 10,000 characters, and the report validation snapshot has list and string bounds./v2/user-policiesand/v2/user-householdsare locked down. The owner comes from the token or the unauthenticated header,user_idis removed from bodies and queries, and another user's row returns 404./v2/user-simulationsand/v2/user-reports.legacy_*_id), each checked to exist in Cloud SQL through the v1 SQLAlchemy models. A missing one returns 422.GET /v2/shared-reports/{id}?country_id=reads any user's saved report with no credential, outside the authenticated router, and never provisions a user (404 when no report has that ID in that country).user_id,user_policy_idandbaseline_user_policy_id, and names the reform the report was compared with instead (baseline_reform: {name}, null for current law or once that reform is deleted).fbb7488ebe8a(parent91af4fc8d8c1):user_identities;usersgainsuser_type(defaultstandard, with a CHECK) andprimary_countrybecomes nullable, required for standard users byck_users_standard_primary_country;user_simulation_associationsanduser_report_associationsare reshaped, dropping their FKs tosimulations/reports. Their oldcountryandlabelcolumns are kept, nullable and unused, so the previous API revision can still load rows; a later revision drops them;user_policy_id(the saved reform the report was run from),baseline_user_policy_id(the saved reform it was compared with, when not current law; both are the same owner's rows, ON DELETE SET NULL, and the baseline's name is that reform's own, so no label is stored),source_note,provisions(each may carry optionalgrowth,growth_start,growth_base,growth_round_toandgrowth_roundsettings),validation, andbudget_window_years(the years of the budget window run with the report, from its year; NULL for none, otherwise 2 to the economy route's 75-year limit, checked byck_user_report_associations_budget_window_years).policyengine_api.data.v2.user_association_migration_qualification. It is wired intoseed-v2-database.ymlbefore the upgrade and fails if the dormant tables hold rows.master): the sign-in and account-deletion libraries join the v2 mypy rule, and the user-service SQL guard is registered asapi.v2.user-services-no-sqlwith proof fixtures.master): this branch's code passes ruff groups 1 and 2, and its 19 model bases are listed inruntime-evaluated-base-classes, so their subclasses keep the imports Pydantic needs at runtime. The staging release-test step now runs.github/scripts/run_release_tests.sh, which also unsets thePUBLIC_API_AUTH_*settings, so the staging job'smake testnever sees them.populace-us-2024-spm-20260915, UKpolicyengine-uk-data-1.58.1(was 1.58.0). UK 2.130.1 adds variables and parameters, so the installed-catalog test expects the new counts.docs/migration/armor/proposed/, which also bounds how fast anyone can create unauthenticated users. An operator applies it.user_type./v2/shared-reports. The legacy builder stays browser-only./v2/user-householdsand/v2/user-simulationshave no app consumer yet.docs/migration/stage-12a-user-login.md.Remaining: none before merge. The Auth0 custom domain
auth.policyengine.orgis verified and serves issuerhttps://auth.policyengine.org/, which matches staging'sPUBLIC_API_AUTH_ISSUER.Done (2026-10-15):
staging:PUBLIC_API_AUTH_REQUIRED=1,PUBLIC_API_AUTH_ISSUER=https://auth.policyengine.org/(final: identities are keyed by(issuer, subject), so it must not change after the first sign-in),PUBLIC_API_AUTH_AUDIENCE=https://api.policyengine.org/.production:PUBLIC_API_AUTH_REQUIRED=0,PUBLIC_API_AUTH_ISSUER=https://auth.policyengine.org/andPUBLIC_API_AUTH_AUDIENCE=https://api.policyengine.org/(set 15 October;masterdoesn't read them, so they take effect with this PR's first production deploy, which runs only after the staging candidate passes the live Auth0 test). Signed-out saving works without Auth0 settings.https://api.policyengine.org/, RS256), with offline access now on. A staging-only CI application ("PolicyEngine API staging CI", password-realm grant) and its test user live in a dedicatedpolicyengine-staging-cidatabase connection with signup disabled, enabled only for that application. A password grant through the tenant domain returned a token for this audience.stagingenvironment. VariablesSTAGING_AUTH0_TEST_DOMAIN=auth.policyengine.org,STAGING_AUTH0_TEST_AUDIENCE=https://api.policyengine.org/andSTAGING_AUTH0_TEST_REALM=policyengine-staging-ci; secretsSTAGING_AUTH0_TEST_CLIENT_ID,STAGING_AUTH0_TEST_CLIENT_SECRET,STAGING_AUTH0_TEST_USERNAMEandSTAGING_AUTH0_TEST_PASSWORD.delete:userson the Management API. Its secret is version 1 ofpolicyengine-api-auth0-management-client-secretin Secret Manager, readable by the production runtime accountpolicyengine-api-cr-runtimeonly.productionhasAUTH0_MANAGEMENT_DOMAIN=policyengine.uk.auth0.com,AUTH0_MANAGEMENT_CLIENT_IDand the secretAUTH0_MANAGEMENT_CLIENT_SECRET_RESOURCE.DELETE /v2/mereads the secret and gets the Management token before deleting anything, so a credential failure returns 503 with nothing deleted. A 401 from Auth0 refreshes the token and retries once.kvvwlyzqrfpvrxinmlwt):user_simulation_associationsanduser_report_associationsempty,user_policies3,user_household_associations0;outcome: ok.kvrifaviwhzjztcbrfpy): all four tables empty;outcome: ok.Behaviour changes
/v2/user-policiesand/v2/user-householdspreviously accepted a caller-supplieduser_idwith no ownership check. Nothing in production calls them; app-v2 never did.PATCH /v2/user-householdsno longer acceptshousehold_id. Repointing goes throughlegacy_household_id.legacy_user_policy_mappingsentry) return 409 on PATCH and DELETE./v2/shared-reports): their name, source note, provisions, budget window, validation pin and compared reform's name, never their owner.DELETE /v2/mereturns 502 if the data was deleted but the Auth0 deletion then failed. A retry is safe.MeItem.emailstays null unless an Auth0 Action addsemail/email_verifiedto access tokens.Rollback
Roll forward whenever possible. The schema still serves the previous revision, but that revision serves
/v2/user-policiesand/v2/user-householdswithout authentication. Rolling the application back would let anyone list, read, rename, delete or create users' saved items.Before any user-owned record exists, an application rollback is safe and needs no database downgrade. Signed-out saving needs no configuration, so treat user-owned records as existing from the moment this revision serves traffic in an environment.
After that, apply the rollback guard first:
docs/migration/armor/proposed/20261013-pol-api-lb-stage-12a-rollback-guard.yaml, a Cloud Armor rule that denies/v2/me*and/v2/user-*with 403.internal-and-cloud-load-balancing, because*.run.appURLs bypass Cloud Armor.Saved work is unavailable while the guard holds. Signed-out flows and v1 routes are unaffected.
No other route in the previous revision exposes Stage 12A data.
A database downgrade is lossy. It fails while legacy-only rows or any simulation or report associations exist.
Testing
masterate76065e8(ruff groups 1 and 2, and thepush.ymlsteps moved into scripts) and updating the bundle to 6.4.1: themake testsuite (tests/to_refactor,tests/unit,tests/contractand the budget-window dedupe test) has 2769 passed, both before and after the bundle update.ruff format --check,ruff checkwithmaster's rule families,mypy(167 files), the quality guards (migration contracts, rules registry, CI scripts) and the rules tool's tests (74) pass. The migration contract docs are unchanged by the exporter.alembic -c alembic-v2.ini checkis clean at headfbb7488ebe8a.budget_window_years: the v2 Alembic lifecycle tests, thentest_v2_user_identity_persistence.pyat head (21 passed together, including a test that the CHECK rejects windows of 1 and 76 years). The persistence tests cover concurrent first signed-out writes making one user, concurrent upgrades converting once, two browsers upgrading at once (one converts, one merges), a merge moving all four association kinds with links intact, signed-out writes racing a conversion, and a standard user named in the header never being locked. The policy and household persistence tests passed earlier (11).test_v2_user_identity_persistence.py: 8 concurrent first sign-ins create one user, account deletion cascades, and qualification is read-only;/v2/me*and/v2/user-*operations, read from the built app's OpenAPI./v2/shared-reports:tests/unit/v2,tests/contract, and the migration artifact, flag, logging and ASGI tests have 971 passed. New tests cover the credential-free read with the private fields left out, the owner read still needing a credential, 404, query and ID validation, its route group, its OpenAPI operation (no security), and the service reading another user's report with its compared reform's name (null once that reform is deleted).mypy,ruffand the quality guards pass; the migration contract docs are regenerated.alembic -c alembic-v2.ini checkis clean; a fresh upgrade, then downgrade -1 and re-upgrade, all succeed. The revision was regenerated with autogenerate under the same ID.ruff format --check,ruff checkandmypyare clean.openspec amendments (local-only specs, not in this PR)
v2-user-policy-associations:user_idis removed;legacy_policy_id, a nullablepolicy_idwith a CHECK,baseline,provenance;legacy_policy_id.v2-user-household-associations: legacy references, owner scoping, andhousehold_idis no longer patchable.user_identitiesprovisioning;/v2/mewith export and deletion order;Authorizationor the unauthenticated header on user-owned routes.Related
🤖 Generated with Claude Code