From 8d78ecbf19d4e811c244fd3b8bcf7cc0399e399d Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Thu, 21 May 2026 19:18:45 +0900 Subject: [PATCH 01/10] fix(credentials): reflect workspace permission in credential member role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workspace admin users were incorrectly assigned 'member' role on credential_member when workspace-scoped secrets were created or synced. Only the workspace owner got 'admin'. Now workspace permissions table is consulted: owner/admin → credential admin, write/read → member. - environment.ts: query workspace permissions in ensureWorkspaceCredentialMemberships - route.ts POST: apply same mapping during credential creation --- apps/sim/app/api/credentials/route.ts | 25 +++++++++++++++++++------ apps/sim/lib/credentials/environment.ts | 22 +++++++++++++++++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index 64a3d3f9511..2ea65e684e9 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' @@ -535,17 +535,30 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) if ((type === 'env_workspace' || type === 'service_account') && workspaceRow?.ownerId) { - const workspaceUserIds = await getWorkspaceMemberUserIds(workspaceId) + const [workspaceUserIds, wsPermissionRows] = await Promise.all([ + getWorkspaceMemberUserIds(workspaceId), + db + .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]) + ) 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 0ace9884075..2b325c70632 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -64,10 +64,20 @@ export async function getUserWorkspaceIds(userId: string): Promise { async function ensureWorkspaceCredentialMemberships( credentialId: string, memberUserIds: string[], - ownerUserId: string + ownerUserId: string, + workspaceId: string ) { if (!memberUserIds.length) return + const workspacePermissionRows = await db + .select({ userId: permissions.userId, permissionType: permissions.permissionType }) + .from(permissions) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))) + + const wsPermissionByUser = new Map( + workspacePermissionRows.map((row) => [row.userId, row.permissionType]) + ) + const existingMemberships = await db .select({ id: credentialMember.id, @@ -87,7 +97,8 @@ async function ensureWorkspaceCredentialMemberships( const now = new Date() for (const memberUserId of memberUserIds) { - const targetRole = memberUserId === ownerUserId ? 'admin' : 'member' + const wsPermission = wsPermissionByUser.get(memberUserId) + const targetRole = memberUserId === ownerUserId || wsPermission === 'admin' ? 'admin' : 'member' const existing = byUserId.get(memberUserId) if (existing) { if (existing.status === 'revoked') { @@ -182,7 +193,12 @@ export async function syncWorkspaceEnvCredentials(params: { } for (const credentialId of credentialIdsToEnsureMembership) { - await ensureWorkspaceCredentialMemberships(credentialId, memberUserIds, workspaceRow.ownerId) + await ensureWorkspaceCredentialMemberships( + credentialId, + memberUserIds, + workspaceRow.ownerId, + workspaceId + ) } if (normalizedKeys.length > 0) { From 1bbf7c6ce49cd6efe5396c22f6011ddf58bcf6d6 Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Thu, 21 May 2026 19:35:28 +0900 Subject: [PATCH 02/10] fix(credentials): apply permission mapping in createWorkspaceEnvCredentials Address Bugbot review: the parallel credential creation path (createWorkspaceEnvCredentials) still used owner-only admin logic. Now queries workspace permissions table for consistent role mapping. --- apps/sim/lib/credentials/environment.ts | 35 +++++++++++++++++-------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 2b325c70632..2fa72709d13 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -269,19 +269,32 @@ export async function createWorkspaceEnvCredentials(params: { if (createdIds.length === 0 || memberUserIds.length === 0) return + const wsPermissionRows = await db + .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]) + ) + // 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() From 71db264902c85549173e117de5eb0868a8cb4f0f Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Thu, 21 May 2026 19:45:18 +0900 Subject: [PATCH 03/10] perf(credentials): hoist permissions query out of per-credential loop Address Bugbot review: permissions query was executed N times (once per credential) inside ensureWorkspaceCredentialMemberships loop. Now queried once in the caller and passed as a Map parameter. --- apps/sim/lib/credentials/environment.ts | 41 ++++++++++++------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 2fa72709d13..ec5c6aec530 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -65,19 +65,10 @@ async function ensureWorkspaceCredentialMemberships( credentialId: string, memberUserIds: string[], ownerUserId: string, - workspaceId: string + wsPermissionByUser: Map ) { if (!memberUserIds.length) return - const workspacePermissionRows = await db - .select({ userId: permissions.userId, permissionType: permissions.permissionType }) - .from(permissions) - .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))) - - const wsPermissionByUser = new Map( - workspacePermissionRows.map((row) => [row.userId, row.permissionType]) - ) - const existingMemberships = await db .select({ id: credentialMember.id, @@ -137,17 +128,25 @@ export async function syncWorkspaceEnvCredentials(params: { actingUserId: string }) { const { workspaceId, envKeys, actingUserId } = params - const [[workspaceRow], memberUserIds] = await Promise.all([ + const [[workspaceRow], memberUserIds, 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 normalizedKeys = Array.from(new Set(envKeys.filter(Boolean))) const existingCredentials = await db .select({ @@ -197,7 +196,7 @@ export async function syncWorkspaceEnvCredentials(params: { credentialId, memberUserIds, workspaceRow.ownerId, - workspaceId + wsPermissionByUser ) } @@ -232,18 +231,25 @@ 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], memberUserIds, 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 now = new Date() const createdIds: string[] = [] @@ -269,15 +275,6 @@ export async function createWorkspaceEnvCredentials(params: { if (createdIds.length === 0 || memberUserIds.length === 0) return - const wsPermissionRows = await db - .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]) - ) - // Bulk-insert memberships for all new credentials × all workspace members in one query const membershipValues = createdIds.flatMap((credentialId) => memberUserIds.map((memberUserId) => { From d1891c9e2be331ddf6996f9d40a1687a0c72d7a9 Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Thu, 21 May 2026 20:08:21 +0900 Subject: [PATCH 04/10] perf(credentials): eliminate redundant permissions query Derive memberUserIds from wsPermissionRows + workspace owner instead of calling getWorkspaceMemberUserIds separately. This removes a duplicate query on the permissions table at every call site. --- apps/sim/app/api/credentials/route.ts | 19 +++++++++---------- apps/sim/lib/credentials/environment.ts | 12 ++++++++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index 2ea65e684e9..8193f892af5 100644 --- a/apps/sim/app/api/credentials/route.ts +++ b/apps/sim/app/api/credentials/route.ts @@ -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,18 +534,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) if ((type === 'env_workspace' || type === 'service_account') && workspaceRow?.ownerId) { - const [workspaceUserIds, wsPermissionRows] = await Promise.all([ - getWorkspaceMemberUserIds(workspaceId), - db - .select({ userId: permissions.userId, permissionType: permissions.permissionType }) - .from(permissions) - .where( - and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId)) - ), - ]) + const wsPermissionRows = await db + .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) diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index ec5c6aec530..9b63709db29 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -128,13 +128,12 @@ export async function syncWorkspaceEnvCredentials(params: { actingUserId: string }) { const { workspaceId, envKeys, actingUserId } = params - const [[workspaceRow], memberUserIds, wsPermissionRows] = 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) @@ -146,6 +145,9 @@ export async function syncWorkspaceEnvCredentials(params: { 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 @@ -231,13 +233,12 @@ export async function createWorkspaceEnvCredentials(params: { const keys = Array.from(new Set(newKeys.filter(Boolean))) if (keys.length === 0) return - const [[workspaceRow], memberUserIds, wsPermissionRows] = 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) @@ -250,6 +251,9 @@ export async function createWorkspaceEnvCredentials(params: { 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 createdIds: string[] = [] From 78b6c883343a338da6d039e03ddc20a448e3b5cd Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Thu, 21 May 2026 20:14:31 +0900 Subject: [PATCH 05/10] fix(credentials): remove session.user.id from admin check for consistency The credential creator (session.user.id) was always granted admin role regardless of their workspace permission. This created inconsistency with environment.ts sync logic which correctly derives role solely from workspace permission. Now both paths use the same mapping. --- apps/sim/app/api/credentials/route.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index 8193f892af5..bdaeff24619 100644 --- a/apps/sim/app/api/credentials/route.ts +++ b/apps/sim/app/api/credentials/route.ts @@ -549,10 +549,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { 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' + const isAdmin = memberUserId === workspaceRow.ownerId || wsPermission === 'admin' await tx.insert(credentialMember).values({ id: generateId(), credentialId, From ad7ee85c324348507cdf34dd375b22ae1861daa9 Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Fri, 22 May 2026 10:31:00 +0900 Subject: [PATCH 06/10] fix(credentials): use tx instead of db for permissions query inside transaction --- apps/sim/app/api/credentials/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index bdaeff24619..ce967ad8e75 100644 --- a/apps/sim/app/api/credentials/route.ts +++ b/apps/sim/app/api/credentials/route.ts @@ -534,7 +534,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) if ((type === 'env_workspace' || type === 'service_account') && workspaceRow?.ownerId) { - const wsPermissionRows = await db + const wsPermissionRows = await tx .select({ userId: permissions.userId, permissionType: permissions.permissionType }) .from(permissions) .where( From c37e20c00332dcaaf32d63dd0802675640109c03 Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Fri, 22 May 2026 10:39:41 +0900 Subject: [PATCH 07/10] refactor(credentials): remove unused getWorkspaceMemberUserIds All callers now derive member IDs from workspace permission rows directly, making this function dead code. --- apps/sim/lib/credentials/environment.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 9b63709db29..b0188bd2dbc 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -16,27 +16,6 @@ function getPostgresErrorCode(error: unknown): string | undefined { return err.code || err.cause?.code } -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 From 80c7061ec98f038522218a6abfa70cb72201a844 Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Fri, 22 May 2026 17:30:35 +0900 Subject: [PATCH 08/10] fix(credentials): restrict credential creation to workspace admin Write-only users could create secrets but got 'member' role, making them unable to edit/delete their own secrets. Now credential creation requires workspace admin permission, consistent with the role mapping. --- apps/sim/app/api/credentials/route.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index ce967ad8e75..dc57ec0f472 100644 --- a/apps/sim/app/api/credentials/route.ts +++ b/apps/sim/app/api/credentials/route.ts @@ -29,7 +29,7 @@ import { ATLASSIAN_SERVICE_ACCOUNT_SECRET_TYPE, } from '@/lib/oauth/types' import { captureServerEvent } from '@/lib/posthog/server' -import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' +import { checkWorkspaceAccess, hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CredentialsAPI') @@ -296,8 +296,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } = parsed.data.body const workspaceAccess = await checkWorkspaceAccess(workspaceId, session.user.id) - if (!workspaceAccess.canWrite) { - return NextResponse.json({ error: 'Write permission required' }, { status: 403 }) + if (!workspaceAccess.hasAccess) { + return NextResponse.json({ error: 'Workspace access required' }, { status: 403 }) + } + + const isAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId) + if (!isAdmin) { + return NextResponse.json( + { error: 'Admin permission required to manage credentials' }, + { status: 403 } + ) } let resolvedDisplayName = displayName?.trim() ?? '' From 305e02ced7b61a05a83cb391a5efa29d782ea9d3 Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Fri, 22 May 2026 18:33:08 +0900 Subject: [PATCH 09/10] fix(credentials): grant admin role to credential creator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert admin-only restriction — write users can create secrets. Ensure the acting user (creator) always gets admin role on the credential via actingUserId parameter in ensureWorkspaceCredentialMemberships and session.user.id check in route.ts POST. Role mapping: - workspace owner → admin - credential creator (actingUserId/session.user.id) → admin - workspace admin permission → admin - write/read → member --- apps/sim/app/api/credentials/route.ts | 19 +++++++------------ apps/sim/lib/credentials/environment.ts | 12 ++++++++---- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index dc57ec0f472..f7858163f6e 100644 --- a/apps/sim/app/api/credentials/route.ts +++ b/apps/sim/app/api/credentials/route.ts @@ -29,7 +29,7 @@ import { ATLASSIAN_SERVICE_ACCOUNT_SECRET_TYPE, } from '@/lib/oauth/types' import { captureServerEvent } from '@/lib/posthog/server' -import { checkWorkspaceAccess, hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CredentialsAPI') @@ -296,16 +296,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } = parsed.data.body const workspaceAccess = await checkWorkspaceAccess(workspaceId, session.user.id) - if (!workspaceAccess.hasAccess) { - return NextResponse.json({ error: 'Workspace access required' }, { status: 403 }) - } - - const isAdmin = await hasWorkspaceAdminAccess(session.user.id, workspaceId) - if (!isAdmin) { - return NextResponse.json( - { error: 'Admin permission required to manage credentials' }, - { status: 403 } - ) + if (!workspaceAccess.canWrite) { + return NextResponse.json({ error: 'Write permission required' }, { status: 403 }) } let resolvedDisplayName = displayName?.trim() ?? '' @@ -557,7 +549,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (workspaceUserIds.length > 0) { for (const memberUserId of workspaceUserIds) { const wsPermission = wsPermissionByUser.get(memberUserId) - const isAdmin = memberUserId === workspaceRow.ownerId || wsPermission === 'admin' + const isAdmin = + memberUserId === workspaceRow.ownerId || + memberUserId === session.user.id || + wsPermission === 'admin' await tx.insert(credentialMember).values({ id: generateId(), credentialId, diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 986be0307f5..6327d622fd0 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -38,7 +38,8 @@ async function ensureWorkspaceCredentialMemberships( credentialId: string, memberUserIds: string[], ownerUserId: string, - wsPermissionByUser: Map + wsPermissionByUser: Map, + actingUserId?: string ) { if (!memberUserIds.length) return @@ -65,7 +66,8 @@ async function ensureWorkspaceCredentialMemberships( const now = new Date() const values = targetUserIds.map((memberUserId) => { const wsPermission = wsPermissionByUser.get(memberUserId) - const isAdmin = memberUserId === ownerUserId || wsPermission === 'admin' + const isAdmin = + memberUserId === ownerUserId || memberUserId === actingUserId || wsPermission === 'admin' return { id: generateId(), credentialId, @@ -173,7 +175,8 @@ export async function syncWorkspaceEnvCredentials(params: { credentialId, memberUserIds, workspaceRow.ownerId, - wsPermissionByUser + wsPermissionByUser, + actingUserId ) } @@ -255,7 +258,8 @@ export async function createWorkspaceEnvCredentials(params: { const membershipValues = createdIds.flatMap((credentialId) => memberUserIds.map((memberUserId) => { const wsPermission = wsPermissionByUser.get(memberUserId) - const isAdmin = memberUserId === ownerUserId || wsPermission === 'admin' + const isAdmin = + memberUserId === ownerUserId || memberUserId === actingUserId || wsPermission === 'admin' return { id: generateId(), credentialId, From 573ebd6f88c2d0018bfacfe5a05b9b2a2b1b0c31 Mon Sep 17 00:00:00 2001 From: "mini.jeong" Date: Fri, 22 May 2026 18:37:20 +0900 Subject: [PATCH 10/10] fix(credentials): remove actingUserId from ensureWorkspaceCredentialMemberships MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actingUserId is not always the credential creator — sync is called from member addition, permission changes, and invitation acceptance. Creator admin is only applied in route.ts POST (direct creation). Sync paths use pure workspace permission mapping. --- apps/sim/lib/credentials/environment.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts index 6327d622fd0..986be0307f5 100644 --- a/apps/sim/lib/credentials/environment.ts +++ b/apps/sim/lib/credentials/environment.ts @@ -38,8 +38,7 @@ async function ensureWorkspaceCredentialMemberships( credentialId: string, memberUserIds: string[], ownerUserId: string, - wsPermissionByUser: Map, - actingUserId?: string + wsPermissionByUser: Map ) { if (!memberUserIds.length) return @@ -66,8 +65,7 @@ async function ensureWorkspaceCredentialMemberships( const now = new Date() const values = targetUserIds.map((memberUserId) => { const wsPermission = wsPermissionByUser.get(memberUserId) - const isAdmin = - memberUserId === ownerUserId || memberUserId === actingUserId || wsPermission === 'admin' + const isAdmin = memberUserId === ownerUserId || wsPermission === 'admin' return { id: generateId(), credentialId, @@ -175,8 +173,7 @@ export async function syncWorkspaceEnvCredentials(params: { credentialId, memberUserIds, workspaceRow.ownerId, - wsPermissionByUser, - actingUserId + wsPermissionByUser ) } @@ -258,8 +255,7 @@ export async function createWorkspaceEnvCredentials(params: { const membershipValues = createdIds.flatMap((credentialId) => memberUserIds.map((memberUserId) => { const wsPermission = wsPermissionByUser.get(memberUserId) - const isAdmin = - memberUserId === ownerUserId || memberUserId === actingUserId || wsPermission === 'admin' + const isAdmin = memberUserId === ownerUserId || wsPermission === 'admin' return { id: generateId(), credentialId,