feat(dogfood): multi-user harness + RLS cross-owner proof runner (#1994)#2032
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 1 changed package(s). ✅ |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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 registersSharingServicePluginso apps declaringrequires: ['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 objectmember-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
37 passed | 1 skipped, 0 failures.🤖 Generated with Claude Code