Skip to content
Closed
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
17 changes: 10 additions & 7 deletions apps/sim/app/api/a2a/agents/[agentId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { a2aAgent, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
Expand Down Expand Up @@ -31,8 +31,8 @@ export async function GET(request: NextRequest, { params }: { params: Promise<Ro
workflow: workflow,
})
.from(a2aAgent)
.innerJoin(workflow, eq(a2aAgent.workflowId, workflow.id))
.where(eq(a2aAgent.id, agentId))
.innerJoin(workflow, and(eq(a2aAgent.workflowId, workflow.id), isNull(workflow.archivedAt)))
.where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt)))
.limit(1)

if (!agent) {
Expand Down Expand Up @@ -94,7 +94,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<Ro
const [existingAgent] = await db
.select()
.from(a2aAgent)
.where(eq(a2aAgent.id, agentId))
.where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt)))
.limit(1)

if (!existingAgent) {
Expand Down Expand Up @@ -164,7 +164,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
const [existingAgent] = await db
.select()
.from(a2aAgent)
.where(eq(a2aAgent.id, agentId))
.where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt)))
.limit(1)

if (!existingAgent) {
Expand All @@ -176,7 +176,10 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))
await db
.update(a2aAgent)
.set({ archivedAt: new Date(), updatedAt: new Date(), isPublished: false })
.where(eq(a2aAgent.id, agentId))

logger.info(`Deleted A2A agent: ${agentId}`)

Expand All @@ -203,7 +206,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
const [existingAgent] = await db
.select()
.from(a2aAgent)
.where(eq(a2aAgent.id, agentId))
.where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt)))
.limit(1)

if (!existingAgent) {
Expand Down
22 changes: 17 additions & 5 deletions apps/sim/app/api/a2a/agents/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { db } from '@sim/db'
import { a2aAgent, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, sql } from 'drizzle-orm'
import { and, eq, isNull, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
Expand Down Expand Up @@ -72,8 +72,8 @@ export async function GET(request: NextRequest) {
)`.as('task_count'),
})
.from(a2aAgent)
.leftJoin(workflow, eq(a2aAgent.workflowId, workflow.id))
.where(eq(a2aAgent.workspaceId, workspaceId))
.leftJoin(workflow, and(eq(a2aAgent.workflowId, workflow.id), isNull(workflow.archivedAt)))
.where(and(eq(a2aAgent.workspaceId, workspaceId), isNull(a2aAgent.archivedAt)))
.orderBy(a2aAgent.createdAt)

logger.info(`Listed ${agents.length} A2A agents for workspace ${workspaceId}`)
Expand Down Expand Up @@ -123,7 +123,13 @@ export async function POST(request: NextRequest) {
isDeployed: workflow.isDeployed,
})
.from(workflow)
.where(and(eq(workflow.id, workflowId), eq(workflow.workspaceId, workspaceId)))
.where(
and(
eq(workflow.id, workflowId),
eq(workflow.workspaceId, workspaceId),
isNull(workflow.archivedAt)
)
)
.limit(1)

if (!wf) {
Expand All @@ -144,7 +150,13 @@ export async function POST(request: NextRequest) {
const [existing] = await db
.select({ id: a2aAgent.id })
.from(a2aAgent)
.where(and(eq(a2aAgent.workspaceId, workspaceId), eq(a2aAgent.workflowId, workflowId)))
.where(
and(
eq(a2aAgent.workspaceId, workspaceId),
eq(a2aAgent.workflowId, workflowId),
isNull(a2aAgent.archivedAt)
)
)
.limit(1)

if (existing) {
Expand Down
8 changes: 4 additions & 4 deletions apps/sim/app/api/a2a/serve/[agentId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Artifact, Message, PushNotificationConfig, TaskState } from '@a2a-
import { db } from '@sim/db'
import { a2aAgent, a2aPushNotificationConfig, a2aTask, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { A2A_DEFAULT_TIMEOUT, A2A_MAX_HISTORY_LENGTH } from '@/lib/a2a/constants'
Expand Down Expand Up @@ -87,7 +87,7 @@ export async function GET(_request: NextRequest, { params }: { params: Promise<R
isPublished: a2aAgent.isPublished,
})
.from(a2aAgent)
.where(eq(a2aAgent.id, agentId))
.where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt)))
.limit(1)

if (!agent) {
Expand Down Expand Up @@ -175,7 +175,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
authentication: a2aAgent.authentication,
})
.from(a2aAgent)
.where(eq(a2aAgent.id, agentId))
.where(and(eq(a2aAgent.id, agentId), isNull(a2aAgent.archivedAt)))
.limit(1)

if (!agent) {
Expand Down Expand Up @@ -222,7 +222,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
const [wf] = await db
.select({ isDeployed: workflow.isDeployed })
.from(workflow)
.where(eq(workflow.id, agent.workflowId))
.where(and(eq(workflow.id, agent.workflowId), isNull(workflow.archivedAt)))
.limit(1)

if (!wf?.isDeployed) {
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/chat/[identifier]/otp/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { chat, verification } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, gt } from 'drizzle-orm'
import { and, eq, gt, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { renderOTPEmail } from '@/components/emails'
Expand Down Expand Up @@ -128,7 +128,7 @@ export async function POST(
title: chat.title,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.where(and(eq(chat.identifier, identifier), eq(chat.isActive, true), isNull(chat.archivedAt)))
.limit(1)

if (deploymentResult.length === 0) {
Expand Down Expand Up @@ -218,7 +218,7 @@ export async function PUT(
authType: chat.authType,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.where(and(eq(chat.identifier, identifier), eq(chat.isActive, true), isNull(chat.archivedAt)))
.limit(1)

if (deploymentResult.length === 0) {
Expand Down
8 changes: 4 additions & 4 deletions apps/sim/app/api/chat/[identifier]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
import { db } from '@sim/db'
import { chat, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { addCorsHeaders, validateAuthToken } from '@/lib/core/security/deployment'
Expand Down Expand Up @@ -75,7 +75,7 @@ export async function POST(
outputConfigs: chat.outputConfigs,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.where(and(eq(chat.identifier, identifier), isNull(chat.archivedAt)))
.limit(1)

if (deploymentResult.length === 0) {
Expand All @@ -91,7 +91,7 @@ export async function POST(
const [workflowRecord] = await db
.select({ workspaceId: workflow.workspaceId })
.from(workflow)
.where(eq(workflow.id, deployment.workflowId))
.where(and(eq(workflow.id, deployment.workflowId), isNull(workflow.archivedAt)))
.limit(1)

const workspaceId = workflowRecord?.workspaceId
Expand Down Expand Up @@ -306,7 +306,7 @@ export async function GET(
outputConfigs: chat.outputConfigs,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.where(and(eq(chat.identifier, identifier), isNull(chat.archivedAt)))
.limit(1)

if (deploymentResult.length === 0) {
Expand Down
8 changes: 6 additions & 2 deletions apps/sim/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
Expand Down Expand Up @@ -108,7 +108,11 @@ export async function POST(request: NextRequest) {

// Check identifier availability and workflow access in parallel
const [existingIdentifier, { hasAccess, workflow: workflowRecord }] = await Promise.all([
db.select().from(chat).where(eq(chat.identifier, identifier)).limit(1),
db
.select()
.from(chat)
.where(and(eq(chat.identifier, identifier), isNull(chat.archivedAt)))
.limit(1),
checkWorkflowAccessForChatCreation(workflowId, session.user.id),
])

Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/chat/validate/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
Expand Down Expand Up @@ -45,7 +45,7 @@ export async function GET(request: NextRequest) {
const existingChat = await db
.select({ id: chat.id })
.from(chat)
.where(eq(chat.identifier, validatedIdentifier))
.where(and(eq(chat.identifier, validatedIdentifier), isNull(chat.archivedAt)))
.limit(1)

const isAvailable = existingChat.length === 0
Expand Down
51 changes: 40 additions & 11 deletions apps/sim/app/api/files/authorization.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { document, workspaceFile } from '@sim/db/schema'
import { document, knowledgeBase, workspaceFile } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq, like, or } from 'drizzle-orm'
import { and, eq, isNull, like, or } from 'drizzle-orm'
import { getFileMetadata } from '@/lib/uploads'
import type { StorageContext } from '@/lib/uploads/config'
import {
Expand Down Expand Up @@ -30,11 +30,13 @@ export interface AuthorizationResult {
* @returns Workspace file info or null if not found
*/
export async function lookupWorkspaceFileByKey(
key: string
key: string,
options?: { includeDeleted?: boolean }
): Promise<{ workspaceId: string; uploadedBy: string } | null> {
try {
const { includeDeleted = false } = options ?? {}
// Priority 1: Check new workspaceFiles table
const fileRecord = await getFileMetadataByKey(key, 'workspace')
const fileRecord = await getFileMetadataByKey(key, 'workspace', { includeDeleted })

if (fileRecord) {
return {
Expand All @@ -51,7 +53,11 @@ export async function lookupWorkspaceFileByKey(
uploadedBy: workspaceFile.uploadedBy,
})
.from(workspaceFile)
.where(eq(workspaceFile.key, key))
.where(
includeDeleted
? eq(workspaceFile.key, key)
: and(eq(workspaceFile.key, key), isNull(workspaceFile.deletedAt))
)
.limit(1)

if (legacyFile) {
Expand Down Expand Up @@ -165,6 +171,17 @@ async function verifyWorkspaceFileAccess(
isLocal?: boolean
): Promise<boolean> {
try {
const anyWorkspaceFileRecord = await getFileMetadataByKey(cloudKey, 'workspace', {
includeDeleted: true,
})
if (anyWorkspaceFileRecord?.deletedAt) {
logger.warn('Workspace file access denied for archived file', {
userId,
cloudKey,
})
return false
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double database lookup in workspace file access verification

Low Severity

verifyWorkspaceFileAccess now calls getFileMetadataByKey with includeDeleted: true to check for archived files, and then lookupWorkspaceFileByKey internally calls getFileMetadataByKey again (with includeDeleted: false) for the same key. This results in two sequential database queries against the workspaceFiles table for every workspace file access check, when a single query with includeDeleted: true would suffice.

Fix in Cursor Fix in Web

// Priority 1: Check database (most reliable, works for both local and cloud)
const workspaceFileRecord = await lookupWorkspaceFileByKey(cloudKey)
if (workspaceFileRecord) {
Expand Down Expand Up @@ -352,7 +369,17 @@ async function verifyKBFileAccess(
): Promise<boolean> {
try {
// Priority 1: Check workspaceFiles table (new system)
const fileRecord = await getFileMetadataByKey(cloudKey, 'knowledge-base')
const fileRecord = await getFileMetadataByKey(cloudKey, 'knowledge-base', {
includeDeleted: true,
})

if (fileRecord?.deletedAt) {
logger.warn('KB file access denied for archived file metadata', {
userId,
cloudKey,
})
return false
}

if (fileRecord?.workspaceId) {
const permission = await getUserEntityPermissions(userId, 'workspace', fileRecord.workspaceId)
Expand Down Expand Up @@ -381,22 +408,24 @@ async function verifyKBFileAccess(
})
.from(document)
.where(
or(
like(document.fileUrl, `%${cloudKey}%`),
like(document.fileUrl, `%${encodeURIComponent(cloudKey)}%`)
and(
isNull(document.deletedAt),
or(
like(document.fileUrl, `%${cloudKey}%`),
like(document.fileUrl, `%${encodeURIComponent(cloudKey)}%`)
)
)
)
.limit(10) // Limit to avoid scanning too many

// Check each document's knowledge base for workspace access
for (const doc of documents) {
const { knowledgeBase } = await import('@sim/db/schema')
const [kb] = await db
.select({
workspaceId: knowledgeBase.workspaceId,
})
.from(knowledgeBase)
.where(eq(knowledgeBase.id, doc.knowledgeBaseId))
.where(and(eq(knowledgeBase.id, doc.knowledgeBaseId), isNull(knowledgeBase.deletedAt)))
.limit(1)

if (kb?.workspaceId) {
Expand Down
Loading