Skip to content

ENG-3083: AccessPolicy + AccessPolicyVersion models and migration#7773

Open
thabofletcher wants to merge 5 commits intomainfrom
ENG-3083-access-policy-model-and-migration
Open

ENG-3083: AccessPolicy + AccessPolicyVersion models and migration#7773
thabofletcher wants to merge 5 commits intomainfrom
ENG-3083-access-policy-model-and-migration

Conversation

@thabofletcher
Copy link
Copy Markdown
Contributor

@thabofletcher thabofletcher commented Mar 27, 2026

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 compliance

The FE (merged in #7725 by @lucanovera) never sees version IDs directly — it uses the stable access_policy.id for all CRUD. Versioning happens transparently on the backend.

Code Changes

  • New model file src/fides/api/models/access_policy.py with AccessPolicy and AccessPolicyVersion
  • Alembic migration a8f3b2c1d5e7 creating both tables with indexes and constraints
  • Registered both models in sql_model_map in sql_models.py

Steps to Confirm

  1. Run alembic upgrade head — both tables should be created
  2. Verify plus_access_policy has: id, name, description, controls, enabled, is_deleted, created_at, updated_at
  3. Verify plus_access_policy_version has: id, access_policy_id (FK), version, yaml, created_at, updated_at
  4. Verify unique constraint on (access_policy_id, version) prevents duplicate versions
  5. Run alembic downgrade -1 — both tables should be dropped cleanly

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration label to the entry
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • No UX review needed
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
    • No migrations
  • Documentation:
    • No documentation updates required

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Mar 28, 2026 0:37am
fides-privacy-center Ignored Ignored Mar 28, 2026 0:37am

Request Review

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.
@thabofletcher thabofletcher force-pushed the ENG-3083-access-policy-model-and-migration branch from 9aa5257 to 4a32603 Compare March 27, 2026 01:48
Add data category annotations for plus_access_policy and
plus_access_policy_version tables to pass fides_db_scan CI check.
@thabofletcher thabofletcher marked this pull request as ready for review March 27, 2026 23:50
@thabofletcher thabofletcher requested a review from a team as a code owner March 27, 2026 23:50
@thabofletcher thabofletcher requested review from vcruces and removed request for a team March 27, 2026 23:50
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. Migration docstring vs down_revision mismatch — the Revises: comment says c7e3a9b1d4f2 but down_revision = "29113e44faec". Functionally harmless since Alembic reads the variable, but the stale comment is confusing. (line 13 in the migration)

  2. Redundant index on access_policy_idix_plus_access_policy_version_access_policy_id duplicates 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=True on the column in the model)

  3. Missing Python-side default for enabled / is_deletedserver_default only applies after a DB round-trip. A freshly constructed but un-flushed AccessPolicy will have enabled = None. Pairing with default=True / default=False matches the pattern used elsewhere in the codebase (e.g. privacy_experience.py).

  4. lazy="selectin" on versions — loads all version rows on every AccessPolicy fetch. 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.

  5. No auto-increment logic for versiondefault=1 means every new AccessPolicyVersion starts at version 1. The unique constraint prevents duplicates, but the service layer must compute MAX(version) + 1 inside a transaction. Just make sure the future service implementation does this atomically.

  6. controls column intent — the field is an unbounded nullable String with 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.

nullable=False,
index=True,
)
version = Column(Integer, nullable=False, default=1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@thabofletcher thabofletcher requested review from galvana and removed request for vcruces March 28, 2026 04:40
@thabofletcher
Copy link
Copy Markdown
Contributor Author

thabofletcher commented Mar 28, 2026

@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

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.

1 participant