You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Concrete defect, measured: the row-level built-in Edit/Delete gate intersects apiOperations — which the spec's own describe text defines as the object's API exposure, principal-independent (the report's A/B account comparison shows 30/30 objects with identical apiOperations and differing allowEdit) — instead of effective permissions, while the same page's toolbar and detail-header gates use the correct source. Two-right-one-wrong on one screen.
Landing: packages/app-shell row-actions surface (apiOperations consumers present in ObjectView.tsx / RelatedRecordActionsBridge.tsx on origin/main @ 1592b21); exact site to be pinned by the dev.
Server-side enforcement is confirmed hard (403 + unchanged DB), so this is a UI gate repair, not an authorization hole — but destructive entry points rendered to unauthorized accounts is user-facing and fully specified: queue.
The body's closing question (should os.user carry permission info in CEL scope) is explicitly out of scope — separate card if pursued.
Claim: PM loop round 1 (maintainer direct dispatch — /pm-dispatch https://github.com/objectstack-ai/objectui/issues/4096)
Session: session_012JW7uQKdaPKkrT8fUpszRX
Branch: claude/issue-4096-row-crud-permission-gate
Worktree: objectui-issue-4096
Domain: objectui whole-repo seat (repo:objectui)
File surface: packages/plugin-grid/src/rowCrudAffordances.ts, packages/plugin-grid/src/ObjectGrid.tsx (the resolveRowCrudAffordances call site only), packages/plugin-grid/src/__tests__/**, and — pending the dev's per-face verdict — packages/app-shell/src/views/RelatedRecordActionsBridge.tsx + its __tests__. ⛔ Declared OUT: packages/core/src/evaluator/** and packages/plugin-grid/src/components/RowActionMenu.tsx (another seat's in-flight surface, see below) — stop and report on breach.
Container & model: M (multi-face semantic gate + fail-open/fail-closed judgment, not mechanical), mode:cloud, model: opus
Serial constraints cleared: objectui has exactly two open PRs (#3598 release, #4093 dependabot) — neither touches this surface. The seven pm:dispatched cards in this repo (#3796, #3792, #3670, #3641, #3561, #3528, #3339) are held by a different account (yinlianghui) and have no branches/PRs yet. The only adjacency is #3796/#3792, which land in evalRowPredicate (packages/core/src/evaluator/**, consumed by RowActionMenu.tsx); both of those paths are excluded from this card's surface above, so the two are file-disjoint. Last main commits on this surface: 9154d9e (#4081), 5bfaabd — no same-day churn.
Premise re-verified against origin/main @ c29ceff before dispatch:
packages/plugin-grid/src/ObjectGrid.tsx:1647 calls resolveRowCrudAffordances({… effectiveApiOperations: effectiveApiOps }) and ANDs no principal-scoped permission — rowCrudAffordances.ts folds only (a) the ADR-0103 bucket, (b) userActions, (c) apiOperations.
The two correct gates on the same screen both AND a principal-scoped verdict on top of the same affordance resolver: toolbar ObjectView.tsx:1798/1809/1863 → affordances.create && can(objectDef.name, 'create'); detail header RecordDetailView.tsx:1899-1902 → objectAffordances.edit && recordWriteAllowed / .delete && recordDeleteAllowed.
MePermissionsProvider.check maps update → allowEdit, delete → allowDelete (MePermissionsProvider.tsx:230-236), so the principal-scoped source the report asks for is already reachable — and ObjectGrid already holds it (usePermissions() at ObjectGrid.tsx:435).
Dispatched: mode:cloud, model: claude-opus-5, dev session session_01FQbuY8A4jabkVwkP3GU9oe, outcome branch claude/issue-4096-row-crud-permission-gate.
Scope handed to the dev, for the record:
The maintainer's own scope line in the body (os.user gaining permission/role info in CEL scope ⇒ separate card) is passed through verbatim as a ruling, not re-opened.
The fix is framed as an intersection tightening: the apiOperations layer stays; a principal-scoped verdict is ANDed on top — matching the toolbar's affordances.create && can(obj, 'create').
Four faces must each get an explicit verdict in the PR body (changed / already compliant with evidence / explicitly out of scope with a reason): the grid row kebab, RelatedRecordActionsBridge related-list rows, ListView's delete affordance, and the bulk-delete path.
Named regression risk: usePermissions() fails open with no provider (can: () => true), while MePermissionsProvider.check fails closed for an authenticated principal whose object is absent from /me/permissions.objects. Both directions plus the no-provider case need pinned tests.
{
"issue": 4096,
"status": "done",
"branch": "claude/issue-4096-row-crud-permission-gate",
"pr": "https://github.com/objectstack-ai/objectui/pull/4108",
"premise_still_valid": true,
"summary": "Premise confirmed verbatim against origin/main @ c29ceff: ObjectGrid.tsx:1647 passed only effectiveApiOperations into resolveRowCrudAffordances, which folds three OBJECT-scoped layers (ADR-0103 bucket / userActions / apiOperations) and no principal-scoped verdict — so the row kebab's built-in Edit/Delete failed open for every account with no write grant, while the toolbar (affordances.create && can(obj,'create')) and the record header (objectAffordances.edit && recordWriteAllowed) on the same screen were correct. Fixed as an INTERSECTION TIGHTENING, not a swap: apiOperations and every other layer stay; can(obj,'update'|'delete') (MePermissionsProvider's allowEdit/allowDelete — the toolbar's own source) is ANDed on top. resolveRowCrudAffordances gained optional permissionUpdate/permissionDelete with the same `undefined => no narrowing` semantics as effectiveApiOperations; ObjectGrid fills them from the usePermissions() it already held at line 435 (no new data channel). Four faces, each with an explicit verdict: (1) grid row kebab — CHANGED; (2) RelatedRecordActionsBridge related-list rows — CHANGED, confirmed the same defect on the child object (create/edit/delete all fail-open; onView deliberately left untouched); (3) ListView.tsx:887 non-grid bulk delete — CHANGED, confirmed the same disease on the kanban/gallery bulk bar; (4) grid bulk-delete path (objectCanDelete / onBulkDelete) — CHANGED by construction (same resolver) and pinned explicitly rather than assumed. On #3492: its requiredPermissions work is an orthogonal ADR-0066 D4 capability gate for DECLARED actions (mayInvoke / useCapabilityGate) and never covered the built-in delete — that is why this entry was still open. All three PM mechanism assumptions verified true, none falsified. Declared-out paths (packages/core/src/evaluator/**, RowActionMenu.tsx) untouched — the verdict reaches RowActionMenu through the existing canEdit/canDelete props. useRecordEditable deliberately NOT used on list rows (per-record HTTP probe, N rows => 2N requests); the object-level verdict is what the toolbar uses and what the issue asked for. Sinking the layer into @object-ui/core's resolveEffectiveCrudAffordances was NOT done — minimum change was the default instruction and the four call sites there already AND can() themselves; recorded as an open question instead.",
"tests": "Dedicated worktree off origin/main @ c29ceff. Dependency closure built first (pnpm --filter '@object-ui/plugin-grid...' --filter '@object-ui/plugin-list...' --filter '@object-ui/app-shell...' build) so the cross-package type surface is not read from stale dist/*.d.ts. | `pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/ packages/app-shell/` => Test Files 400 passed (400), Tests 3900 passed | 1 skipped (3901). | `pnpm exec vitest run apps/console/` => Test Files 29 passed (29), Tests 300 passed (300). | `pnpm type-check` => Tasks: 78 successful, 78 total. | NOTE: the repo's vitest guard refuses package-level `pnpm --filter @object-ui/app-shell test` (it silently runs apps/console and reports a false green); everything above was run from the repo root, which is what CI does. | NEGATIVE CHECK (the new cases are not vacuous): each face's gate temporarily reverted and re-run — rowCrudAffordances principal layer removed => 5 failed | 9 passed | 42 skipped; ListView can(obj,'delete') removed => 'a principal WITHOUT allowDelete loses it' FAILED; RelatedRecordActionsBridge can(...) removed => 'a principal WITHOUT them loses all three' + 'gates create/update/delete independently' FAILED. The 'WITH permission' and 'no provider' cases stayed green throughout — they assert unchanged behavior. | TEST LANDING: shared consistency coverage, no new file — packages/plugin-grid/src/__tests__/rowCrudAffordances.test.ts (pure fn, 9 new cases), packages/plugin-grid/src/__tests__/rowCrudEffectiveOps.test.tsx (real ObjectGrid, row kebab + bulk bar in ONE file so a future divergence fails a test), packages/app-shell/src/views/__tests__/RelatedRecordActionsBridge.effectiveOps.test.tsx, packages/plugin-list/src/__tests__/ListView.permissions.test.tsx. | FAIL-OPEN/FAIL-CLOSED: all three scenarios pinned on all four faces — (a) account WITH the write grant still sees Edit/Delete, (b) account WITHOUT it does not, (c) NO PermissionProvider still sees them. The (c) case in rowCrudEffectiveOps.test.tsx runs the REAL usePermissions (mock keeps the actual module reachable via importOriginal) so it exercises the genuine `can: () => true` fallback, not an imitation; ListView.permissions.test.tsx renders with no provider mounted at all. | CONSOLE fail-closed check: metadata-admin is NOT affected — ResourceListPage.tsx renders its own 'ObjectGrid-like table' and never routes through ObjectGrid. sys_* lists reached via ObjectView -> ObjectGrid already run can(obj,'create') for the New button, so /me/permissions.objects must already carry them (or a '*' entry, which check honors); per-key absence is permissive anyway (allowEdit !== false). No loading flicker: MePermissionsProvider renders loadingFallback and does not mount children until the set resolves (MePermissionsProvider.tsx:303). NOT browser-verified — no backend in this session to serve /me/permissions; flagged in the PR body rather than implied otherwise. | SWEEP DIRECTION: dependents (downstream consumers), prefix form `pnpm --filter '...@object-ui/plugin-grid'` => 9 packages (app-shell, console, example-byo-backend-console, example-console-starter, plugin-designer, plugin-grid, plugin-report, plugin-view, site). resolveRowCrudAffordances has no consumer outside plugin-grid itself; the signature change is additive-optional; type-check covers all 9 and is green. | Changeset present: .changeset/row-crud-permission-gate-4096.md (patch x3: plugin-grid, plugin-list, app-shell). No new t() keys => check:i18n-keys not applicable. CI not polled — PM owns convergence.",
"open_questions": [
"Should the principal layer be SUNK into @object-ui/core's resolveEffectiveCrudAffordances (a third parameter carrying the principal verdict) instead of living at each call site? Four existing call sites there (toolbar / detail / form / related list) already AND can() themselves, so sinking is idempotent for them but touches a much wider scope. Went minimum-change per the card's default; the DRY argument is real and would collapse three of this PR's four gates into one definition. PM's call.",
"RelatedRecordActionsBridge's onCreate is now also gated on can(child,'create') — strictly the issue is about Edit/Delete, but leaving create as the only ungated affordance on that surface would have re-created the exact inconsistency this card is closing. Flagging as a deliberate scope judgement, not an oversight.",
"The fix inherits MePermissionsProvider's fail-closed default for an authenticated principal whose object is absent from /me/permissions.objects (#2926 IV). That is the toolbar's semantics and 'consistent with the toolbar' was this card's standard, and metadata-admin was verified out of range — but it is a behavioural inheritance, not a neutral one, and a host whose permission endpoint under-reports objects will now lose row Edit/Delete where it previously kept them."
],
"out_of_scope_findings": [
"Per the issue author's own closing line, extending the CEL predicate scope so os.user carries permission/role information is NOT in this PR. Worth a separate card: the downstream probe in the issue body established that CEL currently CANNOT express 'does the current user have write permission on this object', so an author whose needs differ from the built-in gate has no self-service channel at all — the only expressible workaround is hardcoding account/role names into object metadata, i.e. a second copy of the permission model that will drift.",
"ListView.permissions.test.tsx's pre-existing fixtures set objectPermissions: { read, create, update, delete } on a role config, but @object-ui/permissions' evaluatePermission (evaluator.ts:66) reads roleConfig.actions?.includes(action) and ignores objectPermissions entirely — objectPermissions only drives the FIELD-level path. The existing FLS tests pass because they assert through checkField. Any future test that expects the config-based PermissionProvider to deny an OBJECT action via objectPermissions will silently assert nothing. The new cases in that file set `actions` explicitly and say so in a comment, but the trap is still there for the next author."
]
}
ACCEPT — PR #4108 (fix(grid,list,app-shell): gate row Edit/Delete on the caller's permission, not just apiOperations), 11 files, +548/−30.
What shipped: apiOperations is kept as the exposure layer and a principal-scoped verdict is ANDed on top — can(obj, 'update' | 'delete') → /me/permissionsallowEdit / allowDelete, the toolbar's own source. resolveRowCrudAffordances() gained optional permissionUpdate / permissionDelete with the same undefined ⇒ no narrowing semantics as effectiveApiOperations; ObjectGrid fills them from the usePermissions() it already held.
Reviewed against GitHub, not against the report:
Four faces, four verdicts, all changed — grid row kebab; RelatedRecordActionsBridge related-list Create/Edit/Delete (verified objectName in that scope is the child — objects.find(o => o.name === objectName) → childDef — so can(objectName, …) is the child's grant, and onView is correctly left alone); ListView's non-grid bulk delete; and the grid bulk bar, which rides objectCanDelete from the same resolver and is now pinned rather than assumed. The dev's read on fix(plugin-grid): 批量操作按钮忽略 requiredPermissions,且布尔 visible 被判为故障导致按钮全员隐藏 #3492 is right and worth keeping: its requiredPermissions work is the ADR-0066 D4 capability gate for declared actions, which never covered the built-in delete — that is why this one survived.
The three fail-open / fail-closed cases are pinned in the shared consistency files, no new test file. The no-provider case runs the realusePermissions via importOriginal, so it exercises the genuine can: () => true fallback rather than an imitation.
The new cases are not vacuous — each face's gate was reverted and the suite re-run; the "WITHOUT permission" cases go red, the "WITH permission" and "no provider" cases stay green throughout. That negative check is what makes the green mean something here.
Cross-package sweep direction stated and correct: dependents (prefix '...@object-ui/plugin-grid', 9 packages), with the dependency closure built before typecheck so the cross-package surface is not read from stale dist/*.d.ts.
Scope clean: Fixes #4096 on the first line, changeset present (patch × 3), no content/docs/releases/ edits, and the declared-out paths (packages/core/src/evaluator/**, RowActionMenu.tsx — another seat's in-flight evalRowPredicate surface) are untouched.
CI: 20 check runs, 18 success + 2 skipped (dependabot, Test (coverage)); Lint and Type Check — the jobs that carry the gate families — are both completed: success.
Two things recorded rather than reworked:
File surface amended.packages/plugin-list/src/ListView.tsx + its test were outside the surface declared in the claim comment. It is face 3 of the four this card explicitly asked the dev to adjudicate, and the PR discloses it; no in-flight card in this repo touches plugin-list, so there is no collision. Recording the amendment here rather than treating a disclosed, authorized expansion as a breach.
Not browser-verified — the dev session had no backend to serve /me/permissions, so the console-grid analysis (metadata-admin renders its own table and never goes through ObjectGrid; sys_* lists already run can(obj, 'create') for the New button) is code + test level. Flagged by the dev rather than implied otherwise, which is the right call; the browser pass belongs to whoever next dogfoods the console.
Also noted: the dev's turn ended without the os-dev-report marker comment. Not a blocker — the PR body carries every element the report contract asks for (premise verdicts, per-face table, commands + output, the two rejected routes with reasons), which is exactly the case the report-loss fallback covers.
Driving to landing now: ready for review + merge queue.
Activity
claude commentedon Aug 10, 2026
Triage:
pm:queue.apiOperations— which the spec's own describe text defines as the object's API exposure, principal-independent (the report's A/B account comparison shows 30/30 objects with identicalapiOperationsand differingallowEdit) — instead of effective permissions, while the same page's toolbar and detail-header gates use the correct source. Two-right-one-wrong on one screen.packages/app-shellrow-actions surface (apiOperationsconsumers present inObjectView.tsx/RelatedRecordActionsBridge.tsxonorigin/main@1592b21); exact site to be pinned by the dev.os.usercarry permission info in CEL scope) is explicitly out of scope — separate card if pursued.visible: false仍然渲染该菜单项——可见性门用真值判断(#3492 在行菜单上的未收口部分) #3758 (declaredvisible: falseignored), fix(plugin-grid): 批量操作按钮忽略 requiredPermissions,且布尔 visible 被判为故障导致按钮全员隐藏 #3492 (bulkrequiredPermissions, fixed), A row-scopedvisiblepredicate is a constanttrueon the bulk selection bar #3067 (rowvisibleon bulk bar) are family; none covers this gate.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
baozhoutao commentedon Aug 10, 2026
Claim: PM loop round 1 (maintainer direct dispatch —
/pm-dispatch https://github.com/objectstack-ai/objectui/issues/4096)Session:
session_012JW7uQKdaPKkrT8fUpszRXBranch:
claude/issue-4096-row-crud-permission-gateWorktree:
objectui-issue-4096Domain: objectui whole-repo seat (
repo:objectui)File surface:
packages/plugin-grid/src/rowCrudAffordances.ts,packages/plugin-grid/src/ObjectGrid.tsx(theresolveRowCrudAffordancescall site only),packages/plugin-grid/src/__tests__/**, and — pending the dev's per-face verdict —packages/app-shell/src/views/RelatedRecordActionsBridge.tsx+ its__tests__. ⛔ Declared OUT:packages/core/src/evaluator/**andpackages/plugin-grid/src/components/RowActionMenu.tsx(another seat's in-flight surface, see below) — stop and report on breach.Container & model: M (multi-face semantic gate + fail-open/fail-closed judgment, not mechanical),
mode:cloud,model: opusSerial constraints cleared: objectui has exactly two open PRs (#3598 release, #4093 dependabot) — neither touches this surface. The seven
pm:dispatchedcards in this repo (#3796, #3792, #3670, #3641, #3561, #3528, #3339) are held by a different account (yinlianghui) and have no branches/PRs yet. The only adjacency is #3796/#3792, which land inevalRowPredicate(packages/core/src/evaluator/**, consumed byRowActionMenu.tsx); both of those paths are excluded from this card's surface above, so the two are file-disjoint. Last main commits on this surface:9154d9e(#4081),5bfaabd— no same-day churn.Premise re-verified against
origin/main@c29ceffbefore dispatch:packages/plugin-grid/src/ObjectGrid.tsx:1647callsresolveRowCrudAffordances({… effectiveApiOperations: effectiveApiOps })and ANDs no principal-scoped permission —rowCrudAffordances.tsfolds only (a) the ADR-0103 bucket, (b)userActions, (c)apiOperations.ObjectView.tsx:1798/1809/1863→affordances.create && can(objectDef.name, 'create'); detail headerRecordDetailView.tsx:1899-1902→objectAffordances.edit && recordWriteAllowed/.delete && recordDeleteAllowed.MePermissionsProvider.checkmapsupdate → allowEdit,delete → allowDelete(MePermissionsProvider.tsx:230-236), so the principal-scoped source the report asks for is already reachable — andObjectGridalready holds it (usePermissions()atObjectGrid.tsx:435).⇒ premise holds; dispatching.
Generated by Claude Code
baozhoutao commentedon Aug 10, 2026
Dispatched:
mode:cloud,model: claude-opus-5, dev sessionsession_01FQbuY8A4jabkVwkP3GU9oe, outcome branchclaude/issue-4096-row-crud-permission-gate.Scope handed to the dev, for the record:
os.usergaining permission/role info in CEL scope ⇒ separate card) is passed through verbatim as a ruling, not re-opened.apiOperationslayer stays; a principal-scoped verdict is ANDed on top — matching the toolbar'saffordances.create && can(obj, 'create').RelatedRecordActionsBridgerelated-list rows,ListView's delete affordance, and the bulk-delete path.usePermissions()fails open with no provider (can: () => true), whileMePermissionsProvider.checkfails closed for an authenticated principal whose object is absent from/me/permissions.objects. Both directions plus the no-provider case need pinned tests.Generated by Claude Code
baozhoutao commentedon Aug 10, 2026
{ "issue": 4096, "status": "done", "branch": "claude/issue-4096-row-crud-permission-gate", "pr": "https://github.com/objectstack-ai/objectui/pull/4108", "premise_still_valid": true, "summary": "Premise confirmed verbatim against origin/main @ c29ceff: ObjectGrid.tsx:1647 passed only effectiveApiOperations into resolveRowCrudAffordances, which folds three OBJECT-scoped layers (ADR-0103 bucket / userActions / apiOperations) and no principal-scoped verdict — so the row kebab's built-in Edit/Delete failed open for every account with no write grant, while the toolbar (affordances.create && can(obj,'create')) and the record header (objectAffordances.edit && recordWriteAllowed) on the same screen were correct. Fixed as an INTERSECTION TIGHTENING, not a swap: apiOperations and every other layer stay; can(obj,'update'|'delete') (MePermissionsProvider's allowEdit/allowDelete — the toolbar's own source) is ANDed on top. resolveRowCrudAffordances gained optional permissionUpdate/permissionDelete with the same `undefined => no narrowing` semantics as effectiveApiOperations; ObjectGrid fills them from the usePermissions() it already held at line 435 (no new data channel). Four faces, each with an explicit verdict: (1) grid row kebab — CHANGED; (2) RelatedRecordActionsBridge related-list rows — CHANGED, confirmed the same defect on the child object (create/edit/delete all fail-open; onView deliberately left untouched); (3) ListView.tsx:887 non-grid bulk delete — CHANGED, confirmed the same disease on the kanban/gallery bulk bar; (4) grid bulk-delete path (objectCanDelete / onBulkDelete) — CHANGED by construction (same resolver) and pinned explicitly rather than assumed. On #3492: its requiredPermissions work is an orthogonal ADR-0066 D4 capability gate for DECLARED actions (mayInvoke / useCapabilityGate) and never covered the built-in delete — that is why this entry was still open. All three PM mechanism assumptions verified true, none falsified. Declared-out paths (packages/core/src/evaluator/**, RowActionMenu.tsx) untouched — the verdict reaches RowActionMenu through the existing canEdit/canDelete props. useRecordEditable deliberately NOT used on list rows (per-record HTTP probe, N rows => 2N requests); the object-level verdict is what the toolbar uses and what the issue asked for. Sinking the layer into @object-ui/core's resolveEffectiveCrudAffordances was NOT done — minimum change was the default instruction and the four call sites there already AND can() themselves; recorded as an open question instead.", "tests": "Dedicated worktree off origin/main @ c29ceff. Dependency closure built first (pnpm --filter '@object-ui/plugin-grid...' --filter '@object-ui/plugin-list...' --filter '@object-ui/app-shell...' build) so the cross-package type surface is not read from stale dist/*.d.ts. | `pnpm exec vitest run packages/plugin-grid/ packages/plugin-list/ packages/app-shell/` => Test Files 400 passed (400), Tests 3900 passed | 1 skipped (3901). | `pnpm exec vitest run apps/console/` => Test Files 29 passed (29), Tests 300 passed (300). | `pnpm type-check` => Tasks: 78 successful, 78 total. | NOTE: the repo's vitest guard refuses package-level `pnpm --filter @object-ui/app-shell test` (it silently runs apps/console and reports a false green); everything above was run from the repo root, which is what CI does. | NEGATIVE CHECK (the new cases are not vacuous): each face's gate temporarily reverted and re-run — rowCrudAffordances principal layer removed => 5 failed | 9 passed | 42 skipped; ListView can(obj,'delete') removed => 'a principal WITHOUT allowDelete loses it' FAILED; RelatedRecordActionsBridge can(...) removed => 'a principal WITHOUT them loses all three' + 'gates create/update/delete independently' FAILED. The 'WITH permission' and 'no provider' cases stayed green throughout — they assert unchanged behavior. | TEST LANDING: shared consistency coverage, no new file — packages/plugin-grid/src/__tests__/rowCrudAffordances.test.ts (pure fn, 9 new cases), packages/plugin-grid/src/__tests__/rowCrudEffectiveOps.test.tsx (real ObjectGrid, row kebab + bulk bar in ONE file so a future divergence fails a test), packages/app-shell/src/views/__tests__/RelatedRecordActionsBridge.effectiveOps.test.tsx, packages/plugin-list/src/__tests__/ListView.permissions.test.tsx. | FAIL-OPEN/FAIL-CLOSED: all three scenarios pinned on all four faces — (a) account WITH the write grant still sees Edit/Delete, (b) account WITHOUT it does not, (c) NO PermissionProvider still sees them. The (c) case in rowCrudEffectiveOps.test.tsx runs the REAL usePermissions (mock keeps the actual module reachable via importOriginal) so it exercises the genuine `can: () => true` fallback, not an imitation; ListView.permissions.test.tsx renders with no provider mounted at all. | CONSOLE fail-closed check: metadata-admin is NOT affected — ResourceListPage.tsx renders its own 'ObjectGrid-like table' and never routes through ObjectGrid. sys_* lists reached via ObjectView -> ObjectGrid already run can(obj,'create') for the New button, so /me/permissions.objects must already carry them (or a '*' entry, which check honors); per-key absence is permissive anyway (allowEdit !== false). No loading flicker: MePermissionsProvider renders loadingFallback and does not mount children until the set resolves (MePermissionsProvider.tsx:303). NOT browser-verified — no backend in this session to serve /me/permissions; flagged in the PR body rather than implied otherwise. | SWEEP DIRECTION: dependents (downstream consumers), prefix form `pnpm --filter '...@object-ui/plugin-grid'` => 9 packages (app-shell, console, example-byo-backend-console, example-console-starter, plugin-designer, plugin-grid, plugin-report, plugin-view, site). resolveRowCrudAffordances has no consumer outside plugin-grid itself; the signature change is additive-optional; type-check covers all 9 and is green. | Changeset present: .changeset/row-crud-permission-gate-4096.md (patch x3: plugin-grid, plugin-list, app-shell). No new t() keys => check:i18n-keys not applicable. CI not polled — PM owns convergence.", "open_questions": [ "Should the principal layer be SUNK into @object-ui/core's resolveEffectiveCrudAffordances (a third parameter carrying the principal verdict) instead of living at each call site? Four existing call sites there (toolbar / detail / form / related list) already AND can() themselves, so sinking is idempotent for them but touches a much wider scope. Went minimum-change per the card's default; the DRY argument is real and would collapse three of this PR's four gates into one definition. PM's call.", "RelatedRecordActionsBridge's onCreate is now also gated on can(child,'create') — strictly the issue is about Edit/Delete, but leaving create as the only ungated affordance on that surface would have re-created the exact inconsistency this card is closing. Flagging as a deliberate scope judgement, not an oversight.", "The fix inherits MePermissionsProvider's fail-closed default for an authenticated principal whose object is absent from /me/permissions.objects (#2926 IV). That is the toolbar's semantics and 'consistent with the toolbar' was this card's standard, and metadata-admin was verified out of range — but it is a behavioural inheritance, not a neutral one, and a host whose permission endpoint under-reports objects will now lose row Edit/Delete where it previously kept them." ], "out_of_scope_findings": [ "Per the issue author's own closing line, extending the CEL predicate scope so os.user carries permission/role information is NOT in this PR. Worth a separate card: the downstream probe in the issue body established that CEL currently CANNOT express 'does the current user have write permission on this object', so an author whose needs differ from the built-in gate has no self-service channel at all — the only expressible workaround is hardcoding account/role names into object metadata, i.e. a second copy of the permission model that will drift.", "ListView.permissions.test.tsx's pre-existing fixtures set objectPermissions: { read, create, update, delete } on a role config, but @object-ui/permissions' evaluatePermission (evaluator.ts:66) reads roleConfig.actions?.includes(action) and ignores objectPermissions entirely — objectPermissions only drives the FIELD-level path. The existing FLS tests pass because they assert through checkField. Any future test that expects the config-based PermissionProvider to deny an OBJECT action via objectPermissions will silently assert nothing. The new cases in that file set `actions` explicitly and say so in a comment, but the trap is still there for the next author." ] }Generated by Claude Code
baozhoutao commentedon Aug 10, 2026
ACCEPT — PR #4108 (
fix(grid,list,app-shell): gate row Edit/Delete on the caller's permission, not just apiOperations), 11 files, +548/−30.What shipped:
apiOperationsis kept as the exposure layer and a principal-scoped verdict is ANDed on top —can(obj, 'update' | 'delete')→/me/permissionsallowEdit/allowDelete, the toolbar's own source.resolveRowCrudAffordances()gained optionalpermissionUpdate/permissionDeletewith the sameundefined ⇒ no narrowingsemantics aseffectiveApiOperations;ObjectGridfills them from theusePermissions()it already held.Reviewed against GitHub, not against the report:
RelatedRecordActionsBridgerelated-list Create/Edit/Delete (verifiedobjectNamein that scope is the child —objects.find(o => o.name === objectName)→childDef— socan(objectName, …)is the child's grant, andonViewis correctly left alone);ListView's non-grid bulkdelete; and the grid bulk bar, which ridesobjectCanDeletefrom the same resolver and is now pinned rather than assumed. The dev's read on fix(plugin-grid): 批量操作按钮忽略 requiredPermissions,且布尔 visible 被判为故障导致按钮全员隐藏 #3492 is right and worth keeping: itsrequiredPermissionswork is the ADR-0066 D4 capability gate for declared actions, which never covered the built-indelete— that is why this one survived.usePermissionsviaimportOriginal, so it exercises the genuinecan: () => truefallback rather than an imitation.'...@object-ui/plugin-grid', 9 packages), with the dependency closure built before typecheck so the cross-package surface is not read from staledist/*.d.ts.Fixes #4096on the first line, changeset present (patch × 3), nocontent/docs/releases/edits, and the declared-out paths (packages/core/src/evaluator/**,RowActionMenu.tsx— another seat's in-flightevalRowPredicatesurface) are untouched.success+ 2skipped(dependabot,Test (coverage)); Lint and Type Check — the jobs that carry the gate families — are bothcompleted: success.Two things recorded rather than reworked:
packages/plugin-list/src/ListView.tsx+ its test were outside the surface declared in the claim comment. It is face 3 of the four this card explicitly asked the dev to adjudicate, and the PR discloses it; no in-flight card in this repo touchesplugin-list, so there is no collision. Recording the amendment here rather than treating a disclosed, authorized expansion as a breach./me/permissions, so the console-grid analysis (metadata-adminrenders its own table and never goes throughObjectGrid;sys_*lists already runcan(obj, 'create')for the New button) is code + test level. Flagged by the dev rather than implied otherwise, which is the right call; the browser pass belongs to whoever next dogfoods the console.Also noted: the dev's turn ended without the
os-dev-reportmarker comment. Not a blocker — the PR body carries every element the report contract asks for (premise verdicts, per-face table, commands + output, the two rejected routes with reasons), which is exactly the case the report-loss fallback covers.Driving to landing now: ready for review + merge queue.
Generated by Claude Code
fix(grid,list,app-shell): gate row Edit/Delete on the caller's permis…
fix(plugin-grid): gate ObjectGrid's inline add-record row on can(obje…
fix(plugin-grid): gate ObjectGrid's inline add-record row on can(obje…