Skip to content
Open
23 changes: 16 additions & 7 deletions apps/sim/app/api/credentials/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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 {
Expand Down Expand Up @@ -535,17 +534,27 @@ 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))
)
Comment thread
cursor[bot] marked this conversation as resolved.
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 || 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',
Comment thread
cursor[bot] marked this conversation as resolved.
status: 'active',
joinedAt: now,
invitedBy: session.user.id,
Expand Down
110 changes: 61 additions & 49 deletions apps/sim/lib/credentials/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,6 @@ interface AccessibleEnvCredential {
updatedAt: Date
}

export async function getWorkspaceMemberUserIds(workspaceId: string): Promise<string[]> {
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<string>(permissionRows.map((row) => row.userId))
if (workspaceRow?.ownerId) {
memberIds.add(workspaceRow.ownerId)
}
return Array.from(memberIds)
}

export async function getUserWorkspaceIds(userId: string): Promise<string[]> {
const [permissionRows, ownedWorkspaceRows] = await Promise.all([
db
Expand Down Expand Up @@ -58,7 +37,8 @@ export async function getUserWorkspaceIds(userId: string): Promise<string[]> {
async function ensureWorkspaceCredentialMemberships(
credentialId: string,
memberUserIds: string[],
ownerUserId: string
ownerUserId: string,
wsPermissionByUser: Map<string, string>
Comment thread
cursor[bot] marked this conversation as resolved.
) {
if (!memberUserIds.length) return

Expand All @@ -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
Expand All @@ -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))),
Comment thread
cursor[bot] marked this conversation as resolved.
])

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({
Expand Down Expand Up @@ -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
)
Comment thread
cursor[bot] marked this conversation as resolved.
}

if (normalizedKeys.length > 0) {
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down