Skip to content

Conversation

@WcaleNieWolny
Copy link
Contributor

@WcaleNieWolny WcaleNieWolny commented Dec 24, 2025

Changes (AI generated)

  • Section 1: has_2fa_enabled functions

    • Added has_2fa_enabled() function that checks if the current authenticated
      user has any verified MFA factors
    • Added has_2fa_enabled(user_id uuid) function that checks if a specific
      user has any verified MFA factors
    • The no-argument version is accessible to authenticated and anon roles
    • The user_id version is private and only accessible to postgres and
      service_role roles
    • Both functions use SECURITY DEFINER to access auth.mfa_factors table
  • Section 2: check_org_members_2fa_enabled function

    • Added check_org_members_2fa_enabled(org_id uuid) function that returns a
      list of all organization members with their 2FA status
    • Function is accessible only to super_admins of the organization
    • Returns a table with user_id and 2fa_enabled columns for all members
    • Validates that the organization exists and that the caller has super_admin
      rights
  • Section 3: Add enforcing_2fa column to orgs table

    • Added enforcing_2fa boolean column to public.orgs table with
      NOT NULL DEFAULT false
    • Column indicates whether an organization requires all members to have 2FA
      enabled
    • Added documentation comment explaining the column's purpose
  • Section 4: Modify check_min_rights to enforce 2FA

    • Updated check_min_rights(user_id uuid, org_id uuid, ...) function to check
      2FA enforcement rules
    • If an organization has enforcing_2fa enabled and the user doesn't have 2FA
      enabled, the function returns false and logs the denial
    • The 2FA check happens before permission checks, ensuring users without 2FA
      are denied access even if they would otherwise have permissions
    • Maintains backward compatibility - if org doesn't enforce 2FA, behavior is
      unchanged
  • Section 4.A: Create get_orgs_v7 with 2FA enforcement

    • Created new get_orgs_v7(userid uuid) function based on get_orgs_v6 with
      2FA enforcement support
    • Added enforcing_2fa field to the return table showing whether the org
      requires 2FA
    • Added 2fa_has_access field indicating if the user has access (true if org
      doesn't enforce 2FA OR user has 2FA enabled)
    • Redacts sensitive fields (paying, trial_left, can_use_more,
      is_canceled, app_count, subscription_start, subscription_end,
      management_email, is_yearly) when user doesn't have 2FA access
    • Uses a CTE (two_fa_access) to compute 2FA access status efficiently
    • Created wrapper function get_orgs_v7() that handles API key and
      authentication logic
  • Section 4.B: Modify get_orgs_v6 to prevent information leakage

    • Updated get_orgs_v6(userid uuid) function to redact sensitive information
      when users don't have 2FA access
    • Prevents information leakage by returning redacted values (false, 0, NULL)
      for sensitive fields when org enforces 2FA and user doesn't have it
    • Uses the same two_fa_access CTE pattern as get_orgs_v7 for consistency
    • Maintains backward compatibility - all existing fields remain, just with
      redaction logic added
  • Section 5: reject_access_due_to_2fa function

    • Added reject_access_due_to_2fa(org_id uuid, user_id uuid) function that
      returns boolean
    • Returns true if org requires 2FA and user doesn't have it enabled, false
      otherwise
    • Returns false if org doesn't enforce 2FA enforcement
    • Returns false if org doesn't exist
    • Function is private and only accessible to postgres and service_role
      roles (not accessible to regular users)
    • Logs access denials using pg_log for audit purposes
  • Tests

    • Added comprehensive SQL tests in 35_test_has_2fa_enabled.sql covering both
      function variants and permission checks
    • Added comprehensive SQL tests in 36_test_check_org_members_2fa_enabled.sql
      covering super_admin access, member 2FA status, and edge cases
    • Added extensive SQL tests in 37_test_check_min_rights_2fa_enforcement.sql
      covering 2FA enforcement blocking access, normal access scenarios, different
      permission levels, and edge cases
    • Added comprehensive SQL tests in 38_test_get_orgs_v7_2fa_enforcement.sql
      covering field visibility, redaction verification, and comparison between
      users with and without 2FA
    • Added SQL tests in 39_test_reject_access_due_to_2fa.sql covering function
      behavior, permission checks, and verification that regular users cannot call
      the private function
    • All tests verify both positive and negative cases, edge cases, and
      permission boundaries

Motivation

This PR is the direct resonse to part one of #1291. I strongly believe that the changes of this PR bring Capgo closer to fully implemeneting the 2FA enforcement feature. As such, I believe this PR is a high priority for the development of capgo and it should be reviewed and merged as soon as possible, so further development of the 2FA enforcement can continue.

Business impact

The feature of 2FA enforcement has been deemed important. (Medium business impact. Enterprise clients often consider 2FA enforcement to provide good operation security for their organization). However, this PR on its own does not fully implement the 2FA enforcement feature, so on its own this PR doesn't have a business impact.

Questions to be ansered during the review

Q: Should the get_orgs_v6 be modified to prevent the leak of sensitive data when 2FA enforcement is enabled?

Q: How to lint the SQL?

I ran npm run lint:sql in 278e0d1

Summary by CodeRabbit

  • New Features

    • Organization admins can require two-factor authentication for members.
    • Users without 2FA are blocked from accessing orgs that enforce it.
    • Org listings surface an "enforcing 2FA" flag and per-user 2FA access; sensitive org fields are redacted when 2FA access is missing.
  • Tests

    • Added comprehensive tests covering 2FA status checks, enforcement behavior, access control permutations, redaction logic, and permission boundaries.

✏️ Tip: You can customize this high-level summary in your review settings.


Note

Implements database-side 2FA enforcement and redaction to protect org data when users lack verified MFA.

  • Add enforcing_2fa column to orgs; update check_min_rights to deny access when enforced and has_2fa_enabled(user) is false (logged via pg_log).
  • New functions: has_2fa_enabled()/has_2fa_enabled(uuid) (MFA status), check_org_members_2fa_enabled(org_id) (super_admin-only member 2FA status), reject_access_due_to_2fa(org_id, user_id) (backend-only gating helper).
  • Add get_orgs_v7(userid) and wrapper get_orgs_v7() returning enforcing_2fa and 2fa_has_access; redact sensitive fields when access is blocked. Modify get_orgs_v6(userid) to apply equivalent redaction.
  • Tighten grants: user-scoped helpers public where safe; uuid variants and backend helpers restricted to postgres/service_role.
  • Add extensive SQL tests covering MFA detection, org-member checks, rights enforcement across scopes, v6/v7 redaction behavior, and function permissions.

Written by Cursor Bugbot for commit 287fd7c. This will update automatically on new commits. Configure here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 24, 2025

📝 Walkthrough

Walkthrough

Adds a single SQL migration implementing org-level 2FA enforcement (functions, column, grants, redaction) and five pgTAP test suites validating 2FA checks, enforcement in rights checks, org-data redaction, permission boundaries, and related utility functions.

Changes

Cohort / File(s) Summary
2FA Enforcement Migration
supabase/migrations/20251224103713_2fa_enforcement.sql
Adds enforcing_2fa to orgs; implements has_2fa_enabled() (session) and has_2fa_enabled(user_id uuid), check_org_members_2fa_enabled(org_id), reject_access_due_to_2fa(org_id,user_id); updates check_min_rights, get_orgs_v6, get_orgs_v7 (wrapper) with redaction and 2FA fields; adjusts owners/GRANTs and revokes public access to private funcs.
has_2fa_enabled tests
supabase/tests/35_test_has_2fa_enabled.sql
New pgTAP tests covering verified/unverified/no-2FA, service_role vs non-service permissions, anon behavior, and function existence/signatures.
check_org_members_2fa_enabled tests
supabase/tests/36_test_check_org_members_2fa_enabled.sql
Tests super_admin-only access, per-member 2FA statuses, counts, and non-existent-org error handling.
check_min_rights 2FA enforcement tests
supabase/tests/37_test_check_min_rights_2fa_enforcement.sql
Validates check_min_rights() behavior with org enforcing_2fa, multiple permission levels, app-scoped permissions, and edge cases (NULL user, missing membership).
get_orgs_v7 / get_orgs_v6 redaction tests
supabase/tests/38_test_get_orgs_v7_2fa_enforcement.sql
Verifies redaction of sensitive org fields when org enforces 2FA and caller lacks 2FA; ensures non-sensitive fields remain visible; exercises Stripe-related fields.
reject_access_due_to_2fa tests
supabase/tests/39_test_reject_access_due_to_2fa.sql
Exercises rejection logic across enforcement/non-enforcement, permission contexts (service_role vs private/anon), non-existent org handling, and repeated calls.
Org management test update
supabase/tests/20_test_org_management_functions.sql
Adds explicit service_role authentication and cleanup calls around get_orgs_v6 test requiring service_role.

Sequence Diagram(s)

(Skipped — changes are internal DB functions and tests; not a multi-component sequential flow suitable for a diagram.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Suggested labels

enhancement

Poem

🐰 I hopped through rows and grants tonight,
I stitched a second key to every site,
When orgs demand a tiny extra leap,
I hide the secrets that they choose to keep,
Nibble carrots, keep your 2FA bright. 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title "feat: 2FA enforcement - database" clearly and specifically describes the main change: adding database-level 2FA enforcement infrastructure.
Description check ✅ Passed The PR description covers all required template sections: comprehensive summary of changes organized by section, detailed test plan with test file references, and a populated checklist. However, the PR is backend-only and lacks screenshots (which is appropriately noted as skippable for backend changes).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 2fa_enforcement_db

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6dc5a0 and 287fd7c.

📒 Files selected for processing (1)
  • supabase/tests/20_test_org_management_functions.sql
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:85-96
Timestamp: 2025-12-25T11:22:19.594Z
Learning: In the 2FA enforcement implementation for supabase/migrations: When an org has enforcing_2fa=true, all users including super_admins must have 2FA enabled before accessing any org functions (including check_org_members_2fa_enabled); this is intentional behavior to ensure consistent security enforcement without exceptions for admins.
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` to back new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Always cover database changes with Postgres-level tests and complement them with end-to-end tests for affected user flows
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/migrations/**/*.sql : A migration that introduces a new table may include seed inserts for that table, treating that seeding as part of the current feature and not modifying previously committed migrations
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.
📚 Learning: 2025-12-24T14:11:10.256Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:10.256Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.

Applied to files:

  • supabase/tests/20_test_org_management_functions.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` to back new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/tests/20_test_org_management_functions.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Run tests
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (1)
supabase/tests/20_test_org_management_functions.sql (1)

58-69: Good addition of service_role authentication for private function access.

The use of tests.authenticate_as_service_role() correctly restricts access to the private get_orgs_v6(uuid) variant. The migration file confirms this function is properly restricted with REVOKE ALL from public/anon/authenticated and GRANT EXECUTE only to postgres and service_role, aligning with the security requirement that inner functions with user_id parameters should not be accessible to anon/authenticated roles. The user_id 'c591b04e-cf29-4945-b9a0-776d0672061a' exists in seed data as the admin user, providing valid test fixtures. Authentication cleanup at line 69 properly isolates this test from subsequent tests.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 SQLFluff (3.5.0)
supabase/tests/20_test_org_management_functions.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
supabase/migrations/20251224103713_2fa_enforcement.sql (2)

246-259: Consider caching the 2FA check result to avoid duplicate calls.

public.has_2fa_enabled(userid) is called twice in the CTE (lines 253 and 256). While PostgreSQL may optimize this, explicitly computing it once would be cleaner:

🔎 Suggested optimization
   two_fa_access AS (
     SELECT
       o.id AS org_id,
       o.enforcing_2fa,
+      public.has_2fa_enabled(userid) AS user_has_2fa
+    FROM public.orgs o
+    JOIN public.org_users ou ON ou.user_id = userid AND o.id = ou.org_id
+  ),
+  two_fa_computed AS (
+    SELECT
+      org_id,
+      enforcing_2fa,
       -- 2fa_has_access: true if enforcing_2fa is false OR (enforcing_2fa is true AND user has 2FA)
       CASE
-        WHEN o.enforcing_2fa = false THEN true
-        ELSE public.has_2fa_enabled(userid)
+        WHEN enforcing_2fa = false THEN true
+        ELSE user_has_2fa
       END AS "2fa_has_access",
       -- should_redact: true if org enforces 2FA and user doesn't have 2FA
-      (o.enforcing_2fa = true AND NOT public.has_2fa_enabled(userid)) AS should_redact
-    FROM public.orgs o
-    JOIN public.org_users ou ON ou.user_id = userid AND o.id = ou.org_id
+      (enforcing_2fa = true AND NOT user_has_2fa) AS should_redact
+    FROM two_fa_access
   )

549-581: Consider combining the existence check with the SELECT.

The function checks if the org exists (line 558), then performs a separate SELECT (lines 563-565). These could be combined to reduce database round-trips:

🔎 Suggested optimization
 DECLARE
     org_enforcing_2fa boolean;
 BEGIN
-    -- Check if org exists
-    IF NOT EXISTS (SELECT 1 FROM public.orgs WHERE public.orgs.id = reject_access_due_to_2fa.org_id) THEN
-        RETURN false;
-    END IF;
-
     -- Check if org has 2FA enforcement enabled
     SELECT enforcing_2fa INTO org_enforcing_2fa
     FROM public.orgs
     WHERE public.orgs.id = reject_access_due_to_2fa.org_id;

+    -- If org doesn't exist, org_enforcing_2fa will be NULL
+    IF org_enforcing_2fa IS NULL THEN
+        RETURN false;
+    END IF;
+
     -- 7.1 If a given org does not enable 2FA enforcement, return false
     IF org_enforcing_2fa = false THEN
         RETURN false;
     END IF;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0591d6b and 595d38b.

📒 Files selected for processing (6)
  • supabase/migrations/20251224103713_2fa_enforcement.sql
  • supabase/tests/35_test_has_2fa_enabled.sql
  • supabase/tests/36_test_check_org_members_2fa_enabled.sql
  • supabase/tests/37_test_check_min_rights_2fa_enforcement.sql
  • supabase/tests/38_test_get_orgs_v7_2fa_enforcement.sql
  • supabase/tests/39_test_reject_access_due_to_2fa.sql
🧰 Additional context used
📓 Path-based instructions (1)
supabase/migrations/**/*.sql

📄 CodeRabbit inference engine (AGENTS.md)

supabase/migrations/**/*.sql: When a feature requires schema changes, create a single migration file with supabase migration new <feature_slug> and keep editing that file until the feature ships; never edit previously committed migrations
A migration that introduces a new table may include seed inserts for that table, treating that seeding as part of the current feature and not modifying previously committed migrations
Do not create new cron jobs; instead update the process_all_cron_tasks function in a new migration file to add your job if needed

Database migrations must be created with supabase migration new <feature_slug> and never modify previously committed migrations

Files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
🧠 Learnings (6)
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` to back new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/tests/35_test_has_2fa_enabled.sql
  • supabase/tests/38_test_get_orgs_v7_2fa_enforcement.sql
  • supabase/tests/36_test_check_org_members_2fa_enabled.sql
  • supabase/tests/39_test_reject_access_due_to_2fa.sql
  • supabase/tests/37_test_check_min_rights_2fa_enforcement.sql
  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Always cover database changes with Postgres-level tests and complement them with end-to-end tests for affected user flows

Applied to files:

  • supabase/tests/35_test_has_2fa_enabled.sql
  • supabase/tests/38_test_get_orgs_v7_2fa_enforcement.sql
  • supabase/tests/39_test_reject_access_due_to_2fa.sql
  • supabase/tests/37_test_check_min_rights_2fa_enforcement.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/migrations/**/*.sql : A migration that introduces a new table may include seed inserts for that table, treating that seeding as part of the current feature and not modifying previously committed migrations

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/migrations/**/*.sql : Database migrations must be created with `supabase migration new <feature_slug>` and never modify previously committed migrations

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/migrations/**/*.sql : When a feature requires schema changes, create a single migration file with `supabase migration new <feature_slug>` and keep editing that file until the feature ships; never edit previously committed migrations

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (11)
supabase/migrations/20251224103713_2fa_enforcement.sql (6)

6-19: LGTM! Well-structured has_2fa_enabled() for session user.

The function correctly:

  • Uses SECURITY DEFINER to access auth.mfa_factors
  • Sets empty search_path to prevent search path injection
  • Uses auth.uid() to get the current user securely
  • Checks for verified status only

25-64: LGTM! Proper permission boundary for the user_id variant.

The overloaded function correctly:

  • Restricts access to postgres and service_role only
  • Revokes from PUBLIC, anon, and authenticated before granting
  • Uses qualified column reference (mfa.user_id) to avoid ambiguity

71-112: LGTM! Properly gated check_org_members_2fa_enabled function.

Good implementation:

  • Validates org existence before proceeding
  • Requires super_admin rights for access
  • Uses COALESCE to handle potential NULL from has_2fa_enabled

118-123: LGTM! Safe schema migration with sensible defaults.

The column addition uses IF NOT EXISTS for idempotency, NOT NULL DEFAULT false ensures backward compatibility, and the comment documents the purpose clearly.


333-396: LGTM! Well-structured wrapper with proper auth flow.

The wrapper correctly:

  • Prioritizes API key authentication
  • Falls back to session identity
  • Handles limited_to_orgs filtering for scoped API keys
  • Logs and raises exceptions for authentication failures

149-158: NULL handling for non-existent orgs is correct.

When org_id doesn't exist, org_enforcing_2fa remains NULL, the 2FA check is skipped (since NULL = true is falsy), and the permission loop also returns false for missing org_users entries. This dual safeguard is appropriate. For added clarity, consider explicitly checking IF org_enforcing_2fa IS NOT NULL AND org_enforcing_2fa = true THEN to make the intent more obvious.

supabase/tests/35_test_has_2fa_enabled.sql (1)

1-157: LGTM! Comprehensive test coverage for has_2fa_enabled.

The test file properly covers:

  • Verified, unverified, and no MFA scenarios
  • Session-based has_2fa_enabled() for all user types
  • Service role access to has_2fa_enabled(user_id)
  • Permission boundaries (authenticated and anon cannot call user_id variant)
  • Function existence verification
supabase/tests/37_test_check_min_rights_2fa_enforcement.sql (1)

1-441: LGTM! Thorough test coverage for check_min_rights 2FA enforcement.

Excellent coverage including:

  • Orgs with/without 2FA enforcement
  • Users with verified/unverified/no 2FA
  • All permission levels (read, write, admin, super_admin)
  • App-scoped permissions
  • Edge cases (NULL user_id, non-existent org, user not in org)
  • Verification that 2FA check precedes permission check
supabase/tests/38_test_get_orgs_v7_2fa_enforcement.sql (1)

1-479: LGTM! Comprehensive redaction testing for get_orgs_v7/v6.

The test file thoroughly validates:

  • 2fa_has_access and enforcing_2fa field visibility
  • Redaction of all sensitive fields (paying, trial_left, management_email, etc.)
  • Non-sensitive fields (name) remain visible to all users
  • Consistency between get_orgs_v7 and get_orgs_v6 redaction behavior
  • Real vs redacted value comparisons with actual stripe_info data
supabase/tests/39_test_reject_access_due_to_2fa.sql (1)

1-231: LGTM! Complete test coverage for reject_access_due_to_2fa.

The test file covers:

  • All user/org 2FA enforcement combinations
  • Non-existent org returns false
  • Permission boundaries (private function accessible only to service_role)
  • Idempotency (multiple calls return same result)
  • Function existence verification
supabase/tests/36_test_check_org_members_2fa_enabled.sql (1)

1-254: LGTM! Thorough test coverage for check_org_members_2fa_enabled.

The test file validates:

  • Super admin access and correct member count
  • 2FA status for verified, unverified, and no MFA members
  • Super admin's own 2FA status is included
  • Permission denial for non-super_admin users
  • Exception for non-existent org
  • Function existence verification

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
supabase/migrations/20251224103713_2fa_enforcement.sql (2)

12-17: Optional: Simplify WHERE clause.

The nested SELECT auth.uid() within the WHERE clause works correctly but can be simplified for readability:

   RETURN EXISTS(
     SELECT 1
     FROM auth.mfa_factors
-    WHERE (SELECT auth.uid()) = user_id 
+    WHERE auth.uid() = user_id 
       AND status = 'verified'
   );

Both versions are functionally equivalent, but the simpler form is more direct.


577-594: Optional: Simplify conditional logic.

The function correctly implements rejection logic, but the conditions can be streamlined:

🔎 Proposed refactoring
     -- Check if org has 2FA enforcement enabled
     SELECT enforcing_2fa INTO org_enforcing_2fa
     FROM public.orgs
     WHERE public.orgs.id = reject_access_due_to_2fa.org_id;
 
-    -- 7.1 If a given org does not enable 2FA enforcement, return false
-    IF org_enforcing_2fa = false THEN
-        RETURN false;
-    END IF;
-
-    -- 7.2 If a given org REQUIRES 2FA, and has_2fa_enabled(user_id) == false, return true
-    IF org_enforcing_2fa = true AND NOT public.has_2fa_enabled(reject_access_due_to_2fa.user_id) THEN
+    -- If org requires 2FA and user doesn't have it, reject access
+    IF COALESCE(org_enforcing_2fa, false) = true AND NOT public.has_2fa_enabled(reject_access_due_to_2fa.user_id) THEN
         PERFORM public.pg_log('deny: REJECT_ACCESS_DUE_TO_2FA', jsonb_build_object('org_id', org_id, 'user_id', user_id));
         RETURN true;
     END IF;
 
-    -- 7.3 Otherwise, return false
+    -- Otherwise, allow access
     RETURN false;

The COALESCE(org_enforcing_2fa, false) handles NULL values explicitly. Both versions are functionally equivalent given the NOT NULL DEFAULT false constraint, but the simplified version is more direct.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 278e0d1 and b6dc5a0.

📒 Files selected for processing (1)
  • supabase/migrations/20251224103713_2fa_enforcement.sql
🧰 Additional context used
📓 Path-based instructions (1)
supabase/migrations/**/*.sql

📄 CodeRabbit inference engine (AGENTS.md)

supabase/migrations/**/*.sql: When a feature requires schema changes, create a single migration file with supabase migration new <feature_slug> and keep editing that file until the feature ships; never edit previously committed migrations
A migration that introduces a new table may include seed inserts for that table, treating that seeding as part of the current feature and not modifying previously committed migrations
Do not create new cron jobs; instead update the process_all_cron_tasks function in a new migration file to add your job if needed

Database migrations must be created with supabase migration new <feature_slug> and never modify previously committed migrations

Files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Always cover database changes with Postgres-level tests and complement them with end-to-end tests for affected user flows
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` to back new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged
📚 Learning: 2025-12-24T14:11:04.619Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:409-539
Timestamp: 2025-12-24T14:11:04.619Z
Learning: In supabase/migrations for get_orgs_v6 and get_orgs_v7: The inner functions with user_id parameter (get_orgs_v6(uuid) and get_orgs_v7(uuid)) should NOT be granted to anon/authenticated roles as this allows any user to query other users' organizations; only the no-argument wrapper functions should be public as they perform authentication checks.

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` to back new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All database operations must use `getPgClient()` or `getDrizzleClient()` from `utils/pg.ts` for PostgreSQL access during active migration to Cloudflare D1

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use Drizzle ORM query patterns with `schema` from `postgress_schema.ts` for all database operations; use `aliasV2()` for self-joins or multiple table references

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/utils/postgress_schema.ts : Schema definitions must be placed in `utils/postgress_schema.ts` using Drizzle ORM and never edited in committed migration files

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
📚 Learning: 2025-12-05T17:34:25.556Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-05T17:34:25.556Z
Learning: Applies to supabase/functions/_backend/** : Backend logic should be organized in `supabase/functions/_backend/` with subdirectories for plugins, private endpoints, public endpoints, triggers, and utilities

Applied to files:

  • supabase/migrations/20251224103713_2fa_enforcement.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (6)
supabase/migrations/20251224103713_2fa_enforcement.sql (6)

118-124: LGTM!

The column addition is safe:

  • NOT NULL DEFAULT false ensures existing orgs are not affected
  • IF NOT EXISTS makes the migration idempotent
  • Clear documentation comment

149-158: LGTM: 2FA check correctly precedes permission checks.

The 2FA enforcement logic is correctly implemented:

  • Checks org-level enforcement before evaluating user permissions
  • Handles NULL values safely (non-existent orgs or NULL enforcing_2fa treated as false)
  • Logs denials consistently with existing patterns

Note: This enforcement applies to all functions using check_min_rights, including check_org_members_2fa_enabled (see earlier comment about potential circular dependency).


184-324: LGTM: get_orgs_v7(userid uuid) correctly implements 2FA-aware redaction.

The inner function properly:

  • Computes 2fa_has_access based on org enforcement and user 2FA status (lines 251-254)
  • Redacts sensitive fields (paying, trial_left, app_count, subscription dates, management_email, etc.) when should_redact = true
  • Restricts access to postgres/service_role only (security fix confirmed per learning)

The redaction logic preserves enough information (org name, logo, user role) for users to see which orgs exist while hiding financial details until they enable 2FA.


338-401: LGTM: Wrapper function correctly handles authentication.

The no-argument wrapper properly:

  • Validates API keys and filters by limited_to_orgs when applicable (lines 371-388)
  • Falls back to session-based authentication (lines 390-397)
  • Logs and raises exceptions on authentication failures
  • Is correctly granted to anon/authenticated since it performs auth checks internally

416-544: LGTM: get_orgs_v6(userid uuid) correctly backports 2FA redaction.

The function properly:

  • Adds the same two_fa_access CTE and redaction logic as v7 (lines 474-524)
  • Maintains backward compatibility by keeping the same return schema (no enforcing_2fa/2fa_has_access fields)
  • Is now correctly restricted to postgres/service_role only (security fix confirmed)

This ensures v6 users also benefit from 2FA-aware data protection.


564-615: LGTM: Rejection logic is correct and properly secured.

The function correctly:

  • Returns false for non-existent orgs (graceful handling)
  • Returns false when 2FA is not enforced
  • Returns true only when org enforces 2FA and user lacks it
  • Logs rejections for audit trails
  • Is properly restricted to postgres/service_role

@sonarqubecloud
Copy link

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