Skip to content
Merged
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
257 changes: 70 additions & 187 deletions apps/sim/app/api/knowledge/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,16 @@ interface ChunkAccessDenied {
export type ChunkAccessCheck = ChunkAccessResult | ChunkAccessDenied

/**
* Check if a user has access to a knowledge base
* Resolve knowledge-base access for a user, gated by read or write permission.
*
* Read (`requireWrite: false`) grants on any workspace permission; write
* (`requireWrite: true`) requires `write`/`admin`. Legacy non-workspace KBs grant
* to the owning user in both modes.
*/
export async function checkKnowledgeBaseAccess(
async function resolveKnowledgeBaseAccess(
knowledgeBaseId: string,
userId: string
userId: string,
requireWrite: boolean
): Promise<KnowledgeBaseAccessCheck> {
const kb = await db
.select({
Expand All @@ -180,10 +185,10 @@ export async function checkKnowledgeBaseAccess(
if (kbData.workspaceId) {
// Workspace KB: use workspace permissions only
const userPermission = await getUserEntityPermissions(userId, 'workspace', kbData.workspaceId)
if (userPermission !== null) {
return { hasAccess: true, knowledgeBase: kbData }
}
return { hasAccess: false }
const permitted = requireWrite
? userPermission === 'write' || userPermission === 'admin'
: userPermission !== null
return permitted ? { hasAccess: true, knowledgeBase: kbData } : { hasAccess: false }
}

// Legacy non-workspace KB: allow owner access
Expand All @@ -195,7 +200,18 @@ export async function checkKnowledgeBaseAccess(
}

/**
* Check if a user has write access to a knowledge base
* Check if a user has read access to a knowledge base.
*/
export async function checkKnowledgeBaseAccess(
knowledgeBaseId: string,
userId: string
): Promise<KnowledgeBaseAccessCheck> {
return resolveKnowledgeBaseAccess(knowledgeBaseId, userId, false)
}

/**
* Check if a user has write access to a knowledge base.
*
* Write access is granted if:
* 1. KB has a workspace: user has write or admin permissions on that workspace
* 2. KB has no workspace (legacy): user owns the KB directly
Expand All @@ -204,52 +220,20 @@ export async function checkKnowledgeBaseWriteAccess(
knowledgeBaseId: string,
userId: string
): Promise<KnowledgeBaseAccessCheck> {
const kb = await db
.select({
id: knowledgeBase.id,
userId: knowledgeBase.userId,
workspaceId: knowledgeBase.workspaceId,
name: knowledgeBase.name,
embeddingModel: knowledgeBase.embeddingModel,
})
.from(knowledgeBase)
.where(and(eq(knowledgeBase.id, knowledgeBaseId), isNull(knowledgeBase.deletedAt)))
.limit(1)

if (kb.length === 0) {
return { hasAccess: false, notFound: true }
}

const kbData = kb[0]

if (kbData.workspaceId) {
// Workspace KB: use workspace permissions only
const userPermission = await getUserEntityPermissions(userId, 'workspace', kbData.workspaceId)
if (userPermission === 'write' || userPermission === 'admin') {
return { hasAccess: true, knowledgeBase: kbData }
}
return { hasAccess: false }
}

// Legacy non-workspace KB: allow owner access
if (kbData.userId === userId) {
return { hasAccess: true, knowledgeBase: kbData }
}

return { hasAccess: false }
return resolveKnowledgeBaseAccess(knowledgeBaseId, userId, true)
}

/**
* Check if a user has write access to a specific document
* Write access is granted if user has write access to the knowledge base
* Resolve document access within a knowledge base, gated by read or write
* permission on the KB (see {@link resolveKnowledgeBaseAccess}).
*/
export async function checkDocumentWriteAccess(
async function resolveDocumentAccess(
knowledgeBaseId: string,
documentId: string,
userId: string
userId: string,
requireWrite: boolean
): Promise<DocumentAccessCheck> {
// First check if user has write access to the knowledge base
const kbAccess = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, userId)
const kbAccess = await resolveKnowledgeBaseAccess(knowledgeBaseId, userId, requireWrite)

if (!kbAccess.hasAccess) {
return {
Expand All @@ -259,50 +243,8 @@ export async function checkDocumentWriteAccess(
}
}

// Check if document exists
const doc = await db
.select({
id: document.id,
filename: document.filename,
fileUrl: document.fileUrl,
fileSize: document.fileSize,
mimeType: document.mimeType,
chunkCount: document.chunkCount,
tokenCount: document.tokenCount,
characterCount: document.characterCount,
enabled: document.enabled,
processingStatus: document.processingStatus,
processingError: document.processingError,
uploadedAt: document.uploadedAt,
processingStartedAt: document.processingStartedAt,
processingCompletedAt: document.processingCompletedAt,
knowledgeBaseId: document.knowledgeBaseId,
// Text tags
tag1: document.tag1,
tag2: document.tag2,
tag3: document.tag3,
tag4: document.tag4,
tag5: document.tag5,
tag6: document.tag6,
tag7: document.tag7,
// Number tags (5 slots)
number1: document.number1,
number2: document.number2,
number3: document.number3,
number4: document.number4,
number5: document.number5,
// Date tags (2 slots)
date1: document.date1,
date2: document.date2,
// Boolean tags (3 slots)
boolean1: document.boolean1,
boolean2: document.boolean2,
boolean3: document.boolean3,
// Connector fields
connectorId: document.connectorId,
sourceUrl: document.sourceUrl,
externalId: document.externalId,
})
.select()
.from(document)
.where(
and(
Expand All @@ -327,60 +269,41 @@ export async function checkDocumentWriteAccess(
}

/**
* Check if a user has access to a document within a knowledge base
* Check if a user has read access to a document within a knowledge base.
*/
export async function checkDocumentAccess(
knowledgeBaseId: string,
documentId: string,
userId: string
): Promise<DocumentAccessCheck> {
// First check if user has access to the knowledge base
const kbAccess = await checkKnowledgeBaseAccess(knowledgeBaseId, userId)

if (!kbAccess.hasAccess) {
return {
hasAccess: false,
notFound: kbAccess.notFound,
reason: kbAccess.notFound ? 'Knowledge base not found' : 'Unauthorized knowledge base access',
}
}

const doc = await db
.select()
.from(document)
.where(
and(
eq(document.id, documentId),
eq(document.knowledgeBaseId, knowledgeBaseId),
eq(document.userExcluded, false),
isNull(document.archivedAt),
isNull(document.deletedAt)
)
)
.limit(1)

if (doc.length === 0) {
return { hasAccess: false, notFound: true, reason: 'Document not found' }
}
return resolveDocumentAccess(knowledgeBaseId, documentId, userId, false)
}

return {
hasAccess: true,
document: doc[0] as DocumentData,
knowledgeBase: kbAccess.knowledgeBase!,
}
/**
* Check if a user has write access to a specific document.
* Write access is granted if user has write access to the knowledge base.
*/
export async function checkDocumentWriteAccess(
knowledgeBaseId: string,
documentId: string,
userId: string
): Promise<DocumentAccessCheck> {
return resolveDocumentAccess(knowledgeBaseId, documentId, userId, true)
}

/**
* Check if a user has access to a chunk within a document and knowledge base
* Resolve chunk access within a document/knowledge base, gated by read or write
* permission on the KB. The document must exist and be fully processed
* (`processingStatus === 'completed'`) before its chunks are accessible.
*/
export async function checkChunkAccess(
async function resolveChunkAccess(
knowledgeBaseId: string,
documentId: string,
chunkId: string,
userId: string
userId: string,
requireWrite: boolean
): Promise<ChunkAccessCheck> {
// First check if user has access to the knowledge base
const kbAccess = await checkKnowledgeBaseAccess(knowledgeBaseId, userId)
const kbAccess = await resolveKnowledgeBaseAccess(knowledgeBaseId, userId, requireWrite)

if (!kbAccess.hasAccess) {
return {
Expand Down Expand Up @@ -410,7 +333,7 @@ export async function checkChunkAccess(

const docData = doc[0] as DocumentData

// Check if document processing is completed
// Chunks are only accessible once the document has finished processing.
if (docData.processingStatus !== 'completed') {
return {
hasAccess: false,
Expand All @@ -436,71 +359,31 @@ export async function checkChunkAccess(
}
}

/**
* Check if a user has read access to a chunk within a document and knowledge base.
*/
export async function checkChunkAccess(
knowledgeBaseId: string,
documentId: string,
chunkId: string,
userId: string
): Promise<ChunkAccessCheck> {
return resolveChunkAccess(knowledgeBaseId, documentId, chunkId, userId, false)
}

/**
* Check if a user has write access to a chunk.
*
* Mirrors `checkChunkAccess` but requires write/admin on the knowledge base's
* workspace (or KB ownership for legacy KBs), matching the permission needed to
* create chunks. Used for chunk mutation (update and delete) so those operations
* require the same permission as creation rather than read.
* Mirrors {@link checkChunkAccess} but requires write/admin on the knowledge
* base's workspace (or KB ownership for legacy KBs), matching the permission
* needed to create chunks. Used for chunk mutation (update and delete) so those
* operations require the same permission as creation rather than read.
*/
export async function checkChunkWriteAccess(
knowledgeBaseId: string,
documentId: string,
chunkId: string,
userId: string
): Promise<ChunkAccessCheck> {
const kbAccess = await checkKnowledgeBaseWriteAccess(knowledgeBaseId, userId)

if (!kbAccess.hasAccess) {
return {
hasAccess: false,
notFound: kbAccess.notFound,
reason: kbAccess.notFound ? 'Knowledge base not found' : 'Unauthorized knowledge base access',
}
}

const doc = await db
.select()
.from(document)
.where(
and(
eq(document.id, documentId),
eq(document.knowledgeBaseId, knowledgeBaseId),
eq(document.userExcluded, false),
isNull(document.archivedAt),
isNull(document.deletedAt)
)
)
.limit(1)

if (doc.length === 0) {
return { hasAccess: false, notFound: true, reason: 'Document not found' }
}

const docData = doc[0] as DocumentData

if (docData.processingStatus !== 'completed') {
return {
hasAccess: false,
reason: `Document is not ready for access (status: ${docData.processingStatus})`,
}
}

const chunk = await db
.select()
.from(embedding)
.where(and(eq(embedding.id, chunkId), eq(embedding.documentId, documentId)))
.limit(1)

if (chunk.length === 0) {
return { hasAccess: false, notFound: true, reason: 'Chunk not found' }
}

return {
hasAccess: true,
chunk: chunk[0] as EmbeddingData,
document: docData,
knowledgeBase: kbAccess.knowledgeBase!,
}
return resolveChunkAccess(knowledgeBaseId, documentId, chunkId, userId, true)
}
Loading