Skip to content

improvement(access-controls): default workspace experience includes all members#5153

Merged
icecrasher321 merged 5 commits into
stagingfrom
improvement/default-access-control-exp
Jun 20, 2026
Merged

improvement(access-controls): default workspace experience includes all members#5153
icecrasher321 merged 5 commits into
stagingfrom
improvement/default-access-control-exp

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

Summary

Selecting workspaces includes all members as the default experience. If members already selected -- then you can still configure them in the Configure modal in the members tab.

Type of Change

  • Other: UX Improvement

Testing

Tested manually

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)

@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Jun 20, 2026 9:09pm

Request Review

@cursor

cursor Bot commented Jun 20, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes who is governed in each workspace (including external members via empty-member groups) and includes a DB backfill that can widen restrictions for grandfathered groups with members; enforcement and admin flows are security-sensitive.

Overview
Enterprise permission groups now treat workspace selection as the primary scope: non-default groups must pick at least one workspace and govern everyone in those workspaces by default (including external members). Adding members in Configure → Members narrows a group to named people; clearing members restores the all-members behavior.

Only the organization default group may be org-wide. Non-default “all workspaces” scope is removed from APIs/contracts/UI, and a data migration backfills legacy non-default all-workspaces groups (member-bearing groups get every org workspace linked; empty groups stay inert).

Governing-group resolution changes to: explicit member on a workspace group → all-members workspace group → org default → unrestricted. Conflict checks now block duplicate all-members groups per workspace and overlapping explicit memberships; removing the last member is transactional and can 409 if another all-members group already exists.

Access Control UI moves member management into the permissions modal, requires workspaces on create, and updates list copy. Docs and tests follow the new rules.

Reviewed by Cursor Bugbot for commit 2bc256d. Configure here.

Comment thread apps/sim/ee/access-control/components/access-control.tsx Outdated
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@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 2bc256d. Configure here.

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@greptile-apps

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes the default experience for permission groups: a non-default group now governs all members of its assigned workspaces unless explicit members are added, replacing the old model where groups started from the member list and an empty group governed nobody. A data migration handles existing groups (pinning member-bearing all-workspaces groups to their current workspaces, clearing workspace associations for zero-member groups so they stay inert).

  • New resolution order in resolveWorkspaceGroup: query all non-default groups targeting the workspace, pick the first one the user is an explicit member of, then fall back to the first group with no members (all-members), then the org default.
  • Conflict enforcement extended: creating a group, updating its scope, or removing the last member now checks findAllMembersWorkspaceConflict inside the org advisory lock to prevent two all-members groups from claiming the same workspace.
  • UI restructure: workspace scope selector for non-default groups no longer shows an "All workspaces" option; the Members section moves into the Configure modal as a new tab.

Confidence Score: 5/5

Safe to merge. All write paths acquire the org advisory lock before the all-members conflict check, the migration correctly idles zero-member groups before the code goes live, and the schema contract rejects the forbidden all-workspaces state for non-default groups before it reaches any route.

The model change is a significant shift (empty group = all-members vs. governs nobody), but every write path that can produce the new state has a corresponding conflict check, the migration handles pre-existing data in all three cases, and the resolver's fallback order is clean and deterministic. No unguarded path was found that would silently widen or narrow access.

No files require special attention. The migration, route handlers, resolver, and UI changes are internally consistent.

Important Files Changed

Filename Overview
packages/db/migrations/0246_convert_grandfathered_all_ws_permission_groups.sql Migration correctly handles three cases: pins member-bearing all-workspaces groups to every current workspace, strips workspace associations from zero-member non-default groups so they stay inert, and clears the all-workspaces flag from all non-default groups. All three steps are idempotent.
apps/sim/ee/access-control/utils/permission-check.ts resolveWorkspaceGroup now starts from permissionGroup (not permissionGroupMember), fetching all non-default groups targeting the workspace, then selects the first explicit-member match, then first no-member group. resolveOrganizationWideGroup removed; getUserPermissionConfigForOrganization simplified to just return the default group config.
apps/sim/app/api/organizations/[id]/permission-groups/utils.ts findScopeConflicts simplified to only check explicit-member conflicts on overlapping workspaces. New findAllMembersWorkspaceConflict detects workspace overlap between two no-member groups.
apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/route.ts DELETE handler now wraps everything in a transaction with the org lock. Removing the last member triggers findAllMembersWorkspaceConflict before allowing deletion; 409 returned on conflict.
apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.ts PUT route adds all-members conflict check, handles demotion-to-inert path, and enforces the non-default-cannot-be-org-wide invariant.
apps/sim/app/api/organizations/[id]/permission-groups/route.ts POST route now acquires the org lock and checks findAllMembersWorkspaceConflict before inserting, since new groups start with no members.
apps/sim/ee/access-control/components/access-control.tsx Members section moved into Configure modal as a new tab; create modal blocks submission with empty workspace selection for non-default groups.
apps/sim/lib/api/contracts/permission-groups.ts Contract now rejects appliesToAllWorkspaces:true for non-default groups at the schema level.
packages/db/schema.ts Added clear docstrings documenting the new scope and membership invariants, including the 'no members = all members' semantics.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[resolveWorkspaceGroup] --> B[Query all non-default groups targeting workspaceId]
    B --> C{isMember = true?}
    C -- Yes --> D[Winner = explicit-member group]
    C -- No --> E{hasMembers = false?}
    E -- Yes --> F[Winner = all-members group]
    E -- No --> G[resolveDefaultGroup]
    D --> H[Return group config]
    F --> H
    G --> H
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[resolveWorkspaceGroup] --> B[Query all non-default groups targeting workspaceId]
    B --> C{isMember = true?}
    C -- Yes --> D[Winner = explicit-member group]
    C -- No --> E{hasMembers = false?}
    E -- Yes --> F[Winner = all-members group]
    E -- No --> G[resolveDefaultGroup]
    D --> H[Return group config]
    F --> H
    G --> H
Loading

Reviews (4): Last reviewed commit: "address zero-member edge case" | Re-trigger Greptile

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

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR redesigns the permission-group model so that a non-default group governs all members of its targeted workspaces by default (rather than only the members explicitly added to it), and narrows to specific people only once members are added. Only the organization's default group remains org-wide.

  • Core model change: resolveWorkspaceGroup is rewritten from a member-keyed join to a group-keyed scan with isMember/hasMembers EXISTS subqueries; explicit-member groups win over all-members groups, which win over the org default.
  • Conflict enforcement: a new findAllMembersWorkspaceConflict guard (run at create, workspace-scope update, and last-member removal) prevents two all-members groups from targeting the same workspace simultaneously.
  • Migration: a data-only SQL migration converts grandfathered non-default all-workspaces groups — inserting workspace rows for member-bearing ones so they keep governing the same people, and leaving empty ones inert.

Confidence Score: 4/5

The access-control logic and migration are correct; the one cosmetic issue (workspace name in both row slots) does not affect permissions enforcement.

The model change is broad but well-tested, with unit tests covering the new isMember/hasMembers resolution paths and the new conflict checks. The migration is data-only, idempotent, and handles the empty-group edge case carefully. The only concrete defect found is cosmetic: workspaces in the detail view are rendered via MemberRow with email set to the workspace name, causing the name to appear in both label and subtitle positions.

apps/sim/ee/access-control/components/access-control.tsx — the MemberRow email field for workspace rows; apps/sim/ee/access-control/utils/permission-check.ts — the new resolveWorkspaceGroup query shape is the heart of the model change and worth a close read.

Important Files Changed

Filename Overview
apps/sim/ee/access-control/utils/permission-check.ts Rewrites resolveWorkspaceGroup from a member-keyed join to a group-keyed scan with isMember/hasMembers EXISTS subqueries; removes resolveOrganizationWideGroup; getUserPermissionConfigForOrganization now ignores userId and reads only the default group. Logic is correct and well-commented.
apps/sim/app/api/organizations/[id]/permission-groups/utils.ts Adds findAllMembersWorkspaceConflict (prevents two all-members groups on one workspace) and simplifies findScopeConflicts to check only explicit-member overlaps; both queries are well-constrained and the early-return guards for empty arrays are correct.
apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/route.ts DELETE handler now wraps the removal in a transaction with acquirePermissionGroupOrgLock and checks findAllMembersWorkspaceConflict before removing the last member; conflict and lock-error paths are all handled.
apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.ts PUT handler gains demotingDefaultToInert path, all-members conflict check on workspace scope update, and properly clears appliesToAllWorkspaces when demoting the prior default group. Edge cases are covered.
apps/sim/ee/access-control/components/access-control.tsx Major UX redesign: moves Members into the Configure modal as a new tab, adds allowAllWorkspaces prop to WorkspaceSelect, adds toast on last-workspace removal attempt, and always resets configTab to providers on open. Minor issue: workspace list rows pass email={ws.name}, duplicating the workspace name in the subtitle slot.
packages/db/migrations/0246_convert_grandfathered_all_ws_permission_groups.sql Data-only migration: inserts workspace rows for member-bearing all-workspaces groups, then clears the appliesToAllWorkspaces flag on all non-default groups. Empty all-workspaces groups are intentionally left without workspaces (inert). Idempotent via ON CONFLICT DO NOTHING.
apps/sim/lib/api/contracts/permission-groups.ts Schema validation now rejects appliesToAllWorkspaces: true for non-default groups at the contract layer, providing defence-in-depth in addition to the server-side guard in the route.
apps/sim/app/api/organizations/[id]/permission-groups/route.ts POST handler correctly validates that non-default groups must target specific workspaces, acquires the org lock before the all-members conflict check, and handles both the conflict 409 and the Postgres lock 503 paths.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[resolveWorkspaceGroup called
userId + workspaceId] --> B[Query: all non-default groups
targeting workspaceId
with isMember + hasMembers EXISTS]
    B --> C{rows.find row.isMember?}
    C -- yes --> D[Return: explicit-member group]
    C -- no --> E{rows.find !row.hasMembers?}
    E -- yes --> F[Return: all-members group
no explicit members]
    E -- no --> G[resolveDefaultGroup]
    G --> H{default group exists?}
    H -- yes --> I[Return: org default group]
    H -- no --> J[Return: null
unrestricted]

    subgraph Conflict Guards
        K[Create group / Update workspaces
with 0 members] --> L[findAllMembersWorkspaceConflict
check per workspace]
        M[Remove last member
from group] --> L
        N[Add member to group] --> O[findScopeConflicts
explicit-member overlap check]
    end
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[resolveWorkspaceGroup called
userId + workspaceId] --> B[Query: all non-default groups
targeting workspaceId
with isMember + hasMembers EXISTS]
    B --> C{rows.find row.isMember?}
    C -- yes --> D[Return: explicit-member group]
    C -- no --> E{rows.find !row.hasMembers?}
    E -- yes --> F[Return: all-members group
no explicit members]
    E -- no --> G[resolveDefaultGroup]
    G --> H{default group exists?}
    H -- yes --> I[Return: org default group]
    H -- no --> J[Return: null
unrestricted]

    subgraph Conflict Guards
        K[Create group / Update workspaces
with 0 members] --> L[findAllMembersWorkspaceConflict
check per workspace]
        M[Remove last member
from group] --> L
        N[Add member to group] --> O[findScopeConflicts
explicit-member overlap check]
    end
Loading

Reviews (2): Last reviewed commit: "improve copy" | Re-trigger Greptile

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

greptile-apps Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes the default experience for workspace-scoped permission groups from member-keyed (governs only explicitly added members) to all-members (governs everyone in the targeted workspaces by default, narrowed to specific people once members are added). The permission resolver, conflict checks, contract validation, migration, and UI are all updated together to implement and enforce this new semantic.

  • Core resolver rewrite (permission-check.ts): resolveWorkspaceGroup now starts from permissionGroup instead of permissionGroupMember, using correlated EXISTS subqueries for isMember/hasMembers to distinguish explicit-member groups from all-members groups in a single query.
  • New conflict guard (utils.ts, routes): findAllMembersWorkspaceConflict enforces at-most-one all-members group per workspace at create, update, and last-member-delete time, all under the existing advisory lock.
  • Migration (0246_convert_grandfathered_all_ws_permission_groups.sql): converts non-default all-workspaces groups — member-bearing ones are scoped to all existing workspaces, zero-member ones are left inert — but does not address pre-existing zero-member workspace-specific groups.

Confidence Score: 3/5

The resolver rewrite and new conflict guards are internally consistent, but the migration leaves a gap that could cause unexpected permission restrictions for existing customers.

Existing non-default workspace-specific groups with zero members were previously inert under the member-keyed resolver. Under the new resolver they match !row.hasMembers and immediately govern everyone in their workspaces. Any org with such a group will see unexpected restrictions applied to all workspace members post-deploy without admin intent.

packages/db/migrations/0246_convert_grandfathered_all_ws_permission_groups.sql needs a step to clear workspace associations for zero-member non-default workspace-specific groups. apps/sim/ee/access-control/components/access-control.tsx has a minor display issue with email={ws.name} in the workspace list.

Important Files Changed

Filename Overview
packages/db/migrations/0246_convert_grandfathered_all_ws_permission_groups.sql Handles non-default all-workspaces groups correctly (member-bearing to scoped, zero-member to inert), but does not address existing non-default workspace-specific groups with zero members, which will silently become all-members groups under the new resolver logic.
apps/sim/ee/access-control/utils/permission-check.ts Core permission resolution rewritten from member-keyed to workspace-keyed with correlated EXISTS subqueries for isMember/hasMembers; getUserPermissionConfigForOrganization drops the userId parameter, consistent with the new design.
apps/sim/app/api/organizations/[id]/permission-groups/utils.ts findScopeConflicts simplified (LEFT JOIN to INNER JOIN, appliesToAllWorkspaces param removed); new findAllMembersWorkspaceConflict utility enforces at-most-one all-members group per workspace.
apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/route.ts DELETE now wrapped in a transaction with advisory lock; last-member check prevents removing the final explicit member when doing so would create a conflicting all-members group.
apps/sim/ee/access-control/components/access-control.tsx Members section moved into Configure modal as a new tab; workspace list now renders inline; minor issue: MemberRow receives ws.name as both name and email for workspace rows.
apps/sim/app/api/organizations/[id]/permission-groups/route.ts POST now acquires the advisory lock and runs the all-members conflict check before inserting; scope resolution simplified to isDefault === true means all-workspaces, else specific.
apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.ts PUT handles the new demotingDefaultToInert path and all-members conflict check when updating scope; prior-default demotion now also clears appliesToAllWorkspaces on the old default group.
apps/sim/lib/api/contracts/permission-groups.ts Schema now rejects appliesToAllWorkspaces: true for non-default groups at the contract layer; tests updated to match.

Reviews (3): Last reviewed commit: "improve copy" | Re-trigger Greptile

Comment thread apps/sim/ee/access-control/components/access-control.tsx
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321 icecrasher321 merged commit 82cb324 into staging Jun 20, 2026
15 checks passed
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