Skip to content

Stage 12A: Sign-in identity and v1-anchored user associations - #3891

Draft
anth-volk wants to merge 10 commits into
masterfrom
feat/stage-12a-user-login-associations
Draft

anth-volk wants to merge 10 commits into
masterfrom
feat/stage-12a-user-login-associations

Conversation

@anth-volk

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

Copy link
Copy Markdown
Collaborator

Fixes #3892

Summary

Stage 12A of the unified API migration, Public API side. It adds verified user identity and server-side user associations anchored to v1 artifacts, so app v3 can offer social sign-on and save a user's work to their account.

  • Sign-in identity:
    • Auth0 RS256 bearer validation (libs/user_auth.py), with a router-level require_user that fails closed: 401 without a token, 403 for machine clients, 503 when auth is unconfigured.
    • New user_identities table, which maps an Auth0 (issuer, subject) to users.id.
    • Just-in-time provisioning on the first authenticated request. It is race-safe: concurrent first requests create exactly one user.
  • /v2/me: GET / PATCH, GET /v2/me/export (UK GDPR), DELETE /v2/me, and idempotent POST /v2/me/imports for the user's existing localStorage items (≤500 items, 1 MiB).
  • Associations:
    • /v2/user-policies and /v2/user-households are locked down. The owner comes from the token, user_id is removed from bodies and queries, and another user's row returns 404.
    • New /v2/user-simulations and /v2/user-reports.
    • All four accept v1 references (legacy_*_id), each checked to exist in Cloud SQL through the v1 SQLAlchemy models. A missing one returns 422.
  • Schema: one autogenerated v2 revision, fbb7488ebe8a (parent 91af4fc8d8c1):
    • creates user_identities;
    • every association gets nullable v1 and v2 references with a CHECK, plus client_ref with UNIQUE(user_id, client_ref);
    • the dormant user_simulation_associations and user_report_associations are reshaped, dropping their FKs to simulations / reports;
    • v3 report fields are added.
  • Persistence: every SQL access goes through SQLAlchemy or SQLModel, and every schema change through Alembic autogeneration. There are no v1 schema changes.
  • Qualification: a read-only SQLAlchemy command, policyengine_api.data.v2.user_association_migration_qualification. It is wired into seed-v2-database.yml before the upgrade and fails if the dormant tables hold rows.
  • Cloud Armor: proposed rate limits for user writes and imports, in preview mode under docs/migration/armor/proposed/. An operator applies them.
  • Docs: docs/migration/stage-12a-user-login.md.

⚠️ Merge prerequisites (merging before these blocks deploys)

  1. GitHub variables (staging and production):
    • PUBLIC_API_AUTH_REQUIRED, set to 0 or 1. Deploy validation fails if it is unset.
    • PUBLIC_API_AUTH_ISSUER and PUBLIC_API_AUTH_AUDIENCE, required when PUBLIC_API_AUTH_REQUIRED=1.
  2. Auth0, staging:
    • the Public API resource server (RS256);
    • a staging CI application with the password grant, plus a dedicated test user.
  3. Live staging test, GitHub staging environment. Without these the test fails and staging deploys stay blocked.
    • Variables: STAGING_AUTH0_TEST_DOMAIN, STAGING_AUTH0_TEST_AUDIENCE, and optionally STAGING_AUTH0_TEST_REALM.
    • Secrets: STAGING_AUTH0_TEST_CLIENT_ID, STAGING_AUTH0_TEST_CLIENT_SECRET, STAGING_AUTH0_TEST_USERNAME, STAGING_AUTH0_TEST_PASSWORD.
  4. Optional, production only: AUTH0_MANAGEMENT_DOMAIN / AUTH0_MANAGEMENT_CLIENT_ID (variables) and AUTH0_MANAGEMENT_CLIENT_SECRET_RESOURCE (secret) let DELETE /v2/me also delete the Auth0 user.
    • Do not set these in staging. The live test deletes its own account, so the CI user would be deleted from Auth0.
  5. Qualification: run the read-only command against staging and production to confirm user_simulation_associations and user_report_associations are empty. The seed workflow also enforces this before upgrading.

Behaviour changes

  • The two existing user routes now require authentication. /v2/user-policies and /v2/user-households previously accepted a caller-supplied user_id with no ownership check. Nothing in production calls them; app-v2 never did.
  • PATCH /v2/user-households no longer accepts household_id. Repointing goes through legacy_household_id.
  • Rows owned by the Stage 10 mirror (those with a legacy_user_policy_mappings entry) return 409 on PATCH and DELETE.
  • Status codes:
    • Validation errors return 422, matching existing v2 behaviour.
    • A body that isn't valid JSON gets 422 before authentication.
    • DELETE /v2/me returns 502 if the data was deleted but the Auth0 deletion then failed. A retry is safe.
  • MeItem.email stays null unless an Auth0 Action adds email / email_verified to access tokens.

Testing

  • uv run pytest tests/unit tests/contract: 2512 passed.
  • Cross-database integration against disposable Postgres 17 and MySQL 8.4: 74 passed. This covers:
    • the v1 and v2 Alembic lifecycles, including a test that dormant rows make the upgrade fail;
    • the Stage 10/11 persistence and mirror tests;
    • the new test_v2_user_identity_persistence.py: 8 concurrent first sign-ins create one user, imports are idempotent under concurrency, account deletion cascades, and qualification is read-only.
  • alembic -c alembic-v2.ini check is clean; a fresh upgrade, then downgrade -1 and re-upgrade, all succeed.
  • ruff format --check, ruff check and mypy are clean.
  • The live staging test has not been run; it needs the Auth0 setup above.

openspec amendments (local-only specs, not in this PR)

  • v2-user-policy-associations:
    • the owner comes from the token, and user_id is removed;
    • 401 / 403 / 404 / 503 semantics;
    • legacy_policy_id, a nullable policy_id with a CHECK, baseline, provenance, client_ref;
    • repointing, and 409 on rows owned by the Stage 10 mirror;
    • the Stage 10 projection sets legacy_policy_id.
  • v2-user-household-associations: legacy references, owner scoping, and household_id is no longer patchable.
  • New capabilities:
    • user_identities provisioning;
    • v1-anchored simulation and report associations;
    • /v2/me with export, import and deletion order;
    • the contract guard that requires Authorization on user-owned routes.

Related

🤖 Generated with Claude Code

anth-volk and others added 10 commits October 12, 2026 23:27
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>
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.20056% with 231 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.58%. Comparing base (21ddf34) to head (5864f9d).

Files with missing lines Patch % Lines
policyengine_api/fastapi_routes/dependencies.py 30.90% 23 Missing and 15 partials ⚠️
policyengine_api/libs/auth0_management.py 80.18% 15 Missing and 6 partials ⚠️
...icyengine_api/services/v2/user_reports/services.py 74.64% 12 Missing and 6 partials ⚠️
...ngine_api/fastapi_routes/v2/user_reports/routes.py 75.43% 13 Missing and 1 partial ⚠️
policyengine_api/libs/user_auth.py 90.83% 6 Missing and 5 partials ⚠️
...ata/v2/user_association_migration_qualification.py 91.11% 5 Missing and 3 partials ⚠️
...ngine_api/services/v2/user_simulations/services.py 86.88% 7 Missing and 1 partial ⚠️
...cyengine_api/services/v2/user_accounts/services.py 91.56% 1 Missing and 6 partials ⚠️
...ces/v2/user_reports/database_connectors/updates.py 50.00% 7 Missing ⚠️
...ne_api/services/v2/user_reports/transformations.py 65.00% 7 Missing ⚠️
... and 31 more
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #3891       +/-   ##
===========================================
+ Coverage   45.22%   87.58%   +42.35%     
===========================================
  Files         183      253       +70     
  Lines       11073    14196     +3123     
  Branches     1905     2421      +516     
===========================================
+ Hits         5008    12433     +7425     
+ Misses       5644     1078     -4566     
- Partials      421      685      +264     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch was successfully deployed

1 active deployment
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: Sign-in identity and v1-anchored user associations

1 participant