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
14 changes: 9 additions & 5 deletions apps/sim/app/api/files/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ import { isUuid } from '@/executor/constants'

const logger = createLogger('FileAuthorization')

/** Thrown by utility functions when file access is denied, so route handlers can return 404. */
export class FileAccessDeniedError extends Error {
constructor() {
super('File not found')
this.name = 'FileAccessDeniedError'
}
}

interface AuthorizationResult {
granted: boolean
reason: string
Expand Down Expand Up @@ -598,18 +606,14 @@ async function authorizeFileAccess(
*/
export async function assertToolFileAccess(
key: unknown,
userId: string | undefined,
userId: string,
requestId: string,
routeLogger: ReturnType<typeof createLogger>
): Promise<NextResponse | null> {
if (typeof key !== 'string' || key.length === 0) {
routeLogger.warn(`[${requestId}] File access check rejected: missing key`)
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
}
if (!userId) {
routeLogger.warn(`[${requestId}] File access check requires userId but none available`)
return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 })
}
const hasAccess = await verifyFileAccess(key, userId)
if (!hasAccess) {
routeLogger.warn(`[${requestId}] File access denied for user`, { userId, key })
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/tools/agiloft/attach/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { RawFileInput } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { agiloftLogin, agiloftLogout, buildAttachFileUrl } from '@/tools/agiloft/utils'

export const dynamic = 'force-dynamic'
Expand All @@ -22,7 +23,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })

if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Agiloft attach attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
Expand Down Expand Up @@ -66,6 +67,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
`[${requestId}] Downloading file for Agiloft attach: ${userFile.name} (${userFile.size} bytes)`
)

const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
const resolvedFileName = data.fileName || userFile.name || 'attachment'

Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/tools/box/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'

export const dynamic = 'force-dynamic'

Expand All @@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })

if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Box upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
Expand Down Expand Up @@ -49,6 +50,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const userFile = userFiles[0]
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)

const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
fileName = validatedData.fileName || userFile.name
} else if (validatedData.fileContent) {
Expand Down
9 changes: 9 additions & 0 deletions apps/sim/app/api/tools/confluence/upload-attachment/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processSingleFileToUserFile, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import { getConfluenceCloudId } from '@/tools/confluence/utils'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'

Expand Down Expand Up @@ -80,6 +81,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}

const denied = await assertToolFileAccess(
userFile.key,
auth.userId,
'confluence-upload',
logger
)
if (denied) return denied

let fileBuffer: Buffer
try {
fileBuffer = await downloadFileFromStorage(userFile, 'confluence-upload', logger)
Expand Down
35 changes: 25 additions & 10 deletions apps/sim/app/api/tools/discord/send-message/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'

export const dynamic = 'force-dynamic'

Expand All @@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })

if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)
return NextResponse.json(
{
Expand All @@ -30,8 +31,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}

const userId = authResult.userId
logger.info(`[${requestId}] Authenticated Discord send request via ${authResult.authType}`, {
userId: authResult.userId,
userId,
})

const parsed = await parseRequest(discordSendMessageContract, request, {})
Expand Down Expand Up @@ -134,17 +136,30 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}
formData.append('payload_json', JSON.stringify(payload))

const downloadedFiles = await Promise.all(
userFiles.map(async (userFile, i) => {
logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`)
const buffer = await downloadFileFromStorage(userFile, requestId, logger)
logger.info(`[${requestId}] Added file ${i}: ${userFile.name} (${buffer.length} bytes)`)
return { userFile, buffer }
const accessResults = await Promise.all(
userFiles.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied

const buffers = await Promise.all(
userFiles.map(async (file, i) => {
try {
logger.info(`[${requestId}] Downloading file ${i}: ${file.name}`)
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
`Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}`
)
}
})
)

for (let i = 0; i < downloadedFiles.length; i++) {
const { userFile, buffer } = downloadedFiles[i]
for (let i = 0; i < userFiles.length; i++) {
const userFile = userFiles[i]
const buffer = buffers[i]
logger.info(`[${requestId}] Added file ${i}: ${userFile.name} (${buffer.length} bytes)`)
filesOutput.push({
name: userFile.name,
mimeType: userFile.type || 'application/octet-stream',
Expand Down
10 changes: 7 additions & 3 deletions apps/sim/app/api/tools/docusign/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'

const logger = createLogger('DocuSignAPI')

Expand Down Expand Up @@ -54,7 +55,7 @@ async function resolveAccount(accessToken: string): Promise<DocuSignAccountInfo>

export const POST = withRouteHandler(async (request: NextRequest) => {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}

Expand Down Expand Up @@ -84,7 +85,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {

switch (operation) {
case 'send_envelope':
return await handleSendEnvelope(apiBase, headers, params)
return await handleSendEnvelope(apiBase, headers, params, authResult.userId)
case 'create_from_template':
return await handleCreateFromTemplate(apiBase, headers, params)
case 'get_envelope':
Expand Down Expand Up @@ -115,7 +116,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
async function handleSendEnvelope(
apiBase: string,
headers: Record<string, string>,
params: Record<string, unknown>
params: Record<string, unknown>,
userId: string
) {
const { signerEmail, signerName, emailSubject, emailBody, ccEmail, ccName, file, status } = params

Expand All @@ -135,6 +137,8 @@ async function handleSendEnvelope(
const userFiles = processFilesToUserFiles([parsed as RawFileInput], 'docusign-send', logger)
if (userFiles.length > 0) {
const userFile = userFiles[0]
const denied = await assertToolFileAccess(userFile.key, userId, 'docusign-send', logger)
if (denied) return denied
const buffer = await downloadFileFromStorage(userFile, 'docusign-send', logger)
documentBase64 = buffer.toString('base64')
documentName = userFile.name
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/tools/dropbox/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { httpHeaderSafeJson } from '@/lib/core/utils/validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'

export const dynamic = 'force-dynamic'

Expand All @@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })

if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Dropbox upload attempt: ${authResult.error}`)
return NextResponse.json(
{ success: false, error: authResult.error || 'Authentication required' },
Expand Down Expand Up @@ -52,6 +53,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const userFile = userFiles[0]
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)

const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
fileName = userFile.name
} else if (validatedData.fileContent) {
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/api/tools/firecrawl/parse/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'

export const dynamic = 'force-dynamic'

Expand Down Expand Up @@ -43,6 +44,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
size: userFile.size,
})

const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied

const buffer = await downloadFileFromStorage(userFile, requestId, logger)

const formData = new FormData()
Expand Down
29 changes: 18 additions & 11 deletions apps/sim/app/api/tools/gmail/draft/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { assertToolFileAccess } from '@/app/api/files/authorization'
import {
base64UrlEncode,
buildMimeMessage,
Expand All @@ -26,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
try {
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })

if (!authResult.success) {
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)
return NextResponse.json(
{
Expand All @@ -37,8 +38,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}

const userId = authResult.userId
logger.info(`[${requestId}] Authenticated Gmail draft request via ${authResult.authType}`, {
userId: authResult.userId,
userId,
})

const parsed = await parseRequest(gmailDraftContract, request, {})
Expand Down Expand Up @@ -85,20 +87,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}

const attachmentBuffers = await Promise.all(
const accessResults = await Promise.all(
attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger))
)
const denied = accessResults.find((r) => r !== null)
if (denied) return denied

const buffers = await Promise.all(
attachments.map(async (file) => {
try {
logger.info(
`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`
)

const buffer = await downloadFileFromStorage(file, requestId, logger)

return {
filename: file.name,
mimeType: file.type || 'application/octet-stream',
content: buffer,
}
return await downloadFileFromStorage(file, requestId, logger)
} catch (error) {
logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error)
throw new Error(
Expand All @@ -108,6 +109,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
)

const attachmentBuffers = attachments.map((file, i) => ({
filename: file.name,
mimeType: file.type || 'application/octet-stream',
content: buffers[i],
}))

const mimeMessage = buildMimeMessage({
to: validatedData.to,
cc: validatedData.cc ?? undefined,
Expand Down
Loading
Loading