Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-calendar,plugin-gantt,plugin-detail,plugin-dashboard,app-shell): FLS-gate $expand at the five remaining build sites - #7428
Merged
os-project-manager merged 2 commits intoSep 3, 2026
Conversation
…hell): FLS-gate `$expand` at the five remaining build sites objectui#7215 / PR #7229 FLS-gated the `$expand` projection at the two sites in its scope. `buildExpandFields` (and `computeLookupExpand`, the dashboard's own whitelist) are reached from more places; this closes the five the card names. Three of them — calendar, gantt and the record page — pass NO column list, so the helper falls back to every declared relation on the object, denied ones included: the maximal ask, by default rather than by configuration. DetailView was INPUT-gated, which is the route #7229 measured as unsound: an emptied column list reads as "no restriction" and WIDENS the request. The gate is on the helper's OUTPUT at every site, copied from #7229 rather than re-derived, so the "checkField answers false for an undeclared key" trap stays structurally unreachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
…g build sites Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Contributor
✅ 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
|
os-project-manager
commented
Sep 3, 2026
CollaboratorAuthor
The out-of-scope finding named at the end of the description is filed as #7429 — the seven Two notes on the description itself, recorded rather than silently corrected:
Generated by Claude Code |
os-project-manager
marked this pull request as ready for review
September 3, 2026 02:16
This was referenced Sep 3, 2026
Uh oh!
There was an error while loading. Please reload this page.
os-project-manager
deleted the
claude/issue-7230-fls-gate-expand-five-sites
branch
September 3, 2026 02:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#7230
All five sites land. The eager-closure constraint the card carried did not bind — measured, not assumed; figures below.
What an unauthorised principal could ask for before, and cannot after
objectui#7215 / PR #7229 FLS-gated the
$expandprojection at the two sites in itsscope (
ObjectGrid,ListView). The projection is built in more places than that. Thiscloses the five the card names.
mainplugin-calendar/src/ObjectCalendar.tsx:351buildExpandFields(objectSchema?.fields)— no column listplugin-gantt/src/ObjectGantt.tsx:675app-shell/src/views/RecordDetailView.tsx:402plugin-detail/src/DetailView.tsx:526plugin-dashboard/src/ObjectDataTable.tsx:675computeLookupExpand(The card's line numbers were
350/674/680; the first two are off by one and thedashboard one by five on current
main. Same calls.)The first three are the sharp ones.
buildExpandFieldsreads an absent column list as"no column restriction" and falls back to every declared relation on the object, denied
ones included. So a standalone calendar, a gantt, and every record page in the console asked
the server to resolve the object's full relation set by default rather than by
configuration.
$selecton a denied lookup asks for a bare foreign key;$expandasks theserver to resolve it and hand back the related record — the larger of the two requests was
the ungated one.
⭐
DetailViewis not what the card says it is, and the difference mattersThe card lists it as "passes a column list, ungated". Measured on
main, it is input-gated:allFieldsis collected fromschema, which isgatedSchema— already FLS-filtered field byfield, and whose own comment names "$expand build" among the uses it means to protect.
So this site is a live instance of exactly the route PR #7229 measured as unsound and rejected.
The consequence is not "a denied lookup slips through in the ordinary case" — it is worse and
narrower:
⇒ Filtering the input WIDENS the request precisely where the principal may read least. A
detail view whose authored fields are all denied had its column list gated down to
[]and its$expandwidened from the relations it asked for to every relation the object declares. Thesame widening is reached with no authored field list at all, where the input filter has nothing
to remove and the expansion is maximal from the start. Both are pinned.
The input filter stays — it is load-bearing for the render half — but it is no longer what
decides the projection.
Reachability and grading — stated narrowly
no authored column list at all, which is their default shape.
mechanism read rather than assumed:
plugin-security'sFieldMasker.maskRecorddoesdelete result[field]on every unreadable key and objectql's expand path writes the resolvedrecord back under that same key, so one statement removes the expanded object and the bare
id alike; the expansion sub-read itself takes the referenced object's full CRUD + RLS + FLS
treatment (objectstack#7626). Same grading objectui#6898 and
$expandcarries no FLS gate at either projection site, so a lookup column the principal cannot read is still expanded and its value returned #7215 were given, for the samereason, and it becomes load-bearing for any backend that does not strip.
The fix — #7229's shape copied, not re-derived
The gate is on each helper's OUTPUT at every site. On the three no-column-list sites the
input-side alternative is not merely unsound but unreachable: the call passes
undefined, sothere is no input to gate. Gating the output also satisfies the ordering requirement
structurally rather than by convention —
buildExpandFieldsreturns a subset of the object'sdeclared reference-bearing fields, and
computeLookupExpandresolves both of its armsthrough the object schema's own field map, so every name either gate judges is declared by
construction. The "
checkFieldanswers false for an undeclared key" trap is thereforeunreachable and a derived / host-joined column is never judged. Neither helper is changed.
An unanswered policy filters nothing (
isLoaded: falseis the no-provider default, forever), andpermsis in each site's dependency list, so the projection is rebuilt the moment the answer lands.One structural note in
RecordDetailView, load-bearing rather than cosmetic:usePermissions()moved ~670 lines up, above the record-load effect. An effect's dependency array is evaluated
during render, so listing
permsthere while the binding was still declared below would throwCannot access 'perms' before initialization— a crash, not a stale value. The later site nowdestructures that one value instead of calling the hook again, so the hook order is unchanged in
shape. Same lesson PR #7229 recorded for
ListView's memo.@object-ui/permissionsis added as a dependency ofplugin-calendar,plugin-ganttandplugin-dashboard(the other two already had it).check:phantom-depspasses.The card required this be measured rather than assumed, and required the
app-shellsite to bedropped if it put the
frameworkchunk over its ceiling. It does not. Two full consolebuilds in the same container,
main(bf244f400) versus this branch's final commit96a284fe2,read from
apps/console/dist/eager-closure.json(exact gzip bytes, not the gate's rounded KB):bf244f400)96a284fe2)frameworkvendor-objectstackui-componentsframeworkdid not move by one byte, so its 177 B of headroom against the 524,000 B ceiling isuntouched and #7399 is not blocked by this PR. The emitted chunks confirm the mechanism rather
than a source-level guess: all three chunks above kept their content hash
(
framework-Cf7M9oOC.js,ui-components-B7CUYYAi.js,vendor-objectstack-CAAM4J8I.js) — theyare byte-identical files.
Why the
app-shellsite was never aframeworkrisk. Theframeworkgroup inapps/console/vite.config.tsis/packages[\\/](core|react|types)[\\/]/—app-shellis not amember and cannot route there. Read off the emitted graph, the whole +59 B is attributed:
plugin-calendar,plugin-ganttandplugin-dashboardare not in the eager closure at all —0 bytes, confirmed by their absence from the report's file list rather than by assuming "this
package is lazy". The aggregate has 13,396 B of headroom and
check:eager-closurepasses on bothbuilds.
⛔ No ceiling was raised, no baseline re-pinned, no gate weakened.
Tests — reproduced first, one file per site
New, all mirroring
plugin-grid/src/__tests__/expandFls-7215.test.tsx:plugin-calendar/src/__tests__/ObjectCalendar.expandFls-7230.test.tsxplugin-gantt/src/ObjectGantt.expandFls-7230.test.tsxapp-shell/src/views/RecordDetailView.expandFls-7230.test.tsxplugin-detail/src/__tests__/DetailView.expandFls-7230.test.tsxplugin-dashboard/src/__tests__/ObjectDataTable.expandFls-7230.test.tsxWhich pins discriminate, said plainly.
DetailView's split is the interesting one and waspredicted before the run: its input filter already delivers the ordinary case, so PIN 1 (denied
lookup among readable authored fields) and PIN 2 (the readable-lookup control) are green in both
directions and are labelled as such in the file. The two reds are the widening — every authored
field denied, and no authored field list at all — which is what this card actually buys at that
site. A green pin proves nothing on its own; naming the non-discriminating ones is the discipline
RecordDetailView.sectionHeadingsRenderPath-6190.test.tsxrecords.Every file carries the live controls, not just the reds: a permitted lookup still expands (a
gate that emptied the expansion would paint raw foreign-key ids — the failure objectui#6453 fixed
on the calendar);
master_detailis pinned besidelookup; an undeclared derived column isuntouched and does not take the expansion down with it; an unanswered policy filters nothing;
and the all-denied case yields no
$expandrather than a widened one. Each harness waits on areal recorded call — and, where the schema arrives asynchronously, on the schema-dependent
query specifically — so "the component stopped fetching" times out instead of reading as an empty
expansion.
Gates run locally, on the final commit
96a284fe2vitest—plugin-calendar,plugin-gantt,plugin-dashboard,plugin-detail(full packages)vitest— all 24RecordDetailView.*.test.tsxvitest— all 19 otherapp-shelltests namingRecordDetailViewtype-check— all 5 packages (tsc --noEmit && tsc -p tsconfig.test.json)turbo run lint— all 5 packages in fullcheck:eager-closurecheck:phantom-depscheck:control-bytescheck:vi-mock-specifiers/check:vi-mock-inheritcheck:self-import/check:side-effects-array/check:sdui-registration-pinscheck-changeset-presence/check-changeset-no-majorDeclared narrowing.
app-shellcarries 608 test files; the four plugin packages alone took9m of a ~10m foreground ceiling, so the full
app-shellsuite was not run locally — the 43 filesabove are every test naming
RecordDetailView, which is the complete set this diff can reach inthat package. Lint was not narrowed:
eslint .ran in full for all five packages, and theconfig is not type-aware (no
parserOptions.project/projectService), so this diff cannotmove the verdict of any file it does not itself contain. CI runs the whole farm regardless.
Out of scope, filed rather than fixed
The card states
buildExpandFields"is called from five more places". Measured on currentmain,it is called from eleven production places outside #7229's two. Seven remain ungated after this
PR — kanban, tree,
ObjectView, map, gallery, timeline and the metadata-adminPagePreview— andall seven pass no column list, i.e. the same sharp shape as the three sharpest here. Filed
separately with the per-site measurements; not touched here, because they are outside this card's
declared file surface.
🤖 Generated with Claude Code
https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Generated by Claude Code