Skip to content
Merged
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
68 changes: 18 additions & 50 deletions apps/sim/app/api/files/upload/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,24 +562,28 @@ 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',
headers: { 'content-length': '1024' },
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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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(
Expand All @@ -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()
Expand Down
45 changes: 25 additions & 20 deletions apps/sim/app/api/files/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
Loading