Skip to content

Fix critical audit findings: FORCE RLS, owner-scoped UPDATEs, canonical email#97

Merged
cmeans merged 1 commit into
mainfrom
fix/critical-audit-findings
Mar 30, 2026
Merged

Fix critical audit findings: FORCE RLS, owner-scoped UPDATEs, canonical email#97
cmeans merged 1 commit into
mainfrom
fix/critical-audit-findings

Conversation

@cmeans
Copy link
Copy Markdown
Owner

@cmeans cmeans commented Mar 30, 2026

Summary

Addresses 4 critical findings from the multi-tenant/OAuth code audit (main@52ed457):

  • C1: FORCE ROW LEVEL SECURITY — new Alembic migration adds FORCE so the table owner role can't bypass RLS policies. Previously ENABLE alone let the connection pool role (table owner) skip all policies.
  • C2: owner_id in UPDATE WHERE clausesupdate_entry.sql, upsert_alert_update.sql, upsert_preference_update.sql now include AND owner_id = %s. Prevents cross-tenant updates if any code path misses Python-side ownership check.
  • C3: canonical_email in OAuth provisioningcreate_user_auto.sql now stores canonical_email, link_oauth_identity.sql matches on canonical_email instead of raw email. Handles Gmail dot/+tag variants (j.doe+work@gmail.comjdoe@gmail.com). canonical_email() extracted from cli.py to helpers.py for shared use.
  • C4: auto_provision defaultAuthMiddleware parameter defaults to False (was True). Server.py already overrides this, but direct instantiation was dangerous.

QA

Prerequisites

  • pip install -e ".[dev]"
  • Deploy to test instance on alternate port (AWARENESS_PORT=8421)

Manual tests (via MCP tools)

    • RLS enforced [FORCE applied; superuser role bypasses — see observation] on table owner
      Connect as the DB owner role, set app.current_user to one user, verify you can't see another user's entries even without the SET:
    -- Should return 0 rows (FORCE blocks unscoped queries)
    SELECT * FROM entries;

    Expected: empty result set when no app.current_user is set

    • UPDATE scoped to owner
    update_entry(entry_id="<entry-from-user-A>", description="hacked")
    

    Expected: as user B, this should fail or return no match (entry belongs to user A)

    • Gmail email variants link correctly
    mcp-awareness-user add alice --email "a.lice+work@gmail.com"
    # Then OAuth login with email "alice@gmail.com" should link to same user
    

    Expected: both email variants resolve to the same canonical_email, user is linked

    • Auto-provision off by default
      Instantiate AuthMiddleware(app, jwt_secret="x") without passing auto_provision — verify it defaults to False

🤖 Generated with Claude Code

…al email, auto_provision default

C1: FORCE ROW LEVEL SECURITY — new migration so table owner can't bypass RLS
C2: Add owner_id to WHERE clause in update_entry, upsert_alert_update,
    upsert_preference_update SQL — prevents cross-tenant updates
C3: OAuth auto-provisioning and identity linking now use canonical_email
    (handles Gmail dot/+tag variants), extract canonical_email to helpers.py
C4: AuthMiddleware.auto_provision defaults to False (was True)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cmeans cmeans added the Dev Active Developer is actively working on this PR; QA should not start label Mar 30, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cmeans cmeans removed the Dev Active Developer is actively working on this PR; QA should not start label Mar 30, 2026
@github-actions github-actions Bot added the Ready for QA Dev work complete — QA can begin review label Mar 30, 2026
@cmeans cmeans added the QA Active QA is actively reviewing; Dev should not push changes label Mar 30, 2026
@github-actions github-actions Bot removed the Ready for QA Dev work complete — QA can begin review label Mar 30, 2026
Copy link
Copy Markdown
Owner Author

@cmeans cmeans left a comment

Choose a reason for hiding this comment

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

QA Review — PR #97: Fix critical audit findings

Code Review

All 4 critical findings addressed correctly:

C1: FORCE ROW LEVEL SECURITY (migration j5e6f7g8h9i0)

  • Adds ALTER TABLE {table} FORCE ROW LEVEL SECURITY to all 4 tables
  • Verified: relforcerowsecurity = true on all tables
  • Observation: FORCE works correctly with non-superuser roles (tested: 0 rows unscoped, 17 scoped). However, the Docker Compose awareness role is a superuser, which bypasses RLS even with FORCE. This is a known PostgreSQL limitation — the audit recommended "use a non-owner app role." The migration is correct; the role separation is a follow-up item.

C2: owner_id in UPDATE WHERE clauses

  • update_entry.sql: WHERE id = %sWHERE id = %s AND owner_id = %s
  • upsert_alert_update.sql: same fix ✅
  • upsert_preference_update.sql: same fix ✅
  • postgres_store.py params updated to include owner_id in all 3 call sites ✅
  • Verified manually: cross-owner UPDATE returns UPDATE 0

C3: canonical_email in OAuth provisioning

  • canonical_email() extracted from cli.py to helpers.py for shared use
  • create_user_auto.sql: now includes canonical_email column ✅
  • link_oauth_identity.sql: matches on canonical_email instead of raw email
  • postgres_store.py: create_user_if_not_exists() and link_oauth_identity() compute canonical before SQL ✅
  • Verified: a.lice+work@Gmail.com → canonical alice@gmail.com

C4: auto_provision default

  • AuthMiddleware.__init__: auto_provision: bool = False (was True) ✅
  • Verified: AuthMiddleware(None, jwt_secret='x').auto_provision == False

CI Gate Check

Check Conclusion
lint ✅ SUCCESS
typecheck ✅ SUCCESS
test (3.10/3.11/3.12) ✅ SUCCESS
codecov/patch SUCCESS

Test Results

Check Result
pytest (490 tests) ✅ 490/490 pass
ruff, mypy ✅ Clean
CI ✅ All green
Manual #1: FORCE RLS ✅ Applied (relforcerowsecurity=t); verified with non-superuser role (0 unscoped, 17 scoped). Note: default Docker superuser bypasses — role separation is a follow-up
Manual #2: UPDATE scoped ✅ Cross-owner UPDATE returns UPDATE 0, entry unchanged
Manual #3: canonical_email a.lice+work@Gmail.comalice@gmail.com
Manual #4: auto_provision default False

Observation

The awareness Docker role is a superuser, which bypasses all RLS (even FORCE). The FORCE migration is correct and effective for non-superuser roles, but production deployments should create a dedicated non-superuser app role for the connection pool. This was noted in the original audit (S-level recommendation). Consider a follow-up PR that creates a awareness_app role in Docker Compose and updates the DSN.

Verdict

Zero blockers. All 4 critical findings fixed correctly. One observation (superuser role bypass — follow-up item, not this PR's scope). Ready for QA Signoff.

@cmeans
Copy link
Copy Markdown
Owner Author

cmeans commented Mar 30, 2026

Adding Ready for QA Signoff — all 4 critical audit findings verified. FORCE RLS applied (works with non-superuser role), owner-scoped UPDATEs confirmed, canonical_email matching verified, auto_provision defaults False. CI green, 490/490 pytest.

@cmeans cmeans added Ready for QA Signoff QA passed — ready for maintainer final review and merge and removed QA Active QA is actively reviewing; Dev should not push changes labels Mar 30, 2026
Copy link
Copy Markdown
Owner Author

@cmeans cmeans left a comment

Choose a reason for hiding this comment

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

LGTM

@cmeans cmeans added QA Approved Manual QA testing completed and passed and removed Ready for QA Signoff QA passed — ready for maintainer final review and merge labels Mar 30, 2026
@cmeans cmeans merged commit cd00c9c into main Mar 30, 2026
43 checks passed
@cmeans cmeans deleted the fix/critical-audit-findings branch March 30, 2026 06:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

QA Approved Manual QA testing completed and passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant