ENG-3083: AccessPolicy + AccessPolicyVersion models and migration#7773
ENG-3083: AccessPolicy + AccessPolicyVersion models and migration#7773thabofletcher wants to merge 5 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
Two-table design for policy versioning: access_policy holds the stable entity (name, description, controls, enabled), while access_policy_version stores immutable YAML snapshots. Each update creates a new version row rather than overwriting, preserving full audit history.
9aa5257 to
4a32603
Compare
Add data category annotations for plus_access_policy and plus_access_policy_version tables to pass fides_db_scan CI check.
…y-model-and-migration
There was a problem hiding this comment.
Code Review — ENG-3083: AccessPolicy + AccessPolicyVersion models and migration
The two-table design is clean and the intent (stable entity ID + immutable version rows) is well-documented. The migration covers the full round-trip (upgrade + downgrade) and the FK/cascade is correct. A few things worth addressing before merge:
Suggestions
-
Migration docstring vs
down_revisionmismatch — theRevises:comment saysc7e3a9b1d4f2butdown_revision = "29113e44faec". Functionally harmless since Alembic reads the variable, but the stale comment is confusing. (line 13 in the migration) -
Redundant index on
access_policy_id—ix_plus_access_policy_version_access_policy_idduplicates coverage already provided by the composite unique index(access_policy_id, version). PostgreSQL can use a composite index for prefix-column lookups, so the single-column index is pure overhead. (lines ~83 in the migration;index=Trueon the column in the model) -
Missing Python-side
defaultforenabled/is_deleted—server_defaultonly applies after a DB round-trip. A freshly constructed but un-flushedAccessPolicywill haveenabled = None. Pairing withdefault=True/default=Falsematches the pattern used elsewhere in the codebase (e.g.privacy_experience.py). -
lazy="selectin"on versions — loads all version rows on everyAccessPolicyfetch. If policies accumulate many versions over time, this is an N-row load where only 1 row is typically needed (latest_version). Worth confirming this is intentional given the append-only nature of the versions table. -
No auto-increment logic for
version—default=1means every newAccessPolicyVersionstarts at version 1. The unique constraint prevents duplicates, but the service layer must computeMAX(version) + 1inside a transaction. Just make sure the future service implementation does this atomically. -
controlscolumn intent — the field is an unbounded nullableStringwith no docstring. A brief comment explaining what it stores (and whether it might eventually need a structured type like JSONB) would help future readers.
Nice to Have
- No model-level tests are included. Even basic integration tests (create policy → create version → verify FK cascade on delete) would guard against migration drift in the future.
Overall the foundation looks solid — the issues above are all relatively small and most are in the suggestion/nice-to-have range.
.../api/alembic/migrations/versions/xx_2026_03_26_1400_a8f3b2c1d5e7_add_access_policy_tables.py
Show resolved
Hide resolved
.../api/alembic/migrations/versions/xx_2026_03_26_1400_a8f3b2c1d5e7_add_access_policy_tables.py
Outdated
Show resolved
Hide resolved
| nullable=False, | ||
| index=True, | ||
| ) | ||
| version = Column(Integer, nullable=False, default=1) |
There was a problem hiding this comment.
version = Column(Integer, nullable=False, default=1) — every new AccessPolicyVersion instance will default to version 1. The unique constraint on (access_policy_id, version) will correctly reject duplicates at the DB level, but the service layer is entirely responsible for computing the next version number. There's no helper or guard here to make that easy to get right.
This is fine as a pure model layer, but worth a note: whichever service creates a new version will need to SELECT MAX(version) + 1 (ideally inside the same transaction) to avoid a race. Just flagging so the service implementation doesn't assume this is automatic.
There was a problem hiding this comment.
The service layer handles it correctly. What is the suggestion you are making for this code review @claude ?
- Fix stale Revises comment in migration docstring - Remove redundant single-column index on access_policy_id (composite unique index already covers prefix lookups) - Add Python-side default=True/False on enabled/is_deleted - Remove lazy="selectin" on versions relationship to avoid loading all version rows on every policy fetch - Add comment explaining controls column format
|
@claude The associated PR includes the tests you are suggesting. What are you suggesting for this PR for testing? I'm not sure what you are suggesting for integration tests on a standalone model PR that needs to be merged before the service layer will work. Please explain |
Ticket ENG-3083
Description Of Changes
Adds the data model and Alembic migration for the PBAC Access Policy feature. This is the OSS-side foundation that the fidesplus API layer builds on.
Two-table design for transparent policy versioning:
plus_access_policy— Stable entity the FE interacts with (name, description, controls, enabled, soft-delete flag)plus_access_policy_version— Immutable version rows. Each policy update creates a new version row rather than overwriting, preserving full audit history for complianceThe FE (merged in #7725 by @lucanovera) never sees version IDs directly — it uses the stable
access_policy.idfor all CRUD. Versioning happens transparently on the backend.Code Changes
src/fides/api/models/access_policy.pywithAccessPolicyandAccessPolicyVersiona8f3b2c1d5e7creating both tables with indexes and constraintssql_model_mapinsql_models.pySteps to Confirm
alembic upgrade head— both tables should be createdplus_access_policyhas: id, name, description, controls, enabled, is_deleted, created_at, updated_atplus_access_policy_versionhas: id, access_policy_id (FK), version, yaml, created_at, updated_atalembic downgrade -1— both tables should be dropped cleanlyPre-Merge Checklist
CHANGELOG.mdupdatedmaindowngrade()migration is correct and works