Skip to content

Conversation

@galvana
Copy link
Contributor

@galvana galvana commented Dec 16, 2025

Ticket ENG-1811

Description Of Changes

Adds support for configuring a read-only database replica with optional credentials. When configured, applications can route read operations to a replica to improve performance and separate read/write traffic.

Code Changes

Configuration (fides/src/fides/config/database_settings.py):

  • Added 5 optional fields: readonly_user, readonly_password, readonly_port, readonly_db, readonly_params
  • Added async_readonly_database_uri computed field
  • Added field validators that auto-populate from primary DB settings when not specified
  • Updated assemble_readonly_db_connection to use new fields and proper scheme (postgresql+psycopg2)
  • Added assemble_async_readonly_db_connection with asyncpg SSL handling

Session Factories:

  • Added readonly_async_session in fides/src/fides/api/db/ctl_session.py (module-level, async)
  • Added get_readonly_autoclose_db_session() in fides/src/fides/api/api/deps.py (context manager, sync)
  • Both automatically fall back to primary database when readonly not configured

Tests (fides/tests/ctl/core/config/test_config.py):

  • Added TestReadOnlyDatabaseConfig class testing URI construction, credential fallback, and SSL parameter handling

Configuration Example:

server = "primary-db.example.com"
user = "app_user"
password = "app_password"

# Minimal config - credentials inherited from primary
readonly_server = "replica-db.example.com"

# Or with custom credentials
readonly_server = "replica-db.example.com"
readonly_user = "readonly_user"
readonly_password = "readonly_password"

Steps to Confirm

  1. Verified in the tests

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

vercel bot commented Dec 16, 2025

Deployment failed with the following error:

You must set up Two-Factor Authentication before accessing this team.

View Documentation: https://vercel.com/docs/two-factor-authentication

@galvana galvana requested a review from a team as a code owner December 16, 2025 22:54
@galvana galvana requested review from erosselli and removed request for a team December 16, 2025 22:54
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Dec 16, 2025

Greptile Overview

Greptile Summary

This PR extends read-only database configuration support by adding new fields for readonly database credentials and creating both synchronous and asynchronous readonly connection strings.

Major changes:

  • Added readonly_user, readonly_password, readonly_port, readonly_db, and readonly_params fields with fallback to primary database credentials
  • Created async_readonly_database_uri for async readonly connections
  • Added get_readonly_autoclose_db_session() context manager in deps.py
  • Initialized readonly_async_engine and readonly_async_session in ctl_session.py
  • Added comprehensive test coverage for readonly configuration

Critical issues found:

  • Password double-escaping bug: readonly_password fallback receives already-escaped password from the primary password field (which applies quote_plus() escaping), but the URI builder will escape it again, causing connection failures with special characters
  • Insecure URL validation in tests: Uses substring matching (e.g., assert "host" in uri) which is vulnerable to malicious URLs that contain expected strings in unexpected positions

Confidence Score: 2/5

  • This PR contains a critical password escaping bug that will cause connection failures and insecure test validation
  • Score reflects a critical logic error in password handling (double-escaping) that will cause authentication failures when passwords contain special characters, plus multiple security issues in test URL validation. The core implementation approach is sound, but the password fallback mechanism needs fixing before merge.
  • src/fides/config/database_settings.py needs password escaping fix, tests/ctl/core/config/test_config.py needs secure URL parsing

Important Files Changed

File Analysis

Filename Score Overview
src/fides/api/api/deps.py 5/5 Added get_readonly_autoclose_db_session() context manager with proper fallback to primary session when readonly not configured
src/fides/api/db/ctl_session.py 5/5 Created readonly async engine and session with proper SSL and keepalive configuration, includes fallback to primary session
src/fides/config/database_settings.py 2/5 Added readonly database config fields and URI builders. Critical issue: readonly_password fallback gets already-escaped password causing double-escaping
tests/ctl/core/config/test_config.py 2/5 Added comprehensive test coverage for readonly config. Uses insecure substring URL validation vulnerable to malicious URLs - needs URL parsing

Copy link
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.

4 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 544 to 548
assert "replica-db.example.com" in db_settings.sqlalchemy_readonly_database_uri
assert "readonly_user" in db_settings.sqlalchemy_readonly_database_uri
assert "5433" in db_settings.sqlalchemy_readonly_database_uri
assert "fides_replica" in db_settings.sqlalchemy_readonly_database_uri
assert "postgresql+psycopg2" in db_settings.sqlalchemy_readonly_database_uri
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Vulnerable URL validation using substring matching

Suggested change
assert "replica-db.example.com" in db_settings.sqlalchemy_readonly_database_uri
assert "readonly_user" in db_settings.sqlalchemy_readonly_database_uri
assert "5433" in db_settings.sqlalchemy_readonly_database_uri
assert "fides_replica" in db_settings.sqlalchemy_readonly_database_uri
assert "postgresql+psycopg2" in db_settings.sqlalchemy_readonly_database_uri
from urllib.parse import urlparse
parsed = urlparse(db_settings.sqlalchemy_readonly_database_uri)
assert parsed.hostname == "replica-db.example.com"
assert parsed.username == "readonly_user"
assert parsed.port == 5433
assert parsed.path == "/fides_replica"
assert parsed.scheme == "postgresql+psycopg2"

Context Used: Rule from dashboard - When writing tests that validate URL properties, always parse the URL into components and assert on ... (source)

Comment on lines 552 to 556
assert "replica-db.example.com" in db_settings.async_readonly_database_uri
assert "readonly_user" in db_settings.async_readonly_database_uri
assert "5433" in db_settings.async_readonly_database_uri
assert "fides_replica" in db_settings.async_readonly_database_uri
assert "postgresql+asyncpg" in db_settings.async_readonly_database_uri
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Vulnerable URL validation using substring matching

Suggested change
assert "replica-db.example.com" in db_settings.async_readonly_database_uri
assert "readonly_user" in db_settings.async_readonly_database_uri
assert "5433" in db_settings.async_readonly_database_uri
assert "fides_replica" in db_settings.async_readonly_database_uri
assert "postgresql+asyncpg" in db_settings.async_readonly_database_uri
parsed_async = urlparse(db_settings.async_readonly_database_uri)
assert parsed_async.hostname == "replica-db.example.com"
assert parsed_async.username == "readonly_user"
assert parsed_async.port == 5433
assert parsed_async.path == "/fides_replica"
assert parsed_async.scheme == "postgresql+asyncpg"

Context Used: Rule from dashboard - When writing tests that validate URL properties, always parse the URL into components and assert on ... (source)

Comment on lines 571 to 574
assert "replica-db.example.com" in db_settings.sqlalchemy_readonly_database_uri
assert "app_user" in db_settings.sqlalchemy_readonly_database_uri
assert "5432" in db_settings.sqlalchemy_readonly_database_uri
assert "fides" in db_settings.sqlalchemy_readonly_database_uri
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Vulnerable URL validation using substring matching

Suggested change
assert "replica-db.example.com" in db_settings.sqlalchemy_readonly_database_uri
assert "app_user" in db_settings.sqlalchemy_readonly_database_uri
assert "5432" in db_settings.sqlalchemy_readonly_database_uri
assert "fides" in db_settings.sqlalchemy_readonly_database_uri
parsed = urlparse(db_settings.sqlalchemy_readonly_database_uri)
assert parsed.hostname == "replica-db.example.com"
assert parsed.username == "app_user"
assert parsed.port == 5432
assert parsed.path == "/fides"

Context Used: Rule from dashboard - When writing tests that validate URL properties, always parse the URL into components and assert on ... (source)

Comment on lines 638 to 641
# sslmode should be converted to ssl in query params
assert "ssl=" in db_settings.async_readonly_database_uri
# sslrootcert should be removed from query params
assert "sslrootcert" not in db_settings.async_readonly_database_uri
Copy link
Contributor

Choose a reason for hiding this comment

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

syntax: Vulnerable URL validation using substring matching

Suggested change
# sslmode should be converted to ssl in query params
assert "ssl=" in db_settings.async_readonly_database_uri
# sslrootcert should be removed from query params
assert "sslrootcert" not in db_settings.async_readonly_database_uri
from urllib.parse import parse_qs
parsed = urlparse(db_settings.async_readonly_database_uri)
query_params = parse_qs(parsed.query)
assert query_params.get("ssl") == ["require"]
assert "sslrootcert" not in query_params

Context Used: Rule from dashboard - When writing tests that validate URL properties, always parse the URL into components and assert on ... (source)

@galvana galvana requested a review from johnewart December 16, 2025 23:14
@codecov
Copy link

codecov bot commented Dec 16, 2025

Codecov Report

❌ Patch coverage is 61.76471% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.26%. Comparing base (c8af6ca) to head (d9bd982).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
src/fides/api/db/ctl_session.py 21.05% 14 Missing and 1 partial ⚠️
src/fides/api/api/deps.py 20.00% 8 Missing ⚠️
src/fides/config/database_settings.py 92.30% 1 Missing and 2 partials ⚠️

❌ Your patch status has failed because the patch coverage (61.76%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7139      +/-   ##
==========================================
- Coverage   87.30%   87.26%   -0.05%     
==========================================
  Files         533      533              
  Lines       35095    35161      +66     
  Branches     4067     4083      +16     
==========================================
+ Hits        30640    30683      +43     
- Misses       3570     3591      +21     
- Partials      885      887       +2     

☔ View full report in Codecov by Sentry.
📢 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@erosselli erosselli left a comment

Choose a reason for hiding this comment

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

Approved with a question! Also let's add a changelog

Comment on lines 356 to 358
scheme="postgresql+psycopg2",
username=info.data.get("readonly_user"),
password=info.data.get("readonly_password"),
Copy link
Contributor

Choose a reason for hiding this comment

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

just checking, the field validators defined above will run before this one, so if I don't provide readonly_user then it will already be populated with user by the time we get here?

@galvana galvana enabled auto-merge December 17, 2025 20:12
@galvana galvana added this pull request to the merge queue Dec 17, 2025
Merged via the queue into main with commit f6ed3ee Dec 17, 2025
65 of 68 checks passed
@galvana galvana deleted the ENG-1811-async-readonly-db-connections branch December 17, 2025 23:09
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.

3 participants