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

vercelBot commented Jun 8, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
docsSkippedSkippedJun 8, 2026 6:49pm

Request Review

@cursor

cursorBot 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
CollaboratorAuthor

@greptile

@icecrasher321

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@greptile-apps

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

FilenameOverview
apps/sim/lib/workspaces/utils.tsAdds 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.tsAdds workflow-ownership reassignment to org removal functions and upgrades the permission upsert to onConflictDoUpdate.
apps/sim/app/api/workspaces/members/[id]/route.tsRefactors DELETE handler to use new shared helpers and adds billing-account guard.
apps/sim/lib/workspaces/utils.test.tsNew 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 threadapps/sim/lib/workspaces/utils.ts
Comment threadapps/sim/lib/billing/organizations/membership.ts
Comment threadapps/sim/lib/workspaces/utils.test.ts
@greptile-apps

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

FilenameOverview
apps/sim/lib/workspaces/utils.tsAdds 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.tsAdds 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.tsReplaces 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.tsAdds 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.tsMirrors 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.tsNew 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 threadapps/sim/lib/workspaces/utils.ts
Comment threadapps/sim/lib/billing/organizations/membership.ts
Comment threadapps/sim/lib/billing/organizations/membership.ts
@greptile-apps

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

FilenameOverview
apps/sim/lib/workspaces/utils.tsAdds transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx and reassignWorkflowOwnershipForWorkspaceMemberRemovalTx; missing null guard on billedAccountUserId allows an attempt to set ownerId = null
apps/sim/lib/billing/organizations/membership.tsAdds 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.tsRefactored to use shared utility functions; properly catches WorkspaceBillingAccountRemovalError and returns 400
apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.tsAdmin 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.tsAdmin workspace member add/remove now transactional with workflow/ownership reassignment
apps/sim/lib/workspaces/utils.test.tsTests for reassignWorkflowOwnershipForWorkspaceMemberRemovalTx only; transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx has no coverage
apps/sim/app/api/organizations/[id]/members/[memberId]/route.tsProperly maps WorkspaceBillingAccountRemovalError to HTTP 400 in both code paths
apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.tsAdmin org member removal correctly maps the new error class to a 400 bad request response
apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsxAdds 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 threadapps/sim/lib/workspaces/utils.ts
Comment threadapps/sim/lib/workspaces/utils.ts
Comment threadapps/sim/lib/workspaces/utils.test.ts
@icecrasher321
icecrasher321 merged commit 2c7b1ca into stagingJun 8, 2026
13 checks passed
@waleedlatif1
waleedlatif1 deleted the improvement/member-removal branch June 8, 2026 22:04
Sign up for freeto 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

@icecrasher321