Skip to content

ENG-2608 Make encryption optional in consent v3 columns#7413

Merged
erosselli merged 13 commits intomainfrom
erosselli/ENG-2608
Feb 23, 2026
Merged

ENG-2608 Make encryption optional in consent v3 columns#7413
erosselli merged 13 commits intomainfrom
erosselli/ENG-2608

Conversation

@erosselli
Copy link
Copy Markdown
Contributor

@erosselli erosselli commented Feb 18, 2026

Ticket ENG-2608

Description Of Changes

This PR adds a new setting that allows disabling the encryption at the column-level for the consent v3 privacy_preferences table. The default (and recommended) value for the setting is keeping encryption enabled.

Code Changes

  • Adds the new consent_v3_encryption_enabled consent setting ,with a default value of True
  • Defines a reusable optionally_encrypted_type for optionally-encrypted columns
  • Adds a new is_encrypted column to the privacy preferences table
  • Updates the record_data column to use this new type , using the value from the new setting
  • Adds a new CLI command, fides db migrate-consent-encryption to easily allow encrypting/decrypting existing records ; this is not meant for production usage, just for easy of dev / testing.

Steps to Confirm

  1. Run fidesplus using this fides branch
  2. Create some consent v3 preferences , check they are encrypted in the DB
  3. Set consent_v3_encryption_enabled to false and restart the server
  4. Run fides db migrate-consent-encryption --direction=decrypt . Check rows are now decrypted
  5. Submit new preferences, check they're stored as decrypted

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • 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
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Feb 18, 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 Feb 23, 2026 0:38am
fides-privacy-center Ignored Ignored Feb 23, 2026 0:38am

Request Review

@erosselli erosselli marked this pull request as ready for review February 18, 2026 19:01
@erosselli erosselli requested a review from a team as a code owner February 18, 2026 19:01
@erosselli erosselli requested review from galvana and johnewart and removed request for a team February 18, 2026 19:01
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 18, 2026

Greptile Summary

This PR introduces optional encryption for the v3 privacy_preferences.record_data column, controlled by a new FIDES__CONSENT__CONSENT_V3_ENCRYPTION_ENABLED config setting (default: True).

  • Adds optionally_encrypted_type utility in db/util.py to conditionally apply StringEncryptedType at model definition time
  • Adds is_encrypted boolean column to privacy_preferences with an Alembic migration that backfills existing rows as true
  • Adds a startup consistency check (check_consent_encryption_consistency) that logs an error if is_encrypted values don't match the current config
  • Adds a dev-only CLI command fides db migrate-consent-encryption for batch encrypting/decrypting existing records, using trial decryption for idempotency
  • Good test coverage for the utility function and migration logic, including roundtrip, idempotency, and batching scenarios

Confidence Score: 4/5

  • This PR is safe to merge with minor improvements recommended around the is_encrypted column default and migration output messaging.
  • The core design is sound — the column type is evaluated at class definition time, the migration is idempotent, and the startup check catches configuration drift. The issues found are style-level (missing server_default, misleading row counts). No logic bugs or security issues were identified. The dev-mode guard on the CLI migration command is appropriate.
  • src/fides/api/models/v3/privacy_preferences.py (missing server_default on is_encrypted column) and src/fides/api/util/consent_encryption_migration.py (misleading total_processed count)

Important Files Changed

Filename Overview
src/fides/api/alembic/migrations/versions/xx_2026_02_18_1814_d3f08ca31314_add_is_encrypted_to_privacy_preferences.py Migration adds is_encrypted boolean column with a backfill-then-alter pattern. The approach is correct but the column lacks a server_default.
src/fides/api/app_setup.py Adds startup consistency check for is_encrypted vs config setting. Creates its own DB session and cleans up properly. Previously flagged log message issues are noted.
src/fides/api/db/util.py Adds optionally_encrypted_type factory function. Clean implementation, well-documented, correctly delegates to StringEncryptedType or returns the plain type.
src/fides/api/models/v3/privacy_preferences.py Updates record_data column to use optionally_encrypted_type. Adds is_encrypted column (Python-side default only, no server_default) and has_encryption_mismatch classmethod.
src/fides/api/util/consent_encryption_migration.py Migration utility for encrypting/decrypting record_data. Uses trial decryption for idempotency. total_processed counts fetched rows, not transformed ones, which may be misleading in output.
src/fides/cli/commands/db.py Adds migrate-consent-encryption CLI command. Imports at module top, proper error handling. Success message may be misleading due to total_processed including skipped rows.
src/fides/config/consent_settings.py Adds consent_v3_encryption_enabled boolean setting with appropriate default and documentation.
tests/api/util/test_consent_encryption_migration.py Comprehensive tests for migration utility including roundtrip, idempotency, batching, and progress callback. Manual teardown was flagged in previous review.

Last reviewed commit: 41d7c2e

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

10 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

@erosselli
Copy link
Copy Markdown
Contributor Author

@greptile re-review

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

10 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +58 to +63
is_encrypted = Column(
Boolean,
nullable=False,
default=lambda: CONFIG.consent.consent_v3_encryption_enabled,
index=True,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing server_default on boolean column

The is_encrypted column uses a Python-side default but has no server_default. This means any insert that bypasses the ORM (e.g., raw SQL, bulk inserts, or future migration scripts) will fail with a NOT NULL constraint violation since the database itself has no default for this column. Adding a server_default makes the schema more robust and is consistent with how other boolean columns in this model are defined (e.g., is_latest on line 67 uses server_default=text("false")).

Suggested change
is_encrypted = Column(
Boolean,
nullable=False,
default=lambda: CONFIG.consent.consent_v3_encryption_enabled,
index=True,
)
is_encrypted = Column(
Boolean,
nullable=False,
default=lambda: CONFIG.consent.consent_v3_encryption_enabled,
server_default=text("true"),
index=True,
)

Context Used: Rule from dashboard - Use server_default="f" for boolean columns in Alembic migrations instead of default=False or oth... (source)

Comment on lines +169 to +170
db.commit()
result.total_processed += len(rows)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

total_processed counts fetched rows, not transformed ones

result.total_processed += len(rows) adds the count of all fetched rows, including those that _process_batch skipped because they were already in the target state. This makes the progress callback and the CLI's final message (e.g., "Encrypted 5000 rows") misleading when some rows were already encrypted/decrypted.

Consider tracking actually-transformed rows separately, or renaming this to something like total_examined to set correct expectations.

Copy link
Copy Markdown
Collaborator

@johnewart johnewart left a comment

Choose a reason for hiding this comment

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

LGTM as a whole but can we put the logic in the service? 🤞

)
updated_at = Column(DateTime(timezone=True), nullable=True)

@classmethod
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would put this in one of the services as it's more of a preferences-as-a-whole question

Copy link
Copy Markdown
Contributor Author

@erosselli erosselli Feb 19, 2026

Choose a reason for hiding this comment

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

hmm we could but then it has to go in fidesplus, which is "further away" from where the is_encrypted field is defined... I was hoping we could leave all the plus code "unaware" of the is_encrypted flag. Do you think it makes sense to move all this into fidesplus?

logger.debug("Connection to cache succeeded")


def check_consent_encryption_consistency() -> None:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Again, I think this can just be in the service - if privacy_preferences_service.has_encryption_mismatch() or something 😄

Copy link
Copy Markdown
Collaborator

@johnewart johnewart left a comment

Choose a reason for hiding this comment

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

👍🏼

@erosselli erosselli added this pull request to the merge queue Feb 23, 2026
Merged via the queue into main with commit f6c8489 Feb 23, 2026
53 of 54 checks passed
@erosselli erosselli deleted the erosselli/ENG-2608 branch February 23, 2026 13:26
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.

2 participants