Read the hub document count with a credential that has privileges - #2512
Conversation
…a credential that has privileges The hub's document count could never load. `environment-facts.ts` counted `public.documents` through the cookie-bound user client, on the reasoning that the `documents owner read` policy would scope it in the database. It cannot: `schema.sql:5299` revokes all `public` table privileges from `anon` and `authenticated` and grants that table to `service_role` only, migration `20260725000000` re-applies the revoke after every earlier grant, and no later migration restores it -- the only `to authenticated` hits after that date are RLS policies. A policy cannot hand back an SQL SELECT privilege the role does not hold, so the read returned permission denied on every hub load and the strip could only ever render "document count unavailable". It was invisible because the module degrades a failed read to `null` by design and its tests mock the client, so nothing on either side could see it. It surfaced only because the corpus-health panel copied this module as its model and hit the same wall, where Codex caught it on #2504. Same shape as the corpus-health fix that already landed on this branch: the cookie-bound client identifies the caller and reads no table, the caller must carry the administrator claim `DeveloperAreaGate` checks, and the count filters on `owner_id` explicitly. That filter is the whole owner-scoping guarantee now, not an addition to row-level security. A non-administrator still gets their email back rather than a blank strip, since it was already read and does not depend on the count. The source assertion named "reads through the user-session client and never the service-role client" was backwards and is replaced rather than dropped: the tests now assert the issued query carries `owner_id` equal to the caller, and that a non-administrator issues no query at all. Every existing test that pins null-not-zero and the rejection guard is kept unchanged. Proven by mutation: five rules broken on purpose, five reds, no survivors, file restored byte-identical by SHA-256. Reverting the read to the user client is one of them, so the regression that caused this cannot come back silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XG7wQurapeZwWRsNhHA1PY
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_33018e8e-aa4d-48a6-bbb2-0fb9c970ffa4) |
CI triageCI failed on this PR. Automated classification of the 2 failed job(s):
Compared with main CI run #14748 (success). That run's conclusion is an aggregate and did not exercise Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger. |
|
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: 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. Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_67d29c01-06b1-42ce-a1c5-4ccf64c955a5) |
Summary
environment-facts.tscountedpublic.documentsthrough the cookie-bound user client, on the reasoning that thedocuments owner readpolicy (owner_id = auth.uid()) would scope it in the database. It cannot, so the read returned permission denied on every hub load and the environment strip could only ever render "document count unavailable".DeveloperAreaGatechecks, and the count filters onowner_idexplicitly.Why the old read could not succeed
supabase/schema.sql:5299runsrevoke all privileges on all tables in schema public from anon, authenticated;, and the grant block below it namespublic.documentsforservice_roleonly.supabase/migrations/20260725000000_audit_security_remediation.sql:81re-applies that same blanket revoke, after every earliergrant select … to authenticated. No migration after20260725000000restores tableSELECTtoauthenticated— the onlyto authenticatedhits in later migrations are RLS policies in20260823090000_user_favourite_sets.sql.Row-level security cannot restore a missing SQL
SELECTprivilege, so the policy sat behind a privilege theauthenticatedrole does not hold.Why nobody saw it. The module degrades a failed read to
nullby design, and its tests mock the Supabase client, so both halves reported exactly what a healthy empty result looks like. It surfaced only because the corpus-health panel copied this module as its model and hit the same wall, where Codex caught it as a P1 on #2504.What changed
The owner filter is now the entire owner-scoping guarantee rather than an addition to row-level security, because the service-role client is not subject to those policies. A non-administrator still gets their email back rather than a blank strip, since it was already read and does not depend on the count. Every failure path still returns
nulland never0, and the rejection guard is unchanged.The test named "reads through the user-session client and never the service-role client" was backwards. It is replaced by two assertions — that the issued query carries
owner_idequal to the caller's id, and that a non-administrator issues no query at all — rather than deleted quietly. Every existing test pinning null-not-zero and the rejection guard is kept unchanged.Verification
npm run verify:pr-local— green on this branch, rebuilt onmainat164859d6a:npm run verify:ui— not applicable; no component, route, style or browser behaviour changes. The only rendered difference is that a number the strip could never fetch can now appear in place of "document count unavailable".npm run check:production-readiness— provider-backed, and not run. This adds no migration, no environment variable, and no new access path; it changes which existing server-side credential performs a read the app already made.Proven by mutation, not by a passing test
Each rule was broken on purpose, the specific test watched go red, then the file restored and confirmed byte-identical by SHA-256. Five mutations, five reds, no survivors:
owner_idfilter → "filters the count by the caller's own owner id" red.0instead ofnull→ "keeps an empty corpus distinct from a count it could not read" red.Risk and rollout
DeveloperAreaGate. The realistic failure would be a count that is not owner-scoped, which mutation 1 exists to prevent.head: truecount the hub already issued, now through the service-role client with an explicit owner filter.Notes
corpus-health.tsand recorded thatenvironment-facts.tsstill carried it; the branch was deleted when Show which documents finished indexing and produced nothing usable #2504 merged, so this is a fresh branch offmaincarrying only the one unmerged commit.🤖 Generated with Claude Code
https://claude.ai/code/session_01XG7wQurapeZwWRsNhHA1PY
Generated by Claude Code
Note
Medium Risk
Uses the service-role client for a read; scoping now depends entirely on the explicit
owner_idfilter and administrator gate rather than RLS, so regressions could leak or mis-scope counts if those checks are removed.Overview
Fixes the developer hub environment strip so the document count can load instead of always showing unavailable. The count no longer goes through the cookie-bound Supabase user client (which lacked
SELECTonpublic.documentsand always failed with permission denied, silently degraded tonull).resolveHubEnvironmentFactsnow mirrors the corpus-health pattern (#2504): session client only resolves the user; administrators get a service-roleheadcount ondocumentswith an explicitowner_idfilter. Signed-in non-admins still receive email but skip the count query. Failed reads still returnnull, not0.Tests split user vs admin client mocks, require administrator
app_metadatafor count cases, assertowner_idon the query, and add coverage that non-admins issue no count — replacing the old (incorrect) assertion that forbade the admin client.Reviewed by Cursor Bugbot for commit b67ad3f. Configure here.