Skip to content

Restrict org status RPCs to authenticated members#1751

Merged
riderx merged 7 commits into
mainfrom
riderx/fix-rpc-billing-status
Mar 13, 2026
Merged

Restrict org status RPCs to authenticated members#1751
riderx merged 7 commits into
mainfrom
riderx/fix-rpc-billing-status

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Mar 8, 2026

Summary (AI generated)

  • Aligned is_onboarding_needed_org SQL test expectations with current contract after recent org function hardening changes.
  • Added/kept secure access-context handling in related org management SQL tests.

Motivation (AI generated)

Latest CI failures showed an expectation mismatch for non-existent org handling in onboarding checks.

Business Impact (AI generated)

Ensures CI reliability and keeps SQL test coverage consistent with current function behavior, preventing false negatives during automated validation.

Test Plan (AI generated)

  • Ran bun lint:backend.
  • Pushed CI fix commit e7eec8533 to riderx/fix-rpc-billing-status.

Summary by CodeRabbit

  • New Features

    • Added organization status checks: subscription payment state, remaining trial days, and an onboarding-needed indicator.
  • Bug Fixes / Behavior

    • Nonexistent orgs now return safe default values (no disclosure of existence).
  • Security

    • Execution restricted to authenticated users and backend service role.
  • Tests

    • New and updated tests covering admin, non-member, anonymous, and service-role behaviors.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds three SECURITY DEFINER PostgreSQL functions in public: is_paying_org(uuid), is_trial_org(uuid), and is_onboarding_needed_org(uuid) that call public.check_min_rights, read public.orgs/public.stripe_info, and restrict EXECUTE to authenticated and service_role. Tests added/updated to validate role-based behavior.

Changes

Cohort / File(s) Summary
Org Status RPC Migration
supabase/migrations/20260308203352_restrict-org-status-rpc-access.sql
Adds public.is_paying_org(orgid uuid) RETURNS boolean and public.is_trial_org(orgid uuid) RETURNS integer as SECURITY DEFINER. Both resolve caller role, call public.check_min_rights, query public.orgs and public.stripe_info for customer_id, status, and trial_at, and set explicit EXECUTE grants (revoke from PUBLIC/anon/authenticated; grant to authenticated and service_role).
Onboarding RPC Migration
supabase/migrations/20260313121928_fix-onboarding-needed-org-nonexistent.sql
Adds public.is_onboarding_needed_org(orgid uuid) RETURNS boolean as SECURITY DEFINER with empty search_path. Returns true only if org exists, is not onboarded, and is not a trial (uses is_onboarded_org and is_trial_org).
New RPC Access Tests
supabase/tests/46_test_org_status_rpcs.sql
Adds transactional tests covering admin/org-member, non-member, anonymous (expects permission denied), and service_role scenarios for is_paying_org and is_trial_org, asserting return values and permission errors.
Updated Org Tests
supabase/tests/06_org_functions.sql, supabase/tests/20_test_org_management_functions.sql
Insert authentication context calls (tests.authenticate_as('test_admin'), tests.authenticate_as_service_role()), add positive is_paying_org assertion under service role, and change expectations for nonexistent orgs (is_trial_org from NULL to 0; is_onboarding_needed_org from NULL to FALSE).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Caller as Caller (anon / authenticated / service_role)
    participant RPC as RPC (is_paying_org / is_trial_org / is_onboarding_needed_org)
    participant Rights as public.check_min_rights
    participant Orgs as public.orgs / public.stripe_info
    Caller->>RPC: CALL <function>(orgid)
    RPC->>Rights: check_min_rights(caller_role, orgid)
    Rights-->>RPC: allowed / denied
    alt allowed
        RPC->>Orgs: SELECT org row -> customer_id, status, trial_at
        Orgs-->>RPC: stripe fields / org presence
        RPC-->>Caller: boolean / integer (days remaining) / onboarding boolean
    else denied
        RPC-->>Caller: false / 0 / false
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through tables, keys, and rows,

I checked who’s allowed and who mustn’t know.
For paying friends I show the light,
For trials I count each sunny night.
For anon I guard — a gentle hop, goodnight.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: restricting organization status RPC functions to authenticated members only.
Description check ✅ Passed The description covers the key changes and motivation but lacks detailed test plan steps and omits documentation/linting checklist items from the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch riderx/fix-rpc-billing-status
📝 Coding Plan
  • Generate coding plan for human review comments

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 (4.0.4)
supabase/migrations/20260313121928_fix-onboarding-needed-org-nonexistent.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

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

supabase/tests/06_org_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

  • 2 others

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

@riderx riderx force-pushed the riderx/fix-rpc-billing-status branch from c6c0200 to bc00e88 Compare March 8, 2026 20:36
Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@supabase/migrations/20260308203352_restrict-org-status-rpc-access.sql`:
- Around line 53-55: The SELECT in function public.is_trial_org (using
stripe_info.trial_at, stripe_info.customer_id and orgs.customer_id via orgid)
can return NULL when no stripe_info row or trial_at is NULL; change the return
expression to guarantee a non-null integer—wrap the computed GREATEST(...)
result in COALESCE(..., 0) or fetch into a local integer variable and return 0
when NULL—so the function always returns a non-null integer as declared.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a4b0ac10-fb4b-4a1b-bc7a-25a42c1fe259

📥 Commits

Reviewing files that changed from the base of the PR and between 2af3a08 and c6c0200.

📒 Files selected for processing (1)
  • supabase/migrations/20260308203352_restrict-org-status-rpc-access.sql

Comment thread supabase/migrations/20260308203352_restrict-org-status-rpc-access.sql Outdated
@riderx riderx force-pushed the riderx/fix-rpc-billing-status branch from e7eec85 to f740ccb Compare March 13, 2026 12:20
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
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: 2

🧹 Nitpick comments (1)
supabase/tests/20_test_org_management_functions.sql (1)

276-277: Scope service-role auth to only the assertions that need it.

tests.authenticate_as_service_role() here appears to stay active for later sections. Add a nearby tests.clear_authentication() after the restricted RPC checks so downstream tests keep their intended context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/tests/20_test_org_management_functions.sql` around lines 276 - 277,
The call to tests.authenticate_as_service_role() is left active for subsequent
tests; after the restricted RPC checks complete, call
tests.clear_authentication() to restore normal auth context. Locate the block
that runs tests.authenticate_as_service_role() before the restricted RPC
assertions and insert a tests.clear_authentication() immediately after those
assertions (i.e., after the restricted RPC checks) so downstream tests run with
the intended context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@supabase/migrations/20260313121928_fix-onboarding-needed-org-nonexistent.sql`:
- Around line 1-17: This new migration duplicates work for the same feature;
remove this separate migration and fold the CREATE OR REPLACE FUNCTION
"public"."is_onboarding_needed_org" definition into the existing in-flight
migration for the feature so there's a single editable migration file,
preserving the function signature, LANGUAGE plpgsql, SECURITY DEFINER, SET
"search_path" = '' and the logic (EXISTS check against public.orgs, NOT
public.is_onboarded_org, and public.is_trial_org = 0); ensure the combined
migration keeps the same idempotent CREATE OR REPLACE FUNCTION statement and any
comments about avoiding org existence disclosure so ordering/drift is avoided.
- Around line 7-15: Revoke EXECUTE on the UUID-parameter functions
is_onboarding_needed_org(uuid), is_paying_org(uuid), and is_trial_org(uuid) from
anon and authenticated and make them service_role-only, and add new public
no-parameter wrapper functions (e.g., is_onboarding_needed_org(),
is_paying_org(), is_trial_org()) that perform caller identity/membership checks
and then call the internal uuid versions with the target org id; update usages
(such as the CHECK in the RETURN block that calls
is_onboarding_needed_org(is_onboarding_needed_org.orgid)) to call the new
wrapper where appropriate or, if the org id must be explicit, perform the
membership check in the wrapper and expose a safe public API that avoids
granting direct EXECUTE on the uuid-parameter functions.

---

Nitpick comments:
In `@supabase/tests/20_test_org_management_functions.sql`:
- Around line 276-277: The call to tests.authenticate_as_service_role() is left
active for subsequent tests; after the restricted RPC checks complete, call
tests.clear_authentication() to restore normal auth context. Locate the block
that runs tests.authenticate_as_service_role() before the restricted RPC
assertions and insert a tests.clear_authentication() immediately after those
assertions (i.e., after the restricted RPC checks) so downstream tests run with
the intended context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c6a4f1da-531d-4a90-99c5-6d8e4a679fe6

📥 Commits

Reviewing files that changed from the base of the PR and between e7eec85 and ea3c17a.

📒 Files selected for processing (5)
  • supabase/migrations/20260308203352_restrict-org-status-rpc-access.sql
  • supabase/migrations/20260313121928_fix-onboarding-needed-org-nonexistent.sql
  • supabase/tests/06_org_functions.sql
  • supabase/tests/20_test_org_management_functions.sql
  • supabase/tests/46_test_org_status_rpcs.sql
🚧 Files skipped from review as they are similar to previous changes (2)
  • supabase/tests/06_org_functions.sql
  • supabase/tests/46_test_org_status_rpcs.sql

Comment on lines +1 to +17
-- Keep onboarding-needed checks false for missing org IDs to avoid org existence disclosure.
CREATE OR REPLACE FUNCTION "public"."is_onboarding_needed_org"("orgid" "uuid") RETURNS boolean
LANGUAGE "plpgsql" SECURITY DEFINER
SET "search_path" = ''
AS $$
BEGIN
RETURN (
EXISTS (
SELECT 1 FROM public.orgs
WHERE id = is_onboarding_needed_org.orgid
)
AND
NOT public.is_onboarded_org(is_onboarding_needed_org.orgid)
AND public.is_trial_org(is_onboarding_needed_org.orgid) = 0
);
END;
$$;
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.

🛠️ Refactor suggestion | 🟠 Major

Please fold this into the existing in-flight migration for this feature.

This introduces a second migration for the same feature scope; keep a single editable migration until ship to avoid split ordering and drift.

As per coding guidelines: "When a feature requires schema changes, create a single migration file with the Supabase CLI (bunx supabase migration new <feature_slug>) and keep editing that file until the feature ships; never edit previously committed migrations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260313121928_fix-onboarding-needed-org-nonexistent.sql`
around lines 1 - 17, This new migration duplicates work for the same feature;
remove this separate migration and fold the CREATE OR REPLACE FUNCTION
"public"."is_onboarding_needed_org" definition into the existing in-flight
migration for the feature so there's a single editable migration file,
preserving the function signature, LANGUAGE plpgsql, SECURITY DEFINER, SET
"search_path" = '' and the logic (EXISTS check against public.orgs, NOT
public.is_onboarded_org, and public.is_trial_org = 0); ensure the combined
migration keeps the same idempotent CREATE OR REPLACE FUNCTION statement and any
comments about avoiding org existence disclosure so ordering/drift is avoided.

Comment on lines +7 to +15
RETURN (
EXISTS (
SELECT 1 FROM public.orgs
WHERE id = is_onboarding_needed_org.orgid
)
AND
NOT public.is_onboarded_org(is_onboarding_needed_org.orgid)
AND public.is_trial_org(is_onboarding_needed_org.orgid) = 0
);
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find function + privilege statements for is_onboarding_needed_org:"
rg -n -C2 'CREATE OR REPLACE FUNCTION "public"\."is_onboarding_needed_org"|REVOKE ALL ON FUNCTION "public"\."is_onboarding_needed_org"|GRANT EXECUTE ON FUNCTION "public"\."is_onboarding_needed_org"' supabase/migrations

echo
echo "Find tests covering anon/non-member behavior for is_onboarding_needed_org:"
rg -n -C2 'is_onboarding_needed_org\(' supabase/tests

Repository: Cap-go/capgo

Length of output: 2066


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Full function body for is_onboarding_needed_org (current) ==="
sed -n '2,15p' supabase/migrations/20260313121928_fix-onboarding-needed-org-nonexistent.sql

echo
echo "=== Check for GRANT/REVOKE statements on is_onboarding_needed_org ==="
rg -n 'GRANT|REVOKE' supabase/migrations/20260313121928_fix-onboarding-needed-org-nonexistent.sql

echo
echo "=== is_paying_org function definition and grants ==="
rg -n -A20 'CREATE OR REPLACE FUNCTION "public"\."is_paying_org"' supabase/migrations | head -50
rg -n 'GRANT.*is_paying_org|REVOKE.*is_paying_org' supabase/migrations

echo
echo "=== is_trial_org function definition and grants ==="
rg -n -A20 'CREATE OR REPLACE FUNCTION "public"\."is_trial_org"' supabase/migrations | head -50
rg -n 'GRANT.*is_trial_org|REVOKE.*is_trial_org' supabase/migrations

echo
echo "=== Check if wrapper functions exist (no-arg versions) ==="
rg -n 'CREATE OR REPLACE FUNCTION "public"\."is_onboarding_needed_org"\s*\(\s*\)' supabase/migrations
rg -n 'CREATE OR REPLACE FUNCTION "public"\."is_paying_org"\s*\(\s*\)' supabase/migrations
rg -n 'CREATE OR REPLACE FUNCTION "public"\."is_trial_org"\s*\(\s*\)' supabase/migrations

Repository: Cap-go/capgo

Length of output: 621


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check base migration for GRANT/REVOKE on these functions ==="
rg -n 'GRANT.*is_onboarding_needed_org|GRANT.*is_paying_org|GRANT.*is_trial_org' supabase/migrations/20250530233128_base.sql

echo
echo "=== Full is_paying_org function (base) ==="
rg -n -A15 'CREATE OR REPLACE FUNCTION "public"\."is_paying_org"' supabase/migrations/20250530233128_base.sql | head -40

echo
echo "=== Full is_trial_org function (base) ==="
rg -n -A15 'CREATE OR REPLACE FUNCTION "public"\."is_trial_org"' supabase/migrations/20250530233128_base.sql | head -40

echo
echo "=== Full is_onboarded_org function (base) ==="
rg -n -A15 'CREATE OR REPLACE FUNCTION "public"\."is_onboarded_org"' supabase/migrations/20250530233128_base.sql | head -40

echo
echo "=== Check for check_min_rights usage in these functions ==="
rg -n -B5 -A20 'check_min_rights' supabase/migrations/20250530233128_base.sql | grep -A20 'is_paying_org\|is_trial_org\|is_onboarding_needed_org'

Repository: Cap-go/capgo

Length of output: 2992


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check if wrapper functions (no-arg versions) exist ==="
rg -n 'CREATE OR REPLACE FUNCTION "public"\."is_onboarding_needed_org"\s*\(\s*\) RETURNS' supabase/migrations
rg -n 'CREATE OR REPLACE FUNCTION "public"\."is_paying_org"\s*\(\s*\) RETURNS' supabase/migrations
rg -n 'CREATE OR REPLACE FUNCTION "public"\."is_trial_org"\s*\(\s*\) RETURNS' supabase/migrations

echo
echo "=== Verify information disclosure risk: test if anon can call the uuid function ==="
rg -n -B3 -A10 'is_onboarding_needed_org.*anon\|anon.*is_onboarding_needed_org' supabase/tests

Repository: Cap-go/capgo

Length of output: 114


Revoke EXECUTE on these uuid-parameter functions from anon and authenticated roles.

The is_onboarding_needed_org(uuid), is_paying_org(uuid), and is_trial_org(uuid) functions are currently GRANT ALL ... TO anon/authenticated, allowing any user to query org existence and trial status via boolean outcomes. These uuid-parameter functions should be service_role-only for internal lookups. Create public wrapper functions with no parameters that call the inner uuid versions; the wrappers perform caller identity/membership checks and become the public-facing API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260313121928_fix-onboarding-needed-org-nonexistent.sql`
around lines 7 - 15, Revoke EXECUTE on the UUID-parameter functions
is_onboarding_needed_org(uuid), is_paying_org(uuid), and is_trial_org(uuid) from
anon and authenticated and make them service_role-only, and add new public
no-parameter wrapper functions (e.g., is_onboarding_needed_org(),
is_paying_org(), is_trial_org()) that perform caller identity/membership checks
and then call the internal uuid versions with the target org id; update usages
(such as the CHECK in the RETURN block that calls
is_onboarding_needed_org(is_onboarding_needed_org.orgid)) to call the new
wrapper where appropriate or, if the org id must be explicit, perform the
membership check in the wrapper and expose a safe public API that avoids
granting direct EXECUTE on the uuid-parameter functions.

@riderx riderx merged commit 5b7a2a3 into main Mar 13, 2026
15 checks passed
@riderx riderx deleted the riderx/fix-rpc-billing-status branch March 13, 2026 12:54
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