Skip to content

Commit 4c6c727

Browse files
authored
fix: permissions check for duplicating workflow (#706)
1 parent 55a9adf commit 4c6c727

File tree

1 file changed

+47
-15
lines changed
  • apps/sim/app/api/workflows/[id]/duplicate

1 file changed

+47
-15
lines changed

apps/sim/app/api/workflows/[id]/duplicate/route.ts

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import crypto from 'crypto'
2-
import { and, eq } from 'drizzle-orm'
2+
import { eq } from 'drizzle-orm'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
55
import { getSession } from '@/lib/auth'
66
import { createLogger } from '@/lib/logs/console-logger'
7+
import { getUserEntityPermissions } from '@/lib/permissions/utils'
78
import { db } from '@/db'
89
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
910
import type { LoopConfig, ParallelConfig, WorkflowState } from '@/stores/workflows/workflow/types'
@@ -24,15 +25,13 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
2425
const requestId = crypto.randomUUID().slice(0, 8)
2526
const startTime = Date.now()
2627

27-
try {
28-
const session = await getSession()
29-
if (!session?.user?.id) {
30-
logger.warn(
31-
`[${requestId}] Unauthorized workflow duplication attempt for ${sourceWorkflowId}`
32-
)
33-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
34-
}
28+
const session = await getSession()
29+
if (!session?.user?.id) {
30+
logger.warn(`[${requestId}] Unauthorized workflow duplication attempt for ${sourceWorkflowId}`)
31+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
32+
}
3533

34+
try {
3635
const body = await req.json()
3736
const { name, description, color, workspaceId, folderId } = DuplicateRequestSchema.parse(body)
3837

@@ -46,19 +45,43 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
4645

4746
// Duplicate workflow and all related data in a transaction
4847
const result = await db.transaction(async (tx) => {
49-
// First verify the source workflow exists and user has access
48+
// First verify the source workflow exists
5049
const sourceWorkflow = await tx
5150
.select()
5251
.from(workflow)
53-
.where(and(eq(workflow.id, sourceWorkflowId), eq(workflow.userId, session.user.id)))
52+
.where(eq(workflow.id, sourceWorkflowId))
5453
.limit(1)
5554

5655
if (sourceWorkflow.length === 0) {
57-
throw new Error('Source workflow not found or access denied')
56+
throw new Error('Source workflow not found')
5857
}
5958

6059
const source = sourceWorkflow[0]
6160

61+
// Check if user has permission to access the source workflow
62+
let canAccessSource = false
63+
64+
// Case 1: User owns the workflow
65+
if (source.userId === session.user.id) {
66+
canAccessSource = true
67+
}
68+
69+
// Case 2: User has admin or write permission in the source workspace
70+
if (!canAccessSource && source.workspaceId) {
71+
const userPermission = await getUserEntityPermissions(
72+
session.user.id,
73+
'workspace',
74+
source.workspaceId
75+
)
76+
if (userPermission === 'admin' || userPermission === 'write') {
77+
canAccessSource = true
78+
}
79+
}
80+
81+
if (!canAccessSource) {
82+
throw new Error('Source workflow not found or access denied')
83+
}
84+
6285
// Create the new workflow first (required for foreign key constraints)
6386
await tx.insert(workflow).values({
6487
id: newWorkflowId,
@@ -346,9 +369,18 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
346369

347370
return NextResponse.json(result, { status: 201 })
348371
} catch (error) {
349-
if (error instanceof Error && error.message === 'Source workflow not found or access denied') {
350-
logger.warn(`[${requestId}] Source workflow ${sourceWorkflowId} not found or access denied`)
351-
return NextResponse.json({ error: 'Source workflow not found' }, { status: 404 })
372+
if (error instanceof Error) {
373+
if (error.message === 'Source workflow not found') {
374+
logger.warn(`[${requestId}] Source workflow ${sourceWorkflowId} not found`)
375+
return NextResponse.json({ error: 'Source workflow not found' }, { status: 404 })
376+
}
377+
378+
if (error.message === 'Source workflow not found or access denied') {
379+
logger.warn(
380+
`[${requestId}] User ${session.user.id} denied access to source workflow ${sourceWorkflowId}`
381+
)
382+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
383+
}
352384
}
353385

354386
if (error instanceof z.ZodError) {

0 commit comments

Comments
 (0)