Skip to content

security(db): enable RLS on all public tables - #131

Open
jon-bell wants to merge 4 commits into
mainfrom
security/enable-rls
Open

security(db): enable RLS on all public tables#131
jon-bell wants to merge 4 commits into
mainfrom
security/enable-rls

Conversation

@jon-bell

@jon-bell jon-bell commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Supabase's PostgREST auto-exposes every table in the public schema over HTTP, authorized by the public anon key that ships to every browser. With RLS disabled, anyone holding that key could read or modify all rows. This migration closes that hole.

  • Enables RLS on every base table in public via a loop, so current and future orphan tables (e.g. generations, dropped from the Drizzle schema when the generation panel was removed) are locked to default-deny rather than silently exposed.
  • Adds owner-scoped authenticated policies to the known application tables, rooted at workspaces.user_id = auth.uid()::text; children inherit via workspace_id or chart_id -> charts -> workspace.
  • workshops and orphan tables get RLS with no policy (server/admin only).
  • No FORCE, so the postgres (Drizzle) and service_role (tests) roles keep bypassing — the app and test suite are unaffected.

Due to the nature of this change, this is currently deployed.

Summary by CodeRabbit

  • Security Enhancements
    • Enabled stronger row-level access controls so users can only read and modify data tied to their own workspaces.
    • Access is denied by default for tables without explicit rules, reducing the chance of unintended exposure.
    • Workspace-linked resources (charts, configs, documents, views, and lens runs) now require ownership checks for reads and writes.
    • Workshop-related access is tightened to limit standard client access unless explicitly authorized.
  • Testing
    • Expanded integration coverage for cross-user isolation to ensure unauthorized access is rejected.

Supabase's PostgREST auto-exposes every table in the `public` schema over
HTTP, authorized by the public anon key that ships to every browser. With
RLS disabled, anyone holding that key could read or modify all rows
(workspaces, prompts, documents, prolific study IDs, and the orphaned
`generations` table). This migration closes that hole.

- Enables RLS on every base table in `public` via a loop, so current and
  future orphan tables (e.g. `generations`, dropped from the Drizzle schema
  when the generation panel was removed) are locked to default-deny rather
  than silently exposed.
- Adds owner-scoped `authenticated` policies to the known application tables,
  rooted at `workspaces.user_id = auth.uid()::text`; children inherit via
  `workspace_id` or `chart_id -> charts -> workspace`.
- `workshops` and orphan tables get RLS with no policy (server/admin only).
- No FORCE, so the `postgres` (Drizzle) and `service_role` (tests) roles keep
  bypassing — the app and test suite are unaffected.

Verified against real Postgres (PGlite): anon sees nothing, an authenticated
user sees only their own rows, cross-owner writes are blocked, service_role
bypasses, and rowsecurity is on for all tables.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workbench Ready Ready Preview, Comment Jul 17, 2026 11:22pm

Request Review

@jon-bell jon-bell self-assigned this Jul 16, 2026
@jon-bell
jon-bell requested a review from AdamBelfki3 July 16, 2026 20:33
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3bad74e8-0c1e-41f2-bae4-a5bdde3397b1

📥 Commits

Reviewing files that changed from the base of the PR and between e19ca18 and 6d5b9aa.

📒 Files selected for processing (10)
  • supabase/migrations/20260716192819_enable_rls.sql
  • workbench/_web/src/app/workbench/components/WorkspaceList.tsx
  • workbench/_web/src/db/__tests__/lensRuns.test.ts
  • workbench/_web/src/db/__tests__/local-db.test.ts
  • workbench/_web/src/lib/api/workspaceApi.ts
  • workbench/_web/src/lib/auth/ownership.ts
  • workbench/_web/src/lib/queries/chartQueries.ts
  • workbench/_web/src/lib/queries/configQueries.ts
  • workbench/_web/src/lib/queries/documentQueries.ts
  • workbench/_web/src/lib/queries/internal.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • workbench/_web/src/lib/queries/configQueries.ts
  • workbench/_web/src/app/workbench/components/WorkspaceList.tsx
  • workbench/_web/src/lib/queries/documentQueries.ts
  • supabase/migrations/20260716192819_enable_rls.sql
  • workbench/_web/src/lib/auth/ownership.ts
  • workbench/_web/src/db/tests/local-db.test.ts
  • workbench/_web/src/lib/queries/chartQueries.ts

📝 Walkthrough

Walkthrough

Changes

The change replaces caller-supplied user IDs with server-derived identity checks across workspace and owned-data operations, adds development identity support and authorization tests, updates lens-run creation, and enables ownership-based RLS policies across public Supabase tables.

Ownership authorization and RLS

Layer / File(s) Summary
Caller identity and authorization contracts
workbench/_web/src/lib/auth/*
Adds development caller identity support and reusable authentication, ownership predicates, and owner-validation helpers.
Workspace ownership API flow
workbench/_web/src/lib/queries/workspaceQueries.ts, workbench/_web/src/lib/api/workspaceApi.ts, workbench/_web/src/app/..., workbench/_web/src/components/..., workbench/_web/src/actions/workshop.ts
Removes explicit user IDs from workspace creation, listing, mutation, and component contracts; query functions derive ownership from the authenticated caller.
Owned data query enforcement
workbench/_web/src/lib/queries/*, workbench/_web/src/lib/api/patchLensApi.ts
Scopes chart, config, document, view, and lens-run reads and writes by workspace or chart ownership, deriving lens-run workspace IDs from chart ownership.
Database RLS policies
supabase/migrations/20260716192819_enable_rls.sql
Enables RLS across public base tables and adds direct and relationship-based ownership policies, while leaving workshops without client-access policies.
Ownership integration tests
workbench/_web/src/db/__tests__/*
Updates fixtures to use scoped development identities and verifies cross-user visibility and mutation restrictions alongside existing persistence behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • ndif-team/workbench#114: Changes Patch Lens lens-run persistence behavior affected by ownership-derived lens-run workspace handling.
  • ndif-team/workbench#127: Updates workshop joining and workspace creation flows using the revised ownership-based workspace APIs.

Suggested reviewers: adambelfki3

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: enabling RLS on all public tables in the database.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/enable-rls

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/migrations/20260716192819_enable_rls.sql`:
- Around line 132-145: Update the RLS policy conditions for lens_runs in both
using and with check to validate that the referenced chart_id belongs to the
same workspace_id as the run, and that this workspace is owned by auth.uid().
Preserve the existing ownership validation while adding the chart/workspace
consistency check.
- Around line 33-40: Update the orphan-table protection around the migration’s
“ORPHAN TABLES” logic so newly created public tables are also prevented from
remaining without RLS. Add an appropriate event trigger that enables RLS for
future base tables in public, or add a CI validation that rejects public tables
lacking RLS; ensure the existing migration-time coverage remains intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a4fa5f9-95b7-4ff5-babb-ad6c24dad95c

📥 Commits

Reviewing files that changed from the base of the PR and between 34ad4f8 and 2f59d06.

📒 Files selected for processing (1)
  • supabase/migrations/20260716192819_enable_rls.sql

Comment on lines +33 to +40
-- ORPHAN TABLES
-- Some tables exist in the live DB but not in the Drizzle schema (e.g.
-- `generations`, left over from the removed generation panel). PostgREST
-- exposes those too. So rather than enable RLS on a hand-listed set, we enable
-- it on EVERY base table in `public` — this self-heals against current and
-- future orphans, locking them to default-deny (bypass roles only) until
-- someone gives them an explicit policy. The owner-scoped policies below then
-- layer onto the known application tables.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the migration and nearby related files first.
git ls-files 'supabase/migrations/*' 'supabase/**' | sed -n '1,200p'

echo
echo '--- migration outline ---'
ast-grep outline supabase/migrations/20260716192819_enable_rls.sql --view expanded || true

echo
echo '--- related references to RLS / default privileges / future tables ---'
rg -n --hidden --glob '!*node_modules*' --glob '!*dist*' --glob '!*build*' \
  -e 'enable row level security|row level security|default privileges|revoke .*anon|revoke .*authenticated|event trigger|future orphans|ORPHAN TABLES|policy' \
  supabase .github . 2>/dev/null | sed -n '1,240p'

echo
echo '--- target migration with line numbers ---'
cat -n supabase/migrations/20260716192819_enable_rls.sql | sed -n '1,220p'

Repository: ndif-team/workbench

Length of output: 17129


Future tables still need explicit RLS. The loop only enables RLS on tables that exist when this migration runs; any later CREATE TABLE public.* will still start without RLS, so the “future orphans” claim is too broad. Add an event trigger or a CI check that rejects new public tables without RLS.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260716192819_enable_rls.sql` around lines 33 - 40,
Update the orphan-table protection around the migration’s “ORPHAN TABLES” logic
so newly created public tables are also prevented from remaining without RLS.
Add an appropriate event trigger that enables RLS for future base tables in
public, or add a CI validation that rejects public tables lacking RLS; ensure
the existing migration-time coverage remains intact.

Comment thread supabase/migrations/20260716192819_enable_rls.sql
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🚀 Preview deployed

The RLS work runs Drizzle as the postgres role (bypasses RLS), so
authorization for the app path lives in the server actions. This
converts the query RPCs to derive the caller from the session
(requireUserId / requireWorkspaceOwner / requireChartOwner) instead of
trusting a client-supplied userId, and closes the gaps found in review:

- workspaceQueries: getWorkspaces / updateWorkspace / deleteWorkspace /
  createWorkspace now derive identity from the session; drop the
  client-supplied userId args and the dead prop/state plumbing that fed
  them (WorkspaceList, CreateWorkspaceDialog, AutoWorkspaceCreator,
  WorkspaceNameEditor, ModelLaunchDialog, LandingPage, page.tsx,
  workspaceApi hooks). setWorkspaceProlificIfEmpty now requires
  workspace ownership — previously it had no authorization at all,
  allowing anyone to stamp Prolific attribution onto any workspace.
- setConfig: only data/type are mutable; never rewrite workspaceId, so
  an owned config can't be relocated into another workspace.
- updateView: a zero-row update is a benign no-op (returns undefined),
  not a ForbiddenError — the ownedByChart predicate already blocks
  cross-user writes, and the debounced autosave races legitimately with
  clear/reset, which previously surfaced a spurious error toast.
- copyChart: single ownership-scoped full-row fetch (drop the redundant
  requireChartOwner + re-select round-trip).
- pushTutorialChart: drop the redundant pre-flight ownership check (the
  create helpers already guard before any write).
- lensRunQueries: drop the vestigial CreateLensRunInput.workspaceId (the
  stored value is derived from requireChartOwner).
- migration: drop the redundant workshops RLS ALTER (the do-loop already
  enables it); keep the deny-by-default intent comment.
- ownership: note the per-RPC getUser() round-trip as a follow-up.

Tests: convert the DB suites to the session-derived signatures (switch
identity via setDevUserId for cross-user seeding), rewrite the
wrong-user update/delete tests, and add ownership-rejection coverage for
getWorkspaces / createWorkspace / setWorkspaceProlificIfEmpty. 76/76 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Formatting-only follow-up to satisfy the Run Prettier CI gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@workbench/_web/src/app/workbench/components/WorkspaceList.tsx`:
- Around line 122-127: Replace the hardcoded ["workspaces"] queryKey in
WorkspaceList with the centralized queryKeys.workspaces.all key, adding the
queryKeys import if needed. Ensure the corresponding key is defined in
src/lib/queryKeys.ts if it does not already exist.

In `@workbench/_web/src/db/__tests__/lensRuns.test.ts`:
- Around line 84-96: Add cross-user fixtures and tests in the lens-run test
setup around createLensRun, using a separate attacker user and attacker-owned
chart. Verify createLensRun rejects the victim’s chart, and that fetch, patch,
clear, and delete operations cannot access runs belonging to that chart; retain
the existing owner-path coverage unchanged.

In `@workbench/_web/src/lib/auth/ownership.ts`:
- Around line 44-49: Update requireUserId so NEXT_PUBLIC_DISABLE_AUTH=true is
honored only in non-production environments; in production, reject the
configuration instead of importing getDevUserId or returning a synthetic
identity. Prefer a server-only environment variable for this development bypass
while preserving normal session authentication and the existing devUser lazy
import behavior.

In `@workbench/_web/src/lib/queries/configQueries.ts`:
- Around line 37-40: Enforce workspace ownership for chart/config links: in
addChartConfigLink, reuse the workspace returned by requireChartOwner(chartId)
and verify configId belongs to that workspace before inserting. In
workbench/_web/src/lib/queries/configQueries.ts lines 37-40, add the validation;
in workbench/_web/src/lib/queries/chartQueries.ts lines 268-282, scope
downstream config lookup to the authorized chart workspace, reject mismatched
links, and audit existing links created by the unguarded endpoint.

In `@workbench/_web/src/lib/queries/internal.ts`:
- Around line 25-37: Update getNextWorkspaceItemPosition and the corresponding
chart/document creation flow so position allocation and insertion are serialized
atomically, using a transaction with appropriate locking or a database-managed
workspace counter with conflict retry. Ensure concurrent creations cannot
receive the same position across charts and documents.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1af1e6e8-78b3-404f-aa24-b153ead2cfa5

📥 Commits

Reviewing files that changed from the base of the PR and between 2f59d06 and e19ca18.

📒 Files selected for processing (25)
  • supabase/migrations/20260716192819_enable_rls.sql
  • workbench/_web/src/actions/workshop.ts
  • workbench/_web/src/app/workbench/components/AutoWorkspaceCreator.tsx
  • workbench/_web/src/app/workbench/components/WorkspaceList.tsx
  • workbench/_web/src/app/workbench/page.tsx
  • workbench/_web/src/components/CreateWorkspaceDialog.tsx
  • workbench/_web/src/components/LandingPage.tsx
  • workbench/_web/src/components/WorkspaceNameEditor.tsx
  • workbench/_web/src/components/models/ModelLaunchDialog.tsx
  • workbench/_web/src/db/__tests__/lensRuns.test.ts
  • workbench/_web/src/db/__tests__/local-db.test.ts
  • workbench/_web/src/db/__tests__/prolific.test.ts
  • workbench/_web/src/db/__tests__/workshops.test.ts
  • workbench/_web/src/lib/api/patchLensApi.ts
  • workbench/_web/src/lib/api/workspaceApi.ts
  • workbench/_web/src/lib/auth/devUser.ts
  • workbench/_web/src/lib/auth/ownership.ts
  • workbench/_web/src/lib/queries/chartQueries.ts
  • workbench/_web/src/lib/queries/configQueries.ts
  • workbench/_web/src/lib/queries/documentQueries.ts
  • workbench/_web/src/lib/queries/internal.ts
  • workbench/_web/src/lib/queries/lensRunQueries.ts
  • workbench/_web/src/lib/queries/tutorialChart.ts
  • workbench/_web/src/lib/queries/viewQueries.ts
  • workbench/_web/src/lib/queries/workspaceQueries.ts
💤 Files with no reviewable changes (1)
  • workbench/_web/src/lib/api/patchLensApi.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • supabase/migrations/20260716192819_enable_rls.sql

Comment thread workbench/_web/src/app/workbench/components/WorkspaceList.tsx
Comment thread workbench/_web/src/db/__tests__/lensRuns.test.ts Outdated
Comment thread workbench/_web/src/lib/auth/ownership.ts
Comment thread workbench/_web/src/lib/queries/configQueries.ts
Comment thread workbench/_web/src/lib/queries/internal.ts Outdated
…-closed)

Follow-up hardening from the PR #131 review pass:

- ownership: requireUserId() fails closed in production — a build with
  NEXT_PUBLIC_DISABLE_AUTH=true throws instead of handing every RPC the
  synthetic dev identity.
- addChartConfigLink: verify the config lives in the chart's owned
  workspace (chart ownership alone didn't establish config ownership,
  letting a caller link — and via copyChart, read — another tenant's
  config). copyChart's config lookup is likewise scoped to the chart's
  workspace and refuses a cross-workspace link.
- RLS: tighten the lens_runs policy so chart_id and workspace_id must
  resolve to the same owned workspace, and the chart_config_links policy
  so the config shares the chart's owned workspace (defense-in-depth
  behind the app-layer checks above).
- internal.ts: allocate the next list position inside the INSERT via a
  portable subquery instead of a separate read-then-write, removing the
  app-level window where concurrent creates collide on position.
- WorkspaceList / workspaceApi: use the centralized queryKeys.workspaces.all
  instead of a hardcoded ["workspaces"] key.

Tests: add lens-run cross-user coverage (createLensRun rejects another
user's chart; list / heatmap / patch / clear / delete can't reach that
chart's runs). 79/79 DB tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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