Skip to content

Commit 9584b99

Browse files
committed
address bugbot
1 parent 140f870 commit 9584b99

File tree

3 files changed

+114
-48
lines changed

3 files changed

+114
-48
lines changed

apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@
2222
*/
2323

2424
import { db } from '@sim/db'
25-
import { credential, credentialMember, permissions, user, workspace } from '@sim/db/schema'
25+
import { permissions, user, workspace } from '@sim/db/schema'
2626
import { createLogger } from '@sim/logger'
27-
import { and, eq, inArray } from 'drizzle-orm'
27+
import { and, eq } from 'drizzle-orm'
28+
import { revokeWorkspaceCredentialMemberships } from '@/lib/credentials/access'
2829
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
2930
import {
3031
badRequestResponse,
@@ -215,27 +216,7 @@ export const DELETE = withAdminAuthParams<RouteParams>(async (_, context) => {
215216

216217
await db.delete(permissions).where(eq(permissions.id, memberId))
217218

218-
// Revoke credential memberships for all credentials in this workspace
219-
const workspaceCredentialIds = await db
220-
.select({ id: credential.id })
221-
.from(credential)
222-
.where(eq(credential.workspaceId, workspaceId))
223-
224-
if (workspaceCredentialIds.length > 0) {
225-
await db
226-
.update(credentialMember)
227-
.set({ status: 'revoked', updatedAt: new Date() })
228-
.where(
229-
and(
230-
eq(credentialMember.userId, existingMember.userId),
231-
eq(credentialMember.status, 'active'),
232-
inArray(
233-
credentialMember.credentialId,
234-
workspaceCredentialIds.map((c) => c.id)
235-
)
236-
)
237-
)
238-
}
219+
await revokeWorkspaceCredentialMemberships(workspaceId, existingMember.userId)
239220

240221
logger.info(`Admin API: Removed member ${memberId} from workspace ${workspaceId}`, {
241222
userId: existingMember.userId,

apps/sim/app/api/workspaces/members/[id]/route.ts

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { db } from '@sim/db'
2-
import { credential, credentialMember, permissions, workspace } from '@sim/db/schema'
2+
import { permissions, workspace } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { and, eq, inArray } from 'drizzle-orm'
4+
import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { getSession } from '@/lib/auth'
8+
import { revokeWorkspaceCredentialMemberships } from '@/lib/credentials/access'
89
import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils'
910

1011
const logger = createLogger('WorkspaceMemberAPI')
@@ -101,27 +102,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
101102
)
102103
)
103104

104-
// Revoke credential memberships for all credentials in this workspace
105-
const workspaceCredentialIds = await db
106-
.select({ id: credential.id })
107-
.from(credential)
108-
.where(eq(credential.workspaceId, workspaceId))
109-
110-
if (workspaceCredentialIds.length > 0) {
111-
await db
112-
.update(credentialMember)
113-
.set({ status: 'revoked', updatedAt: new Date() })
114-
.where(
115-
and(
116-
eq(credentialMember.userId, userId),
117-
eq(credentialMember.status, 'active'),
118-
inArray(
119-
credentialMember.credentialId,
120-
workspaceCredentialIds.map((c) => c.id)
121-
)
122-
)
123-
)
124-
}
105+
await revokeWorkspaceCredentialMemberships(workspaceId, userId)
125106

126107
return NextResponse.json({ success: true })
127108
} catch (error) {

apps/sim/lib/credentials/access.ts

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { db } from '@sim/db'
2-
import { credential, credentialMember } from '@sim/db/schema'
3-
import { and, eq } from 'drizzle-orm'
2+
import { credential, credentialMember, workspace } from '@sim/db/schema'
3+
import { createLogger } from '@sim/logger'
4+
import { and, eq, inArray, ne } from 'drizzle-orm'
45
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
56

7+
const logger = createLogger('CredentialAccess')
8+
69
type ActiveCredentialMember = typeof credentialMember.$inferSelect
710
type CredentialRecord = typeof credential.$inferSelect
811

@@ -60,3 +63,104 @@ export async function getCredentialActorContext(
6063
isAdmin,
6164
}
6265
}
66+
67+
/**
68+
* Revokes all credential memberships for a user across a workspace.
69+
* Before revoking, ensures the workspace owner is an admin on any credential
70+
* where the removed user is the sole active admin, preventing orphaned credentials.
71+
*/
72+
export async function revokeWorkspaceCredentialMemberships(
73+
workspaceId: string,
74+
userId: string
75+
): Promise<void> {
76+
const workspaceCredentialIds = await db
77+
.select({ id: credential.id })
78+
.from(credential)
79+
.where(eq(credential.workspaceId, workspaceId))
80+
81+
if (workspaceCredentialIds.length === 0) return
82+
83+
const credIds = workspaceCredentialIds.map((c) => c.id)
84+
85+
const [workspaceRow] = await db
86+
.select({ ownerId: workspace.ownerId })
87+
.from(workspace)
88+
.where(eq(workspace.id, workspaceId))
89+
.limit(1)
90+
91+
const ownerId = workspaceRow?.ownerId
92+
93+
if (ownerId && ownerId !== userId) {
94+
const userAdminMemberships = await db
95+
.select({ credentialId: credentialMember.credentialId })
96+
.from(credentialMember)
97+
.where(
98+
and(
99+
eq(credentialMember.userId, userId),
100+
eq(credentialMember.role, 'admin'),
101+
eq(credentialMember.status, 'active'),
102+
inArray(credentialMember.credentialId, credIds)
103+
)
104+
)
105+
106+
for (const { credentialId: credId } of userAdminMemberships) {
107+
const otherAdmins = await db
108+
.select({ id: credentialMember.id })
109+
.from(credentialMember)
110+
.where(
111+
and(
112+
eq(credentialMember.credentialId, credId),
113+
eq(credentialMember.role, 'admin'),
114+
eq(credentialMember.status, 'active'),
115+
ne(credentialMember.userId, userId)
116+
)
117+
)
118+
.limit(1)
119+
120+
if (otherAdmins.length > 0) continue
121+
122+
const now = new Date()
123+
const [existingOwnerMembership] = await db
124+
.select({ id: credentialMember.id, status: credentialMember.status })
125+
.from(credentialMember)
126+
.where(and(eq(credentialMember.credentialId, credId), eq(credentialMember.userId, ownerId)))
127+
.limit(1)
128+
129+
if (existingOwnerMembership) {
130+
await db
131+
.update(credentialMember)
132+
.set({ role: 'admin', status: 'active', updatedAt: now })
133+
.where(eq(credentialMember.id, existingOwnerMembership.id))
134+
} else {
135+
await db.insert(credentialMember).values({
136+
id: crypto.randomUUID(),
137+
credentialId: credId,
138+
userId: ownerId,
139+
role: 'admin',
140+
status: 'active',
141+
joinedAt: now,
142+
invitedBy: ownerId,
143+
createdAt: now,
144+
updatedAt: now,
145+
})
146+
}
147+
148+
logger.info('Assigned workspace owner as credential admin before member removal', {
149+
credentialId: credId,
150+
ownerId,
151+
removedUserId: userId,
152+
})
153+
}
154+
}
155+
156+
await db
157+
.update(credentialMember)
158+
.set({ status: 'revoked', updatedAt: new Date() })
159+
.where(
160+
and(
161+
eq(credentialMember.userId, userId),
162+
eq(credentialMember.status, 'active'),
163+
inArray(credentialMember.credentialId, credIds)
164+
)
165+
)
166+
}

0 commit comments

Comments
 (0)