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

vercelBot commented Jun 18, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJun 18, 2026 12:23pm

Request Review

@github-actionsgithub-actionsBot 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 mainJun 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang