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
feat(spec): declare requiredPermissions on BulkActionDefSchema (#6257) (#6332)
The selection bar has filtered buttons on def.requiredPermissions since
objectui#3492, but the .strict() schema never declared the key, so no
legal metadata could reach that filter — enforced ≠ declarable. The forms
with no workaround were the inline data-plane defs (operation:
'update' | 'delete'): they dispatch no action, so unlike a def promoted
from bulkActions: ['<name>'] they have nothing to inherit a gate from,
and a declarative bulk delete stayed visible to every caller who could
open the list, rejected only per record after the click.
- BulkActionDefSchema: optional requiredPermissions: string[], with
action.requiredPermissions semantics verbatim (absent/empty pass,
entries AND, unresolvable caller capabilities fail open; on a
data-plane def the gate governs visibility only — the write is still
authorized by the data API). ActionSchema's near-miss aliases
(permissions/capabilities/requiresPermissions/requiredCapabilities/acl)
rename onto the key here too, and it joins the did-you-mean pool.
- Regenerated authorable-surface/ui.json and the bulk-action reference
page; new guide paragraph in content/docs/ui/views.mdx.
- Showcase: the two inline gated defs the #6157 matrix could not pin, on
showcase_project.default — relabel_ops (update + patch, gated on the
Ops-held showcase.export_data) and purge_restricted (delete, gated on
the granted-to-nobody showcase.restricted_ops) — plus a playwright spec
pinning the admin-negative cells of the selection bar.
- Schema tests: every def form accepts the gate, empty array and
optionality pinned, non-array rejected, aliases rename.
No renderer change: objectui's BulkActionDef type and BulkActionBar
filter shipped in objectui 11 (objectui#3548).
Claude-Session: https://claude.ai/code/session_01PD7tZG1vENc5peMaLQC1uD
Co-authored-by: Claude <noreply@anthropic.com>
|**visible**|`string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }`| optional | Eligibility predicate (CEL), same shape as `action.visible`. Evaluated once PER SELECTED RECORD with that record bound: the button is offered when at least one passes, the run covers only those, and the rest are reported as skipped. A record-free predicate (`features.x`, `current_user.y`) therefore behaves as a plain button-level gate. Fail-closed — a predicate that faults excludes the record. |
53
+
|**requiredPermissions**|`string[]`| optional |[ADR-0066 D4] Capability gate on the button, `action.requiredPermissions` semantics verbatim: absent or empty always passes, several are AND-ed, and a client that cannot resolve the caller's capabilities fails OPEN (the server stays the authority). This key exists for INLINE defs — notably the `update`/`delete` data-plane forms, which dispatch no action and so have nothing to inherit a gate from; a def promoted from `bulkActions: ['<name>']` (or an aggregate def naming a declared action) inherits the action's own declaration instead. On a data-plane def the gate governs visibility only — the write itself is still authorized by the data API's object permissions and server hooks. |
53
54
|**maxRecords**|`integer`| optional | Selection size above which the run is blocked. Set it on defs whose server work is expensive — an aggregate def carries every selected id in one request. |
54
55
|**batchSize**|`integer`| optional | Records per executor batch (default 200). Data-plane operations only — an aggregate run is a single call by definition. |
visible: ExpressionInputSchema.optional().describe('Eligibility predicate (CEL), same shape as `action.visible`. Evaluated once PER SELECTED RECORD with that record bound: the button is offered when at least one passes, the run covers only those, and the rest are reported as skipped. A record-free predicate (`features.x`, `current_user.y`) therefore behaves as a plain button-level gate. Fail-closed — a predicate that faults excludes the record.'),
218
+
requiredPermissions: z.array(z.string()).optional().describe("[ADR-0066 D4] Capability gate on the button, `action.requiredPermissions` semantics verbatim: absent or empty always passes, several are AND-ed, and a client that cannot resolve the caller's capabilities fails OPEN (the server stays the authority). This key exists for INLINE defs — notably the `update`/`delete` data-plane forms, which dispatch no action and so have nothing to inherit a gate from; a def promoted from `bulkActions: ['<name>']` (or an aggregate def naming a declared action) inherits the action's own declaration instead. On a data-plane def the gate governs visibility only — the write itself is still authorized by the data API's object permissions and server hooks."),
211
219
maxRecords: z.number().int().positive().optional().describe('Selection size above which the run is blocked. Set it on defs whose server work is expensive — an aggregate def carries every selected id in one request.'),
212
220
batchSize: z.number().int().positive().optional().describe('Records per executor batch (default 200). Data-plane operations only — an aggregate run is a single call by definition.'),
0 commit comments