Repository navigation
fix(metadata-protocol): an object's references list a permission set named only by its fields keys (#23539) - #23554
Conversation
…ed only by its fields keys Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm Co-authored-by: Claude <noreply@anthropic.com>
…ferences-permission-fields-keys
…s keys in an object's references
A composite-key site (permission.fields, keyed OBJECT.FIELD) is seeded in
the derived reference index, kept out of byTarget, with derived premises:
the source still declares fields as a root record and ObjectSchema.name
still rejects '.'. findReferencesToMeta reads it through its own walk,
matching the text before a key's first '.', at path fields{key}.
Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 4c0b45469a3fe69691124ff611510106e7396397 && git checkout 4c0b45469a3fe69691124ff611510106e7396397
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8226755ee94cd6fe26a1580daea58251e4af99ad 8ca891b1d5e9a83ee57b6ed14bf4f67c0e8eeb73 && git checkout -B drift-repro 8226755ee94cd6fe26a1580daea58251e4af99ad && git merge --no-ff 8ca891b1d5e9a83ee57b6ed14bf4f67c0e8eeb73
node scripts/docs-audit/affected-docs.mjs --json 8226755ee94cd6fe26a1580daea58251e4af99ad
|
席位核对
Verdict: ACCEPT. Not governed (
|
Fixes #23539
Clause-②: no
Summary
findReferencesToMetaservesGET /api/v1/meta/object/:name/references, the admin "Used by" panel. It now lists a permission set that names an object only in itsfieldskeys.PermissionSetSchema.fieldsis keyedOBJECT.FIELD, and the security plugin applies a set's field rules to an object by that key prefix (PermissionEvaluator.getFieldPermissions). Before this change, such a set was missing from the object's answer. The panel could then read "Safe to delete." while the set still masked the object's fields.What changed
packages/metadata-protocol/src/reference-sites.ts. A composite-key seed,COMPOSITE_KEY_REFERENCE_SITES:permission.fieldsnames anobjectthrough the text before a key's first.. It is exposed as a new index member,compositeKeyByTarget, and kept OUT ofbyTarget.nameVocabularyRejectsKeystyle, and fail closed.permissionmust still declarefieldsas a root record, andObjectSchema.namemust still reject.. If either stops holding, the seed drops out and the derivation pin goes red.'object'alone.nameVocabularyRejectsKey's schema read is factored intonameVocabularyVerdict. Its answers are unchanged: the existingunanswerableTargetTypespins still pass.packages/metadata-protocol/src/protocol.ts, two regions only.collectCompositeKeyHeadHits, placed besidecollectReferenceHits.referencedNamesAtandcollectReferenceHitsare byte-unchanged..(dot index above 0)..full_name) names no object.fields{key}.findReferencesToMetafolds the composite sites into the same one-read-per-source-type matchers. The new row's kind ispermission fields. Every existing row, the response shape and thefieldtarget's 501 are unchanged.duplicatePackageis not touched, and itsbyTarget.get('object')reading is unchanged.Why the seed is not a
byTargetrowThis was measured in round 1 (report comment 6179812710, experiment E1) with a plain
{ permission, fields, object }row inbyTarget. The answer did not change, because the walk compares a WHOLE value with the object's name. AndduplicatePackagebroke. It readsbyTarget.get('object')as a flat set of object-naming property names, so a copied object's ownfieldskey was renamed.protocol-duplicate-package-unprefixed-object.test.tswent red, withnoteCopy.fields.ticketundefined. The derivation pin now asserts thatbyTarget('object')has nopermission.fieldssite.Why the split is the first
.ObjectSchema.nameandFieldSchema.nameare both^[a-z_][a-z0-9_]*$, so neither can contain.. The evaluator (key.startsWith(object + '.')), lintsecurity-fls-unknown-fieldandduplicatePackage's field-rule rewrite all make the same split.Save-door readings (
saveMetaItem, real kernel):customer.full_name.extra) is refused with 422security-fls-unknown-field.security-fls-unqualified-key.no_such_object.x) and an empty-object-part key (.full_name) are stored.Evidence (final head
8ca891b1d5)packages/objectql/src/protocol-references-permission-field-keys.test.ts: ObjectKernel + ObjectQLPlugin + memory driver; every item passes the real save door.main(round 1,9e67e008e1over7b64a3d73d):Tests 2 failed | 2 passed (4). The two failures wereexpected [ Array(1) ] to include 'refsrc_customer_fls@fields{key}'andexpected [] to deeply equal [ 'refsrc_invoice_fls@fields{key}' ].Tests 4 passed (4).fields{key}. (2) Negative control: a set whose keys name only another object is not listed, even where its FIELD part spells the asked object; it is listed once for the object it does name. (3) Theobjects{key}row is unchanged. (4) Thefieldtarget still refuses withNOT_IMPLEMENTED/ 501.fieldtarget byte-identity. A real-kernel probe capturedfindReferencesToMeta({ type: 'field', name: 'refsrc_customer.full_name' })before and after the fix. Both runs gaveNOT_IMPLEMENTED 501 refusal:true, message sha2567b4aee8698a564f69599ba38e1b3f54ec2623cd5336a8e57e267b093ea4b2457. The probe was a throwaway file, deleted afterwards.refsrc_customeranswered the grant row only. After, it answers that same row plus the newfields{key}row.collectCompositeKeyHeadHitsinfindReferencesToMeta.ablation-replace.mjs, anchor 1→0, blob5be7b1c1adc7→09028903ed91.dist/index.jsanddist/index.cjs.ablation-dist-preflight --absent: marker absent from dist.Tests 2 failed | 2 passed (4), the same two listing assertions.git diff HEADempty. The rebuild re-emitted both declaration files (check-dts-emitted 2/2). Preflight: marker present in both JS files, and the tree is clean. Pin:Tests 4 passed (4).pnpm --filter @objectstack/metadata-protocol exec vitest run:Test Files 236 passed | 3 skipped (239),Tests 28412 passed | 19 skipped.pnpm --filter @objectstack/objectql exec vitest run --project local:Test Files 419 passed (419),Tests 8112 passed. This includesprotocol-references,protocol-duplicate-package-unprefixed-objectandprotocol-duplicate-package-permission-field-keys.reference-sites.derivation.test.ts:Tests 25 passed (25).pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.--listFilesincludesreference-sites.ts,protocol.tsandreference-sites.derivation.test.ts.pnpm --filter @objectstack/objectql typecheck: exit 0, withcheck:test-typecheck: OK — 40 file(s) / 234 error(s) / 65 pinned signature(s) held.8ca891b1d5:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 67 commands from the 5 changed paths. All 67 were run, each exit captured before any pipe. 66 exited 0.pnpm check:dual-build-cjs-loadsexited 3 with PREREQUISITE NOT MET: it needs about 70 packages built, which this worktree does not hold, so it is NOT MEASURED here and CI builds them.pnpm check:lean-entry-closurefirst exited 3 on a missingpackages/objectql/dist; after an objectql build it was re-measured at exit 0.--ranreconciliation:67 derived, 66 run, 1 NOT-MEASURED, 0 UNRUN. The 11 wide-population families and the 6 workflow-valued families are CI's.Acceptance notes
fields{key}follows the existingobjects{key}grammar. A set naming an object in bothobjectsandfieldsanswers two rows, one per path. That is the walk's existing one-row-per-path behaviour.fieldtarget's refusal text says every property that names a field holds the bare field name.permission.fieldsis the one derived field site whose keys are the composite form, so the sentence is inaccurate for that one site. The refusal itself stays honest, and the card keeps thefieldanswer unmoved.duplicatePackagereadsbyTarget('object')as a source-type-blind property-name set. This is why the seed lives in its own map, and the derivation pin guards it..full_name) went to the seat for filing. It is out of this PR's scope.Generated by Claude Code