fix(plugin-detail): fail closed when a related-list column has no resolvable identity - #9058
fix(plugin-detail): fail closed when a related-list column has no resolvable identity#9058claude[bot] wants to merge 2 commits into
Conversation
…olvable identity
`record:related_list` filters `columns` against the field-security allow-list
built from `enforceFieldSecurity` / `redactFields`, and its else-branch kept any
entry whose identity it could not resolve. The block resolves identity through
`columnIdentity`, which deliberately refuses the table library's own
`accessorKey` (objectui#3104), while `RelatedList` renders a column as
`accessorKey || columnIdentity(c)`. A column authored `{ accessorKey: 'salary' }`
was therefore named by nobody in the filter, skipped both the FLS check and the
redact list, and painted its real values through the table's own key.
An entry the fold cannot name is now excluded. The filtering path is the only
one touched: with neither key set the fold does not run and `columns` is handed
down by reference, exactly as before.
Pinned end to end over the real table, with the live control in the same render
(a resolvable, allowed column must still show its values) and the counter-probe
that bounds the change. The member-level pin that recorded the fail-open branch
is flipped, and the console parity ledger's prose for the key moves with it.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Blast-radius measurement for the related-list identity fix turned up a second,
independent hole on the same path: when the security fold removes every authored
column, `RelatedList` reads the empty array as "no columns authored" and derives
a replacement set from the child object's schema — a path `redactFields` never
reaches, so the redacted field comes back. Reachable today without the fix (a
resolvable `{ field: 'salary' }` redacted on its own empties the array the same
way), filed as objectui#9053, and pinned here as the current behaviour it is so
the bound on this repair is legible in the tests rather than only in prose.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
⛔ HELD IN DRAFT — this seat is not landing this PR. It goes to the maintainer.The manual-floor fence on objectui#8793 said this seat dispatches the measurement and the repair but does not land it on its own authority if the blast radius turns out to be the wrong shape. It has, though not in the shape this seat wrote down. The stop I specified did NOT fire, and the measurement is goodI fenced on "the blast radius includes columns that are legitimately visible today and simply authored in the
|
Ordering hold DISCHARGED — objectui#9090 landed. ⛔ The second hold stands, so this is not being landed.
What changed for this PRobjectui#9090 merged as ⭐ That retires this PR's own loudest finding. This body's "The second hole" section says:
That is no longer true on ⛔ Why it is still not landingA second, independent hold was written by this PR's own author and is untouched by objectui#9090 — the last line of the body:
The predecessor seat recorded the same thing upward, as "the landing call · is ⇒ held in draft, awaiting one maintainer sentence. The lane's third dispatch slot is reserved for the rebase so it can move the moment that arrives. What the rebase owes when it is released — recorded now so it is not re-derived
⛔ Not re-reviewedNothing in this PR's own verification is re-litigated here: its ablation, its lit controls, its six falsification verdicts (A–F, two of which produced objectui#9053 and objectui#9054 — both of which this lane has since worked) all stand as delivered. ⭐ Its assumption F — "closing the filter cannot render zero columns" — held for a reason that has now been repaired at the source, which is the best possible outcome for a finding that was filed rather than patched. PM seat · Generated by Claude Code |
Fixes #8793
record:related_list's field-security fold kept any column entry whose identityit could not resolve. That entry then rendered its real value through the table
library's own key. The else-branch now excludes it.
The dispatch's stop condition was: does the measured blast radius include
columns that are legitimately visible today and simply authored in the
accessorKeyspelling?Measured answer: no. Zero columns in this repository stop rendering. The
measurement is below, with its commands and a lit positive control. The stop did
not fire.
But the same measurement turned up something louder, and it is not the thing the
stop was watching for — see "The second hole" below. It is filed, not patched.
The defect
Two read points resolve a related-list column's identity and they disagree; the
security filter uses only one of them.
columnIdentity, which deliberatelyrefuses
accessorKey(TABLE_ADAPTER_COLUMN_KEY, objectui#3104 — it isTanStack Table's column key, not ObjectStack metadata identity), plus a
keytail fallback this block adds.
RelatedListresolves a column asaccessorKey || columnIdentity(c), whereaccessorKeyis first-class.So a column authored
{ accessorKey: 'salary' }was named by nobody in thefilter, took the
: truearm, skipped bothenforceFieldSecurityandredactFields— and then paintedsalarythrough the table's own key.The repair
One site, one arm: the filter's else-branch now excludes rather than keeps. The
comment at the site records why, because the next reader will otherwise read a
falseas timidity.⛔
columnIdentityis not widened to acceptaccessorKey— objectui#3104excluded it deliberately, and merging the table library's vocabulary into
metadata identity is a larger decision that would leak the same confusion into
every other consumer.
Blast radius — who loses which column
The rule. A column stops rendering when, and only when, both hold:
enforceFieldSecurity: trueor a non-emptyredactFields— the fold does not run at all otherwise, andcolumnsishanded down by reference; and
field,name,fieldName,key. The canonical instance is{ accessorKey: 'salary' }; akeyless decorative or action column object is the other.
Narrower than that, once the downstream is measured.
RelatedListruns itsOWN FLS filter (
filterFLS), and that one resolvesaccessorKey || columnIdentitybefore calling
perms.checkField. So anaccessorKey-spelled column denied byfield security was ALREADY dropped downstream whenever a permission provider was
mounted and loaded — pinned in the new test file's CENSUS case. With no provider
loaded,
checkFielddefault-allows and nothing was denied on either side.⇒ the user-visible movement is confined to:
redactFieldsleg, which has no second gate anywhere; andpseudo key), which the permission evaluator default-allows downstream and the
block now drops up front.
The sweep. Population: all 7328 tracked files.
POSITIVE CONTROL for command 2: it is lit. It finds
packages/plugin-detail/src/__tests__/DetailView.relatedEntryRetired-7997.test.tsx,which really does author a related-list column as
columns: [{ accessorKey: 'name', header: 'Full Name' }]. The command finds theshape it is looking for; the finding is what each instance turns out to be.
The enumeration — every live instance, classified:
RelatedList.addGateDataSource.test.tsx,RelatedList.addPickerGuard.test.tsxRelatedListdirectly, so the block's fold never runs; and neither sets a gate key.DetailView.relatedEntryRetired-7997.test.tsxaccessorKeyauthoring is the retiredDetailViewSchema.relatedshape, pinned as refused by name (objectui#7997).content/docs/api/schema-reference.mdaccessorKeyblockstatic-table'sStaticTableColumn, a different block.packages/app-shell/.../metadata-admin/i18n.tspackages/plugin-detail/README.mdcolumns: ['name','email','phone']and warns that the hand-spelled{ accessorKey, header }form "froze both" the header and the cell.In-repo producers, all of them, all string-spelled:
deriveRelatedLists.tsforwards
relatedListColumns(every in-repo instance is['name','status']);buildDefaultPageSchema.tsforwardsrel.columns; the designer's block configfor
record:related_listexposes onlyobjectName/relationshipField/title/limit— it cannot author a column at all. And the protocol-declaredspelling is a field-name string (
RecordRelatedListProps.columns), whichresolves.
Boundary of the sweep, stated: it covers this repository. A host application
outside it could pass the undeclared
enforceFieldSecurity/redactFieldsprops AND author
accessorKeycolumns; those columns stop rendering. Neither keyis on
@objectstack/spec'sRecordRelatedListPropsnor on this block'sregistered
inputs, so no authoring surface produces them — but the block readsthem off the schema, so a host can.
The second hole (louder than the first, and NOT fixed here)
The brief asked whether closing the filter can make a list render ZERO columns.
It cannot — and the reason is worse than if it could.
When the fold removes EVERY authored member,
RelatedListreads the empty arrayas "no columns were authored" and derives a replacement set from the child
object's schema. That derivation runs the FK filter,
pruneEmptyand its ownFLS filter — but
redactFieldsis a block-level concept that never reaches it.So the redacted field comes back. Measured on the real table:
That reproduction uses spec-canonical authoring and is reachable today: this
branch neither opens that hole nor repairs it. Reported as objectui#9053, and pinned in
the new test file as the current behaviour it is, so the bound on this repair is
legible in the tests and not only in prose.
Assumptions tested against
origin/maincolumnIdentityexcludesaccessorKeyper objectui#3104TABLE_ADAPTER_COLUMN_KEY, docblock "NOT an identity key — deliberately excluded", rationale exactly as triage stated.: truearm is the only fail-open on this pathrecord-details.tsx'sfilterListcarries the identicalidentity-unresolved ⇒ keepbranch, with an even narrower reader (nokeyfallback). Not touched here; reported as objectui#9054.record-highlights.tsxfails CLOSED already (it drops entries without a stringnamebefore the allow-list) and is the contrast case.enforceFieldSecurity/redactFieldsare the only two security functions on this pathRelatedList.filterFLSis a third, and it readsaccessorKey. This is why the movement is confined to the redact leg — see the blast radius above. Both are reached once the fold is closed: the allow-list is still built fromreadableFields()andredactFields.colName,filteredColumns,filterFLS,effectiveColumns); no line citation appears in the diff.RelatedCountStore.fetchtakes an object name plus$filter/$top/$countand has no column input at any point; the badge is column-independent, so no count moves.redactFields.Tests
New:
RecordRelatedListRenderer.unresolvedIdentityFailClosed-8793.test.tsx— sixcases over the REAL
RelatedListand the realdata-table, reading renderedcells: the reported leg, the three-row boundary (allowed+resolvable /
denied+resolvable / unresolvable), the FLS leg, the counter-probe that bounds the
change (filter off, same column, still renders), the downstream-FLS census, and
the pinned limit of the repair.
Every positive carries a LIVE CONTROL in the same render: a resolvable, allowed
column must still show its values. Without it, "the redacted value is gone" is
equally satisfied by a fold that filtered everything out.
Flipped: the member-level pin in
RecordRelatedListRenderer.columnMembers.test.tsxthat recorded the fail-open branch, which was written to red when this landed. It
now also pins that the drop is by unresolvability rather than by matching the
redacted name, and that an unfiltered list still hands the member down untouched.
The console parity ledger's prose for the key moves with it.
Ablation, from the committed state (predicted split written down first, and it
matched): the else-branch was mutated back to
trueon disk, proven on disk(blob
0e93902dto979a6b72, plus a grep count of each arm before and after),run, then restored and the restore proven by bytes (
git diff HEADempty, blobback to
0e93902d).The three live controls stayed green on both legs, which is what separates this
repair from "filter everything out".
Verification
Lint is narrowed to the changed files and the narrowing is declared: the config's
own population is
**/*.{ts,tsx}(4624 tracked files); this run linted 4 files(count read from
--format json), 0 errors, 41 pre-existingno-explicit-anyandreact-refreshwarnings; andeslint.config.jsextendstseslint.configs.recommendedwith noparserOptions.projectand noprojectService, so type-aware linting is off and this diff cannot move theverdict on any untouched file. Measured at
fb1fb875b, the final commit. Therepo-wide run is CI's.
Scope
The triage star — that both read points should end up agreeing — is correct and
out of scope; unifying them is a larger change than this branch. Successor cards:
objectui#9053 (the emptied-array fallback), objectui#9054 (the
record:detailssibling). Neither is touched here.
decides whether the
accessorKeyspelling should be authorable on this key atall, which is the question behind the whole card.
Session:
https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVBGenerated by Claude Code