Skip to content

feat(access-control): page-based permission groups, tool-level deny-list, settings row-action consistency#5216

Merged
waleedlatif1 merged 8 commits into
stagingfrom
worktree-access-control-page-redesign
Jun 25, 2026
Merged

feat(access-control): page-based permission groups, tool-level deny-list, settings row-action consistency#5216
waleedlatif1 merged 8 commits into
stagingfrom
worktree-access-control-page-redesign

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Replace the cramped Access Control configure modal with a full-surface, tabbed page (General / Model Providers / Blocks / Platform) with a sticky save bar — far more room and consolidated onto the design system
  • Add a deniedTools denylist to permission groups: admins can deny individual tools within an allowed integration (e.g. allow Slack, deny "Create Canvas"). Enforced server-side at the universal executeTool chokepoint via ToolNotAllowedError, and hidden from the operation dropdown for governed users. JSONB config field — no DB migration, existing groups unaffected
  • Add per-section Select/Deselect All on the Blocks tab and expandable per-tool deny rows (mirrors the Providers → Models pattern)
  • Standardize every settings list row on the canonical ... DropdownMenu (custom-tools, mcp, workflow-mcp-servers, api-keys, secrets, credential-sets) and align badges (ChipTag), member avatars (MemberAvatar), inputs (ChipInput), and the Mothership env picker (ChipSelect)

Type of Change

  • New feature
  • Improvement (settings consistency)

Testing

  • tsc clean, biome clean across all touched files
  • 51 permission-check unit tests pass, including 5 new deniedTools enforcement cases
  • bun run check:api-validation passes

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…ist, settings row-action consistency

- Replace the cramped configure modal with a full-surface tabbed Access Control page (General/Model Providers/Blocks/Platform) with a sticky save bar
- Add deniedTools denylist to permission groups: deny individual tools within an allowed integration; enforced at the universal executeTool chokepoint via ToolNotAllowedError, and hidden from the operation dropdown for governed users
- Add per-section Select/Deselect All on the Blocks tab and expandable per-tool deny rows (mirrors Providers->Models)
- Standardize every settings list row on the canonical "..." DropdownMenu (custom-tools, mcp, workflow-mcp-servers, api-keys, secrets, credential-sets) and align badges (ChipTag), avatars (MemberAvatar), inputs (ChipInput), and the mothership env picker (ChipSelect)
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 25, 2026 10:54pm

Request Review

@cursor

cursor Bot commented Jun 25, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes permission-group config shape and enforcement paths (deniedTools, refactored admin UX); mistakes could block tools or integrations for governed users, though server checks and tests mitigate this.

Overview
Access Control is reworked so group configuration lives on a full-page GroupDetail (General / Model Providers / Blocks / Platform) with a sticky save bar, instead of the old configure modal. WorkspaceSelect is extracted for reuse.

Permission groups gain a deniedTools JSONB denylist: admins can allow an integration but block specific tools (expandable rows on the Blocks tab, with pruning when integrations change). Enforcement adds ToolNotAllowedError in assertPermissionsAllowed, isToolAllowed in the client hook, API schema updates, and tests. The workflow block operation dropdown hides denied operations and avoids defaulting to a blocked value.

Settings UI standardizes list row actions on a MoreHorizontal DropdownMenu (API keys with copy name, secrets, credential sets, custom tools, MCP, workflow MCP servers) and aligns tokens (ChipInput / ChipSelect, ChipTag, border/text vars) on admin, Mothership, and data retention.

Reviewed by Cursor Bugbot for commit 887e009. Configure here.

Comment thread apps/sim/ee/access-control/components/group-detail.tsx Outdated
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces three coordinated changes: (1) a full-surface tabbed settings page for access-control group configuration replaces the old cramped modal, (2) a deniedTools per-tool denylist lets admins allow an integration block while blocking specific operations within it, enforced server-side at the universal executeTool chokepoint and hidden in the UI dropdown for governed users, and (3) every settings list row is standardized to a ... DropdownMenu pattern.

  • deniedTools enforcement — stored in the JSONB config field, parsed by permissionGroupConfigSchema, checked in assertPermissionsAllowed after the integration-level gate, and integrated with normalizeToolId so UUID-suffixed tool calls are correctly matched. A pruneDeniedTools invariant clears stale entries when an integration is disabled.
  • New GroupDetail component — 1,784-line replacement for the old inline modal; exposes General / Model Providers / Blocks / Platform tabs with sticky save bar, unsaved-changes guard, and per-section Select/Deselect All; mirrors the provider→models expandable pattern for the new per-tool deny rows.
  • Settings row consistency — six settings components (api-keys, credential-sets, custom-tools, mcp, secrets, workflow-mcp-servers) migrated from scattered Chip buttons to a canonical DropdownMenu with ... trigger.

Confidence Score: 5/5

Safe to merge. The new per-tool denylist is enforced server-side at the universal executeTool chokepoint; the client-side dropdown filtering is additive UX only. The pruneDeniedTools invariant prevents stale tool denials, and existing groups are unaffected because deniedTools defaults to [].

Core enforcement logic is thoroughly tested with 5 targeted cases including the exempt-block edge case. Type definitions, schema validation, config parsing, and the runtime check are all updated in sync. No DB migration is required. The UI changes are cosmetic (settings row consistency) and the new GroupDetail component correctly mirrors established patterns for the per-tool deny rows.

No files require special attention. The most complex new file, group-detail.tsx, is well-structured with clear separation of editing state, scope writes, and member management.

Important Files Changed

Filename Overview
apps/sim/ee/access-control/components/group-detail.tsx New 1,784-line tabbed GroupDetail component. pruneDeniedTools correctly purges stale tool denials when an integration is disabled; monotonic scopeWriteSeqRef prevents stale scope responses from clobbering local state.
apps/sim/ee/access-control/utils/permission-check.ts Adds ToolNotAllowedError, new toolId field on PermissionAssertion, and enforces deniedTools after the integration-level gate. Early-exit guard correctly updated to include !toolId; exempt block types still reach the denylist check when a toolId is supplied.
apps/sim/tools/index.ts Changed assertPermissionsAllowed call from kinded-tools-only to all tools (when userId and workspaceId are present), passing normalizedToolId so the denylist check covers every integration tool execution.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx Adds deniedOperationIds to hide denied tools from the operation picker. Uses a try/catch when calling selectTool({ operation }) with partial context — safe because server enforcement is authoritative.
apps/sim/lib/permission-groups/types.ts Adds deniedTools: string[] to the Zod schema, TypeScript interface, default config, and parsePermissionGroupConfig — all in sync; no DB migration needed since the field lives in the JSONB config column.
apps/sim/ee/access-control/utils/permission-check.test.ts Five new test cases cover: deny on denylist, allow when not listed, empty denylist, deny inside an allowed integration, and deny even on an exempt block type.
apps/sim/hooks/use-permission-config.ts New isToolAllowed memoized function added consistently to both the return value and the dependency array.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[executeTool called] --> B{scope.userId && scope.workspaceId?}
    B -- No --> E[Execute tool directly]
    B -- Yes --> C[assertPermissionsAllowed\ntoolId: normalizedToolId\ntoolKind: mcp/custom/skill/undefined]

    C --> D{blockTypeExempt &&\n!model && !toolKind\n&& !toolId?}
    D -- Yes --> E
    D -- No --> F[Load getUserPermissionConfig]

    F --> G{blockType passed\n& not exempt\n& config?}
    G -- Yes --> H{allowedIntegrations\nexcludes blockType?}
    H -- Yes --> I[throw IntegrationNotAllowedError]
    H -- No --> J{toolId in\ndeniedTools?}
    G -- No --> J

    J -- Yes --> K[throw ToolNotAllowedError]
    J -- No --> L{toolKind checks\nmcp/custom/skill}
    L --> M[Pass / Execute tool]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[executeTool called] --> B{scope.userId && scope.workspaceId?}
    B -- No --> E[Execute tool directly]
    B -- Yes --> C[assertPermissionsAllowed\ntoolId: normalizedToolId\ntoolKind: mcp/custom/skill/undefined]

    C --> D{blockTypeExempt &&\n!model && !toolKind\n&& !toolId?}
    D -- Yes --> E
    D -- No --> F[Load getUserPermissionConfig]

    F --> G{blockType passed\n& not exempt\n& config?}
    G -- Yes --> H{allowedIntegrations\nexcludes blockType?}
    H -- Yes --> I[throw IntegrationNotAllowedError]
    H -- No --> J{toolId in\ndeniedTools?}
    G -- No --> J

    J -- Yes --> K[throw ToolNotAllowedError]
    J -- No --> L{toolKind checks\nmcp/custom/skill}
    L --> M[Pass / Execute tool]
Loading

Reviews (6): Last reviewed commit: "fix(access-control): don't seed a denied..." | Re-trigger Greptile

Comment thread apps/sim/ee/access-control/components/group-detail.tsx
…ges save fails

The unsaved-changes dialog's Save action navigated back unconditionally after handleSaveConfig, but that helper swallows mutation errors — so a failed save still exited the view and silently dropped the edits. handleSaveConfig now returns success, and the dialog only closes + navigates back when the save actually succeeded.
deniedTools only matters while a block is allowed, but toggleIntegration/setBlocksAllowed left a disabled block's denied tools in the config. Disabling then re-enabling an integration would silently re-apply the old per-tool denials. Both handlers now prune deniedTools to the set of allowed blocks, keeping the invariant that deniedTools only holds tools of currently-allowed integrations.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/ee/access-control/components/group-detail.tsx Outdated
…en pruning

A tool id can appear in more than one block's tools.access. The single tool->block map meant pruneDeniedTools (and the per-block denied count) attributed a shared tool to only one block, so disabling that block could drop a denial while the tool was still exposed by another allowed block. Tools now map to all exposing block types; a denial is pruned only when no allowed block exposes the tool, and the per-block count is derived from each block's own tool list.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ae1abfa. Configure here.

… filter

The Platform tab's bulk Select/Deselect All toggled every feature regardless of the active search, unlike the Blocks tab which scopes its per-section toggle to the filtered view. Both the all-visible check and the bulk update now operate on filteredPlatformFeatures for consistent behavior while searching.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Addressed the Platform-tab note from the Greptile summary in a0fae05: the Platform Select/Deselect All now scopes to the active search filter (filteredPlatformFeatures), matching the Blocks tab. Re-running reviews.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/ee/access-control/components/group-detail.tsx Outdated
… search filter

Like the Platform fix, the Providers tab's bulk action toggled every provider via allProviderIds regardless of the active search. Added setProvidersAllowed (mirroring setBlocksAllowed) so the bulk toggle and its label operate on filteredProviders, keeping all three tabs (Blocks/Platform/Providers) consistent while searching.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

The operation dropdown hides denied tools from the picker, but defaultOptionValue returned the block's defaultValue without checking deniedOperationIds, so a new block could start on an operation the user isn't allowed to run. It now falls back to the first allowed option when the configured default is denied. Existing stored operation values are intentionally left untouched (auto-rewriting a user's saved block would be destructive; the server remains the authoritative gate).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 887e009. Configure here.

Convert declaration-level rationale comments to TSDoc (/** */) and trim redundant/verbose inline comments added during review, per the project's TSDoc convention.
@waleedlatif1 waleedlatif1 merged commit 7ba0e23 into staging Jun 25, 2026
15 checks passed
@waleedlatif1 waleedlatif1 deleted the worktree-access-control-page-redesign branch June 25, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant