Repository navigation
Declare v2 table privileges and check them in CI and deploys - #3985
Merged
Merged
Conversation
Adds the privilege manifest (policyengine_api/data/v2/privileges.py), the connector analysis used by the static check, the reconcile/verify logic and its command-line script, unit tests, and rule proof fixtures. Workflow wiring, the integration run as the real roles, rules registry entries, docs and the changelog fragment are not done yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the real roles (#3980) - seed-v2-database.yml runs `reconcile_v2_privileges.py apply` straight after the v2 schema upgrade, then `verify`, before the catalog is seeded. Both use the migration credential. - v2-integration-check.yml creates the managed roles in the disposable database, grants and verifies their declared privileges, and runs the new integration tests. - The identity persistence tests now run the user services as policyengine_v2_runtime. New integration tests run the comparison-run services as policyengine_stage12_test and cover the reconcile script: a second apply grants nothing, an extra privilege is reported and kept, and a missing role blocks every grant. - The Stage 12 record builders move to tests/unit/v2/comparison_run_support.py so the unit and integration tests share them. - The reconcile module runs as a program and joins the modules allowed to print; the reconcile script is executable; lint fixes in the new files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds api.v2.runtime-privileges-declared, api.v2.runtime-privileges-tested-as-runtime-role and api.v2.deployed-privileges-match to rules/registry.toml, regenerates docs/generated/rules.md and AGENTS.md, documents the declared privileges in the Alembic and v2 code organization guides, corrects the Stage 8 target's description of runtime grants, and adds changelog fragments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3985 +/- ##
===========================================
+ Coverage 46.17% 84.62% +38.44%
===========================================
Files 239 258 +19
Lines 13987 15684 +1697
Branches 2272 2642 +370
===========================================
+ Hits 6458 13272 +6814
+ Misses 6958 1637 -5321
- Partials 571 775 +204 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
anth-volk
marked this pull request as ready for review
October 20, 2026 18:25
anth-volk
added a commit
that referenced
this pull request
Oct 20, 2026
The privilege reconciliation appends its GitHub step summary through Path.open, the annotation scan in privilege_requirements extends its list from one generator, and the manifest proof test builds its no-access map with dict.fromkeys. Behaviour is 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 #3980
Summary
Declares every managed v2 database role's table privileges in the repository, grants them on every deploy, checks them before the catalog is seeded, and runs the cross-database integration tests as those roles instead of the database superuser. This is the design agreed in #3980 (comment), written after #3977, where production's runtime role had no SELECT on
legacy_policy_mappingsand every reform save returned 503.policyengine_api/data/v2/privileges.pyhas one entry per v2 table (plusalembic_version) for each ofpolicyengine_v2_runtime,policyengine_v2_catalog_writerandpolicyengine_stage12_<environment>. "No access" is an explicit entry, not an omission. The migration role owns every table and isn't listed.api.v2.runtime-privileges-declared):policyengine_api/data/v2/privilege_requirements.pyreads each connector module withast.reads*needs SELECT;creates*/updates*/deletes*need INSERT/UPDATE/DELETE plus SELECT;with_for_updateneeds UPDATE on the tables it locks. A test fails when a connector needs a privilege its role isn't declared to hold. The proof fixture repeats the v2/user-policies returns 503 for logged-in users: reforms cannot be saved #3977 mistake.api.v2.deployed-privileges-match):scripts/reconcile_v2_privileges.py applyruns inseed-v2-database.ymlstraight after the schema upgrade, thenverify, before the catalog is seeded. Both connect with the migration credential, which owns every table.applygrants each declared privilege a role lacks, never revokes, reports privileges the file doesn't declare and the projects' default privileges in the job summary, and grants nothing if a declared role or table is missing.verifychecks every declared privilege withhas_table_privilegeand exits 1 on any that is missing. The report names roles, tables and privileges only, never a connection URL.api.v2.runtime-privileges-tested-as-runtime-role):v2-integration-check.ymlcreates the managed roles in the disposable database (--create-missing-rolesis refused for any persistent database), grants and verifies their privileges, and then:policyengine_v2_runtime(setup and cleanup stay on the superuser);policyengine_stage12_test, and checks that role can't readusers;applygrants nothing, an extra privilege is reported and kept, and a missing role blocks every grant.rules/registry.tomlwith regenerateddocs/generated/rules.mdandAGENTS.md; a "Table privileges" section indocs/engineering/skills/alembic-migrations.md; a paragraph inv2-code-organization.md; and a correction todocs/migration/stage-8-supabase-target.md, which said ordinary read/write was granted to the runtime role. The changes underrules/need approval from a core developer other than the author (api.process.rule-machinery-code-owned).What the first deploys will grant and report
Planned offline from each project's grants, read with read-only catalog queries on 2026-10-20. Both projects have every declared role and table.
policyengine_v2_runtime: DELETE onlegacy_policy_mappingsand onlegacy_household_mappingspolicyengine_v2_runtime: INSERT onpolicies,householdsandparameter_values; INSERT and UPDATE onlegacy_policy_mappingsandlegacy_household_mappingsThe production grants are a deliberate choice. The code that needs them is the Stage 10 dual-write copy into
legacy_policy_mappings/legacy_household_mappings(off in production:DB_WRITE_POLICY=cloud_sql) and the writes behind the nativePOST /v2/policiesandPOST /v2/householdsroutes (production sends these to the Flask implementation:ROUTE_IMPL_POLICY=flask_fallback). We chose to count those code paths, so the declarations match what the code can do in any configuration, rather than exclude them. UPDATE on the mapping tables comes fromread_legacy_policy_mapping/read_legacy_household_mappinglocking rows withFOR UPDATE. The two mapping tables are planned for removal; when they go, these declarations and the related rules entries will be narrowed again.Production's current grants also include the SELECT on
legacy_policy_mappings,legacy_household_mappings,policiesandhouseholdsthat was applied by hand to fix #3977. That grant is now declared in the repository.Decisions in the design
services/v2/comparison_runs) are checked againstpolicyengine_stage12_<environment>; every other connector againstpolicyengine_v2_runtime. The catalog writer's privileges are copied from production, because its code isn't organized as connector modules; the deploy check covers them.Known limits
text()SQL and SQL issued outsidedatabase_connectors/. The integration tests as the real roles cover the user services and comparison-run services. The policy, household and dual-write integration tests still run as the superuser.applyreport lists them.Environment variables
Every variable the new steps and scripts read, its provider, and how it reaches them:
V2_MIGRATION_DATABASE_URL(seed job)applyandverifystepsstagingandproduction(both confirmed withgh secret list --env)env:mapssecrets.V2_MIGRATION_DATABASE_URL, as the existing steps do;push.ymlcallsseed-v2-database.ymlwithsecrets: inheritfor staging and for productionV2_SUPABASE_PROJECT_REFstagingandproduction(confirmed withgh variable list --env)env:(already present)V2_SUPABASE_ENVIRONMENTstagingandproduction(confirmed)env:(already present)DEPLOYMENT_ENVIRONMENT(seed job)run(): names the Stage 12 role; must bestagingorproductionfor a persistent databasedeployment_environmentinput: literalstaging/productioninpush.ymlenv:(already present)DEPLOYMENT_ENVIRONMENT(integration job)testcatalog-runtime-integrationjobenv:V2_ALEMBIC_DISPOSABLE_TEST--create-missing-rolesrequires it"1"in the integration job; absent from the seed jobenv:(already present)V2_MIGRATION_DATABASE_URL(integration job)env:(already present)GITHUB_STEP_SUMMARYapply/verifyreportIn a persistent database,
run()callsqualify_v2_connection, which requires the connection'scurrent_userto bepolicyengine_v2_migratorwith CREATE on schemapublic. The existing schema-upgrade step already passes the same check with the same secret.Inline suppressions
None added.
privilege_reconciliation.pyprints its report and runs as a program (if __name__ == "__main__":), so it joins the existingT201file-level exception for such modules (api.quality.command-line-modules-ruff-exemptions), like the migration qualification modules.Follow-up
The simulation API runs as
policyengine_stage12_<environment>and needs its own check before deploys: PolicyEngine/policyengine-sim-api#855.Testing
uv run pytest tests/unit/v2/test_privilege_manifest.py tests/unit/v2/test_privilege_requirements.py tests/unit/v2/test_privilege_reconciliation.py -q: 70 passed.make test: 2,866 passed.python scripts/run_quality_guards.py: migration-contracts, rules-registry and ci-scripts passed.python scripts/export_migration_contracts.py: no changes.python -m scripts.rules guard --base-ref origin/master: passed (three new rules noted).python -m unittest discover -s scripts/rules/tests -t .: 80 passed.ruff format --check .andruff check .: clean.make typecheck-v2: no issues; the new modules also pass mypy, though they aren't in[tool.mypy].files.DEPLOYMENT_ENVIRONMENT=test):alembic upgrade head;apply --create-missing-rolescreated the three roles and granted every declared privilege; a secondapplyreported "Nothing: every declared privilege was already granted.";verifyexited 0. After revoking the runtime role's SELECT onlegacy_policy_mappingsby hand,verifyexited 1 naming it, andapplygranted it back.🤖 Generated with Claude Code