mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(mothership): enforce ownership check on workflow resource attachments (#4468)
* fix(mothership): enforce ownership check on workflow resource attachments * fix(mothership): fix table and knowledgebase BOLA in resource attachment resolution * fix(mothership): apply workspace scope to table in processContextsServer * fix(mothership): verify workspace membership before resolving workspace branch * fix(data-drains): use const for timeoutId in sleepUntilAborted * fix(test): mock db.select and drizzle and for workspace permissions check * fix(mothership): always derive workspace from workflow record in workflow branch
This commit is contained in:
@@ -93,10 +93,18 @@ vi.mock('@sim/db', () => ({
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
select: vi.fn(() => ({
|
||||
from: vi.fn(() => ({
|
||||
where: vi.fn(() => ({
|
||||
limit: vi.fn().mockResolvedValue([{ permissionType: 'write' }]),
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
and: vi.fn(() => ({})),
|
||||
eq: vi.fn(() => ({})),
|
||||
sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({ strings, values }),
|
||||
}))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type Context as OtelContext, context as otelContextApi } from '@opentelemetry/api'
|
||||
import { db } from '@sim/db'
|
||||
import { copilotChats } from '@sim/db/schema'
|
||||
import { copilotChats, permissions } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { generateId } from '@sim/utils/id'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { and, eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { isZodError, validationErrorResponse } from '@/lib/api/server'
|
||||
@@ -506,14 +506,12 @@ async function resolveBranch(params: {
|
||||
}
|
||||
|
||||
const resolvedWorkflowId = resolved.workflowId
|
||||
let resolvedWorkspaceId = requestedWorkspaceId
|
||||
if (!resolvedWorkspaceId) {
|
||||
try {
|
||||
const workflow = await getWorkflowById(resolvedWorkflowId)
|
||||
resolvedWorkspaceId = workflow?.workspaceId ?? undefined
|
||||
} catch {
|
||||
// best effort; downstream calls can still proceed
|
||||
}
|
||||
let resolvedWorkspaceId: string | undefined
|
||||
try {
|
||||
const workflow = await getWorkflowById(resolvedWorkflowId)
|
||||
resolvedWorkspaceId = workflow?.workspaceId ?? requestedWorkspaceId
|
||||
} catch {
|
||||
resolvedWorkspaceId = requestedWorkspaceId
|
||||
}
|
||||
|
||||
const selectedModel = model || DEFAULT_MODEL
|
||||
@@ -569,6 +567,22 @@ async function resolveBranch(params: {
|
||||
return createBadRequestResponse('workspaceId is required when workflowId is not provided')
|
||||
}
|
||||
|
||||
const [permissionRow] = await db
|
||||
.select({ permissionType: permissions.permissionType })
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.userId, authenticatedUserId),
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, requestedWorkspaceId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!permissionRow) {
|
||||
return createBadRequestResponse('Workspace not found or access denied')
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'workspace',
|
||||
workspaceId: requestedWorkspaceId,
|
||||
|
||||
@@ -116,8 +116,8 @@ export async function processContextsServer(
|
||||
currentWorkspaceId
|
||||
)
|
||||
}
|
||||
if (ctx.kind === 'table' && ctx.tableId) {
|
||||
const result = await resolveTableResource(ctx.tableId)
|
||||
if (ctx.kind === 'table' && ctx.tableId && currentWorkspaceId) {
|
||||
const result = await resolveTableResource(ctx.tableId, currentWorkspaceId)
|
||||
if (!result) return null
|
||||
return { type: 'table', tag: ctx.label ? `@${ctx.label}` : '@', content: result.content }
|
||||
}
|
||||
@@ -701,7 +701,7 @@ export async function resolveActiveResourceContext(
|
||||
resourceType: string,
|
||||
resourceId: string,
|
||||
workspaceId: string,
|
||||
_userId: string,
|
||||
userId: string,
|
||||
chatId?: string
|
||||
): Promise<AgentContext | null> {
|
||||
try {
|
||||
@@ -709,10 +709,10 @@ export async function resolveActiveResourceContext(
|
||||
case 'workflow': {
|
||||
const ctx = await processWorkflowFromDb(
|
||||
resourceId,
|
||||
undefined,
|
||||
userId,
|
||||
'@active_resource',
|
||||
'current_workflow',
|
||||
undefined,
|
||||
workspaceId,
|
||||
chatId
|
||||
)
|
||||
if (!ctx) return null
|
||||
@@ -721,7 +721,7 @@ export async function resolveActiveResourceContext(
|
||||
case 'knowledgebase': {
|
||||
const ctx = await processKnowledgeFromDb(
|
||||
resourceId,
|
||||
undefined,
|
||||
userId,
|
||||
'@active_resource',
|
||||
workspaceId
|
||||
)
|
||||
@@ -729,7 +729,7 @@ export async function resolveActiveResourceContext(
|
||||
return { type: 'active_resource', tag: '@active_resource', content: ctx.content }
|
||||
}
|
||||
case 'table': {
|
||||
return await resolveTableResource(resourceId)
|
||||
return await resolveTableResource(resourceId, workspaceId)
|
||||
}
|
||||
case 'file': {
|
||||
return await resolveFileResource(resourceId, workspaceId)
|
||||
@@ -745,9 +745,13 @@ export async function resolveActiveResourceContext(
|
||||
return null
|
||||
}
|
||||
}
|
||||
async function resolveTableResource(tableId: string): Promise<AgentContext | null> {
|
||||
async function resolveTableResource(
|
||||
tableId: string,
|
||||
workspaceId: string
|
||||
): Promise<AgentContext | null> {
|
||||
const table = await getTableById(tableId)
|
||||
if (!table) return null
|
||||
if (table.workspaceId !== workspaceId) return null
|
||||
return {
|
||||
type: 'active_resource',
|
||||
tag: '@active_resource',
|
||||
|
||||
@@ -99,12 +99,11 @@ function sign(body: Buffer, secret: string, timestamp: number): string {
|
||||
function sleepUntilAborted(ms: number, signal: AbortSignal): Promise<void> {
|
||||
if (signal.aborted) return Promise.resolve()
|
||||
return new Promise((resolve) => {
|
||||
let timeoutId: ReturnType<typeof setTimeout>
|
||||
const onAbort = () => {
|
||||
clearTimeout(timeoutId)
|
||||
resolve()
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
signal.removeEventListener('abort', onAbort)
|
||||
resolve()
|
||||
}, ms)
|
||||
|
||||
Reference in New Issue
Block a user