Skip to content

feat(dogfood): multi-user harness + RLS cross-owner proof runner (#1994)#2032

Merged
os-zhuang merged 1 commit into
mainfrom
feat/dogfood-rls-proof
Jun 18, 2026
Merged

feat(dogfood): multi-user harness + RLS cross-owner proof runner (#1994)#2032
os-zhuang merged 1 commit into
mainfrom
feat/dogfood-rls-proof

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Why

The verifier (#2027) proves CRUD fidelity, but the most expensive class of metadata bug is authorization — exactly #1994, a by-id write that skipped the row-level predicate, letting a member edit a record it couldn't even see. This adds the machinery to derive that proof.

What

  • harnesssignUp(email) mints a fresh member (the first user is the seeded admin, so a sign-up is a plain member with no grants — exactly what cross-owner proofs need). Also registers SharingServicePlugin so apps declaring requires: ['sharing'] have their record-share grants enforced instead of silently inert (a harness-fidelity fix: the verifier was under-testing authorization).
  • src/rls.ts — the app-agnostic invariant: "a user who cannot READ a record must not be able to WRITE it." Per object: admin creates a record; a fresh member tries to read it, then mutate it by id; re-reading as admin is the ground truth for whether the row actually changed. member-can't-read + row-changed = the fix(security)[P0]: enforce RLS on by-id writes — close member-edits-others'-records hole (#1985) #1994 hole, regardless of the app's sharing config.
  • test/rls-runner.test.ts — unit-proves the classifier: flags a hole / passes when consistent / reports member-visible. The detection logic is guaranteed correct.
  • test/auto-verify-rls.dogfood.test.ts — live smoke over CRM + showcase with a real second user.

Honest current state (and the next step)

The harness boots single-tenant, so org-scoped RLS is stripped and a fresh member falls back to member_default (broad read). So both the example apps and hotcrm report every object member-visible — the #1994 by-id-write path isn't exercised live yet. I'm not dressing that up as a working gate.

A hard, revert-provable gate needs an owner-scoped fixture: a private-default object + a member permission set carrying RLS.ownerPolicy('obj','created_by') + SecurityPlugin({ fallbackPermissionSet }). The runner is unit-proven and ready; the live invariant (zero holes) still guards against a regression that makes a member able to mutate a record it can't read. The fixture is tracked as a follow-up.

This is itself a finding worth stating: the verifier must register an app's required security plugins (and a tenancy mode that doesn't strip RLS) or it silently under-tests authorization — which is why a "member sees everything" result must be read as "not yet isolated here," not "secure."

Notes

  • Private package — no changeset. Full dogfood suite: 37 passed | 1 skipped, 0 failures.
  • Scope per ADR-0054 (#2024).

🤖 Generated with Claude Code

Adds the authorization dimension to the verifier — the machinery to catch the
#1994 class (a by-id write that skips the row-level predicate, letting a member
edit a record it cannot even see).

- harness: `signUp(email)` mints a fresh member (the first user is the seeded
  admin, so a sign-up is a plain member with no grants — exactly what
  cross-owner proofs need). Also registers SharingServicePlugin so apps that
  declare `requires: ['sharing']` have their record-share grants enforced rather
  than silently inert (a harness-fidelity fix).
- src/rls.ts: the app-agnostic invariant — "a user who cannot READ a record must
  not be able to WRITE it." Per object: admin creates a record; a fresh member
  tries to read it, then mutate it by id; re-read as admin decides if the row
  actually changed. Member-can't-read + row-changed = the #1994 hole.
- test/rls-runner.test.ts: unit-proves the classifier flags a hole / passes when
  consistent / reports member-visible — so the detection logic is guaranteed.
- test/auto-verify-rls.dogfood.test.ts: live smoke over CRM + showcase.

HONEST CURRENT STATE: the harness boots single-tenant, so org-scoped RLS is
stripped and a fresh member falls back to `member_default` (broad read). So both
the example apps AND hotcrm report every object `member-visible` — the #1994
by-id-write path isn't exercised live yet. Making it a hard, revert-provable
gate needs an owner-scoped fixture (private-default object + a member permission
set carrying RLS.ownerPolicy + SecurityPlugin.fallbackPermissionSet). The runner
is unit-proven and ready; the live invariant (zero holes) still guards against a
regression. Fixture tracked as the next step.

Private package — no changeset.

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

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 18, 2026 12:23pm

Request Review

@github-actions github-actions Bot added size/m dependencies Pull requests that update a dependency file tests and removed size/m labels Jun 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 1 changed package(s). ✅

@os-zhuang
os-zhuang merged commit c890c59 into main Jun 18, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the feat/dogfood-rls-proof branch June 18, 2026 12:23
os-zhuang added a commit that referenced this pull request Jun 18, 2026
… gate

The cross-owner RLS proof runner (src/rls.ts, #2032) was unit-proven but had
nothing live to exercise it: the harness boots single-tenant, so the org
tenant_isolation policy is stripped and a fresh member falls back to
member_default (broad read) — every object reports `member-visible`, so the
#1994 by-id-write invariant ("can't read ⇒ can't write") never fired live.

Two faithful ways to create real isolation, both now live:

1. Owner-scoped fixture (test/rls-fixture.dogfood.test.ts + fixtures/):
   one object `rls_note` whose member fallback permission set carries
   RLS.ownerPolicy('rls_note','created_by'). Keyed on created_by/current_user.id
   (not organization_id), so it survives single-tenant stripping — a fresh
   member genuinely can't read the admin's note.
   - GREEN: owner policy on `all` ops → rls-consistent (pre-image check denies
     the by-id write).
   - AUTOMATED RED: owner policy on `select` only → rls-hole (read scoped, no
     write policy applies, by-id write lands) — #1994 hole class every CI run.
   - MANUAL REVERT PROOF (README): disabling the pre-image check flips the green
     fixture to "GET 404 yet MUTATED it by id (PATCH 200)". Verified, restored.

2. Multi-tenant org-scoped (test/rls-multitenant.dogfood.test.ts):
   bootDogfoodStack gains `{ multiTenant: true }` → registers plugin-org-scoping
   before SecurityPlugin so organization_id policies APPLY. CRM flips from
   every-object-member-visible (single-tenant) to 4 consistent / 0 holes /
   0 member-visible — confirming member-visible was single-tenant RLS-stripping,
   not a broad-read default (the model real apps like hotcrm rely on).

harness: BootOptions gains `security?` (SecurityPlugin override for the fixture
fallback) and `multiTenant?`. README documents both paths + the revert proof.

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

dependencies Pull requests that update a dependency file tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant