feat(db): enforce workspace RLS at runtime#18
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (7)
WalkthroughThis PR enforces PostgreSQL Row-Level Security at runtime. It adds an rlsExecutor that applies per-request session scope via set_config(), a migration enabling/forcing RLS and policies (tenant and platform scopes) plus runtime-role NOBYPASSRLS hardening, updates the migration runner to accept DATABASE_RUNTIME_ROLE, refactors PostgresRepository to run queries and command handlers inside RLS-scoped transactions, adds PGlite-based tests verifying fail-closed behavior, and updates project documentation marking ADR 0044 done. Sequence Diagram(s)sequenceDiagram
participant Client
participant Repository
participant rlsExecutor
participant BaseExecutor
participant Postgres
Client->>Repository: API call / command
Repository->>rlsExecutor: withScope(scope, execute)
rlsExecutor->>BaseExecutor: transaction()
rlsExecutor->>Postgres: set_config('app.workspace_id' / 'app.platform')
BaseExecutor->>Postgres: SQL (SELECT/INSERT/UPDATE)
Postgres->>Postgres: RLS policies evaluate (fail-closed if unset)
BaseExecutor->>Repository: return filtered results / errors
Repository->>Client: response
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CodeRabbit follow-ups on PR #18: - Add btree indexes on workspace_id for upload_session_files and artifact_files so the new RLS USING predicates can use an index lookup instead of full scans. idempotency_records is already covered by its composite primary key on (workspace_id, ...). - Validate the optional role identifier passed to the PGlite test executor against /^[a-z_][a-z0-9_]*$/ before interpolating it into `set local role`. Call sites are hardcoded today, but the defensive check costs nothing and removes the SQLi-shaped seam. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
CodeRabbit triage (1 critical / 1 major / 7 minor / 15 trivial): Addressed in cb95435:
Not addressed (deliberately):
|
Enable Postgres row-level security on every tenant table and route every PostgresRepository call through a transaction that issues SET LOCAL app.workspace_id (tenant) or app.platform = 'on' (pre-auth lookups, admin sweeps, public Agent View resolve). Migration also accepts DATABASE_RUNTIME_ROLE and strips BYPASSRLS from the Hyperdrive role. New rls.test.ts uses PGlite (real Postgres engine, not a mock) under a non-superuser role to prove cross-workspace reads return zero rows, mismatched-workspace inserts are rejected by WITH CHECK, and unscoped reads return zero rows. Closes the runtime half of ADR 0044. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CodeRabbit follow-ups on PR #18: - Add btree indexes on workspace_id for upload_session_files and artifact_files so the new RLS USING predicates can use an index lookup instead of full scans. idempotency_records is already covered by its composite primary key on (workspace_id, ...). - Validate the optional role identifier passed to the PGlite test executor against /^[a-z_][a-z0-9_]*$/ before interpolating it into `set local role`. Call sites are hardcoded today, but the defensive check costs nothing and removes the SQLi-shaped seam. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
agent-paste PR preview resources were cleaned up. The pr-preview-${context.issue.number} environment is left in place; remove it from the GitHub UI if desired. |
Summary
Closes the runtime half of ADR 0044. Workspace isolation is now enforced by Postgres, not by application-layer
WHERE workspace_id = ?discipline.Changes
packages/db/migrations/0003_rls_runtime.sqlenables andFORCEs RLS on every tenant table (workspaces,api_keys,upload_sessions,upload_session_files,artifacts,artifact_files,operation_events,idempotency_records). Two permissive policies per table cover tenant scope (current_setting('app.workspace_id', true)) and platform scope (current_setting('app.platform', true) = 'on').packages/db/src/postgres/rls.tsaddsrlsExecutor(executor, scope)that opens a tx and appliesSET LOCALfor either scope before the wrapped query runs. Setting is transaction-local so Hyperdrive pooling cannot leak identity across requests.packages/db/src/postgres/repository.tsroutes every public method through a scoped executor. AuthenticatedrunCommandpaths use the tenant scope; pre-authverifyApiKey, the public Agent View resolve, admin lookups, and cleanup sweeps use the platform scope.packages/db/scripts/migrate.mjsreadsDATABASE_RUNTIME_ROLEand forwards it via a GUC so the migration'sdoblock canalter role <runtime> nobypassrls. Hyperdrive role getsNOBYPASSRLSat migration time once the env is set on the migration workflow.packages/db/src/postgres/rls.test.tsruns against PGlite (real Postgres engine, not a mock) under a non-superuseragent_paste_runtimerole. Five assertions: tenant-scoped read returns only the tenant's rows, cross-workspace read by id returns zero rows, cross-workspace insert is rejected byWITH CHECK, unscoped read returns zero rows (fail-closed), platform scope sees every row.docs/ops/project-status.mdremoves RLS from the backlog, renumbers, adds a Recently Completed entry, flips ADR 0044 row to Done, drops the "RLS still not applied" line from open security follow-ups.Risk
alter role ... nobypassrlsonly whenapp.runtime_roleis set inside the migration session, so existing preview/production deploys are unaffected until the migration workflow passesDATABASE_RUNTIME_ROLE=<hyperdrive-role>. That env wiring is a follow-up operator task per the bootstrap-hosting checklist; the migration itself is safe to run today.FORCE ROW LEVEL SECURITYapplies RLS even to the table owner, so a future migration that runs as the owner without settingapp.platform = 'on'will get zero rows. The migration role isBYPASSRLSso it is unaffected; the test confirms.WHERE workspace_id = ?filters are belt-and-braces with RLS now.Test plan
pnpm verify(61 turbo tasks green, including newrls.test.ts)pnpm --filter @agent-paste/db test(12/12 pass)0003_rls_runtime.sqlto preview Neon branch viapnpm migrate:previewthen runpnpm smoke:previewDATABASE_RUNTIME_ROLEin the production-migration workflow before next migration deploySummary by CodeRabbit
New Features
Infrastructure
Tests
Documentation