Skip to content

Commit 6d3e484

Browse files
fix(api): align v2 permissions and resource behavior (#6557)
* fix(api): align v2 permissions and resource behavior * fix(api): refine resource authorization boundaries * fix(tables): restore large durable imports * fix(files): classify missing archive targets
1 parent d6505f6 commit 6d3e484

77 files changed

Lines changed: 685 additions & 264 deletions

File tree

Some content is hidden

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

apps/docs/openapi-v2-tables.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7017,7 +7017,7 @@
70177017
"size": {
70187018
"type": "integer",
70197019
"minimum": 1,
7020-
"maximum": 26214400,
7020+
"maximum": 5368709120,
70217021
"description": "Exact CSV file size in bytes."
70227022
}
70237023
},

apps/sim/app/api/table/[tableId]/import/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { statusForOrchestrationError } from '@/lib/core/orchestration/types'
1616
import { isMultipartError, readMultipart } from '@/lib/core/utils/multipart'
1717
import { generateRequestId } from '@/lib/core/utils/request'
1818
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
19-
import { CSV_MAX_FILE_SIZE_BYTES, type CsvHeaderMapping } from '@/lib/table'
19+
import { CSV_SYNC_MAX_FILE_SIZE_BYTES, type CsvHeaderMapping } from '@/lib/table'
2020
import { performTableCsvImport } from '@/lib/table/orchestration'
2121
import { getUserSettings } from '@/lib/users/queries'
2222
import {
@@ -53,7 +53,7 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
5353
let parsed: Awaited<ReturnType<typeof readMultipart>>
5454
try {
5555
parsed = await readMultipart(request, {
56-
maxFileBytes: CSV_MAX_FILE_SIZE_BYTES,
56+
maxFileBytes: CSV_SYNC_MAX_FILE_SIZE_BYTES,
5757
requiredFieldsBeforeFile: ['workspaceId'],
5858
signal: request.signal,
5959
})

apps/sim/app/api/table/import-csv/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { isMultipartError, readMultipart } from '@/lib/core/utils/multipart'
1010
import { generateRequestId } from '@/lib/core/utils/request'
1111
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1212
import { findActiveFolder } from '@/lib/folders/queries'
13-
import { CSV_MAX_FILE_SIZE_BYTES } from '@/lib/table'
13+
import { CSV_SYNC_MAX_FILE_SIZE_BYTES } from '@/lib/table'
1414
import { performCreateTableFromCsv } from '@/lib/table/orchestration'
1515
import { getUserSettings } from '@/lib/users/queries'
1616
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
@@ -39,7 +39,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
3939
let parsed: Awaited<ReturnType<typeof readMultipart>>
4040
try {
4141
parsed = await readMultipart(request, {
42-
maxFileBytes: CSV_MAX_FILE_SIZE_BYTES,
42+
maxFileBytes: CSV_SYNC_MAX_FILE_SIZE_BYTES,
4343
requiredFieldsBeforeFile: ['workspaceId'],
4444
signal: request.signal,
4545
})

apps/sim/app/api/v2/custom-tools/[id]/route.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
*/
44
import { NextRequest } from 'next/server'
55
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
import {
7+
InsufficientWorkspacePermissionsError,
8+
NoWorkspaceAccessError,
9+
} from '@/lib/core/application'
610

711
const { mocks, MockV2ApiKeyUnauthenticatedError } = vi.hoisted(() => {
812
class MockV2ApiKeyUnauthenticatedError extends Error {}
@@ -160,4 +164,15 @@ describe('/api/v2/custom-tools/[id]', () => {
160164
expect(response.status).toBe(401)
161165
expect(mocks.update).not.toHaveBeenCalled()
162166
})
167+
168+
it('conceals cross-tenant access while preserving same-workspace role denials', async () => {
169+
mocks.get.mockRejectedValueOnce(new NoWorkspaceAccessError())
170+
expect((await GET(request('GET'), context)).status).toBe(404)
171+
172+
mocks.update.mockRejectedValueOnce(new InsufficientWorkspacePermissionsError())
173+
expect(
174+
(await PATCH(request('PATCH', { workspaceId: WORKSPACE_ID, code: 'return 2' }), context))
175+
.status
176+
).toBe(403)
177+
})
163178
})

apps/sim/app/api/v2/custom-tools/[id]/route.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import {
44
v2UpdateCustomToolContract,
55
} from '@/lib/api/contracts/v2/custom-tools'
66
import {
7+
createV2ResourceConcealmentPolicy,
78
defineV2JsonRoute,
89
v2ApiKeyAuth,
9-
v2OrchestrationErrorPolicy,
1010
v2RateLimits,
1111
} from '@/lib/api/server/routes'
1212
import { customToolOperations } from '@/lib/custom-tools/application/operations'
@@ -20,13 +20,17 @@ import { toV2CustomTool } from '@/app/api/v2/custom-tools/utils'
2020
export const dynamic = 'force-dynamic'
2121
export const revalidate = 0
2222

23+
const customToolResourceErrorPolicy = createV2ResourceConcealmentPolicy({
24+
notFoundMessage: 'Custom tool not found',
25+
})
26+
2327
/** GET /api/v2/custom-tools/[id] — Fetch a single custom tool. */
2428
export const GET = defineV2JsonRoute({
2529
contract: v2GetCustomToolContract,
2630
operation: customToolOperations.read,
2731
auth: v2ApiKeyAuth,
2832
rateLimit: v2RateLimits.publicApi,
29-
errorPolicy: v2OrchestrationErrorPolicy,
33+
errorPolicy: customToolResourceErrorPolicy,
3034
mapInput: ({ params, query }) => ({ workspaceId: query.workspaceId, toolId: params.id }),
3135
useCase: getWorkspaceCustomToolUseCase,
3236
present: ({ tool }) => ({ data: { customTool: toV2CustomTool(tool) } }),
@@ -38,7 +42,7 @@ export const PATCH = defineV2JsonRoute({
3842
operation: customToolOperations.update,
3943
auth: v2ApiKeyAuth,
4044
rateLimit: v2RateLimits.publicApi,
41-
errorPolicy: v2OrchestrationErrorPolicy,
45+
errorPolicy: customToolResourceErrorPolicy,
4246
mapInput: ({ params, body }) => ({
4347
...body,
4448
toolId: params.id,
@@ -54,7 +58,7 @@ export const DELETE = defineV2JsonRoute({
5458
operation: customToolOperations.delete,
5559
auth: v2ApiKeyAuth,
5660
rateLimit: v2RateLimits.publicApi,
57-
errorPolicy: v2OrchestrationErrorPolicy,
61+
errorPolicy: customToolResourceErrorPolicy,
5862
mapInput: ({ params, query }) => ({
5963
workspaceId: query.workspaceId,
6064
toolId: params.id,

apps/sim/app/api/v2/files/[fileId]/metadata/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ describe('GET /api/v2/files/[fileId]/metadata', () => {
113113
expect(mocks.readMetadata).not.toHaveBeenCalled()
114114
})
115115

116-
it('conceals an authorization failure as not found', async () => {
116+
it('conceals cross-workspace authorization as not found', async () => {
117117
mocks.readMetadata.mockRejectedValue(new NoWorkspaceAccessError())
118118

119119
const response = await callGet(`workspaceId=${WORKSPACE_ID}`)

apps/sim/app/api/v2/files/[fileId]/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ describe('v2 single-file routes', () => {
141141
})
142142
})
143143

144-
it('conceals download authorization failures', async () => {
144+
it('conceals cross-workspace download authorization', async () => {
145145
mocks.download.mockRejectedValue(new NoWorkspaceAccessError())
146146

147147
const response = await GET(

apps/sim/app/api/v2/files/uploads/utils.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,20 @@ function uploadStatus(status: string): V2UploadStatus {
3838

3939
import type { Principal } from '@sim/auth/principal'
4040
import type { NextRequest, NextResponse } from 'next/server'
41+
import { createV2ResourceConcealmentPolicy } from '@/lib/api/server/routes'
4142
import { authenticateV2ApiKey } from '@/lib/api/server/routes/v2-api-key-auth'
42-
import { v2CaughtOrchestrationError, v2Error } from '@/app/api/v2/lib/response'
43+
44+
const uploadControlErrorPolicy = createV2ResourceConcealmentPolicy({
45+
notFoundMessage: 'Upload session not found',
46+
})
4347

4448
/** Re-authenticates the API key for each upload control leg. */
4549
export async function authenticateUploadPrincipal(request: NextRequest): Promise<Principal> {
4650
const auth = await authenticateV2ApiKey(request.headers.get('x-api-key'))
4751
return auth.principal
4852
}
4953

50-
/** Resource-ID upload controls conceal authorization failures as absence. */
54+
/** Conceals cross-tenant upload-session authorization while preserving same-workspace denials. */
5155
export function v2UploadControlError(error: unknown): NextResponse | null {
52-
const response = v2CaughtOrchestrationError(error)
53-
if (!response) return null
54-
return response.status === 403 ? v2Error('NOT_FOUND', 'Upload session not found') : response
56+
return uploadControlErrorPolicy.render(error)
5557
}

apps/sim/app/api/v2/knowledge/[id]/documents/[documentId]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const DELETE = defineV2JsonRoute({
7171
auth: v2ApiKeyAuth,
7272
operation: knowledgeOperations.deleteDocument,
7373
rateLimit: v2RateLimits.publicApi,
74-
errorPolicy: v2KnowledgeErrorPolicies.default,
74+
errorPolicy: v2KnowledgeErrorPolicies.concealKnowledgeBaseAuthorization,
7575
mapInput: ({ params, query }) => ({
7676
knowledgeBaseId: params.id,
7777
documentId: params.documentId,

apps/sim/app/api/v2/knowledge/[id]/documents/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export const POST = defineV2BodyLifecycleRoute({
106106
auth: v2ApiKeyAuth,
107107
operation: knowledgeOperations.uploadDocument,
108108
rateLimit: v2RateLimits.publicApi,
109-
errorPolicy: v2KnowledgeErrorPolicies.documentUpload,
109+
errorPolicy: v2KnowledgeErrorPolicies.concealKnowledgeBaseUploadAuthorization,
110110
admission: {
111111
mapInput: ({ params, query }) => ({
112112
knowledgeBaseId: params.id,

0 commit comments

Comments
 (0)