diff --git a/apps/sim/app/api/files/upload/route.test.ts b/apps/sim/app/api/files/upload/route.test.ts index bbc42c5e743..f6df57eb59d 100644 --- a/apps/sim/app/api/files/upload/route.test.ts +++ b/apps/sim/app/api/files/upload/route.test.ts @@ -562,16 +562,9 @@ describe('File Upload Security Tests', () => { return formData } - beforeEach(() => { - setupFileApiMocks({ - cloudEnabled: false, - storageProvider: 'local', - }) - }) - - it('rejects execution uploads without workspaceId', async () => { + const postExecutionUpload = async (workspaceId: string | null = 'test-workspace-id') => { const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }) - const formData = createExecutionFormData(file, null) + const formData = createExecutionFormData(file, workspaceId) const req = new Request('http://localhost/api/files/upload', { method: 'POST', @@ -579,7 +572,18 @@ describe('File Upload Security Tests', () => { body: formData, }) - const response = await POST(req as unknown as NextRequest) + return POST(req as unknown as NextRequest) + } + + beforeEach(() => { + setupFileApiMocks({ + cloudEnabled: false, + storageProvider: 'local', + }) + }) + + it('rejects execution uploads without workspaceId', async () => { + const response = await postExecutionUpload(null) expect(response.status).toBe(400) const data = await response.json() @@ -590,16 +594,7 @@ describe('File Upload Security Tests', () => { it('rejects execution uploads for a read-only workspace member', async () => { permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('read') - const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }) - const formData = createExecutionFormData(file) - - const req = new Request('http://localhost/api/files/upload', { - method: 'POST', - headers: { 'content-length': '1024' }, - body: formData, - }) - - const response = await POST(req as unknown as NextRequest) + const response = await postExecutionUpload() expect(response.status).toBe(403) const data = await response.json() @@ -610,16 +605,7 @@ describe('File Upload Security Tests', () => { it('rejects execution uploads for a member with no workspace permission', async () => { permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue(null) - const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }) - const formData = createExecutionFormData(file) - - const req = new Request('http://localhost/api/files/upload', { - method: 'POST', - headers: { 'content-length': '1024' }, - body: formData, - }) - - const response = await POST(req as unknown as NextRequest) + const response = await postExecutionUpload() expect(response.status).toBe(403) expect(mocks.mockUploadExecutionFile).not.toHaveBeenCalled() @@ -628,16 +614,7 @@ describe('File Upload Security Tests', () => { it('allows execution uploads for a write-permission workspace member', async () => { permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('write') - const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }) - const formData = createExecutionFormData(file) - - const req = new Request('http://localhost/api/files/upload', { - method: 'POST', - headers: { 'content-length': '1024' }, - body: formData, - }) - - const response = await POST(req as unknown as NextRequest) + const response = await postExecutionUpload() expect(response.status).toBe(200) expect(mocks.mockUploadExecutionFile).toHaveBeenCalledWith( @@ -656,16 +633,7 @@ describe('File Upload Security Tests', () => { it('allows execution uploads for an admin-permission workspace member', async () => { permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('admin') - const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' }) - const formData = createExecutionFormData(file) - - const req = new Request('http://localhost/api/files/upload', { - method: 'POST', - headers: { 'content-length': '1024' }, - body: formData, - }) - - const response = await POST(req as unknown as NextRequest) + const response = await postExecutionUpload() expect(response.status).toBe(200) expect(mocks.mockUploadExecutionFile).toHaveBeenCalled() diff --git a/apps/sim/app/api/files/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index a68fb9da045..802a6f1d9c4 100644 --- a/apps/sim/app/api/files/upload/route.ts +++ b/apps/sim/app/api/files/upload/route.ts @@ -92,6 +92,29 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const usingCloudStorage = storageService.hasCloudStorage() logger.info(`Using storage mode: ${usingCloudStorage ? 'Cloud' : 'Local'} for file upload`) + // Execution context requires a workspace write/admin permission check. Resolve it once per + // request (not per file) since workspaceId is invariant across all files in the upload. + let executionUploadContext: + | { workspaceId: string; workflowId: string; executionId: string } + | undefined + if (context === 'execution') { + if (!workflowId || !executionId || !workspaceId) { + throw new InvalidRequestError( + 'Execution context requires workflowId, executionId, and workspaceId parameters' + ) + } + + const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) + if (permission !== 'write' && permission !== 'admin') { + return NextResponse.json( + { error: 'Write or Admin access required for execution uploads' }, + { status: 403 } + ) + } + + executionUploadContext = { workspaceId, workflowId, executionId } + } + const uploadResults = [] for (const file of files) { @@ -110,28 +133,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) // Handle execution context - if (context === 'execution') { - if (!workflowId || !executionId || !workspaceId) { - throw new InvalidRequestError( - 'Execution context requires workflowId, executionId, and workspaceId parameters' - ) - } - - const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId) - if (permission !== 'write' && permission !== 'admin') { - return NextResponse.json( - { error: 'Write or Admin access required for execution uploads' }, - { status: 403 } - ) - } - + if (context === 'execution' && executionUploadContext) { const { uploadExecutionFile } = await import('@/lib/uploads/contexts/execution') const userFile = await uploadExecutionFile( - { - workspaceId, - workflowId, - executionId, - }, + executionUploadContext, buffer, originalName, file.type,