diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index 64a3d3f951..f7858163f6 100644 --- a/apps/sim/app/api/credentials/route.ts +++ b/apps/sim/app/api/credentials/route.ts @@ -1,6 +1,6 @@ import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' -import { account, credential, credentialMember, workspace } from '@sim/db/schema' +import { account, credential, credentialMember, permissions, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getPostgresErrorCode } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' @@ -22,7 +22,6 @@ import { normalizeAtlassianDomain, validateAtlassianServiceAccount, } from '@/lib/credentials/atlassian-service-account' -import { getWorkspaceMemberUserIds } from '@/lib/credentials/environment' import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth' import { getServiceConfigByProviderId } from '@/lib/oauth' import { @@ -535,17 +534,30 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) if ((type === 'env_workspace' || type === 'service_account') && workspaceRow?.ownerId) { - const workspaceUserIds = await getWorkspaceMemberUserIds(workspaceId) + const wsPermissionRows = await tx + .select({ userId: permissions.userId, permissionType: permissions.permissionType }) + .from(permissions) + .where( + and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId)) + ) + const wsPermissionByUser = new Map( + wsPermissionRows.map((row) => [row.userId, row.permissionType]) + ) + const workspaceUserIds = Array.from( + new Set([workspaceRow.ownerId, ...wsPermissionRows.map((row) => row.userId)]) + ) if (workspaceUserIds.length > 0) { for (const memberUserId of workspaceUserIds) { + const wsPermission = wsPermissionByUser.get(memberUserId) + const isAdmin = + memberUserId === workspaceRow.ownerId || + memberUserId === session.user.id || + wsPermission === 'admin' await tx.insert(credentialMember).values({ id: generateId(), credentialId, userId: memberUserId, - role: - memberUserId === workspaceRow.ownerId || memberUserId === session.user.id - ? 'admin' - : 'member', + role: isAdmin ? 'admin' : 'member', status: 'active', joinedAt: now, invitedBy: session.user.id, diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 6013af5349..986be0307f 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -10,27 +10,6 @@ interface AccessibleEnvCredential { updatedAt: Date } -export async function getWorkspaceMemberUserIds(workspaceId: string): Promise { - const [workspaceRows, permissionRows] = await Promise.all([ - db - .select({ ownerId: workspace.ownerId }) - .from(workspace) - .where(eq(workspace.id, workspaceId)) - .limit(1), - db - .select({ userId: permissions.userId }) - .from(permissions) - .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))), - ]) - const workspaceRow = workspaceRows[0] - - const memberIds = new Set(permissionRows.map((row) => row.userId)) - if (workspaceRow?.ownerId) { - memberIds.add(workspaceRow.ownerId) - } - return Array.from(memberIds) -} - export async function getUserWorkspaceIds(userId: string): Promise { const [permissionRows, ownedWorkspaceRows] = await Promise.all([ db @@ -58,7 +37,8 @@ export async function getUserWorkspaceIds(userId: string): Promise { async function ensureWorkspaceCredentialMemberships( credentialId: string, memberUserIds: string[], - ownerUserId: string + ownerUserId: string, + wsPermissionByUser: Map ) { if (!memberUserIds.length) return @@ -83,17 +63,21 @@ async function ensureWorkspaceCredentialMemberships( if (targetUserIds.length === 0) return const now = new Date() - const values = targetUserIds.map((memberUserId) => ({ - id: generateId(), - credentialId, - userId: memberUserId, - role: (memberUserId === ownerUserId ? 'admin' : 'member') as 'admin' | 'member', - status: 'active' as const, - joinedAt: now, - invitedBy: ownerUserId, - createdAt: now, - updatedAt: now, - })) + const values = targetUserIds.map((memberUserId) => { + const wsPermission = wsPermissionByUser.get(memberUserId) + const isAdmin = memberUserId === ownerUserId || wsPermission === 'admin' + return { + id: generateId(), + credentialId, + userId: memberUserId, + role: (isAdmin ? 'admin' : 'member') as 'admin' | 'member', + status: 'active' as const, + joinedAt: now, + invitedBy: ownerUserId, + createdAt: now, + updatedAt: now, + } + }) // `joinedAt` uses COALESCE so a non-null existing value is preserved but null is backfilled. await db @@ -117,17 +101,27 @@ export async function syncWorkspaceEnvCredentials(params: { actingUserId: string }) { const { workspaceId, envKeys, actingUserId } = params - const [[workspaceRow], memberUserIds] = await Promise.all([ + const [[workspaceRow], wsPermissionRows] = await Promise.all([ db .select({ ownerId: workspace.ownerId }) .from(workspace) .where(eq(workspace.id, workspaceId)) .limit(1), - getWorkspaceMemberUserIds(workspaceId), + db + .select({ userId: permissions.userId, permissionType: permissions.permissionType }) + .from(permissions) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))), ]) if (!workspaceRow) return + const wsPermissionByUser = new Map( + wsPermissionRows.map((row) => [row.userId, row.permissionType]) + ) + const memberUserIds = Array.from( + new Set([workspaceRow.ownerId, ...wsPermissionRows.map((row) => row.userId)]) + ) + const normalizedKeys = Array.from(new Set(envKeys.filter(Boolean))) const existingCredentials = await db .select({ @@ -175,7 +169,12 @@ export async function syncWorkspaceEnvCredentials(params: { } for (const credentialId of credentialIdsToEnsureMembership) { - await ensureWorkspaceCredentialMemberships(credentialId, memberUserIds, workspaceRow.ownerId) + await ensureWorkspaceCredentialMemberships( + credentialId, + memberUserIds, + workspaceRow.ownerId, + wsPermissionByUser + ) } if (normalizedKeys.length > 0) { @@ -209,18 +208,27 @@ export async function createWorkspaceEnvCredentials(params: { const keys = Array.from(new Set(newKeys.filter(Boolean))) if (keys.length === 0) return - const [[workspaceRow], memberUserIds] = await Promise.all([ + const [[workspaceRow], wsPermissionRows] = await Promise.all([ db .select({ ownerId: workspace.ownerId }) .from(workspace) .where(eq(workspace.id, workspaceId)) .limit(1), - getWorkspaceMemberUserIds(workspaceId), + db + .select({ userId: permissions.userId, permissionType: permissions.permissionType }) + .from(permissions) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))), ]) if (!workspaceRow) return const ownerUserId = workspaceRow.ownerId + const wsPermissionByUser = new Map( + wsPermissionRows.map((row) => [row.userId, row.permissionType]) + ) + const memberUserIds = Array.from( + new Set([ownerUserId, ...wsPermissionRows.map((row) => row.userId)]) + ) const now = new Date() const inserted = await db @@ -245,17 +253,21 @@ export async function createWorkspaceEnvCredentials(params: { // Bulk-insert memberships for all new credentials × all workspace members in one query const membershipValues = createdIds.flatMap((credentialId) => - memberUserIds.map((memberUserId) => ({ - id: generateId(), - credentialId, - userId: memberUserId, - role: (memberUserId === ownerUserId ? 'admin' : 'member') as 'admin' | 'member', - status: 'active' as const, - joinedAt: now, - invitedBy: ownerUserId, - createdAt: now, - updatedAt: now, - })) + memberUserIds.map((memberUserId) => { + const wsPermission = wsPermissionByUser.get(memberUserId) + const isAdmin = memberUserId === ownerUserId || wsPermission === 'admin' + return { + id: generateId(), + credentialId, + userId: memberUserId, + role: (isAdmin ? 'admin' : 'member') as 'admin' | 'member', + status: 'active' as const, + joinedAt: now, + invitedBy: ownerUserId, + createdAt: now, + updatedAt: now, + } + }) ) await db.insert(credentialMember).values(membershipValues).onConflictDoNothing()