Repository navigation
fix(core)!: an import's reference resolution asks the lookup target its declared exposure before matching a cell (#22739) - #22770
Conversation
…s declared exposure before matching a cell (#22739) Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…sure on both doors; classify it in the enumeration pin (#22739) Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…22739) Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…port-ref-exposure
…since the target is read through referenceTargetOf (#22739) Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…port-ref-exposure
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 27 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 25ebc0607bcc94fd6196a9873ac3abc62bdb9980 && git checkout 25ebc0607bcc94fd6196a9873ac3abc62bdb9980
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e84aeb36ce14169a633670f14ce8280fc998e2b9 c06ec74a8799c72ffed17d51466de12860dcc285 && git checkout -B drift-repro e84aeb36ce14169a633670f14ce8280fc998e2b9 && git merge --no-ff c06ec74a8799c72ffed17d51466de12860dcc285
node scripts/docs-audit/affected-docs.mjs --json e84aeb36ce14169a633670f14ce8280fc998e2b9
|
…port-ref-exposure
…s one, reading its target through referenceTargetOf; pin the user-field control (#22739) Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
… and its producer census (#22739) Claude-Session: https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T Co-authored-by: Claude <noreply@anthropic.com>
…port-ref-exposure # Conflicts: # packages/core/src/security/second-object-read-exposure.pin.test.ts
Contract reviewServed-tier: Inputs, and nothing else: card #22739 (body; comments 6104521404 claim, 6105593563 round-1 dev report, 6105610630 REWORK, 6106294041 round-2 dev report), parent card #22661 (body; comments 6096329009 triage, 6101260211 claim, 6102815147 dev report, 6102873261 census split, 6102951996 ACCEPT, 6104471901 landing) with its contract review 6102920614 on PR #22735 and #22737's contract review 6106147584 on PR #22768, PR #22770 (body, its 7-file list, the net diff from merge-base ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #22739
Clause-②: yes (narrowing)
Census row 7 of #22661: the import door's reference resolution matched a lookup cell against a TARGET object nobody addressed, without asking that target its declared exposure. A match stored the target record's id and a miss answered
reference_not_foundper row, so the row report told the two apart even for a target every data route refuses.What changed
servesReferenceTarget(new,packages/core/src/utils/import-runner.ts) asks the spec's one exposure decision,canServeApiOperation(@objectstack/spec/data), of the target's ownenableblock, read through the protocol'sgetMetaItem.resolveRef(theRefResolverthatrunImportbuilds) asks it first, once per target per import. No second rule and no second list: the decision function is the one security(data, analytics): a lookup target's exposure declaration is not judged when the data door's$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661 settled.reference_not_found, and nothing of the target is read. A declaration that cannot be read withholds too (fail-closed). No new error code, no status change.list. Matching a cell is a predicate read over the target (findDatawith awhereon one candidate field), the runtimefindthe spec maps tolistinDATA_ACTION_TO_API_OPERATION, and the same read a list route with a field filter performs.getwould keep serving a name match on a get-only target, which refuses exactly that read; security(data, analytics): a lookup target's exposure declaration is not judged when the data door's$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's two reads askedgetbecause each turns an id the caller already holds into a record.reference.buildFieldMetaMap(import-field-meta.ts) treats exactlymain's set of fields as references (those carrying areferencestring), and reads such a field's target throughreferenceTargetOf. So the security(data, analytics): a lookup target's exposure declaration is not judged when the data door's$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661 enumeration pin now sees this caller and classifies itdecided(operationlist, decisionservesReferenceTarget). Auserfield written withoutreferenceis unchanged (REWORK 6105610630); its gap is carried by import: auserfield written withoutreferencefailsreference_not_foundon import, while the spec's arbiter gives itsys_user#22785.ImportProtocolLikegains one optional member,getMetaItem. A protocol without it (plugin-auth's identity import, whose reads run under the system identity) has no declaration to judge and is not judged; none is fabricated.The decision takes no caller, so an administrator, a member and a system context (the connector pull, which drives
runImportthrough the real protocol) are answered the same, as #22661's two reads answer every caller the same.Measured on a real stack (fixture objects only)
@objectstack/verifyboot with the real SecurityPlugin, ObjectQL, SQL driver and REST layers; the #22661 fixture targets plus a row-scoped one; an administrator and a member; the synchronous import door and the async jobs door; three cells per target (naming the record, naming none, the record's id).bf515e724d)list(create-only, get-only); deny-all pinned in the core unit test onlyreference_not_foundreference_not_foundenableblock / whitelist grantinglistreference_not_foundreference_not_foundIdentical for both personas and both doors (the member's job report was read with a read grant on its own import jobs added to the fixture member). The refused rows carry the precedent's exact sentence shape (field label, then the cell). Before-readings reproduce #22661's census row 7.
Pins and ablation
packages/core/src/utils/import-runner-reference-exposure.test.ts(14): per persona, four refusing declarations answer match, miss and id alike and never callfindData; two serving declarations resolve (controls); one fail-closed case; and theuser-field control (auserfield withoutreferencehas no target and its cell reaches the write unresolved, as onmain).packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts(11): per persona, an ARMED leg (each target's own list answer read off the data door), then per door (sync and jobs) a withheld leg and a served control; plus the precedent leg.packages/core/src/security/second-object-read-exposure.pin.test.ts(4): the newdecidedrow.Ablation, each through
scripts/ablation-replace.mjson committed22bfc2770b, anchors 1 to 0, each restore proven (blob equals HEAD,git diff HEADempty):resolveRef: core pin 9 red / 4 green (every refusing leg plus fail-closed).get: 4 red / 9 green (get-only refused legs and list-only controls flip).import-field-meta.tsback on the raw carrier: enumeration pin 2 red (the stale classification and the matcher leg).f06912e442), the guard removed (reference: referenceTargetOf(f)): theuser-field control 1 red / 13 green.ablation-dist-preflightmarker present (exit 0), dogfood pin 4 red / 7 green (the withheld leg of both doors for both personas); restore rebuilt, preflight--absentexit 0, rerun 11 / 11.Verification
Each reading names the commit it was taken on. The final head is
c06ec74a87. The rework mergedorigin/maintwice:0d326bfb12, thenc06ec74a87, because #22766 edited the enumeration pin. The decided lists were united:servesExpansionTarget,servesLabelTarget,servesPayloadDisplayTarget,servesReferenceTarget,servesSummaryTitleTarget.@objectstack/coreatc06ec74a87, after a full workspace build:test93 files / 2349 passed;test:repo5 files / 55 passed;typecheckexit 0, includingcheck:test-typecheck.@objectstack/dogfoodatc06ec74a87: this pin 11 / 11 and security(data, analytics): a lookup target's exposure declaration is not judged when the data door's$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661'ssecond-object-exposurepin 25 / 25 (36 / 36);typecheckexit 0.c06ec74a87:@objectstack/resttypecheckexit 0, and its 38 test files that reach the import, export or template doors 38 / 38;@objectstack/plugin-authadmin-import-users2 files / 50;@objectstack/service-automationconnector pull 3 files / 22.eslint --no-inline-configon the 6 changed.tsfiles; population read from--print-config;--format json6 files, 0 errors, 0 warnings.eslint.config.mjsenables no type-aware linting. Repo-widepnpm lintis CI's.c06ec74a87:dispatch-gates --commandsderived 69 (the dispatch's 51 are a subset); all 69 exit 0;--ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.File surface
packages/rest/src/export-format.test.tsis outside the claim's surface. Its presentation-keys fixture declaredreferenceon anumberfield; read through the arbiter a non-reference type names no target, so the fixture now declares alookup(the fixture declared a key that is inert on anumberfield (FieldSchemahas no per-type refinement onreference)). Test-only, two literals.$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661 fixture without editing it; the member read grant on its import jobs is added in the new test file.Acceptance notes
Clause-②: yes (narrowing)(edited by the seat after the contract review 6106439614): no accept set widens, butImportProtocolLike, exported from@objectstack/core, gains one optional member (getMetaItem?). That is an additive, type-level public-surface change, spelledyesby the fleet's practice. The changeset's line follows on this PR's next head.userfields written withoutreference. The field set the import, export and template doors treat as references is exactlymain's, so such a field is unchanged here, and pinned. The door disagrees with the spec's arbiter, which gives itsys_user. That gap is measured onmainand carried by import: auserfield written withoutreferencefailsreference_not_foundon import, while the spec's arbiter gives itsys_user#22785, because fixing it is a Clause-② widening.The residual narrowing: a non-reference type that declares
reference. Read through the arbiter, it names no target. The only population whose answer moves is the legacy, schema-refusedtype: 'reference'spelling, which is still listed in the doors' own type tables. An AST census over every git-tracked non-test source underpackages/andexamples/(3,636 files at0d326bfb12, the same atc06ec74a87) found 0 shipped field declaring it. Controls: 30 reference-typed literals and 80Field.lookup/masterDetail/user/treecalls. Positive control: three planted shapes were all found. The changeset states it in FROM → TO.Measured producers. On
origin/mainbf515e724d, twelve in-repo objects refuselistby declaration and exactly one lookup points into any of them, from an object that is itselfapiEnabled: false; control: 75 lookups intosys_user. No shipped object's import answer changes.toFailedResultis untouched; [finding] import: a sandbox's own fault (CPU budget, wall-clock ceiling) reaches an import row as its debug wrapper, where the data doors answerInternal server error#22741 follows in this file.Generated by Claude Code