Skip to content

Restrict record_build_time RPC access#1712

Closed
riderx wants to merge 23 commits intomainfrom
riderx/fix-rpc-build-log-auth
Closed

Restrict record_build_time RPC access#1712
riderx wants to merge 23 commits intomainfrom
riderx/fix-rpc-build-log-auth

Conversation

@riderx
Copy link
Member

@riderx riderx commented Feb 27, 2026

Summary (AI generated)

  • Added a follow-up backend test hardening update in tests/build_time_tracking.test.ts.
  • Converted build-time tracking test cases in this file to it.concurrent() to align with the repo’s parallel test guidance.
  • Added an explicit regression test asserting record_build_time is rejected for authenticated callers, matching the permission posture enforced by the migration.

Motivation (AI generated)

  • A security-sensitive RPC previously reachable by anon was also vulnerable through authenticated sessions because permission changes revoke both anon and authenticated; adding dedicated regression coverage ensures both paths remain blocked and prevents future privilege regression.

Business Impact (AI generated)

  • Protects billing/usage integrity by preventing unauthorized write access to build usage logs via record_build_time from either unauthenticated or authenticated API-key clients.
  • Strengthens CI regression coverage so permission drift is caught before deployment.

Test Plan (AI generated)

  • bun lint
  • Reviewed git diff origin/main...HEAD for only the expected migration and backend test updates
  • Full backend test suite for build_time_tracking (not run in this pass)

Screenshots

  • Not applicable (backend/database + test updates only).

Checklist

  • My code follows the code style of this project and passes bun run lint:backend && bun run lint.
  • 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

Generated with AI

Summary by CodeRabbit

  • Security

    • Restricted access to build-time tracking to service-role only; anonymous and standard user sessions can no longer invoke it.
  • Tests

    • Added coverage confirming unauthorized (anonymous/authenticated) calls are rejected and negative build times are rejected; test suite expanded and RPC-related tests now run concurrently.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Warning

Rate limit exceeded

@riderx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 28 minutes and 40 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 73a08df and 011b41d.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (74)
  • .gitsecret/paths/mapping.cfg
  • internal/AuthKey_8P7Y3V99PJ.p8.secret
  • internal/CICD.mobileprovision.secret
  • internal/Certificates.p12.secret
  • internal/Certificates_p12.p12.secret
  • internal/capgo-394818-68ad1517d330.json.secret
  • internal/cloudflare/.env.local.secret
  • internal/cloudflare/.env.preprod.secret
  • internal/cloudflare/.env.prod.secret
  • internal/forgr-key.jks.base64.secret
  • internal/forgr-key.jks.secret
  • internal/how-to-deploy.md.secret
  • internal/supabase/.env.local.secret
  • messages/de.json
  • messages/en.json
  • messages/es.json
  • messages/fr.json
  • messages/hi.json
  • messages/id.json
  • messages/it.json
  • messages/ja.json
  • messages/ko.json
  • messages/pl.json
  • messages/pt-br.json
  • messages/ru.json
  • messages/tr.json
  • messages/vi.json
  • messages/zh-cn.json
  • package.json
  • read_replicate/update_readreplica_passwords.sh
  • src/components/dashboard/DemoOnboardingGate.vue
  • src/components/dashboard/DemoOnboardingModal.vue
  • src/modules/auth.ts
  • src/modules/i18n.ts
  • src/pages/delete_account.vue
  • src/pages/resend_email.vue
  • src/pages/settings/organization/Security.vue
  • src/types/supabase.types.ts
  • supabase/functions/_backend/private/role_bindings.ts
  • supabase/functions/_backend/public/apikey/post.ts
  • supabase/functions/_backend/public/build/request.ts
  • supabase/functions/_backend/public/build/upload.ts
  • supabase/functions/_backend/public/organization/members/delete.ts
  • supabase/functions/_backend/utils/cloudflare.ts
  • supabase/functions/_backend/utils/supabase.ts
  • supabase/functions/_backend/utils/supabase.types.ts
  • supabase/functions/_backend/utils/version.ts
  • supabase/migrations/20250530233128_base.sql
  • supabase/migrations/20260225120000_restrict_webhooks_select_for_admin_only.sql
  • supabase/migrations/20260226090000_require_verified_email_for_delete_user.sql
  • supabase/migrations/20260226153000_restrict_apikey_oracle_rpcs.sql
  • supabase/migrations/20260227000000_fix_rescind_invitation_rpc_access.sql
  • supabase/migrations/20260227000001_secure_record_build_time_rpc.sql
  • supabase/migrations/20260227010000_restrict_upsert_version_meta_exec.sql
  • supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql
  • supabase/migrations/20260228000000_role_bindings_rls_assignable.sql
  • supabase/migrations/20260228000100_delete_member_cascade_bindings.sql
  • supabase/migrations/20260228000200_prevent_last_super_admin_delete.sql
  • supabase/migrations/20260228000300_fix_apikey_hashed_lookup.sql
  • supabase/migrations/20260228172308_fix_prevent_last_super_admin_cascade.sql
  • supabase/migrations/20260228172309_fix_rbac_test_compatibility.sql
  • supabase/migrations/20260302000000_rbac_default_for_new_orgs.sql
  • supabase/migrations/20260302185011_fix_rbac_check_effective_user.sql
  • supabase/schemas/prod.sql
  • supabase/seed.sql
  • supabase/tests/28_test_new_migration_functions.sql
  • supabase/tests/34_test_rbac_rls.sql
  • supabase/tests/35_test_is_admin_rbac.sql
  • supabase/tests/37_test_check_min_rights_2fa_enforcement.sql
  • supabase/tests/40_test_password_policy_enforcement.sql
  • tests/builder-payload.unit.test.ts
  • tests/organization-api.test.ts
  • tests/password-policy.test.ts
  • tests/private-error-cases.test.ts
📝 Walkthrough

Walkthrough

Migration tightens execution privileges on the public.record_build_time function (revokes PUBLIC/anon/authenticated; grants only to service_role). Tests updated to run many RPC cases concurrently and add checks that anonymous and authenticated clients are rejected and that negative build_time is rejected.

Changes

Cohort / File(s) Summary
Database Access Control
supabase/migrations/20260227091500_restrict_record_build_time_access.sql
Revokes all privileges on public.record_build_time(...) from PUBLIC, "anon", and "authenticated"; grants EXECUTE only to "service_role".
Test suite: build time tracking
tests/build_time_tracking.test.ts
Converts multiple tests to run concurrently, adds tests asserting anonymous and authenticated clients are rejected when calling the RPC, adds explicit negative build_time rejection test, and updates imports to use createClient and USER_PASSWORD.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Suggested labels

💰 Rewarded

Poem

🐰
A guarded burrow, quiet and bright,
Service-role holds the lantern's light.
Anonymous hops turn away,
Tests thump the ground to prove their say.
Hoppy patch — sleep safe tonight. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately and concisely summarizes the main security-focused change: restricting access to the record_build_time RPC function.
Description check ✅ Passed The PR description comprehensively covers the summary, motivation, business impact, test plan, and checklist sections from the template, with clear explanations of the security changes and their implications.
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-build-log-auth

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: 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 `@tests/build_time_tracking.test.ts`:
- Around line 348-366: Add a second test next to the anonymous denial case that
calls the same RPC record_build_time but using an authenticated Supabase client
instead of supabaseAnon: create a client via
createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!
or the project API key that maps to the authenticated role) with
auth.persistSession: false, invoke rpc('record_build_time', {...}) with the same
params (ORG_ID, USER_ID, randomUUID(), 'ios', 60) and assert the returned error
is truthy; place the test near the existing one so it covers the revoked
authenticated role path.
- Line 348: Replace all synchronous test declarations using it(...) with
parallel tests using it.concurrent(...) throughout the file; specifically update
the eight test cases including the one with description "should reject
record_build_time calls from anonymous clients" and the other tests at the
listed locations (e.g., tests with their respective descriptions on lines 138,
201, 254, 277, 300, 334, 368) so each call to it becomes it.concurrent,
preserving the test names and callback functions (no changes to assertions or
async handling).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e719357 and f3f105e.

📒 Files selected for processing (2)
  • supabase/migrations/20260227091500_restrict_record_build_time_access.sql
  • tests/build_time_tracking.test.ts

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/build_time_tracking.test.ts (2)

7-10: ⚠️ Potential issue | 🟠 Major

Use per-test fixture identifiers instead of module-level shared constants.

All 9 tests in this describe block run concurrently (it.concurrent()), but lines 7–10 define a single shared ORG_ID and APPNAME that all tests reference. Although beforeEach() attempts to reset these rows (lines 62–120), concurrent execution means test A and test B can modify and read each other's data simultaneously, causing cross-test interference and flakes.

Per the coding guidelines, create dedicated seed data with unique identifiers per test rather than sharing module-level fixtures. Refactor to generate scoped IDs per test (e.g., using test name or UUID suffix) and instantiate fixtures within each test's scope instead of in beforeEach().

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

In `@tests/build_time_tracking.test.ts` around lines 7 - 10, The tests currently
use module-level shared constants (testRunId, ORG_ID, APPNAME,
STRIPE_CUSTOMER_ID) which causes cross-test interference when tests run
concurrently; change to generate unique identifiers per test by moving UUID
generation into each test scope (or into a beforeEach that assigns to
test-scoped variables) and instantiate seed data/fixtures (org/app/stripe
customer) inside each test using those per-test IDs, updating all references to
ORG_ID, APPNAME, STRIPE_CUSTOMER_ID and any setup/teardown that uses testRunId
so each test operates on its own isolated data.

305-312: ⚠️ Potential issue | 🟠 Major

Assert the first RPC call result in the upsert test.

The first record_build_time call (lines 305-312) does not capture or assert the error result. If that call fails, the test continues, and the second call can still succeed and update the record, causing the final assertion to pass despite the first insert failing. Upsert behavior cannot be properly validated without checking both calls.

Proposed fix
     // First call
-    await supabase.rpc('record_build_time', {
+    const { error: firstRpcError } = await supabase.rpc('record_build_time', {
       p_org_id: ORG_ID,
       p_user_id: USER_ID,
       p_build_id: buildId,
       p_platform: 'ios',
       p_build_time_unit: 600,
     })
+    expect(firstRpcError).toBeFalsy()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/build_time_tracking.test.ts` around lines 305 - 312, Capture the result
of the first supabase.rpc('record_build_time', {...}) call into a variable
(e.g., insertRes) and assert its error is null (and/or check expected data)
before making the second RPC call; then likewise capture and assert the second
call result so the test actually verifies both the initial insert and the
subsequent upsert. Refer to the rpc call invocation and use the result variable
names around record_build_time to locate where to add the assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/build_time_tracking.test.ts`:
- Line 4: The named imports on the import line are out of alphabetical order and
violate perfectionist/sort-named-imports; reorder the specifiers in the import
from './test-utils.ts' so they are sorted alphabetically (e.g., fetchWithRetry,
getEndpointUrl, getSupabaseClient, PRODUCT_ID, resetAppData, resetAppDataStats,
TEST_EMAIL, USER_ID, USER_PASSWORD) to satisfy the linter rule for the import
statement.
- Around line 365-366: Replace the weak truthy assertions in the two
auth-rejection tests with explicit permission-denied checks: in the anonymous
client rejection test and the authenticated client rejection test in
tests/build_time_tracking.test.ts, assert that the caught error indicates
permission denial (for example, expect(error.message).toContain("permission
denied for function") or expect((error as any).code).toBe("permission-denied")),
so the tests verify RLS semantics rather than any generic failure.

---

Outside diff comments:
In `@tests/build_time_tracking.test.ts`:
- Around line 7-10: The tests currently use module-level shared constants
(testRunId, ORG_ID, APPNAME, STRIPE_CUSTOMER_ID) which causes cross-test
interference when tests run concurrently; change to generate unique identifiers
per test by moving UUID generation into each test scope (or into a beforeEach
that assigns to test-scoped variables) and instantiate seed data/fixtures
(org/app/stripe customer) inside each test using those per-test IDs, updating
all references to ORG_ID, APPNAME, STRIPE_CUSTOMER_ID and any setup/teardown
that uses testRunId so each test operates on its own isolated data.
- Around line 305-312: Capture the result of the first
supabase.rpc('record_build_time', {...}) call into a variable (e.g., insertRes)
and assert its error is null (and/or check expected data) before making the
second RPC call; then likewise capture and assert the second call result so the
test actually verifies both the initial insert and the subsequent upsert. Refer
to the rpc call invocation and use the result variable names around
record_build_time to locate where to add the assertions.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3f105e and 73a08df.

📒 Files selected for processing (1)
  • tests/build_time_tracking.test.ts

import { createClient } from '@supabase/supabase-js'
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import { fetchWithRetry, getEndpointUrl, getSupabaseClient, PRODUCT_ID, resetAppData, resetAppDataStats, TEST_EMAIL, USER_ID } from './test-utils.ts'
import { USER_PASSWORD, fetchWithRetry, getEndpointUrl, getSupabaseClient, PRODUCT_ID, resetAppData, resetAppDataStats, TEST_EMAIL, USER_ID } from './test-utils.ts'
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 | 🟡 Minor

Fix named-import order to satisfy ESLint.

Line 4 currently violates perfectionist/sort-named-imports, which will keep lint red.

Proposed fix
-import { USER_PASSWORD, fetchWithRetry, getEndpointUrl, getSupabaseClient, PRODUCT_ID, resetAppData, resetAppDataStats, TEST_EMAIL, USER_ID } from './test-utils.ts'
+import { fetchWithRetry, getEndpointUrl, getSupabaseClient, PRODUCT_ID, resetAppData, resetAppDataStats, TEST_EMAIL, USER_ID, USER_PASSWORD } from './test-utils.ts'
📝 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
import { USER_PASSWORD, fetchWithRetry, getEndpointUrl, getSupabaseClient, PRODUCT_ID, resetAppData, resetAppDataStats, TEST_EMAIL, USER_ID } from './test-utils.ts'
import { fetchWithRetry, getEndpointUrl, getSupabaseClient, PRODUCT_ID, resetAppData, resetAppDataStats, TEST_EMAIL, USER_ID, USER_PASSWORD } from './test-utils.ts'
🧰 Tools
🪛 ESLint

[error] 4-4: Expected "fetchWithRetry" to come before "USER_PASSWORD".

(perfectionist/sort-named-imports)

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

In `@tests/build_time_tracking.test.ts` at line 4, The named imports on the import
line are out of alphabetical order and violate perfectionist/sort-named-imports;
reorder the specifiers in the import from './test-utils.ts' so they are sorted
alphabetically (e.g., fetchWithRetry, getEndpointUrl, getSupabaseClient,
PRODUCT_ID, resetAppData, resetAppDataStats, TEST_EMAIL, USER_ID, USER_PASSWORD)
to satisfy the linter rule for the import statement.

Comment on lines +365 to +366
expect(error).toBeTruthy()
})
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:

cat -n tests/build_time_tracking.test.ts | sed -n '350,410p'

Repository: Cap-go/capgo

Length of output: 1940


🏁 Script executed:

find . -type f -name "*.sql" | xargs grep -l "record_build_time" | head -5

Repository: Cap-go/capgo

Length of output: 223


🏁 Script executed:

cat ./supabase/migrations/20260227091500_restrict_record_build_time_access.sql

Repository: Cap-go/capgo

Length of output: 1032


🏁 Script executed:

grep -n "permission\|Permission\|PERMISSION" tests/build_time_tracking.test.ts

Repository: Cap-go/capgo

Length of output: 38


🏁 Script executed:

find tests -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "error.*message" | head -3

Repository: Cap-go/capgo

Length of output: 133


🏁 Script executed:

grep -n "error.*message" tests/bundle.test.ts | head -10

Repository: Cap-go/capgo

Length of output: 132


🏁 Script executed:

grep -n "\.rpc(" tests/*.test.ts | head -10

Repository: Cap-go/capgo

Length of output: 1057


🏁 Script executed:

cat -n tests/build_time_tracking.test.ts | sed -n '200,220p'

Repository: Cap-go/capgo

Length of output: 1045


🏁 Script executed:

cat -n tests/build_time_tracking.test.ts | sed -n '340,370p'

Repository: Cap-go/capgo

Length of output: 1123


🌐 Web query:

Supabase RLS policy violation error message format

💡 Result:

Supabase’s “RLS policy violation” errors usually come from PostgREST, so the response body follows the PostgreSQL error shape:

{
  "code": "42501",
  "details": null,
  "hint": null,
  "message": "new row violates row-level security policy for table \"<table>\""
}

This is commonly returned with HTTP 403 Forbidden (or 401 if unauthenticated) because 42501 maps to “insufficient privileges.” [1], [2]

For Storage endpoints, you may instead (or additionally) see Supabase’s gateway-style envelope like:

{
  "statusCode": "403",
  "error": "Unauthorized",
  "message": "new row violates row-level security policy"
}

[3], [4]

Sources: PostgREST error format + HTTP mapping [1]; Supabase/PostgREST example payload [2]; Storage-style envelope examples [3], [4].

[1] (docs.postgrest.org)
[2] (github.com)
[3] (reddit.com)
[4] (github.com)

Citations:


🌐 Web query:

Supabase PostgreSQL REVOKE EXECUTE function permission denied error message

💡 Result:

In Supabase (Postgres), the error:

ERROR: permission denied for function <fn>

means the role you’re using (often anon/authenticated via PostgREST) does not have EXECUTE on that function. By default, functions are executable by PUBLIC (so everyone can run them) unless you revoke it. (supabase.com)

Revoke EXECUTE (the Supabase-recommended pattern)

You typically need both revokes (because PUBLIC is the default grant path): (supabase.com)

revoke execute on function public.foo() from public;
revoke execute on function public.foo() from anon;          -- and/or authenticated

(Include the exact signature foo(argtype, ...) if the function is overloaded.)

If you still get “permission denied” after GRANT/REVOKE

Make sure the caller role also has USAGE on the schema that contains the function (or any function it calls); otherwise you can still see permission errors in SECURITY DEFINER / nested-function setups. (stackoverflow.com)

grant usage on schema public to anon, authenticated;

Prevent future functions from being executable by default

Use default privileges (note: revoke at the “global” level, not per-schema). (stackoverflow.com)

alter default privileges for role postgres
  revoke execute on functions from public;

alter default privileges for role postgres
  revoke execute on functions from anon;

If the REVOKE command itself fails

You must run REVOKE as the function owner (or a sufficiently privileged/admin role). On Supabase, the API commonly runs as anon/authenticated, not as an admin role. (supabase.com)

Citations:


Assert permission-denied semantics explicitly in auth-rejection tests.

Lines 365-366 and 392-393 only assert that error is truthy, which passes for any error (network failures, validation errors, etc.). To ensure these tests guard the migration intent, explicitly check for permission-denied semantics:

Proposed fix
-    expect(error).toBeTruthy()
+    expect(error).toBeTruthy()
+    expect((error?.message ?? '').toLowerCase()).toContain('permission')

Apply to both the anonymous client rejection test (line 365-366) and authenticated client rejection test (line 392-393).

This validates that the RLS policy correctly denies function execution with a "permission denied for function" error, rather than allowing the test to pass from unrelated failures.

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

In `@tests/build_time_tracking.test.ts` around lines 365 - 366, Replace the weak
truthy assertions in the two auth-rejection tests with explicit
permission-denied checks: in the anonymous client rejection test and the
authenticated client rejection test in tests/build_time_tracking.test.ts, assert
that the caught error indicates permission denial (for example,
expect(error.message).toContain("permission denied for function") or
expect((error as any).code).toBe("permission-denied")), so the tests verify RLS
semantics rather than any generic failure.

@riderx riderx closed this Mar 3, 2026
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 3, 2026

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