Skip to content

improvement(perms): member removal reassignment policies#4906

Merged
icecrasher321 merged 3 commits into
stagingfrom
improvement/member-removal
Jun 8, 2026
Merged

improvement(perms): member removal reassignment policies#4906
icecrasher321 merged 3 commits into
stagingfrom
improvement/member-removal

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

Summary

  • Org-level member removal: departing-owned workspaces transfer to the organization owner, and the org owner is granted/admin-promoted for those workspaces.

  • Workflow ownership: departing-owned workflows still transfer to the workspace billing account, so provider config/env resolution has a valid workspace identity.

  • Direct workspace member removal: workspace owner transfer remains to the workspace billing account, matching the existing workspace-level rule we were applying there.

Type of Change

  • Other: UX Improvement

Testing

N/A

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 8, 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 8, 2026 6:49pm

Request Review

@cursor

cursor Bot commented Jun 8, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes membership and billing-adjacent invariants (workflow.userId, workspace owner/billed account) across multiple API paths; failures are guarded with explicit errors but incorrect reassignment could affect execution and billing identity.

Overview
Member removal now reassigns ownership and workflows before access is revoked, and blocks removing or demoting the workspace billing account without reassignment.

Org removal still moves departing-owned workspaces to the organization owner (with admin permissions via upsert). It additionally runs reassignWorkflowOwnershipForWorkspaceMemberRemovalTx so workflows owned by the departing user move to each workspace’s billed account; unresolved cases (e.g. departing user is the billed account) abort with WORKSPACE_BILLING_ACCOUNT_REMOVAL_ERROR (400 on org APIs).

Workspace removal (user and admin) uses shared transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx when the departing member is the workspace owner (owner → billed account + admin permission). The same workflow reassignment runs inside the removal transaction. Admin routes also reject removing the billing account and require billing account members to stay admin on permission updates.

Admin workspace deletes are transactional: ownership transfer, workflow reassignment, permission/credential/permission-group cleanup, with WorkspaceBillingAccountRemovalError mapped to 400.

Reviewed by Cursor Bugbot for commit 24640fb. Configure here.

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@icecrasher321

Copy link
Copy Markdown
Collaborator Author

@greptile

@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces reassignment policies for member removal at both the workspace and organization level: departing-owned workspaces now transfer ownerId to the org owner (with an onConflictDoUpdate admin-permission grant), and workflows owned by the departing member are bulk-reassigned to the workspace billedAccountUserId inside the same transaction.

  • Two new shared transaction helpers replace inline ownership-transfer code across five route handlers, and a new WorkspaceBillingAccountRemovalError propagates the blocked-removal signal to a consistent 400 HTTP response.
  • All four workspace/org removal routes are updated to call the new helpers inside a single atomic transaction covering permissions, credential memberships, permission-group memberships, and workflow ownership.
  • The reassignOwnedOrganizationWorkspacesTx upsert is upgraded from onConflictDoNothing to onConflictDoUpdate, ensuring the org owner is promoted to admin on reassigned workspaces.

Confidence Score: 3/5

Safe to merge for workspaces where departing users are not the billing account; the blocking scenario should be resolved before production rollout.

The core transaction logic is well-structured and the onConflictDoUpdate fix is a genuine improvement. The concern is that workspaces where billedAccountUserId equals departingUserId are added to unresolved before any workflow count is checked, permanently blocking removal of a user who is billing account for a workspace with zero of their own workflows.

apps/sim/lib/workspaces/utils.ts (unresolved-check logic) and apps/sim/lib/workspaces/utils.test.ts (missing coverage for transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx)

Important Files Changed

Filename Overview
apps/sim/lib/workspaces/utils.ts Adds two new transactional helpers; the unresolved-check in reassignWorkflowOwnershipForWorkspaceMemberRemovalTx marks workspaces unresolved regardless of workflow count, blocking valid removals.
apps/sim/lib/billing/organizations/membership.ts Adds workflow-ownership reassignment to org removal functions and upgrades the permission upsert to onConflictDoUpdate.
apps/sim/app/api/workspaces/members/[id]/route.ts Refactors DELETE handler to use new shared helpers and adds billing-account guard.
apps/sim/lib/workspaces/utils.test.ts New test file covers three cases of reassignWorkflowOwnershipForWorkspaceMemberRemovalTx but omits transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Member Removal Request] --> B{Is billing account?}
    B -- Yes --> C[Return 400 early guard]
    B -- No --> D[Open DB Transaction]
    D --> E[transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx]
    E --> F{Is departing user workspace owner?}
    F -- No --> G[Return false no-op]
    F -- Yes --> H{billedAccountUserId === departingUserId?}
    H -- Yes --> I[Throw WorkspaceBillingAccountRemovalError]
    H -- No --> J[Update workspace.ownerId]
    J --> K[Upsert admin permission for new owner]
    G --> L[reassignWorkflowOwnershipForWorkspaceMemberRemovalTx]
    K --> L
    L --> M{billedAccountUserId === departingUserId?}
    M -- Yes --> N[Add to unresolved - no workflow count check]
    M -- No --> O[Add to reassignmentWorkspaceIds]
    O --> P[Bulk UPDATE workflow.userId]
    N --> Q{unresolved.length > 0?}
    P --> Q
    Q -- Yes --> I
    Q -- No --> R[Delete permissions and memberships]
    R --> S[Commit Transaction]
    I --> T[Rollback - 400 response]
Loading

Reviews (1): Last reviewed commit: "improve UI notif" | Re-trigger Greptile

Comment thread apps/sim/lib/workspaces/utils.ts
Comment thread apps/sim/lib/billing/organizations/membership.ts
Comment thread apps/sim/lib/workspaces/utils.test.ts
@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds reassignment policies for both workspace ownership and workflow ownership when org or workspace members are removed. Two new transaction-scoped utilities centralize the logic, and all member-removal routes are updated to use them atomically, with billing-account guards preventing removal of the workspace's billing identity.

  • New utils: transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx transfers workspace ownerId to the billed account when the owner departs; reassignWorkflowOwnershipForWorkspaceMemberRemovalTx bulk-reassigns workflow.userId to each workspace's billed account via an in-transaction SQL subquery.
  • Org-level path: workspace ownership goes to the org owner via reassignOwnedOrganizationWorkspacesTx (now using onConflictDoUpdate); workflow ownership goes to the workspace billing account.
  • Workspace-level path: both ownership and workflow identity are transferred to the billing account in a single transaction, with permissionGroupMember cleanup and credential revocation included.

Confidence Score: 3/5

Safe to review further before merging — one function in the new utils layer will write a null workspace owner to the DB when billedAccountUserId is unset, relying on a later sibling call in the same transaction to roll things back rather than being self-contained.

transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx skips a null check for billedAccountUserId. In that case it issues UPDATE workspace SET ownerId = NULL and subsequently tries to upsert a permission row with userId = NULL, both inside the same transaction. The transaction does roll back because the following reassignWorkflowOwnershipForWorkspaceMemberRemovalTx call flags the workspace as unresolved and throws, but that safety net is entirely implicit and depends on call order staying the same.

apps/sim/lib/workspaces/utils.ts (the null-guard gap in transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx) and apps/sim/lib/billing/organizations/membership.ts (two silent catch blocks for billing-account errors).

Important Files Changed

Filename Overview
apps/sim/lib/workspaces/utils.ts Adds two new transaction-scoped helpers; transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx has a missing null-guard for billedAccountUserId, while reassignWorkflowOwnershipForWorkspaceMemberRemovalTx correctly handles null via the unresolved path.
apps/sim/lib/billing/organizations/membership.ts Adds workflow ownership reassignment to removeUserFromOrganization and removeExternalUserFromOrganizationWorkspaces; fixes reassignOwnedOrganizationWorkspacesTx to use onConflictDoUpdate. Two WorkspaceBillingAccountRemovalError catch blocks silently return without logging.
apps/sim/app/api/workspaces/members/[id]/route.ts Replaces inline ownership-transfer logic with calls to the new shared utilities; wraps all member-removal side effects in a single DB transaction with correct rollback on unresolved workflow assignment.
apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts Adds billing-account guard to both PATCH and DELETE, and wraps DELETE in a full transaction including ownership transfer and credential revocation.
apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts Mirrors the billing-account guard and transactional DELETE from the per-member route; correctly includes permissionGroupMember cleanup and credential revocation inside the transaction.
apps/sim/lib/workspaces/utils.test.ts New test file covering reassign-happy-path, departing-user-is-billing-account, and no-billing-account-configured cases; happy-path test does not assert the specific userId SQL subquery used for reassignment.

Sequence Diagram

sequenceDiagram
    participant Route as API Route
    participant Guard as Pre-check
    participant TX as DB Transaction
    participant OwnerTx as transferWorkspaceOwnershipTx
    participant WorkflowTx as reassignWorkflowOwnershipTx
    participant DB as Database
    Route->>Guard: Is departing user the billing account?
    alt "billedAccountUserId === userId"
        Guard-->>Route: 400 Bad Request
    else
        Guard-->>Route: OK proceed
        Route->>TX: db.transaction()
        TX->>OwnerTx: transfer workspace ownerId
        OwnerTx->>DB: SELECT workspace
        alt departingUser is owner
            OwnerTx->>DB: UPDATE workspace ownerId
            OwnerTx->>DB: UPSERT permissions admin
            OwnerTx-->>TX: true
        else
            OwnerTx-->>TX: false
        end
        TX->>WorkflowTx: reassign workflow.userId
        WorkflowTx->>DB: SELECT billedAccountUserId
        alt null or equals departingUser
            WorkflowTx-->>TX: unresolved
            TX-->>Route: rollback 400
        else
            WorkflowTx->>DB: UPDATE workflow userId
            WorkflowTx-->>TX: reassigned
        end
        TX->>DB: DELETE permissions
        TX->>DB: revoke credentials
        TX->>DB: DELETE permissionGroupMember
        TX-->>Route: success
        Route-->>Route: 200 OK
    end
Loading

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

Comment thread apps/sim/lib/workspaces/utils.ts
Comment thread apps/sim/lib/billing/organizations/membership.ts
Comment thread apps/sim/lib/billing/organizations/membership.ts
@greptile-apps

greptile-apps Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements member-removal reassignment policies across org-level and workspace-level removal paths. When a member departs, their owned workspaces transfer to the org owner (org removal) or the workspace billing account (direct removal), and their workflows are re-attributed to the workspace billed account so provider config and env-var resolution remain valid.

  • New utilities in utils.ts: transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx and reassignWorkflowOwnershipForWorkspaceMemberRemovalTx, plus a shared WorkspaceBillingAccountRemovalError class used consistently across all removal paths.
  • All removal routes updated to call both utilities inside a single transaction, catch the new error class, and return HTTP 400 to callers.
  • UI improvement: The teammates settings panel now shows a toast error when member removal is blocked.

Confidence Score: 3/5

The core logic is sound, but a missing null guard in the ownership-transfer utility can cause an unexpected DB error whenever a workspace has no billing account and its owner is being removed.

The transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx function skips the null case for billedAccountUserId, falling through to set ownerId = null. This produces either a DB constraint error (500) or temporary data corruption before rollback, rather than the intended clean 400. All other changes — the workflow subquery reassignment, transaction wrapping across routes, the admin PATCH permission guard, and the UI toast — are correct.

apps/sim/lib/workspaces/utils.ts — the null guard on billedAccountUserId inside transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx should be added before merging.

Important Files Changed

Filename Overview
apps/sim/lib/workspaces/utils.ts Adds transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx and reassignWorkflowOwnershipForWorkspaceMemberRemovalTx; missing null guard on billedAccountUserId allows an attempt to set ownerId = null
apps/sim/lib/billing/organizations/membership.ts Adds workflow ownership reassignment to org-level member removal; relies on the invariant that billedAccountUserId = org owner for org workspaces
apps/sim/app/api/workspaces/members/[id]/route.ts Refactored to use shared utility functions; properly catches WorkspaceBillingAccountRemovalError and returns 400
apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts Admin workspace member removal wrapped in transaction with ownership/workflow reassignment; pre-check guards equality case but not null billedAccountUserId
apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts Admin workspace member add/remove now transactional with workflow/ownership reassignment
apps/sim/lib/workspaces/utils.test.ts Tests for reassignWorkflowOwnershipForWorkspaceMemberRemovalTx only; transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx has no coverage
apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts Properly maps WorkspaceBillingAccountRemovalError to HTTP 400 in both code paths
apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts Admin org member removal correctly maps the new error class to a 400 bad request response
apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx Adds onError toast handler so users see a meaningful error when workspace billing account removal is blocked

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as API Route
    participant TX as DB Transaction
    participant OwnerUtil as transferWorkspaceOwnership...
    participant WorkflowUtil as reassignWorkflowOwnership...

    Client->>Route: DELETE /workspaces/members/[id]
    Route->>Route: "Pre-check billedAccount === userId"
    Route->>TX: db.transaction()
    TX->>OwnerUtil: transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx
    alt departing user is workspace owner
        OwnerUtil->>TX: "UPDATE workspace SET ownerId = billedAccountUserId"
        OwnerUtil->>TX: UPSERT permissions admin for new owner
    end
    TX->>WorkflowUtil: reassignWorkflowOwnershipForWorkspaceMemberRemovalTx
    WorkflowUtil->>TX: SELECT workspace.billedAccountUserId
    alt billedAccountUserId null or equals departingUserId
        WorkflowUtil-->>TX: unresolved workspace
        TX-->>Route: throw WorkspaceBillingAccountRemovalError
        Route-->>Client: 400 Bad Request
    else valid billed account
        WorkflowUtil->>TX: UPDATE workflow SET userId via correlated subquery
    end
    TX->>TX: DELETE permissions for departing user
    TX->>TX: revokeWorkspaceCredentialMembershipsTx
    TX->>TX: DELETE permissionGroupMember
    TX-->>Route: ownershipTransferred and workflowOwnershipReassignment
    Route-->>Client: 200 success
Loading

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

Comment thread apps/sim/lib/workspaces/utils.ts
Comment thread apps/sim/lib/workspaces/utils.ts
Comment thread apps/sim/lib/workspaces/utils.test.ts
@icecrasher321 icecrasher321 merged commit 2c7b1ca into staging Jun 8, 2026
13 checks passed
@waleedlatif1 waleedlatif1 deleted the improvement/member-removal branch June 8, 2026 22:04
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