fix(qa,platform-objects,docs): re-premise member_default's removed wildcard and replace two vacuous D7 denial cases (#6964) - #7149
Conversation
…ldcard and replace two vacuous D7 denial cases (#6964)
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
PM review — PASS. Marked ready and enqueued ( Part 2 was the load-bearing deliverable, and it was answered by a real runThe dispatch envelope said plainly that a green suite is not evidence here, and that answering the vacuity question needs a live stack. It got one:
Row 1 is the world the old assertion claimed to exclude — and The replacement discriminates, which is the whole pointFixed in the #5046 wholesale-replace shape rather than reworded. The new case reads The three-row table is written into the test file itself, so the next reader can see why that object was chosen instead of rediscovering it. Reverse verification — the prediction that mattered was the third linePredicted before running: strip the declared-default wiring → the positive case RED, the new denial case RED, and the old (deleted) denial cases GREEN. That third line is the vacuity, stated as a falsifiable prediction. Measured: Verified independently by this seatCI: 26 runs, none non-green (ESLint ✅, TypeScript Type Check ✅, Check Changeset ✅, Dogfood Regression Gate ✅). Scope 7 files, +168 −43. Boundaries: Part 1 kept the two premises apart, which was the stated trapSix locations, each rewritten against its own expired premise — (a) the Follow-ups#7151 filed by the author for two sites outside this card's named face — a published customer skill and The author also recorded three grep hits it checked and deliberately did not touch, including one false positive ( Generated by Claude Code |
Fixes #6964
#5491(PR #6684) removedmember_default's plain'*'object grant; ADR-0095 D1 retired its wildcardtenant_isolationRLS policy. PR #6958 fixed the twoplugin-securitysurfaces#6842named. This PR is the non-overlapping remainder, four packages further out — and Part 2 turned out to be a real defect, not prose.Every anchor was re-measured against
origin/mainat this branch point (3e8e669c0); none had drifted.Part 2 first — the two D7 denials were vacuous, and this is how it was measured
showcase-default-profileandshowcase-d7-default-profileeach proved ADR-0056 D7 withexpect(status).not.toBe(200)on an app object, justified inline by "member_defaulthas a wildcard grant, so this would be 200". Since#5491that baseline grants nothing on app objects, so the denial is the trivially expected outcome either way.Confirmed on a live stack —
bootStackfrom@objectstack/verifybooting the real showcase app in-process, one freshsignUpper wiring, three wirings, HTTP status read offGET /data/{object}:showcase_announcementshowcase_private_noteshowcase_contactsys_user_preferencemember_default(built-in)showcase_demo_defaultshowcase_member_defaultRow 1 is the world the two assertions claimed to exclude — and the objects they read are 403 there too. Both cases held identically whether or not the declared default was in force, i.e. they passed because nothing is produced, which is the
#5046"replace wholesale" shape and not something re-wording can fix.The same run settles the risk that would have killed the replacement: a named
fallbackPermissionSetreplacesmember_defaultrather than merging additively on top of it. That makessys_user_preference— granted by the built-in baseline and by nothing else here — 200 if and only if the built-in baseline governs. Both denial cases now read it.Reverse verification — direction predicted before the run
Predicted, for a mutation that strips the declared-default wiring so the built-in baseline governs: positive case RED, new denial case RED, old (deleted) denial case GREEN — that last row being the vacuity itself.
Measured, both files in one run:
Tests 4 failed | 3 passed (7).All three predicted rows landed. Honest delta, not predicted in advance: the third green. That case reads
showcaseStack.permissionsonly and never touches the boot, so it is insensitive to this mutation by construction. It is not a gap — it pins the helper's extraction — but it is worth naming, because#7001's own header says a name-only assertion "could not tell 'wired' from 'wired but inert'", which is exactly why the behavioural pair exists beside it. Unmutated:Test Files 2 passed (2)/Tests 5 passed (5).Part 1 — the named-location prose sweep
Two distinct expired premises are mixed across these locations and are handled separately, never conflated: (a) the
'*'grant removed by#5491, (b) the wildcardtenant_isolationRLS retired by ADR-0095 D1.sys-scim-provider.object.ts:40— premise (a). The gate is not redundant; its live reason is stronger.requiredPermissionsis a capability AND-gate evaluated before the CRUD grant (security-plugin.tsstep 1.5), so a caller missing the capability is denied "regardless of how permissive their grants are" — including a grant an app-declared profile or a customer-authored set names on this object.sys-sso-provider.object.ts:38— premise (b), and:43— premise (a).:38is re-premised on the Layer 0 tenant wall: the table has anorganization_idcolumn that better-auth never stamps, so a wall AND-composingorganization_id == < caller org >would deny every row;tenancy.enabled:falsesetstenancyDisabled,computeTenantLayer0Filterreturnsnull, Layer 0 contributes nothing. Also corrected: under ADR-0095 W2 theviewAllRecordssuperuser bit alone no longer crosses the wall, so this opt-out is now the only thing opening the table up.rls-multitenant.dogfood.test.ts:7— premise (b). The investigation narrative is kept as history and tensed correctly, then brought to the current mechanism: the tenant scope is Layer 0, inert under thesingleposture by construction rather than by policy stripping. The conclusion the file is built on survives — a single-tenant boot applies no org row scope to reads, andmember_defaultstill carries no owner-scoped READ policy (owner_only_*areupdate/deleteonly).content/docs/permissions/index.mdx:47— premise (b), the sharpest piece: a published security page asserting the retired policy as shipped behaviour, contradictingreleases/implementation-status.mdx:294/:435in the same repo. Per the dispatch's direction, the doc moved and the status page was not touched; the replacement is worded to match it.Scope
No runtime behaviour changes — comments, published prose, and two test fixtures.
Deliberately not done:
audience-anchor-set-claims.pin.test.ts'swatchedSurfaces()is left at its two surfaces. #6964 states that whether the pin should grow a repo-wide surface, and whetherplatform-objects/qashould depend onplugin-securityto get one, is a scoping decision for triage — the triage grading did not rule on it, so assuming it here would be the guess the filing warned against.A consumption-radius sweep for the same two premises turned up two further stale sites outside this card's named file face —
skills/objectstack-data/SKILL.md(both premises, in a published customer-facing skill) andpackages/plugins/plugin-security/README.md(premise (b)). Filed separately rather than folded in. Verified correct and left alone:content/docs/permissions/access-recipes.mdx:65already names Layer 0;serve-verify-security-parity.contract.test.ts:18is correctly historical; andplugin-approvals/lifecycle-hooks.ts:423's "wildcardcreated_by == current_user.idRLS" is a grep false positive — that policy isobject: '*'and still ships.Gates
pnpm lint(ESLint,--no-inline-config)pnpm --filter @objectstack/platform-objects --filter @objectstack/dogfood typecheckpnpm --filter @objectstack/platform-objects testTest Files 11 passed (11)/Tests 289 passed (289)pnpm --filter @objectstack/dogfood test(full suite)Test Files 85 passed | 1 skipped (86)/Tests 526 passed | 3 skipped (529)check:nul-bytes+grep -naPself-scan of every touched fileadr-anchors,doc-authoring,docs-audit-scope,role-word,quick-reference-counts,authz-resolver,tenant-chokepoint,wildcard-fallthrough,error-code-casing,route-envelope,engine-double-contract,empty-changeset,skill-frame-syncThe one skip is
rls-multitenantitself, which skips loudly in this workspace because the enterprise@objectstack/organizationspackage is not linked in — pre-existing and unrelated to this change.Generated by Claude Code