Skip to content

Commit 48f520b

Browse files
authored
feat(files): added file manager table, enforce permissions for viewing files (#1766)
* feat(files): added file manager table, enforce permissions for viewing files * rename types * cleanup * cleanup * confirm local file system works with all contexts * clean * remove isAsync * ignore expiresAt * add relative imports instead of absolute ones * absl imports * remove redundant comments
1 parent da30c25 commit 48f520b

File tree

75 files changed

+10392
-1606
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

75 files changed

+10392
-1606
lines changed

apps/sim/app/api/__test-utils__/utils.ts

Lines changed: 50 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -911,49 +911,44 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions =
911911
},
912912
}))
913913

914-
vi.doMock('@/lib/uploads/core/setup', () => ({
914+
vi.doMock('@/lib/uploads/config', () => ({
915915
USE_S3_STORAGE: provider === 's3',
916916
USE_BLOB_STORAGE: provider === 'blob',
917917
USE_LOCAL_STORAGE: provider === 'local',
918918
getStorageProvider: vi.fn().mockReturnValue(provider),
919+
S3_CONFIG: {
920+
bucket: 'test-s3-bucket',
921+
region: 'us-east-1',
922+
},
923+
S3_KB_CONFIG: {
924+
bucket: 'test-s3-kb-bucket',
925+
region: 'us-east-1',
926+
},
927+
S3_CHAT_CONFIG: {
928+
bucket: 'test-s3-chat-bucket',
929+
region: 'us-east-1',
930+
},
931+
BLOB_CONFIG: {
932+
accountName: 'testaccount',
933+
accountKey: 'testkey',
934+
containerName: 'test-container',
935+
},
936+
BLOB_KB_CONFIG: {
937+
accountName: 'testaccount',
938+
accountKey: 'testkey',
939+
containerName: 'test-kb-container',
940+
},
941+
BLOB_CHAT_CONFIG: {
942+
accountName: 'testaccount',
943+
accountKey: 'testkey',
944+
containerName: 'test-chat-container',
945+
},
919946
}))
920947

921948
if (provider === 's3') {
922-
vi.doMock('@/lib/uploads/s3/s3-client', () => ({
949+
vi.doMock('@/lib/uploads/providers/s3/client', () => ({
923950
getS3Client: vi.fn().mockReturnValue({}),
924-
sanitizeFilenameForMetadata: vi.fn((filename) => filename),
925-
}))
926-
927-
vi.doMock('@/lib/uploads/setup', () => ({
928-
S3_CONFIG: {
929-
bucket: 'test-s3-bucket',
930-
region: 'us-east-1',
931-
},
932-
S3_KB_CONFIG: {
933-
bucket: 'test-s3-kb-bucket',
934-
region: 'us-east-1',
935-
},
936-
S3_CHAT_CONFIG: {
937-
bucket: 'test-s3-chat-bucket',
938-
region: 'us-east-1',
939-
},
940-
BLOB_CONFIG: {
941-
accountName: 'testaccount',
942-
accountKey: 'testkey',
943-
containerName: 'test-container',
944-
},
945-
BLOB_KB_CONFIG: {
946-
accountName: 'testaccount',
947-
accountKey: 'testkey',
948-
containerName: 'test-kb-container',
949-
},
950-
BLOB_CHAT_CONFIG: {
951-
accountName: 'testaccount',
952-
accountKey: 'testkey',
953-
containerName: 'test-chat-container',
954-
},
955951
}))
956-
957952
vi.doMock('@aws-sdk/client-s3', () => ({
958953
PutObjectCommand: vi.fn(),
959954
}))
@@ -983,29 +978,9 @@ export function createStorageProviderMocks(options: StorageProviderMockOptions =
983978
}),
984979
}
985980

986-
vi.doMock('@/lib/uploads/blob/blob-client', () => ({
981+
vi.doMock('@/lib/uploads/providers/blob/client', () => ({
987982
getBlobServiceClient: vi.fn().mockReturnValue(mockBlobServiceClient),
988-
sanitizeFilenameForMetadata: vi.fn((filename) => filename),
989-
}))
990-
991-
vi.doMock('@/lib/uploads/setup', () => ({
992-
BLOB_CONFIG: {
993-
accountName: 'testaccount',
994-
accountKey: 'testkey',
995-
containerName: 'test-container',
996-
},
997-
BLOB_KB_CONFIG: {
998-
accountName: 'testaccount',
999-
accountKey: 'testkey',
1000-
containerName: 'test-kb-container',
1001-
},
1002-
BLOB_CHAT_CONFIG: {
1003-
accountName: 'testaccount',
1004-
accountKey: 'testkey',
1005-
containerName: 'test-chat-container',
1006-
},
1007983
}))
1008-
1009984
vi.doMock('@azure/storage-blob', () => ({
1010985
BlobSASPermissions: {
1011986
parse: vi.fn(() => 'w'),
@@ -1355,6 +1330,25 @@ export function setupFileApiMocks(
13551330
authMocks.setUnauthenticated()
13561331
}
13571332

1333+
vi.doMock('@/lib/auth/hybrid', () => ({
1334+
checkHybridAuth: vi.fn().mockResolvedValue({
1335+
success: authenticated,
1336+
userId: authenticated ? 'test-user-id' : undefined,
1337+
error: authenticated ? undefined : 'Unauthorized',
1338+
}),
1339+
}))
1340+
1341+
vi.doMock('@/app/api/files/authorization', () => ({
1342+
verifyFileAccess: vi.fn().mockResolvedValue(true),
1343+
verifyWorkspaceFileAccess: vi.fn().mockResolvedValue(true),
1344+
verifyKBFileAccess: vi.fn().mockResolvedValue(true),
1345+
verifyCopilotFileAccess: vi.fn().mockResolvedValue(true),
1346+
lookupWorkspaceFileByKey: vi.fn().mockResolvedValue({
1347+
workspaceId: 'test-workspace-id',
1348+
uploadedBy: 'test-user-id',
1349+
}),
1350+
}))
1351+
13581352
mockFileSystem({
13591353
writeFileSuccess: true,
13601354
readFileContent: 'test content',
@@ -1510,11 +1504,10 @@ export function mockUploadUtils(
15101504
isUsingCloudStorage: vi.fn().mockReturnValue(isCloudStorage),
15111505
}))
15121506

1513-
vi.doMock('@/lib/uploads/setup', () => ({
1507+
vi.doMock('@/lib/uploads/config', () => ({
15141508
UPLOAD_DIR: '/test/uploads',
15151509
USE_S3_STORAGE: isCloudStorage,
15161510
USE_BLOB_STORAGE: false,
1517-
ensureUploadsDirectory: vi.fn().mockResolvedValue(true),
15181511
S3_CONFIG: {
15191512
bucket: 'test-bucket',
15201513
region: 'test-region',

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ const logger = createLogger('ChatIdentifierAPI')
1818
export const dynamic = 'force-dynamic'
1919
export const runtime = 'nodejs'
2020

21-
// This endpoint handles chat interactions via the identifier
2221
export async function POST(
2322
request: NextRequest,
2423
{ params }: { params: Promise<{ identifier: string }> }
@@ -29,15 +28,13 @@ export async function POST(
2928
try {
3029
logger.debug(`[${requestId}] Processing chat request for identifier: ${identifier}`)
3130

32-
// Parse the request body once
3331
let parsedBody
3432
try {
3533
parsedBody = await request.json()
3634
} catch (_error) {
3735
return addCorsHeaders(createErrorResponse('Invalid request body', 400), request)
3836
}
3937

40-
// Find the chat deployment for this identifier
4138
const deploymentResult = await db
4239
.select({
4340
id: chat.id,
@@ -60,13 +57,11 @@ export async function POST(
6057

6158
const deployment = deploymentResult[0]
6259

63-
// Check if the chat is active
6460
if (!deployment.isActive) {
6561
logger.warn(`[${requestId}] Chat is not active: ${identifier}`)
6662
return addCorsHeaders(createErrorResponse('This chat is currently unavailable', 403), request)
6763
}
6864

69-
// Validate authentication with the parsed body
7065
const authResult = await validateChatAuth(requestId, deployment, request, parsedBody)
7166
if (!authResult.authorized) {
7267
return addCorsHeaders(
@@ -75,26 +70,20 @@ export async function POST(
7570
)
7671
}
7772

78-
// Use the already parsed body
7973
const { input, password, email, conversationId, files } = parsedBody
8074

81-
// If this is an authentication request (has password or email but no input),
82-
// set auth cookie and return success
8375
if ((password || email) && !input) {
8476
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
8577

86-
// Set authentication cookie
8778
setChatAuthCookie(response, deployment.id, deployment.authType)
8879

8980
return response
9081
}
9182

92-
// For chat messages, create regular response (allow empty input if files are present)
9383
if (!input && (!files || files.length === 0)) {
9484
return addCorsHeaders(createErrorResponse('No input provided', 400), request)
9585
}
9686

97-
// Get the workflow and workspace owner for this chat
9887
const workflowResult = await db
9988
.select({
10089
isDeployed: workflow.isDeployed,
@@ -141,20 +130,22 @@ export async function POST(
141130
const { SSE_HEADERS } = await import('@/lib/utils')
142131
const { createFilteredResult } = await import('@/app/api/workflows/[id]/execute/route')
143132

144-
// Generate executionId early so it can be used for file uploads and workflow execution
145133
const executionId = crypto.randomUUID()
146134

147135
const workflowInput: any = { input, conversationId }
148136
if (files && Array.isArray(files) && files.length > 0) {
149-
logger.debug(`[${requestId}] Processing ${files.length} attached files`)
150-
151137
const executionContext = {
152-
workspaceId: deployment.userId,
138+
workspaceId: workflowResult[0].workspaceId || '',
153139
workflowId: deployment.workflowId,
154140
executionId,
155141
}
156142

157-
const uploadedFiles = await ChatFiles.processChatFiles(files, executionContext, requestId)
143+
const uploadedFiles = await ChatFiles.processChatFiles(
144+
files,
145+
executionContext,
146+
requestId,
147+
deployment.userId
148+
)
158149

159150
if (uploadedFiles.length > 0) {
160151
workflowInput.files = uploadedFiles
@@ -205,7 +196,6 @@ export async function POST(
205196
}
206197
}
207198

208-
// This endpoint returns information about the chat
209199
export async function GET(
210200
request: NextRequest,
211201
{ params }: { params: Promise<{ identifier: string }> }
@@ -216,7 +206,6 @@ export async function GET(
216206
try {
217207
logger.debug(`[${requestId}] Fetching chat info for identifier: ${identifier}`)
218208

219-
// Find the chat deployment for this identifier
220209
const deploymentResult = await db
221210
.select({
222211
id: chat.id,
@@ -241,13 +230,11 @@ export async function GET(
241230

242231
const deployment = deploymentResult[0]
243232

244-
// Check if the chat is active
245233
if (!deployment.isActive) {
246234
logger.warn(`[${requestId}] Chat is not active: ${identifier}`)
247235
return addCorsHeaders(createErrorResponse('This chat is currently unavailable', 403), request)
248236
}
249237

250-
// Check for auth cookie first
251238
const cookieName = `chat_auth_${deployment.id}`
252239
const authCookie = request.cookies.get(cookieName)
253240

@@ -256,7 +243,6 @@ export async function GET(
256243
authCookie &&
257244
validateAuthToken(authCookie.value, deployment.id)
258245
) {
259-
// Cookie valid, return chat info
260246
return addCorsHeaders(
261247
createSuccessResponse({
262248
id: deployment.id,
@@ -270,7 +256,6 @@ export async function GET(
270256
)
271257
}
272258

273-
// If no valid cookie, proceed with standard auth check
274259
const authResult = await validateChatAuth(requestId, deployment, request)
275260
if (!authResult.authorized) {
276261
logger.info(
@@ -282,7 +267,6 @@ export async function GET(
282267
)
283268
}
284269

285-
// Return public information about the chat including auth type
286270
return addCorsHeaders(
287271
createSuccessResponse({
288272
id: deployment.id,

0 commit comments

Comments
 (0)