Skip to content
Closed
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
97 changes: 90 additions & 7 deletions apps/sim/app/api/files/upload/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ function setupFileApiMocks(
})
}

/**
* Build a multipart upload request shared across test suites in this file
*/
function createUploadRequest(formData: FormData): NextRequest {
return new NextRequest('http://localhost:3000/api/files/upload', {
method: 'POST',
headers: { 'content-length': '1024' },
body: formData,
})
}

describe('File Upload API Route', () => {
const createMockFormData = (files: File[], context = 'workspace'): FormData => {
const formData = new FormData()
Expand All @@ -187,19 +198,13 @@ describe('File Upload API Route', () => {
return new File([content], name, { type })
}

const createUploadRequest = (formData: FormData): NextRequest =>
new NextRequest('http://localhost:3000/api/files/upload', {
method: 'POST',
headers: { 'content-length': '1024' },
body: formData,
})

beforeEach(() => {
vi.clearAllMocks()
})

afterEach(() => {
vi.clearAllMocks()
vi.unstubAllGlobals()
})

it('should upload a file to local storage', async () => {
Expand Down Expand Up @@ -379,6 +384,7 @@ describe('File Upload Security Tests', () => {

afterEach(() => {
vi.clearAllMocks()
vi.unstubAllGlobals()
})

describe('File Extension Validation', () => {
Expand Down Expand Up @@ -552,4 +558,81 @@ describe('File Upload Security Tests', () => {
expect(data.error).toBe('Unauthorized')
})
})

describe('Execution Context Authorization', () => {
const createExecutionFormData = (workspaceId?: string): FormData => {
const formData = new FormData()
const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' })
formData.append('file', file)
formData.append('context', 'execution')
formData.append('workflowId', 'test-workflow-id')
formData.append('executionId', 'test-execution-id')
if (workspaceId !== undefined) {
formData.append('workspaceId', workspaceId)
}
return formData
}

beforeEach(() => {
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'test-user-id' } })
storageServiceMockFns.mockHasCloudStorage.mockReturnValue(false)
storageServiceMockFns.mockUploadFile.mockResolvedValue({
key: 'execution/test-workspace-id/test-workflow-id/test-execution-id/test.pdf',
path: '/test/path',
})
storageServiceMockFns.mockGeneratePresignedDownloadUrl.mockResolvedValue(
'https://example.com/signed'
)
})

it('rejects execution uploads when the caller lacks write or admin access', async () => {
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('read')

const req = createUploadRequest(createExecutionFormData('test-workspace-id'))
const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(403)
expect(data.error).toBe('Write or Admin access required for execution uploads')
expect(storageServiceMockFns.mockUploadFile).not.toHaveBeenCalled()
})

it('allows execution uploads when the caller has write access', async () => {
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('write')

const req = createUploadRequest(createExecutionFormData('test-workspace-id'))
const response = await POST(req)

expect(response.status).toBe(200)
expect(permissionsMockFns.mockGetUserEntityPermissions).toHaveBeenCalledWith(
'test-user-id',
'workspace',
'test-workspace-id'
)
expect(storageServiceMockFns.mockUploadFile).toHaveBeenCalled()
})

it('allows execution uploads when the caller has admin access', async () => {
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('admin')

const req = createUploadRequest(createExecutionFormData('test-workspace-id'))
const response = await POST(req)

expect(response.status).toBe(200)
expect(storageServiceMockFns.mockUploadFile).toHaveBeenCalled()
})

it('rejects execution uploads missing workspaceId instead of defaulting it', async () => {
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('admin')

const req = createUploadRequest(createExecutionFormData(undefined))
const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(400)
expect(data.message).toContain('workspaceId')
expect(permissionsMockFns.mockGetUserEntityPermissions).not.toHaveBeenCalled()
expect(storageServiceMockFns.mockUploadFile).not.toHaveBeenCalled()
})
})
})
14 changes: 11 additions & 3 deletions apps/sim/app/api/files/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,24 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

// Handle execution context
if (context === 'execution') {
if (!workflowId || !executionId) {
if (!workflowId || !executionId || !workspaceId) {
throw new InvalidRequestError(
'Execution context requires workflowId and executionId parameters'
'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 }
)
}

const { uploadExecutionFile } = await import('@/lib/uploads/contexts/execution')
const userFile = await uploadExecutionFile(
{
workspaceId: workspaceId || '',
workspaceId,
workflowId,
executionId,
},
Expand Down
Loading