Skip to content

fix(plugin-grid): re-apply FLS on generateColumns()'s authored columns path - #6897

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6799-objectgrid-authored-columns-fls
Aug 30, 2026
Merged

fix(plugin-grid): re-apply FLS on generateColumns()'s authored columns path#6897
os-sam merged 1 commit into
mainfrom
claude/issue-6799-objectgrid-authored-columns-fls

Conversation

@os-sam

@os-sam os-sam commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Fixes #6799

Executes the maintainer ruling of 2026-08-30 12:21Z (verbatim 「第一批其余同意」): 补检查, same
direction as #6723. Option B (host owns it) was refused and is not re-raised here.

generateColumns() has three default paths. Two re-applied field-level security and the authored
columns path did not — the last one left after #6723 closed the inline-data path, and the worst
of the three to leave: it is the most reachable, running whether the grid fetches its own rows
or not, while the inline-data path needs a host to hand rows down.

Both arms (ListColumn[] and string[]) now filter through
perms.checkField(objectName, fieldName, 'read') under the same perms.isLoaded && schema.objectName deferral the other two paths already use.

Measurements owed before implementing

1. The card's own reading, re-verified on today's merged main (b392674c5). The card's
evidence was measured on d06059f24, before #6723 merged. It no longer reproduces literally, and
the difference is fully explained:

reading card (d06059f24) today (b392674c5)
git grep -n checkField -- packages/plugin-grid/src/ObjectGrid.tsx 2 lines (1 comment, 1 call) 4 lines (2 comments, 2 calls)
the extra pair #6723's inline-data fix, lines 2352 / 2368
authored columns block re-applies FLS no no — confirmed

The load-bearing claim holds: the if (cols) block (lines 1961–2289 on b392674c5, both arms)
contained 0 occurrences of checkField or perms. Both arms return before reaching the
object-schema path's gate. The path table is correct as written; only the raw grep count moved.

2. The composition census — the impact denominator nobody had produced. Searched packages/,
apps/, examples/ for sites reaching the authored-columns path without passing through
checkField first. It is not zero:

site composition filters first?
packages/plugin-view/src/ObjectView.tsx (L1470 / L1822) objectName + columns from the named view / table.columns no — 0 checkField in file
packages/plugin-designer/src/ObjectManager.tsx (L113 / L252) objectName: 'object_definition', 4 authored ListColumns no
packages/plugin-designer/src/FieldDesigner.tsx (L166 / L414) objectName: 'field_definition', authored ListColumns no
apps/console/src/dev/DevRowActions.tsx (L45) dev harness, authored columns + inline data no
packages/plugin-grid/demo/bulk-actions.tsx (L294) demo no

3 shipped + 2 dev/demo. The control that must hit in the same query is non-zero as required:
ListView (L2166) emits type: 'object-grid' with columns: effectiveFields, and
effectiveFields is gated (L2060–2067) — 32 in-repo ListView references. So a zero above
would have been a reading; it was not zero anyway. One count serves this card and #6723.

3. Whether rewriting #6598's rationale disturbs its own pin. It does not. Its three files plus
#6723's ListView.inlineFlsNoop ran 18/18 green with this fix in place and again after the
rationale rewrite. The grid-side gate is a measured no-op through ListView, which is the
redundancy the ruling wants.

The limit, and which key is judged

⚠️ Only keys the object declares are judged; everything else passes through untouched. This
matters more here than on the inline-data path: a ListColumn carries label/link/action/
prefix/width, so a column whose field the object does not declare is not a mistake but a
legitimate authored derived column, and deleting it would destroy authoring work. checkField
answers false for a field the policy has never heard of, so judging derived keys is a different,
wrong rule — not a stricter reading of the same one. hasOwnProperty keeps an inherited name
(constructor) from being mistaken for a declared field.

⛔ The judged key is read through columnIdentity, never off a bare string, as the ruling
requires by name. It folds the three authored identity spellings — 'salary',
{ field: 'salary' } and the legacy { name: 'salary' } — which is why one predicate serves
both arms. A gate reading col.field directly would find no identity on the legacy spelling and
wave a denied declared field straight through; PIN 5 pins exactly that.
resolvesToDataColumn still runs first and keeps owning its own decisions, so the gate narrows
what survives and never resurrects a hidden or unresolvable column.

#6598's rationale, rewritten in this same commit (ruling requirement)

The stale premise was in three places, not one — a repo-wide sweep found them all:

  1. packages/plugin-list/src/ListView.tsx — the hasAuthoredColumns docblock.
  2. .../__tests__/ListView.unauthoredColumnProjection-6598.test.tsx — the file docblock.
  3. the same file — the inline comment on the final assertion.

All three said the predicate is correct because the grid would not re-check. That is now
false. The predicate is unchanged and stays correct for the half that never depended on the grid:
an empty projection is the author's projection after filtering, and handing the grid
"unauthored" would replace it with the object's default columns — fields the author never
declared. FLS-checked now, but still not what was authored. Authoring intent, not the field
gate, is what #6598 protects. Each site names the retired reason explicitly so the next reader
sees a correction rather than a silent edit.

Verification

Red first, and it is a security assertion. On untouched main, the new pin file ran
5 failed / 8 passed — every red the denied column rendering:

AssertionError: expected [ 'Opportunity Name', 'Salary' ] to deeply equal [ 'Opportunity Name' ]

After the fix: 13/13 green.

Reverse-verification (prediction stated before the run). Predicted: reverting only the two
passesFieldGate filter lines turns exactly 5 tests red — PIN 2, PIN 2b, PIN 2c, PIN 5, WIRING —
each with the message above; the 8 controls do not move; #6598's pins stay green. Observed exactly
that: Tests 5 failed | 14 passed (19), with the two #6598 files green in the same run. The 8
unmoved controls are the half that says the guard is narrow.

  • Mutation confirmed on disk, not by the editor's exit code: passesFieldGate( call sites
    2 → 0, and git hash-object (9c9c06ba…) differing from git rev-parse HEAD:path
    (0356ce42…).
  • Restore proven byte-exact: git hash-object == git rev-parse HEAD:<path> (both
    0356ce42d41e4aa1a0fc3b251ad7097eb2029799), git diff HEAD empty, git status clean, 2 call
    sites back.
  • trap restore EXIT INT TERM with an absolute REPO_ROOT-anchored path; restore names HEAD
    explicitly rather than a bare git checkout --.
  • No build stands between edit and run: root vitest.config.mts aliases every @object-ui/*
    specifier to that package's src (verified in this PR, not taken on faith), and the test imports
    ../ObjectGrid relatively — so the ablation reads source directly.

Union at the final commit 74ef4bd25 (working tree clean, run after the last commit):

check result
pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/ Test Files 153 passed (153) · Tests 1632 passed (1632)
pnpm --filter @object-ui/plugin-grid --filter @object-ui/plugin-list run type-check exit 0 (script echoed as type-check, the hyphenated name)
pnpm --filter … run lint ✖ 699 problems (0 errors, 699 warnings) / ✖ 442 problems (0 errors, 442 warnings) — exit 0

An earlier type-check reported TS2307 on every @object-ui/* import; that is the unbuilt-closure
signature and was read as NOT MEASURED, not as red. After
pnpm --workspace-concurrency=2 --filter '<pkg>^...' build it is clean. Coverage was confirmed
rather than assumed: tsc -p tsconfig.test.json --listFiles shows all four edited source/test
files in the checked population.

Lint is reported on errors, which is what this repo's own gate measures — lint.yml states
--max-warnings is deliberately unset because repo-wide warnings "run into the thousands …
known historical debt, not a signal". The two affected packages' full lint populations were
run (not a narrowing), and separately all 5 changed files were confirmed present in eslint's own
--format json population (5 of 5, none ignored, 0 errors). Type-aware linting is not enabled
(no project / projectService in languageOptions) and no eslint config is touched, so this
diff cannot move the verdict on any untouched file.

Gates derived from the actual diff, each quoting its own verdict line:

  • check:control-bytes✅ OK (scanned 5745 tracked text file(s); skipped 85 binary)
  • check:vi-mock-specifiers✅ OK (…499 carry a mock…) (the new test uses vi.mock)
  • check:element-data-source-declarationOK — 13 gate-consuming file(s) checked
  • check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.
  • check:self-import✅ No package names itself inside its own src/.
  • check-changeset-presence.mjs✅ 4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)

No gate or tooling script is edited by this diff, so no gate self-test suite is owed.

Notes


Generated by Claude Code

…ns` path

`generateColumns()` has three default paths. Two re-applied field-level
security and the authored `columns` path did not — the last one left after
objectui#6723 closed the inline-data path, and the worst of the three to leave:
it is the MOST reachable, running whether the grid fetches its own rows or not,
while the inline-data path needs a host to hand rows down.

Both arms (`ListColumn[]` and `string[]`) now filter through
`perms.checkField(objectName, fieldName, 'read')` under the same
`perms.isLoaded && schema.objectName` deferral the other two paths use.

Only keys the OBJECT DECLARES are judged. Host-joined and derived columns pass
through untouched — a `ListColumn` carries `label`/`link`/`action`/`prefix`/
`width`, so a column whose `field` the object does not declare is a legitimate
authored derived column, and an over-eager filter would delete authoring work.
`hasOwnProperty` keeps an inherited name (`constructor`) from being mistaken
for a declared field.

The judged key is read through `columnIdentity`, never off a bare string: it
folds `'salary'`, `{ field: 'salary' }` and the legacy `{ name: 'salary' }`, so
one predicate serves both arms. A gate reading `col.field` directly would find
no identity on the legacy spelling and pass a denied declared field through.
`resolvesToDataColumn` still runs first and keeps owning its own decisions.

objectui#6598's `hasAuthoredColumns` predicate is unchanged and its rationale
is rewritten here, in the same commit: it rested on "the grid would not
re-check", which this change makes false. It now rests on the half that never
depended on the grid — an empty projection is the author's projection after
filtering, and the object's default columns are not what was authored whether
or not they are FLS-checked on the way out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 45 chunks) 3178.0 KB 3222.7 KB
Main entry chunk (gzip) 143.6 KB 350 KB
Entry file index-BBahf6Vp.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 12.46KB 4.71KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 512.13KB 116.43KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 174.02KB 48.28KB
fields (index.js) 243.65KB 61.63KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.92KB 12.93KB
plugin-charts (index.js) 64.68KB 18.35KB
plugin-chatbot (index.js) 190.53KB 45.18KB
plugin-dashboard (index.js) 133.48KB 34.51KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.43KB 62.46KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 133.32KB 32.69KB
plugin-gantt (index.js) 165.23KB 40.37KB
plugin-grid (index.js) 202.08KB 54.61KB
plugin-kanban (index.js) 53.14KB 14.64KB
plugin-list (index.js) 113.15KB 27.59KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 28.95KB 8.33KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.83KB 21.11KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 76.75KB 25.49KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 3.11KB 1.48KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

os-sam commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

Review — ACCEPT

Landing gated on CI. ⛔ No clause-② review, and its absence is deliberate: the ruling states this
is a runtime permission/security behaviour change ⇒ human floor, the control is the ruling
itself
. You flagged that in the PR body, which is the right place for it.

⭐ The census changed what this card is

The card's own axis ① said "zero measured victims" and both triage and the maintainer reasoned on
that basis. Nobody had produced the denominator. You did, and it is not zero — I verified the
security-relevant half independently rather than taking it:

site checkField occurrences on main
plugin-view/src/ObjectView.tsx 0
plugin-designer/src/ObjectManager.tsx 0
plugin-designer/src/FieldDesigner.tsx 0
controlplugin-list/src/ListView.tsx 3

The control hits, so the zeros are a reading. Three shipped components reach the
authored-columns path without filtering first, plus two dev/demo. This was argued as a
defence-in-depth hardening against a hypothetical future host; it is in fact a live gap in three
shipped surfaces today. That is worth stating plainly on the card, and it retroactively
strengthens the ruling rather than questioning it.

Re-verifying the card's reading was handled exactly right

My order said: re-run it on today's main, and if it no longer holds, that is the finding.
It didn't hold literally — git grep -n checkField gives 4 lines now, not the card's 2 —
and you did the right third thing instead of either proceeding blindly or stopping: you
established why (the extra pair is #6723's own inline-data fix at 2352/2368) and then
re-established the load-bearing claim separately — the if (cols) block, lines 1961–2289, contains
0 occurrences of checkField or perms, and both arms return before the object-schema gate.

"The path table is correct as written; only the raw grep count moved" is the correct conclusion,
and it is only available because you separated the claim from the instrument reading that
happened to support it
. A seat that treated the grep count as the claim would have stopped on a
false alarm.

The two things most likely to have been got wrong, both got right

1. Derived columns survive. This was the constraint I flagged as most likely to break under an
over-eager filter, and your reasoning is sharper than "only judge declared keys": checkField
answers false for a field the policy has never heard of, so judging derived keys is a
different, wrong rule — not a stricter reading of the same one.
A ListColumn carrying
label/link/action/prefix/width for an undeclared field is authoring work, and deleting
it would destroy it. hasOwnProperty keeping constructor from posing as a declared field is the
kind of detail that only shows up if you actually think about the lookup.

2. columnIdentity, and PIN 5. The ruling said "never a bare string" and you found what that
protects against: the identity folds three authored spellings — 'salary', { field: 'salary' },
and the legacy { name: 'salary' } — so a gate reading col.field directly would find no
identity on the legacy spelling and wave a denied declared field straight through. That is a
security hole inside the security fix, and PIN 5 pins exactly it. One predicate serving both arms
falls out of that rather than being a convenience.

#6598's rationale: the ruling said one site, the sweep found three

The stale premise lived in the ListView.tsx docblock and the test file's docblock and the
inline comment on its final assertion. All three said the predicate is correct because the grid
would not re-check
, which this PR makes false.

And the rewrite is a real re-derivation, not a patch over the words: the predicate stays correct
for the half that never depended on the grid — an empty projection is the author's
projection after filtering, and handing the grid "unauthored" would substitute the object's default
columns, fields the author never declared. Authoring intent, not the field gate, is what #6598
protects.
Naming the retired reason at each site so the next reader sees a correction rather than
a silent edit is exactly the standard this lane has been converging on all day.

Confirming the pin was undisturbed by measurement (18/18 green with the fix, and again after the
rewrite) closes the third confidence gap triage recorded.

Verification

Red-first is a security assertion, not a shape one — 5 failed / 8 passed on untouched main,
every red the denied column rendering, expected [ 'Opportunity Name', 'Salary' ] to deeply equal [ 'Opportunity Name' ]. Reverse-verification stated its prediction first and matched exactly:
5 red (PIN 2 / 2b / 2c / 5 / WIRING), the 8 controls unmoved — that unmoved set is what says the
guard is narrow
— and #6598's pins green in the same run. Mutation proven on disk by call-site
count 2 → 0 plus a differing hash-object; restore byte-exact at 0356ce42… with git diff HEAD empty. And you established there is no build between edit and run by checking the root
vitest aliases, rather than assuming it.

TS2307 on every @object-ui/* import read as NOT MEASURED, not red — correct, and the
unbuilt-closure signature is now well-established on this lane.

The out-of-scope note is right to be a note: the query projection at L1402 building $select from
authored columns is fetch-shaping the server enforces, and the ruling scopes this card to
generateColumns(). Filing rather than absorbing it is the same discipline that kept #6884 bounded.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-grid: after #6723, ObjectGrid still skips the field-level-security re-check on the authored columns path

2 participants