Skip to content

Restrict global_stats access to platform admins#1706

Open
riderx wants to merge 2 commits intomainfrom
riderx/fix-global-stats
Open

Restrict global_stats access to platform admins#1706
riderx wants to merge 2 commits intomainfrom
riderx/fix-global-stats

Conversation

@riderx
Copy link
Member

@riderx riderx commented Feb 26, 2026

Summary (AI generated)

  • Added a migration that removes public access to global_stats and restricts SELECT to authenticated admins via public.is_admin(auth.uid()).
  • Updated RLS policy test expectations in supabase/tests/26_test_rls_policies.sql to match the new Allow admin users to select global_stats policy.
  • Updated RLS scenario tests in supabase/tests/27_test_rls_scenarios.sql to enforce denied access for anon and non-admin users, and allow access for test_admin.

Test plan (AI generated)

  • bun lint.
  • bun lint:backend.
  • Not yet executed: targeted database policy test suite due environment prerequisites.

Screenshots (AI generated)

  • Not applicable (backend/security-only changes).

Checklist (AI generated)

  • My code follows the code style of this project and passes lint checks.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • My change has adequate E2E test coverage.
  • I have tested my code manually, and I have provided steps how to reproduce my tests.

Summary by CodeRabbit

  • Access Control: Global statistics are now restricted to administrator access only. Other users will receive an access denied message when attempting to view this data.
  • Tests: Updated security and access control verification tests.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

The PR restricts access to public.global_stats from permissive anonymous read to admin-only read access. A database migration updates RLS policies to deny non-admin users, while tests verify the new permission constraints across anonymous, authenticated non-admin, and authenticated admin user roles.

Changes

Cohort / File(s) Summary
Database Migration
supabase/migrations/20260227000000_restrict_global_stats_public_access.sql
Removes permissive anonymous read policy on global_stats table and replaces it with admin-only read policy. Revokes privileges from anon and authenticated roles, then re-grants SELECT access only to authenticated users meeting admin criteria.
RLS Policy Tests
supabase/tests/26_test_rls_policies.sql, supabase/tests/27_test_rls_scenarios.sql
Updates test expectations and scenarios to verify the new restricted access model. Test 26 updates policy name expectation; Test 27 expands from single anonymous-access test into three distinct permission checks for anonymous, authenticated non-admin, and authenticated admin users.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Suggested labels

💰 Rewarded

Poem

🐰 A policy refined, anon access banned,
Only admins now access this land,
Tests confirm the rules are tight,
Permission denied, except when right.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: restricting global_stats access to platform admins, which aligns with the migration and test updates in the changeset.
Description check ✅ Passed The description includes all required sections (Summary, Test plan, Screenshots, Checklist) with relevant details about changes and testing status, though some checklist items remain unchecked.
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-global-stats

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/20260227000000_restrict_global_stats_public_access.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

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66390bc97b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@@ -0,0 +1,27 @@
-- =============================================================================

Choose a reason for hiding this comment

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

P1 Badge Rename migration to a unique timestamp

This migration uses the 20260226000000 version prefix, but that prefix is already used by 20260226000000_org_rls_require_self_2fa_update.sql; Supabase migration ordering/history is version-based, so this collision can make one migration unapplied or non-deterministic across environments, which risks leaving global_stats access policy changes out of sync.

Useful? React with 👍 / 👎.

Comment on lines +72 to +76
throws_ok (
'SELECT COUNT(*) FROM public.global_stats',
'42501',
'permission denied',
'Authenticated non-admin users should not be able to select from global_stats'

Choose a reason for hiding this comment

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

P2 Badge Check non-admin access with row visibility, not throws

This assertion expects authenticated non-admin reads to raise 42501, but the migration grants SELECT to authenticated and enforces admin-only visibility via RLS, which yields an empty result set rather than a permission error; as written, this test will fail even when the policy is working as intended.

Useful? React with 👍 / 👎.

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: 2

🤖 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/20260227000000_restrict_global_stats_public_access.sql`:
- Around line 25-27: The grant/revoke setup on table public.global_stats creates
asymmetric behavior: anon lacks SELECT and gets error 42501 while authenticated
has SELECT but is blocked by the RLS policy and therefore returns an empty
result (not an error), causing Test 4b in the RLS scenarios to fail; fix by
either adjusting the test expectation for authenticated non-admin users in the
test named Test 4b of the RLS scenarios to assert is_empty() instead of
throws_ok(42501), or change the privilege setup in the migration
(public.global_stats) to revoke SELECT from authenticated entirely and grant
SELECT only to a specific role that admins have so the test can continue to
expect a 42501 permission denied error.

In `@supabase/tests/27_test_rls_scenarios.sql`:
- Around line 67-77: The test currently sets "SET LOCAL role TO authenticated"
and calls throws_ok('SELECT COUNT(*) FROM public.global_stats', ...) expecting
error 42501, but RLS grants SELECT and returns zero rows (no error) and
auth.uid() is NULL because no JWT claims are set; update the test to assert
empty results instead of expecting an error (replace throws_ok with an
emptiness/count check such as is_empty or assert count = 0 for the query 'SELECT
COUNT(*) FROM public.global_stats') and before running the query set the JWT
claims/user context so auth.uid() is populated (e.g., set appropriate jwt.claims
or session context for a non-admin user) to mirror the intended non-admin
scenario rather than relying only on SET LOCAL role TO authenticated.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ab763b and 655ddee.

📒 Files selected for processing (3)
  • supabase/migrations/20260227000000_restrict_global_stats_public_access.sql
  • supabase/tests/26_test_rls_policies.sql
  • supabase/tests/27_test_rls_scenarios.sql

Comment on lines +25 to +27
-- Remove table privileges for low-trust roles.
REVOKE ALL PRIVILEGES ON TABLE public.global_stats FROM anon, authenticated;
GRANT SELECT ON TABLE public.global_stats TO authenticated;
Copy link
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:

# First, check if the migration file exists and read it
fd "20260227000000_restrict_global_stats_public_access.sql" supabase/migrations/

Repository: Cap-go/capgo

Length of output: 130


🏁 Script executed:

# Check the test file mentioned in the review
fd "27_test_rls_scenarios.sql" supabase/tests/

Repository: Cap-go/capgo

Length of output: 96


🏁 Script executed:

# Search for is_admin function definition
rg -n "CREATE.*FUNCTION.*is_admin" supabase/migrations/ -A 20

Repository: Cap-go/capgo

Length of output: 6595


🏁 Script executed:

# Look for the global_stats table and any RLS policies
rg -n "global_stats" supabase/migrations/ -B 5 -A 10

Repository: Cap-go/capgo

Length of output: 32179


🏁 Script executed:

# Read the full migration file
cat -n supabase/migrations/20260227000000_restrict_global_stats_public_access.sql

Repository: Cap-go/capgo

Length of output: 1279


🏁 Script executed:

# Read the test file to check Test 4b expectations
cat -n supabase/tests/27_test_rls_scenarios.sql

Repository: Cap-go/capgo

Length of output: 5134


🏁 Script executed:

# Check if there are more details about the test setup
wc -l supabase/tests/27_test_rls_scenarios.sql

Repository: Cap-go/capgo

Length of output: 100


Test expectation mismatch: authenticated non-admin users will receive empty results, not error 42501.

The privilege and policy configuration creates asymmetric behavior:

  • anon → REVOKE removes SELECT privilege → error 42501 (permission denied)
  • authenticated non-admin → GRANT SELECT grants privilege, but RLS policy denies access → returns 0 rows (not an error)

The test at lines 67-77 in supabase/tests/27_test_rls_scenarios.sql (Test 4b) expects throws_ok with error 42501 for authenticated non-admin users, but will fail because non-admin authenticated users will receive an empty result set instead of an error. RLS policies return empty results when a role has the base privilege but fails the policy condition, not an error.

Either update Test 4b to use is_empty() for non-admin authenticated users, or revoke SELECT from authenticated entirely and grant it only to a role that admins possess.

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

In `@supabase/migrations/20260227000000_restrict_global_stats_public_access.sql`
around lines 25 - 27, The grant/revoke setup on table public.global_stats
creates asymmetric behavior: anon lacks SELECT and gets error 42501 while
authenticated has SELECT but is blocked by the RLS policy and therefore returns
an empty result (not an error), causing Test 4b in the RLS scenarios to fail;
fix by either adjusting the test expectation for authenticated non-admin users
in the test named Test 4b of the RLS scenarios to assert is_empty() instead of
throws_ok(42501), or change the privilege setup in the migration
(public.global_stats) to revoke SELECT from authenticated entirely and grant
SELECT only to a specific role that admins have so the test can continue to
expect a 42501 permission denied error.

Comment on lines +67 to +77
-- Test 4b: Authenticated users should not be able to read global_stats
SET
LOCAL role TO authenticated;

SELECT
throws_ok (
'SELECT COUNT(*) FROM public.global_stats',
'42501',
'permission denied',
'Authenticated non-admin users should not be able to select from global_stats'
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Test 4b will fail: authenticated non-admin users get empty results, not an error.

The migration grants SELECT to authenticated users, so they won't receive a "permission denied" error. Instead, the RLS policy will evaluate to FALSE for non-admins, returning 0 rows. The throws_ok assertion expects error 42501, which won't be thrown.

Additionally, no JWT claims are set before this test, so auth.uid() returns NULL.

🧪 Proposed fix: use is_empty instead of throws_ok
 -- Test 4b: Authenticated users should not be able to read global_stats
 SET
   LOCAL role TO authenticated;

+SET
+  LOCAL request.jwt.claims TO '{"sub": "6aa76066-55ef-4238-ade6-0b32334a4097"}';
+
 SELECT
-  throws_ok (
-    'SELECT COUNT(*) FROM public.global_stats',
-    '42501',
-    'permission denied',
+  is_empty (
+    'SELECT * FROM public.global_stats',
     'Authenticated non-admin users should not be able to select from global_stats'
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- Test 4b: Authenticated users should not be able to read global_stats
SET
LOCAL role TO authenticated;
SELECT
throws_ok (
'SELECT COUNT(*) FROM public.global_stats',
'42501',
'permission denied',
'Authenticated non-admin users should not be able to select from global_stats'
);
-- Test 4b: Authenticated users should not be able to read global_stats
SET
LOCAL role TO authenticated;
SET
LOCAL request.jwt.claims TO '{"sub": "6aa76066-55ef-4238-ade6-0b32334a4097"}';
SELECT
is_empty (
'SELECT * FROM public.global_stats',
'Authenticated non-admin users should not be able to select from global_stats'
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/tests/27_test_rls_scenarios.sql` around lines 67 - 77, The test
currently sets "SET LOCAL role TO authenticated" and calls throws_ok('SELECT
COUNT(*) FROM public.global_stats', ...) expecting error 42501, but RLS grants
SELECT and returns zero rows (no error) and auth.uid() is NULL because no JWT
claims are set; update the test to assert empty results instead of expecting an
error (replace throws_ok with an emptiness/count check such as is_empty or
assert count = 0 for the query 'SELECT COUNT(*) FROM public.global_stats') and
before running the query set the JWT claims/user context so auth.uid() is
populated (e.g., set appropriate jwt.claims or session context for a non-admin
user) to mirror the intended non-admin scenario rather than relying only on SET
LOCAL role TO authenticated.

@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.

1 participant