Repository navigation
feat(security): the guest's grants channel, with a share-links-only row scope (ADR-0138 E1, ruling C) - #23321
Conversation
…D4's organization, then the guest anchor's bindings (ADR-0138 E1) Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…'s binding, the form door keeps its own answer, the anchor's definition is sealed at the metadata door Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…, unit pins and dogfood pins in place, PR not opened Done: the guest entry resolves ADR-0138 D4's organization (resolveGuestOrganization over resolveSystemWriteOrganization, moved to core and re-exported unchanged by objectql), then the guest anchor's bindings through the shared position-bound read; the name fold is closed for the guest; the CRUD gate raises the located refusal (NOT_IMPLEMENTED, 501) for a guest with no organization where D4 refuses. Remains: the plugin-security and runtime typecheck, the package test suites, the full gate list, the PR, and a ruling on the guest's row scope (D3 item 6) and on the missing authoring door for a guest binding (D3 item 5). Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
Resume of the E1 branch on main 6d7f4df; no path overlaps this branch. Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
ADR-0138 D3 item 6 is executed as written and pinned as measured; the comment beside the pin names the open decision that may change it, so a later ruling flips the numbers in one place. Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
Brings #23056 S8 (the catalog row objects retired) and ADR-0138 D2 slice 1; both sets of core/security barrel exports are kept. Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
… filter — share links only (ADR-0138 D3 item 6, ruling C) The maintainer's ruling C (comment 6148183570 on #23266) replaces the as-written row scope: the guest reaches rows only through share links, never through the object's sharing model. SharingService.buildReadFilter answers deny-all for the guest principal before the OWD branch (and its owner-less and phantom-anchor fall-throughs). Share links are redeemed at their own door and never pass through this filter. The E1 row-scope pin flips to 0 rows for every sharing model, with an admin control per object; a share-link control redeems a link (and holds eligibility at the mint and at redemption). Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…n control reads the data door's own shape Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…s plugin-sharing Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…elevation of its own; the engine-double ledger records its unit file's pinned findOne Both callers of resolveOperationPrincipals return for a system context before it, so the guest rule's own isSystem read was redundant, and the system-context census counted it as an unanchored elevation read. Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
…nd its slots are typed The core unit file's fake WHERE matcher refuses a combinator instead of reading it as a field name; the dogfood door test reads the engine and security slots through their contract types. Claude-Session: https://claude.ai/code/session_01P88ehrZ6hM19hBzdpsQTTo Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 38 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 12 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 57 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5e52e5b2188f1a5703de3abbc2ad08e14bcd65b4 && git checkout 5e52e5b2188f1a5703de3abbc2ad08e14bcd65b4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc 1d394b81c310005c5f54f36fe9113623fb8fcdf0 && git checkout -B drift-repro 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc && git merge --no-ff 1d394b81c310005c5f54f36fe9113623fb8fcdf0
node scripts/docs-audit/affected-docs.mjs --json 404fc477e52f30aa9b77cd2ad52f7adb0e3009fc
|
Contract reviewServed-tier: Inputs: card #23224 (body and all 8 comments, 6144021196 through 6152923496), #23266's body and its 3 comments (ruling 6148183570 read in full), PR #23321 (body, the 15-file list, the three-dot diff against ① Derived judgmentsPublished surface.
Accept set. No
② Semver level
③ Boundary flagsHand-off 6145588579,
Final report 6152923496,
Out-of-scope findings and acceptance notes:
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #23224
Clause-②: yes
ADR-0138 E1, the grants channel (D3, D4): the guest entry finds D4's organization, then the
guestanchor's bindings, and raises the located refusal where D4 refuses. It also applies the maintainer's ruling C on #23266 (comment 6148183570): the guest's row scope is share links only.This resumes the released branch (hand-off 6145588579, claim 6146352069).
mainwas merged twice. The second merge brought #23056 S8 (PR #23222) and ADR-0138 D2 slice 1. Bothcore/securitybarrel export sets are kept.Landing: this is a draft. The seat arms it only after the ruling-C pins are on its head (they are, at
1d394b81c3). The one-sentence ADR-0138 D3 item 6 amendment is a separate Tier H PR from the seat. ⛔ Nodocs/adr/**path is in this diff.What changed
@objectstack/coreresolveAuthzContext's anonymous branch takes a newguestEntryinput. On a face that sets it, the branch reads the sets theguestposition's definition names, through the same position-bound read every position uses (positionBoundSetNames, factored out of the user path). It grants them only in the organizationresolveGuestOrganizationderives.resolveGuestOrganizationasks ADR-0131 D9's predicate,resolveSystemWriteOrganization. The predicate moved from objectql into core because core cannot import objectql, and D4 item 2 forbids a copy.guest-organization.ts:GuestOrganizationUnresolvedError, which carriesNOT_IMPLEMENTEDand501from the ADR-0112 catalog, plusisGuestOrganizationUnresolvedError.minorandClause-②: yes.@objectstack/objectqltenancy/system-write-organization.tsre-exports the predicate and its two types from core as the same bindings. Identity is pinned.@objectstack/plugin-securityresolvePermissionSetsForContextUnmemoizedno longer folds the guest's position into the set request. The guest requests exactly the sets its envelope carries.resolveOperationPrincipalscalls a new privaterefuseGuestWithoutOrganization, guarded by one new field. A guest that reaches a data operation with no organization, on a deployment where the organization step refuses, gets the located refusal. This happens before any set resolves, and ahead of the prologue's catch-all, which would turn it into a403. The operator's log names the reason once per reason.@objectstack/plugin-sharing(ruling C)SharingService.buildReadFilteranswers deny-all for the guest principal, before the sharing-model branch and its owner-less and phantom-anchor fall-throughs. The guest reaches rows only through share links.publicSharing.eligibilityand read the one linked record. It never passes through this filter.externalSharingModelis still read for no caller (security:externalSharingModelis declared and authored but has no runtime reader — an object'sexternalSharingModel: privatechanges no answer for any principal (ADR-0090 D11 declared-not-enforced) #23268 is its own card).@objectstack/runtimeresolveExecutionContextpassesguestEntry: true. This is the one face that takes the explicit guest entry.ADR-0087 disposition: none. No authorable key, accept set or stored shape moves.
Bindings that start granting on the reference apps: 0.
app-showcase,app-crmandapp-multi-packageare counted by a dogfood census with a fixture control.app-tododeclares no positions or sets.The seat's answers to the forks, as built:
501 NOT_IMPLEMENTED;guestanchor — ADR-0138 D3 item 5 has no enforcer (PUT /meta/position/guestanswers 403 NOT_OVERRIDABLE; guest suggestions are refused) #23267).The record row's pins and their negative controls
403 PERMISSION_DENIED, guest identity, no set200on the bound object, envelope carries the set and the derived organization, resolved sets exactly that set;403on an unbound object. core unit (15 cases)guestEntry): 2 core pins redeveryoneand the name fold never reach the guest403for the guest (the admin reads it:200); the guest resolves neither the baseline noreveryone's set while a member resolves both. plugin-security unit (3 cases with human controls)501,error.codeNOT_IMPLEMENTED, message names the site binding, nothing served. plugin-security unit:501for find and insert, with single-org403and no-organization-object403controls201, row stamped with the organization; two organizations500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED(the form door's own answer, never the guest refusal)501private,public_read,public_readwithexternalSharingModel: public_read,public_read_write, apublic_readparent and itscontrolled_by_parentchild. Each is admitted (200) and reads 0 rows; the admin reads more than 0 of each. plugin-sharing unit: deny-all for 5 shapes, with a member control and a system controlprivatestays 0 under A5 by construction, because the guest was never owner-matched there404 INVALID_OR_EXPIRED) and at the mint (422 RECORD_NOT_ELIGIBLE)Ablations are one-shot proofs, run through
scripts/ablation-replace.mjs. For every leg the anchor hit was 1 and the blob changed. Each restore was proven: the blob equals HEAD andgit diff HEADis empty. The door-level legs also ranscripts/ablation-dist-preflight.mjs(--absenton the mutation, present on the restore, rebuilt each way). No ablation file is kept.1d394b81c3.df4be9b562.02caf8afc6. The read filter is unchanged since then.Tests (final head
1d394b81c3unless noted)a001a387ce; after the matcher change, the guest-grants file re-run: 15/15a001a387ce; objectql unchanged since)a001a387ce(package unchanged since)resolve-execution-contextand the endpoint integration testsa001a387ce; runtime unchanged since)Gates
node scripts/pm/dispatch-gates.mjs --commandswas run on1d394b81c3and derived 85 commands. All 85 ran:check:query-options-erasureexceeded the battery's 400 s timeout and was re-run alone, exit 0.--ranreconciliation: 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN.Four gates went red on the hand-off's files during this round and were fixed here:
check-system-context-census: the guest refusal's redundant system read was removed. Both callers return for a system context first.check:engine-double-contract: the ledger records the plugin-security unit file's pinnedfindOne, through--write, one row.check:slot-lookup: the dogfood file's engine and security slots are typed.check:where-matcher: the core unit double refuses a combinator.File surface against the claim
security-plugin.tsgrew by 4 comment lines, and the guest rule lost its redundant system read.packages/plugins/plugin-sharing/src/sharing-service.tsand its test. This is ruling C's site (domain:services), directed by PM comment 6149308636.scripts/engine-double-contract.pinned.json, one ledger row that the gate requires for the hand-off's unit file.Acceptance notes
sys_user,sys_organization,sys_member,sys_record_share,sys_user_permission_set) still skips record scoping for every principal, the guest included. This is the platform bypass, not the OWD branch, so ruling C does not reach it.guestmay name one of these objects at all is the anchor tier's question.guestanchor — ADR-0138 D3 item 5 has no enforcer (PUT /meta/position/guestanswers 403 NOT_OVERRIDABLE; guest suggestions are refused) #23267.checkEdit,checkDelete,buildWriteFilter) still take the OWD branch for a guest. A guest never reaches them: the guest tier refuses every edit and delete bit (describeAnchorForbiddenBits), so a guest update or delete is refused at the object level first. Noted.500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED, as before (D4 item 4 keeps it). It is pinned unchanged here, not altered.Generated by Claude Code