Skip to content
Open
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
34 changes: 34 additions & 0 deletions apps/sim/app/api/files/serve/[...path]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { hybridAuthMockFns, storageServiceMock, storageServiceMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createTestRuntimePrincipal } from '@/lib/auth/runtime-principal.test-support'
import { MAX_BUFFERED_TRANSFER_BYTES } from '@/lib/uploads/shared/types'

vi.mock('@sim/logger', () => ({
Expand Down Expand Up @@ -370,6 +371,39 @@ describe('File Serve API Route', () => {
expect(mockVerifyFileAccess).not.toHaveBeenCalled()
})

it('serves an actorless workflow file without synthesizing a user owner', async () => {
const principal = createTestRuntimePrincipal({
principal: {
kind: 'system',
serviceId: 'schedule',
workspaceId: 'test-workspace-id',
workflowId: 'workflow-1',
},
})
mockResolveStoredFileContext.mockResolvedValue('workspace')
mockParseWorkspaceFileKey.mockReturnValue('test-workspace-id')
mockAuthenticateWorkspaceFile.mockResolvedValue(principal)

const response = await GET(
new NextRequest(
'http://localhost:3000/api/files/serve/workspace/test-workspace-id/report.pdf'
),
{
params: Promise.resolve({
path: ['workspace', 'test-workspace-id', 'report.pdf'],
}),
}
)

expect(response.status).toBe(200)
expect(mockResolveServableDocBytes).toHaveBeenCalledWith(
expect.objectContaining({
filePrincipal: principal,
ownerKey: 'workspace:test-workspace-id',
})
)
})

it('serves a mothership chat attachment stored under a workspace key', async () => {
/**
* The attachment shares the `workspace/…` prefix but is recorded as
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/files/serve/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Principal, requirePrincipalSubjectUserId } from '@sim/auth/principal'
import { type Principal, resolvePrincipalSubjectUserId } from '@sim/auth/principal'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import type { NextRequest } from 'next/server'
Expand Down Expand Up @@ -328,7 +328,8 @@ async function handleWorkspaceFile(
input: { key, assertedWorkspaceId: workspaceId },
request,
})
const ownerKey = `user:${requirePrincipalSubjectUserId(principal)}`
const subjectUserId = resolvePrincipalSubjectUserId(principal)
const ownerKey = subjectUserId ? `user:${subjectUserId}` : `workspace:${workspaceId}`
const resolved = await resolveServableBytes({
buffer: content,
filename: file.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ export const PATCH = defineInternalJsonRoute({
reason: 'Preserve existing internal connector-update behavior',
}),
errorPolicy: internalKnowledgeErrorPolicies.connectors,
mapInput: ({ params, body }, { principal, request }) => ({
mapInput: ({ params, body }, { principal, request, authTransport }) => ({
connectorId: params.connectorId,
knowledgeBaseId: params.id,
updates: body,
resolveBillingAttribution: (workspaceId: string) =>
resolveInternalKnowledgeBillingAttribution(request, principal, workspaceId),
resolveInternalKnowledgeBillingAttribution(request, principal, workspaceId, authTransport),
source: 'ui' as const,
}),
useCase: updateKnowledgeConnector,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ export const POST = defineInternalJsonRoute({
reason: 'Preserve existing internal connector-sync behavior',
}),
errorPolicy: internalKnowledgeErrorPolicies.connectors,
mapInput: ({ params, query }, { principal, request }) => ({
mapInput: ({ params, query }, { principal, request, authTransport }) => ({
connectorId: params.connectorId,
knowledgeBaseId: params.id,
rehydrate: query.rehydrate,
resolveBillingAttribution: (workspaceId: string) =>
resolveInternalKnowledgeBillingAttribution(request, principal, workspaceId),
resolveInternalKnowledgeBillingAttribution(request, principal, workspaceId, authTransport),
source: 'ui' as const,
}),
useCase: syncKnowledgeConnector,
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/knowledge/[id]/connectors/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ export const POST = defineInternalJsonRoute({
reason: 'Preserve existing internal connector-create behavior',
}),
errorPolicy: internalKnowledgeErrorPolicies.connectors,
mapInput: ({ params, body }, { principal, request }) => ({
mapInput: ({ params, body }, { principal, request, authTransport }) => ({
knowledgeBaseId: params.id,
connectorType: body.connectorType,
credentialId: body.credentialId,
apiKey: body.apiKey,
sourceConfig: body.sourceConfig,
syncIntervalMinutes: body.syncIntervalMinutes,
resolveBillingAttribution: (workspaceId: string) =>
resolveInternalKnowledgeBillingAttribution(request, principal, workspaceId),
resolveInternalKnowledgeBillingAttribution(request, principal, workspaceId, authTransport),
source: 'ui' as const,
}),
useCase: createKnowledgeConnector,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {
getKnowledgeChunkContract,
updateKnowledgeChunkContract,
} from '@/lib/api/contracts/knowledge'
import { defineInternalJsonRoute, internalRateLimits } from '@/lib/api/server/routes'
import {
defineInternalJsonRoute,
type InternalAuthTransport,
internalRateLimits,
} from '@/lib/api/server/routes'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import {
internalKnowledgeAuthType,
Expand All @@ -32,13 +36,19 @@ function resolveContentProvenance(
principal: Principal,
payload: unknown,
workspaceId: string | undefined,
includeContent: boolean
includeContent: boolean,
authTransport: InternalAuthTransport | undefined
) {
const resolved = resolveKnowledgeWriteSecretProvenance({
headers: request.headers,
payload,
authType: internalKnowledgeAuthType(principal),
userId: internalKnowledgeProvenanceUserId(request.headers, principal, workspaceId),
authType: internalKnowledgeAuthType(authTransport),
userId: internalKnowledgeProvenanceUserId(
request.headers,
principal,
workspaceId,
authTransport
),
...(workspaceId ? { workspaceId } : {}),
selectionKeys: includeContent ? ['chunk-content'] : [],
})
Expand All @@ -61,11 +71,16 @@ export const GET = defineInternalJsonRoute({
}),
useCase: readKnowledgeChunk,
present: ({ chunk }) => ({ success: true as const, data: toInternalKnowledgeChunk(chunk) }),
finalizeResponse: ({ request, principal, result, body }) =>
finalizeResponse: ({ request, principal, result, body, authTransport }) =>
finalizeKnowledgePersistedResponse({
headers: request.headers,
authType: internalKnowledgeAuthType(principal),
userId: internalKnowledgeProvenanceUserId(request.headers, principal, result.workspaceId),
authType: internalKnowledgeAuthType(authTransport),
userId: internalKnowledgeProvenanceUserId(
request.headers,
principal,
result.workspaceId,
authTransport
),
workspaceId: result.workspaceId,
body,
chunks: [
Expand All @@ -87,22 +102,34 @@ export const PUT = defineInternalJsonRoute({
reason: 'Preserve existing internal chunk-update behavior',
}),
errorPolicy: internalKnowledgeErrorPolicies.chunks,
mapInput: ({ params, body }, { principal, request }) => ({
mapInput: ({ params, body }, { principal, request, authTransport }) => ({
knowledgeBaseId: params.id,
documentId: params.documentId,
chunkId: params.chunkId,
content: body.content,
enabled: body.enabled,
resolveContentProvenance: ({ workspaceId }: { workspaceId?: string }) =>
resolveContentProvenance(request, principal, body, workspaceId, body.content !== undefined),
resolveContentProvenance(
request,
principal,
body,
workspaceId,
body.content !== undefined,
authTransport
),
}),
useCase: updateKnowledgeChunk,
present: ({ chunk }) => ({ success: true as const, data: toInternalKnowledgeChunk(chunk) }),
finalizeResponse: ({ request, principal, result, body }) =>
finalizeResponse: ({ request, principal, result, body, authTransport }) =>
finalizeKnowledgePersistedResponse({
headers: request.headers,
authType: internalKnowledgeAuthType(principal),
userId: internalKnowledgeProvenanceUserId(request.headers, principal, result.workspaceId),
authType: internalKnowledgeAuthType(authTransport),
userId: internalKnowledgeProvenanceUserId(
request.headers,
principal,
result.workspaceId,
authTransport
),
workspaceId: result.workspaceId,
body,
chunks: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { authMockFns, createMockRequest } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createTestRuntimePrincipal } from '@/lib/auth/runtime-principal.test-support'

const mocks = vi.hoisted(() => ({
list: vi.fn(),
Expand Down Expand Up @@ -32,8 +33,9 @@ vi.mock('@/lib/knowledge/api/secret-provenance', () => ({
resolveKnowledgeWriteSecretProvenance: vi.fn(),
}))

import { internalKnowledgeSessionOrExecutorAuth } from '@/lib/knowledge/api/route-policies'
import { KnowledgeDocumentNotReadyError } from '@/lib/knowledge/application/chunk-errors'
import { GET } from '@/app/api/knowledge/[id]/documents/[documentId]/chunks/route'
import { GET, POST } from '@/app/api/knowledge/[id]/documents/[documentId]/chunks/route'

const params = () => ({
params: Promise.resolve({ id: 'knowledge-1', documentId: 'document-1' }),
Expand All @@ -60,4 +62,54 @@ describe('/api/knowledge/[id]/documents/[documentId]/chunks internal route compo
retryAfter: 5,
})
})

it('passes the executor transport workspace into chunk reads', async () => {
const principal = createTestRuntimePrincipal()
vi.spyOn(
internalKnowledgeSessionOrExecutorAuth,
'authenticateWithTransport'
).mockResolvedValueOnce({
principal,
transport: 'executor_jwt',
executionWorkspaceId: 'workspace-canonical',
})
mocks.list.mockResolvedValueOnce({
chunks: [],
pagination: { total: 0, limit: 50, offset: 0, hasMore: false },
workspaceId: 'workspace-canonical',
documentId: 'document-1',
})

const response = await GET(createMockRequest('GET'), params())

expect(response.status).toBe(200)
expect(mocks.list.mock.calls[0][0]).toMatchObject({
principal,
input: { assertedWorkspaceId: 'workspace-canonical' },
})
})

it('passes the executor transport workspace into chunk writes', async () => {
const principal = createTestRuntimePrincipal()
vi.spyOn(
internalKnowledgeSessionOrExecutorAuth,
'authenticateWithTransport'
).mockResolvedValueOnce({
principal,
transport: 'executor_jwt',
executionWorkspaceId: 'workspace-canonical',
})
mocks.create.mockRejectedValueOnce(new Error('stop after input mapping'))

const response = await POST(
createMockRequest('POST', { content: 'hello', enabled: true }),
params()
)

expect(response.status).toBe(500)
expect(mocks.create.mock.calls[0][0]).toMatchObject({
principal,
input: { assertedWorkspaceId: 'workspace-canonical' },
})
})
})
Loading
Loading