Repository navigation
fix(metadata): the object read's sortability follows the caller's field permission for every caller class (#22250) - #22308
Conversation
…runtime projections An exempt caller's posture now carries a lazy `runtime` question (that caller's own field permission), and `resolveObjectSchemaRuntimeView` derives the view the runtime projections beside the schema read from. Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
… on every item exit The cached arm folds the runtime view's fingerprint into the ETag and judges the conditional request itself for a posture carrying runtime. Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…red runtime view Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…field permission Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…of a second isSystem read Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…he test layer typechecks Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 145 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 c04c5691ed821c9e0965e32cad5fb34be863f480 && git checkout c04c5691ed821c9e0965e32cad5fb34be863f480
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dc4a5c630844f04587e790c21ca807f306239518 a337d0cc537ea09b8a2bee82f9b80270df735b1e && git checkout -B drift-repro dc4a5c630844f04587e790c21ca807f306239518 && git merge --no-ff a337d0cc537ea09b8a2bee82f9b80270df735b1e
node scripts/docs-audit/affected-docs.mjs --json dc4a5c630844f04587e790c21ca807f306239518
|
…ssion as an absence case Text only: the module docblock's category list gains case 5, and the ObjectSortabilitySchema.fields description no longer claims the domain is the served field map for a caller exempt from the object-schema mask. Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…ed spec text Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #22250 (body; triage 6057391955; claim 6058980283 as edited; os-dev-reports 6061977564 and 6063483631), PR #22308 (body, 11-file list, the net diff from merge-base ① Derived judgmentsDirection (triage: tighten, never loosen) — right.
D4 sick-service invariant — right. Caching / D3 — right.
Public surface (judged on the
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #22250
Clause-②: yes (widening)
What this does
For one caller, the object metadata read and the data route disagreed about a field that caller's field-level permission marks unreadable. The
sortabilityprojection served beside the schema onGET /meta/object/NAMEmarked that fieldsortable: truefor a platform-admin or organization-admin caller, while the data route refuses a sort or filter on it with403 PERMISSION_DENIED.After this PR, the
sortabilityprojection is derived from the caller's own field permission for every caller class. The object definition itself (item.fields) is unchanged: whole for the exempt classes, because Studio and Setup authoring need it, and masked for everyone else. The data route and the permission read are unchanged. The spec's description of the projection is amended to match.Measured first: the exemption and its stated reason
isObjectSchemaMaskExemptinpackages/metadata-core/src/object-schema-fls.ts(ADR-0106 D4). It exemptsisSystemand any caller holdingmanage_metadata,studio.accessorsetup.access. The platform-admin set holds all three. An organization admin holdssetup.access.resolveObjectSchemaMaskPostureanswers such a callerpassthrough/exemptbefore it asks the security service.PUTwould delete the fields it withheld. So this is the triage ruling's first branch: keep the definition read for authoring, and make the runtime projections agree with the caller's field permission.translateMetaEnvelopeinpackages/rest/src/meta-item-read-gate.ts, which both transports call) already read the post-mask document. For an exempt caller, though, that document is the unmasked one, so the projection inherited the exemption.sortabilityis the only runtime projection the object read serves beside the definition. The list read and the layered view serve definitions only, with no projection.item.fieldsis itself the definition the ruling keeps for authoring. objectui handles its pickers against the permission read on its side (objectui#11925, objectui#11943).Door table:
examples/app-showcase, real bootSetup:
showcase_client_liaison(which marksshowcase_project.budget,spentandbudget_remainingasreadable: false) is bound to theeveryoneposition. Callers: the seeded platform admin;phone.demopromoted to organizationadmin;auditor.demoas a member.The table was measured at base
c8bb3c8d9cand at head8b412a83e2, then re-measured on the final heada337d0cc53, which carries805f1ba09a's change to the exemption predicate's source and the spec text. Every head cell below reads the same on8b412a83e2and ona337d0cc53.GET /auth/me/permissions,budgetreadable: false→ samereadable: false→ samereadable: false→ sameGET /data/showcase_projectGET /data/showcase_project?sort=budget(and?budget=100)403 PERMISSION_DENIED→ same403→ same403→ sameGET /meta/object/showcase_project,item.fields.budgetsortability.fields.budget{ sortable: true }(23 entries) → absent (20 entries){ sortable: true }(23) → absent (20)ETag"39ce56dc"→"39ce56dc~3c4b8092""39ce56dc"→"39ce56dc~3c4b8092""39ce56dc~32c59ec2"→ sameGET /meta/object/showcase_project/layers(authoring)effective22 fields withbudget→ samebudget→ sameAt head, all three classes are served byte-equal
sortability. Conditional reads, the same on both heads:304, and so does the organization admin's own ETag (ona337d0cc53)."39ce56dc"answers200.200.After unbinding the set, the unrestricted admin's ETag is
"39ce56dc"again (byte-identical, ADR-0106 D3), andbudgetis{ sortable: true }with 23 entries.How
@objectstack/metadata-core(object-schema-fls.ts). Theexemptposture of a caller exempt by capability, in a deployment with asecurityservice, carries a lazyruntimequestion. That question asks the samegetMetadataReadableFieldsthe mask asks a non-exempt caller, at most once. It never rejects: a throw or anundefinedanswer settlesundetermined, with onewarn, so D4's "a sick security service cannot fault an exempt caller" holds.resolveObjectSchemaRuntimeView(document, posture)returns the view the runtime projections are derived from: the served document for every other caller, and for an exempt caller thefieldstheir permission reads, plus a fingerprint of what it withholds. Each withheld name in that fingerprint carries a qualifier (@occurs in no machine name), so an exempt caller and a projected caller denied the same field never share a validator.isObjectSchemaMaskExemptnow delegates its capability half to a private helper, which the resolver asks alone. That adds no second elevation read (check:system-context-census).@objectstack/rest.translateMetaEnvelopetakes an optionalruntimeDocument(default: the served document) and derivessortabilityfrom it.createMetaItemAnswerstep 5 computes the runtime view and passes it through thetranslateEnvelopeport, which gains an optional third parameter.rest-server.ts, the cached arm ofGET /meta/:type/:nametreats a posture carryingruntimelike a projecting posture: the protocol is not handedIf-None-Match, the runtime view's fingerprint is folded into the ETag, and the304is judged against that folded value. The privatetranslateMetaEnvelopeandmetaItemAnswerSourcesforward the view.@objectstack/runtime(domains/meta.ts). OnlyanswerMetaItem'stranslateEnvelopeport, which forwardsruntimeDocument. Every dispatcher item read already goes throughcreateMetaItemAnswer.Spec text (cross-lane,
domain:spec)Declared by the seat on
domain:spec's seat post. The change is to text only: no shape, key, enum or accept-set change.packages/spec/src/api/sortability.zod.ts.ObjectSortabilitySchema.fields.describe()now says: "The domain is the served field map plus the always-provisionedid, less any field the caller's own field permission withholds: a caller exempt from the object-schema mask (ADR-0106 D4) is served such fields initemand gets no entry for them; for every other caller the served map already excludes them." The list of absent names gains "a field the caller may not read".resolveObjectSortability's docblock says that, for a D4-exempt caller, it runs on the document narrowed to the fields the caller's own permission reads.content/docs/references/api/sortability.mdxandcontent/docs/references/api/protocol.mdxwere regenerated bypnpm --filter @objectstack/spec check:generated --fix. That forced a spec build, found onlycheck:docsstale (14 of 15 green), and regenerated it. A secondcheck:generatedrun then reported all 15 artifacts current. Nothing generated was hand-edited.@objectstack/specentry was added, because no changeset gate requires one for a description-only change:Check Changeset(pr-automation.yml) counts the changesets a PR adds;check-changeset-no-major.mjs, driven offline with this body as its event, readsClause-②: yes (widening)and is satisfied bymetadata-coreandrestgradedminor, and it lists no ungraded spec package;check-empty-changeset.mjsis green.Tests
packages/metadata-core/src/object-schema-fls.test.ts(new describe block):fields;undefinedand a throw both settle undetermined with awarn;isSystemand a deployment with no service carry no question.One existing assertion went from
toEqualtotoMatchObjectplus atypeof runtime === 'function'check, because the exempt posture now carries the function. Its "asks nothing" pin (asked === 0) is kept.packages/rest/src/meta-object-sortability-caller-fls.test.ts(new), per class, on the cached (default) and uncached branches:sortabilitynever names the unreadable field, and it is equal across classes;resolveObjectSortability(item);304, the member's ETag does not cross over, and an unrestricted admin keeps"v1"and its304;200with the definition and the unprojectedsortability.packages/runtime/src/domains/meta-object-sortability-caller-fls.test.ts(new): the same per-class pins on the dispatcher's protocol-backed and registry-backed item reads.Reverse verification (from committed state, on-disk proof by
scripts/ablation-replace.mjs):translateMetaEnvelopewas mutated to derivesortabilityfrom the served document (the behaviour before this PR). The REST suite went 5 failed / 8 passed. Afterpnpm --filter @objectstack/rest build,ablation-dist-preflightfound the marker indist, and the runtime suite went 4 failed / 4 passed. Restore: blob equal toHEAD,git diff HEADempty; rebuild; preflight--absentwith a clean tree; 13/13 and 8/8 green.runtime. The metadata-core file went 7 failed / 29 passed, all assertion failures. Restored, blob equal toHEAD.The first B attempt used an undefined identifier and went red on a
ReferenceError, so that reading is void. The second was refused by the tool because the replacement contained the anchor, so nothing was mutated.Local verification (final head
a337d0cc53)All suites ran through the shared verify lock. Exit codes were read from its
VERDICTline.@objectstack/spectest: 625 files, 18711 passed and 1 todo.typecheckexit 0, which includescheck:scripts-typecheckandcheck:test-typecheck.dispatch-gates --commandsderives 111 commands for the full touched path set (11 paths). All 111 ran and exited 0.--ranreconciliation: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero: every entry records its exit code).scripts/platform-object-tenancy-census.json, changed onmainsince this branch's merge base, so that family read this branch's copy.sortability.zod.ts:eslint --no-inline-config --format jsonreports 1 file, 0 errors and 0 warnings.--print-config) enables 5 syntax rules andparserOptions.projectisnull.@objectstack/metadata-core: 18 files, 425 passed;typecheckexit 0.@objectstack/restat3768ffad20: 268 files, 5142 passed and 327 skipped;typecheckexit 0.@objectstack/runtime: 340 files, 5508 passed and 19 skipped at805f1ba09a;typecheckexit 0 at3768ffad20.3768ffad20.mainwas merged once, at9865012548(fix(rest,runtime): ?package=all names no package on every /meta door of both hosts (#22188) #22265 touchedrest-server.tsandruntime/src/domains/meta.tsin a different region). Against the currentorigin/main,git merge-tree --write-treeexits 0 and no file overlaps, so no second merge was made.Answered by the seat
yes (widening), withminorformetadata-coreandrestandpatchforruntime. The claim's line was revised to match. Two transport-local copies of the posture logic were refused.packages/speclifted for exactly this text. It is declared cross-lane ondomain:spec's seat post.Acceptance notes
These are observations only, read from source and not measured. No card is filed for them.
packages/core/src/utils/column-sortability.tsrepeats the sentence this PR amends ("the served field map plus the always-provisionedid"). That is another repo and is not edited here. Carrier: objectuidomain:uiseat 1 (objectui#11925's filer).sortabilitykeys on readability, while the security service's query guard also refuses a predicate or sort on a field served partially masked (maskingRule). PergetQueryableFields's docblock, a masked field is readable but not queryable. No in-repo app declares amaskingRuletoday (showcasepermission-sets.ts). Whoever touchesresolveObjectSortabilitynext should weigh it.externalobject, an author-declared platform-injected column that the caller cannot read is re-added byunprovisionedInjectedColumnsassortable: trueplus a caveat once the mask removes the declaration. This is the same for every class.Generated by Claude Code