Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions apps/sim/app/api/credentials/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

const now = new Date()
const credentialId = generateId()
const { ownerId: workspaceOwnerId, memberUserIds: workspaceMemberUserIds } =
const { ownerId: workspaceOwnerId, memberUserIds: workspaceMemberUserIds, adminUserIds: workspaceAdminUserIds } =
await getWorkspaceMembership(workspaceId)

await db.transaction(async (tx) => {
Expand Down Expand Up @@ -543,12 +543,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
if ((type === 'env_workspace' || type === 'service_account') && workspaceOwnerId) {
if (workspaceMemberUserIds.length > 0) {
for (const memberUserId of workspaceMemberUserIds) {
const isAdmin = memberUserId === session.user.id
await tx.insert(credentialMember).values({
id: generateId(),
credentialId,
userId: memberUserId,
role: isAdmin ? 'admin' : 'member',
role: workspaceAdminUserIds.has(memberUserId) ? 'admin' : 'member',
status: 'active',
joinedAt: now,
invitedBy: session.user.id,
Expand Down
23 changes: 15 additions & 8 deletions apps/sim/lib/credentials/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface WorkspaceMembership {
ownerId: string | null
/** All workspace members: the owner plus everyone with a workspace permission. */
memberUserIds: string[]
/** Subset of memberUserIds with admin-level workspace permission (owner + explicit admins). */
adminUserIds: Set<string>
}

/**
Expand All @@ -23,18 +25,22 @@ export async function getWorkspaceMembership(workspaceId: string): Promise<Works
.where(eq(workspace.id, workspaceId))
.limit(1),
db
.select({ userId: permissions.userId })
.select({ userId: permissions.userId, permissionType: permissions.permissionType })
.from(permissions)
.where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))),
])

const ownerId = workspaceRows[0]?.ownerId ?? null
const memberUserIds = new Set<string>(permissionRows.map((row) => row.userId))
const adminUserIds = new Set<string>(
permissionRows.filter((row) => row.permissionType === 'admin').map((row) => row.userId)
)
if (ownerId) {
memberUserIds.add(ownerId)
adminUserIds.add(ownerId)
}

return { ownerId, memberUserIds: Array.from(memberUserIds) }
return { ownerId, memberUserIds: Array.from(memberUserIds), adminUserIds }
}

export interface WorkspaceEnvKeyAdminAccess {
Expand Down Expand Up @@ -122,7 +128,8 @@ export async function getUserWorkspaceIds(userId: string): Promise<string[]> {
async function ensureWorkspaceCredentialMemberships(
credentialId: string,
memberUserIds: string[],
invitedBy: string
invitedBy: string,
adminUserIds: Set<string>
) {
if (!memberUserIds.length) return

Expand Down Expand Up @@ -151,7 +158,7 @@ async function ensureWorkspaceCredentialMemberships(
id: generateId(),
credentialId,
userId: memberUserId,
role: 'member' as const,
role: (adminUserIds.has(memberUserId) ? 'admin' : 'member') as 'admin' | 'member',
status: 'active' as const,
joinedAt: now,
invitedBy,
Expand Down Expand Up @@ -180,7 +187,7 @@ export async function syncWorkspaceEnvCredentials(params: {
actingUserId: string
}) {
const { workspaceId, envKeys, actingUserId } = params
const { ownerId, memberUserIds } = await getWorkspaceMembership(workspaceId)
const { ownerId, memberUserIds, adminUserIds } = await getWorkspaceMembership(workspaceId)

if (!ownerId) return

Expand Down Expand Up @@ -231,7 +238,7 @@ export async function syncWorkspaceEnvCredentials(params: {
}

for (const credentialId of credentialIdsToEnsureMembership) {
await ensureWorkspaceCredentialMemberships(credentialId, memberUserIds, ownerId)
await ensureWorkspaceCredentialMemberships(credentialId, memberUserIds, ownerId, adminUserIds)
}

if (normalizedKeys.length > 0) {
Expand Down Expand Up @@ -265,7 +272,7 @@ export async function createWorkspaceEnvCredentials(params: {
const keys = Array.from(new Set(newKeys.filter(Boolean)))
if (keys.length === 0) return

const { ownerId, memberUserIds } = await getWorkspaceMembership(workspaceId)
const { ownerId, memberUserIds, adminUserIds } = await getWorkspaceMembership(workspaceId)

if (!ownerId) return

Expand Down Expand Up @@ -297,7 +304,7 @@ export async function createWorkspaceEnvCredentials(params: {
id: generateId(),
credentialId,
userId: memberUserId,
role: (memberUserId === actingUserId ? 'admin' : 'member') as 'admin' | 'member',
role: (adminUserIds.has(memberUserId) ? 'admin' : 'member') as 'admin' | 'member',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write creator loses credential admin

Medium Severity

Replacing the acting-user check with only adminUserIds stops workspace Write members from receiving credential_member admin when they create shared secrets. Env PUT/DELETE treats existing keys as editable only when workspace permission is admin or the user has per-key credential admin, so those creators can add secrets but later edits or deletes are denied.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 999bc60. Configure here.

status: 'active' as const,
joinedAt: now,
invitedBy: actingUserId,
Expand Down