Skip to content

improvement(perm-groups): allow workspace filter for permission groups#5070

Merged
icecrasher321 merged 11 commits into
stagingfrom
improvement/perm-groups
Jun 16, 2026
Merged

improvement(perm-groups): allow workspace filter for permission groups#5070
icecrasher321 merged 11 commits into
stagingfrom
improvement/perm-groups

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

Summary

Allow workspace specific application of permission groups.

No backwards compatibility issues.

Type of Change

  • New Feature

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 15, 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 16, 2026 3:50am

Request Review

@cursor

cursor Bot commented Jun 15, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes enterprise access-control resolution, membership invariants, and a DB migration that removes the per-org single-group constraint—incorrect scope or conflict handling could let users see wrong restrictions or dual governance on a workspace.

Overview
Enterprise permission groups can now target all workspaces or a specific subset, and users may belong to multiple groups instead of being limited to one per organization. Effective restrictions per workspace follow specific → all-workspaces → org default via shared resolveWorkspaceGroup (executor and /api/permission-groups/user).

Backend: migration adds permission_group_workspace and applies_to_all_workspaces, drops the one-group-per-user DB unique constraint, and uses org advisory locks plus findScopeConflicts so adds/scope updates reject overlaps (409) rather than silently moving members; bulk add reports skipped (already in group) and fails the whole batch on scope conflicts.

API/UI: create/update/list groups carry workspace scope; new GET org workspaces for the picker; Access Control UI adds workspace multi-select and detail layout tweaks; modal fix for stale pointer-events on body when nested dropdowns close.

Docs and contract tests updated for the new membership and precedence rules.

Reviewed by Cursor Bugbot for commit da3bc56. Configure here.

Comment thread apps/sim/app/api/organizations/[id]/permission-groups/route.ts Outdated
Comment thread apps/sim/ee/access-control/components/access-control.tsx Outdated
Comment thread apps/sim/ee/access-control/components/access-control.tsx Outdated
Comment thread apps/sim/ee/access-control/components/access-control.tsx
@greptile-apps

greptile-apps Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends permission groups to support workspace-scoped targeting: groups can now govern all workspaces (existing behavior, default) or a specific subset. A new permission_group_workspace join table tracks the targeted workspaces, the permission_group_member_organization_user_unique DB constraint is replaced by an application-level advisory lock + conflict-check system, and the resolution chain becomes: specific-scope group → all-workspaces group → org default → unrestricted.

  • DB + schema: Adds permission_group_workspace table with proper FK cascades and a unique index on (permissionGroupId, workspaceId); downgrades permission_group_member_organization_user_unique to a plain index to allow multi-group membership.
  • Conflict enforcement: Every membership write (add, bulk-add) and scope change (PUT) acquires a per-org advisory lock (pg_advisory_xact_lock) before running findScopeConflicts, then atomically commits — replacing the old single-group-per-org DB unique index with a more nuanced invariant.
  • Resolution: resolveWorkspaceGroup and resolveOrganizationWideGroup are introduced as shared resolution functions used by both the /api/permission-groups/user route and the executor path, eliminating drift between the two consumers.

Confidence Score: 5/5

Safe to merge — the TOCTOU race flagged in the previous review is now properly addressed with a per-org advisory lock, conflict detection runs inside the same transaction as the write, and existing groups are unaffected by the migration default.

The advisory lock serializes all membership and scope writes for an org, and findScopeConflicts + the insert are now atomic. DB migration defaults appliesToAllWorkspaces = true so all existing groups continue to govern every workspace. Resolution logic is a single LEFT JOIN query with clear precedence rules. No correctness issues were found in conflict detection, workspace scope validation, or the Zod contract refinements.

No files require special attention — the core locking and conflict logic in utils.ts and the three member/scope mutation routes are the most sensitive paths and all look correct.

Important Files Changed

Filename Overview
packages/db/migrations/0238_workspace_scoped_permission_groups.sql Creates permission_group_workspace table with proper FK cascades; drops the org-user unique index from permission_group_member and replaces it with a plain btree index; adds appliesToAllWorkspaces column with DEFAULT true so existing groups are unaffected.
apps/sim/app/api/organizations/[id]/permission-groups/utils.ts Core of the new scope logic: advisory lock helper, workspace helpers, and findScopeConflicts. Conflict detection and locking logic is correct; the unfiltered permissionGroupWorkspace join returns O(workspaces) rows per group but the in-memory set lookup keeps correctness.
apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/route.ts Single-user member add now acquires the advisory lock, re-reads the group scope under the lock, and checks conflicts atomically before inserting — addressing the previous TOCTOU race.
apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/members/bulk/route.ts Bulk add is now all-or-nothing for scope conflicts (409 on any conflict, no partial adds); already-in-group users are skipped silently. Advisory lock properly serializes concurrent bulk adds and scope changes.
apps/sim/app/api/organizations/[id]/permission-groups/[groupId]/route.ts PUT acquires the advisory lock only when scope fields are present in the request, re-checks member conflicts with the new scope, then atomically replaces permissionGroupWorkspace rows.
apps/sim/ee/access-control/utils/permission-check.ts Introduces resolveWorkspaceGroup (specific-scope → all-workspaces → org-default) and resolveOrganizationWideGroup, consumed by both the API route and the executor path. Single LEFT JOIN query handles all three priority tiers in one round-trip.
apps/sim/app/api/permission-groups/user/route.ts Replaced the inline two-query resolution with resolveWorkspaceGroup; enterprise check is preserved before the call. Returns already-parsed config from ResolvedPermissionGroup correctly.
apps/sim/lib/api/contracts/permission-groups.ts Adds appliesToAllWorkspaces/workspaceIds to create/update schemas with a superRefine guard; adds listOrganizationWorkspacesContract; bulk-add response semantics updated to conflict-first (409 path).
packages/db/schema.ts New permissionGroupWorkspace table with FK cascades and unique index on (permissionGroupId, workspaceId); permissionGroupMember org-user unique index downgraded to a plain index to allow multi-group membership.
apps/sim/ee/access-control/components/access-control.tsx Adds WorkspaceSelect component, workspace scope controls in the create modal and group detail view, and handleScopeChange with optimistic updates guarded by a monotonic sequence token to prevent stale out-of-order writes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant MemberRoute as POST /members (or /bulk)
    participant DB as Postgres

    Client->>MemberRoute: "POST {userId(s)}"
    MemberRoute->>DB: loadGroupInOrganization() [outside tx]
    MemberRoute->>DB: BEGIN TRANSACTION
    MemberRoute->>DB: "SET lock_timeout = 5000ms"
    MemberRoute->>DB: pg_advisory_xact_lock(hash(org_id))
    Note over MemberRoute,DB: Concurrent writes for same org queue here
    MemberRoute->>DB: loadGroupInOrganization() [re-read under lock]
    MemberRoute->>DB: getGroupWorkspaces() if specific scope
    MemberRoute->>DB: findScopeConflicts(candidateUserIds)
    Note over MemberRoute,DB: Checks: all-vs-all OR specific-vs-specific(shared workspace)
    alt conflicts found
        MemberRoute->>DB: ROLLBACK
        MemberRoute-->>Client: 409 conflict message
    else no conflicts
        MemberRoute->>DB: INSERT permissionGroupMember(s)
        MemberRoute->>DB: COMMIT
        MemberRoute-->>Client: "201 {member(s)}"
    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"}}}%%
sequenceDiagram
    participant Client
    participant MemberRoute as POST /members (or /bulk)
    participant DB as Postgres

    Client->>MemberRoute: "POST {userId(s)}"
    MemberRoute->>DB: loadGroupInOrganization() [outside tx]
    MemberRoute->>DB: BEGIN TRANSACTION
    MemberRoute->>DB: "SET lock_timeout = 5000ms"
    MemberRoute->>DB: pg_advisory_xact_lock(hash(org_id))
    Note over MemberRoute,DB: Concurrent writes for same org queue here
    MemberRoute->>DB: loadGroupInOrganization() [re-read under lock]
    MemberRoute->>DB: getGroupWorkspaces() if specific scope
    MemberRoute->>DB: findScopeConflicts(candidateUserIds)
    Note over MemberRoute,DB: Checks: all-vs-all OR specific-vs-specific(shared workspace)
    alt conflicts found
        MemberRoute->>DB: ROLLBACK
        MemberRoute-->>Client: 409 conflict message
    else no conflicts
        MemberRoute->>DB: INSERT permissionGroupMember(s)
        MemberRoute->>DB: COMMIT
        MemberRoute-->>Client: "201 {member(s)}"
    end
Loading

Reviews (5): Last reviewed commit: "Merge branch 'staging' into improvement/..." | Re-trigger Greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread packages/db/migrations/0238_workspace_scoped_permission_groups.sql Outdated
@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

bugbot run

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

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