@@ -153,11 +153,16 @@ interface ChunkAccessDenied {
153153export type ChunkAccessCheck = ChunkAccessResult | ChunkAccessDenied
154154
155155/**
156- * Check if a user has access to a knowledge base
156+ * Resolve knowledge-base access for a user, gated by read or write permission.
157+ *
158+ * Read (`requireWrite: false`) grants on any workspace permission; write
159+ * (`requireWrite: true`) requires `write`/`admin`. Legacy non-workspace KBs grant
160+ * to the owning user in both modes.
157161 */
158- export async function checkKnowledgeBaseAccess (
162+ async function resolveKnowledgeBaseAccess (
159163 knowledgeBaseId : string ,
160- userId : string
164+ userId : string ,
165+ requireWrite : boolean
161166) : Promise < KnowledgeBaseAccessCheck > {
162167 const kb = await db
163168 . select ( {
@@ -180,10 +185,10 @@ export async function checkKnowledgeBaseAccess(
180185 if ( kbData . workspaceId ) {
181186 // Workspace KB: use workspace permissions only
182187 const userPermission = await getUserEntityPermissions ( userId , 'workspace' , kbData . workspaceId )
183- if ( userPermission !== null ) {
184- return { hasAccess : true , knowledgeBase : kbData }
185- }
186- return { hasAccess : false }
188+ const permitted = requireWrite
189+ ? userPermission === 'write' || userPermission === 'admin'
190+ : userPermission !== null
191+ return permitted ? { hasAccess : true , knowledgeBase : kbData } : { hasAccess : false }
187192 }
188193
189194 // Legacy non-workspace KB: allow owner access
@@ -195,7 +200,18 @@ export async function checkKnowledgeBaseAccess(
195200}
196201
197202/**
198- * Check if a user has write access to a knowledge base
203+ * Check if a user has read access to a knowledge base.
204+ */
205+ export async function checkKnowledgeBaseAccess (
206+ knowledgeBaseId : string ,
207+ userId : string
208+ ) : Promise < KnowledgeBaseAccessCheck > {
209+ return resolveKnowledgeBaseAccess ( knowledgeBaseId , userId , false )
210+ }
211+
212+ /**
213+ * Check if a user has write access to a knowledge base.
214+ *
199215 * Write access is granted if:
200216 * 1. KB has a workspace: user has write or admin permissions on that workspace
201217 * 2. KB has no workspace (legacy): user owns the KB directly
@@ -204,52 +220,20 @@ export async function checkKnowledgeBaseWriteAccess(
204220 knowledgeBaseId : string ,
205221 userId : string
206222) : Promise < KnowledgeBaseAccessCheck > {
207- const kb = await db
208- . select ( {
209- id : knowledgeBase . id ,
210- userId : knowledgeBase . userId ,
211- workspaceId : knowledgeBase . workspaceId ,
212- name : knowledgeBase . name ,
213- embeddingModel : knowledgeBase . embeddingModel ,
214- } )
215- . from ( knowledgeBase )
216- . where ( and ( eq ( knowledgeBase . id , knowledgeBaseId ) , isNull ( knowledgeBase . deletedAt ) ) )
217- . limit ( 1 )
218-
219- if ( kb . length === 0 ) {
220- return { hasAccess : false , notFound : true }
221- }
222-
223- const kbData = kb [ 0 ]
224-
225- if ( kbData . workspaceId ) {
226- // Workspace KB: use workspace permissions only
227- const userPermission = await getUserEntityPermissions ( userId , 'workspace' , kbData . workspaceId )
228- if ( userPermission === 'write' || userPermission === 'admin' ) {
229- return { hasAccess : true , knowledgeBase : kbData }
230- }
231- return { hasAccess : false }
232- }
233-
234- // Legacy non-workspace KB: allow owner access
235- if ( kbData . userId === userId ) {
236- return { hasAccess : true , knowledgeBase : kbData }
237- }
238-
239- return { hasAccess : false }
223+ return resolveKnowledgeBaseAccess ( knowledgeBaseId , userId , true )
240224}
241225
242226/**
243- * Check if a user has write access to a specific document
244- * Write access is granted if user has write access to the knowledge base
227+ * Resolve document access within a knowledge base, gated by read or write
228+ * permission on the KB (see { @link resolveKnowledgeBaseAccess}).
245229 */
246- export async function checkDocumentWriteAccess (
230+ async function resolveDocumentAccess (
247231 knowledgeBaseId : string ,
248232 documentId : string ,
249- userId : string
233+ userId : string ,
234+ requireWrite : boolean
250235) : Promise < DocumentAccessCheck > {
251- // First check if user has write access to the knowledge base
252- const kbAccess = await checkKnowledgeBaseWriteAccess ( knowledgeBaseId , userId )
236+ const kbAccess = await resolveKnowledgeBaseAccess ( knowledgeBaseId , userId , requireWrite )
253237
254238 if ( ! kbAccess . hasAccess ) {
255239 return {
@@ -259,50 +243,8 @@ export async function checkDocumentWriteAccess(
259243 }
260244 }
261245
262- // Check if document exists
263246 const doc = await db
264- . select ( {
265- id : document . id ,
266- filename : document . filename ,
267- fileUrl : document . fileUrl ,
268- fileSize : document . fileSize ,
269- mimeType : document . mimeType ,
270- chunkCount : document . chunkCount ,
271- tokenCount : document . tokenCount ,
272- characterCount : document . characterCount ,
273- enabled : document . enabled ,
274- processingStatus : document . processingStatus ,
275- processingError : document . processingError ,
276- uploadedAt : document . uploadedAt ,
277- processingStartedAt : document . processingStartedAt ,
278- processingCompletedAt : document . processingCompletedAt ,
279- knowledgeBaseId : document . knowledgeBaseId ,
280- // Text tags
281- tag1 : document . tag1 ,
282- tag2 : document . tag2 ,
283- tag3 : document . tag3 ,
284- tag4 : document . tag4 ,
285- tag5 : document . tag5 ,
286- tag6 : document . tag6 ,
287- tag7 : document . tag7 ,
288- // Number tags (5 slots)
289- number1 : document . number1 ,
290- number2 : document . number2 ,
291- number3 : document . number3 ,
292- number4 : document . number4 ,
293- number5 : document . number5 ,
294- // Date tags (2 slots)
295- date1 : document . date1 ,
296- date2 : document . date2 ,
297- // Boolean tags (3 slots)
298- boolean1 : document . boolean1 ,
299- boolean2 : document . boolean2 ,
300- boolean3 : document . boolean3 ,
301- // Connector fields
302- connectorId : document . connectorId ,
303- sourceUrl : document . sourceUrl ,
304- externalId : document . externalId ,
305- } )
247+ . select ( )
306248 . from ( document )
307249 . where (
308250 and (
@@ -327,60 +269,41 @@ export async function checkDocumentWriteAccess(
327269}
328270
329271/**
330- * Check if a user has access to a document within a knowledge base
272+ * Check if a user has read access to a document within a knowledge base.
331273 */
332274export async function checkDocumentAccess (
333275 knowledgeBaseId : string ,
334276 documentId : string ,
335277 userId : string
336278) : Promise < DocumentAccessCheck > {
337- // First check if user has access to the knowledge base
338- const kbAccess = await checkKnowledgeBaseAccess ( knowledgeBaseId , userId )
339-
340- if ( ! kbAccess . hasAccess ) {
341- return {
342- hasAccess : false ,
343- notFound : kbAccess . notFound ,
344- reason : kbAccess . notFound ? 'Knowledge base not found' : 'Unauthorized knowledge base access' ,
345- }
346- }
347-
348- const doc = await db
349- . select ( )
350- . from ( document )
351- . where (
352- and (
353- eq ( document . id , documentId ) ,
354- eq ( document . knowledgeBaseId , knowledgeBaseId ) ,
355- eq ( document . userExcluded , false ) ,
356- isNull ( document . archivedAt ) ,
357- isNull ( document . deletedAt )
358- )
359- )
360- . limit ( 1 )
361-
362- if ( doc . length === 0 ) {
363- return { hasAccess : false , notFound : true , reason : 'Document not found' }
364- }
279+ return resolveDocumentAccess ( knowledgeBaseId , documentId , userId , false )
280+ }
365281
366- return {
367- hasAccess : true ,
368- document : doc [ 0 ] as DocumentData ,
369- knowledgeBase : kbAccess . knowledgeBase ! ,
370- }
282+ /**
283+ * Check if a user has write access to a specific document.
284+ * Write access is granted if user has write access to the knowledge base.
285+ */
286+ export async function checkDocumentWriteAccess (
287+ knowledgeBaseId : string ,
288+ documentId : string ,
289+ userId : string
290+ ) : Promise < DocumentAccessCheck > {
291+ return resolveDocumentAccess ( knowledgeBaseId , documentId , userId , true )
371292}
372293
373294/**
374- * Check if a user has access to a chunk within a document and knowledge base
295+ * Resolve chunk access within a document/knowledge base, gated by read or write
296+ * permission on the KB. The document must exist and be fully processed
297+ * (`processingStatus === 'completed'`) before its chunks are accessible.
375298 */
376- export async function checkChunkAccess (
299+ async function resolveChunkAccess (
377300 knowledgeBaseId : string ,
378301 documentId : string ,
379302 chunkId : string ,
380- userId : string
303+ userId : string ,
304+ requireWrite : boolean
381305) : Promise < ChunkAccessCheck > {
382- // First check if user has access to the knowledge base
383- const kbAccess = await checkKnowledgeBaseAccess ( knowledgeBaseId , userId )
306+ const kbAccess = await resolveKnowledgeBaseAccess ( knowledgeBaseId , userId , requireWrite )
384307
385308 if ( ! kbAccess . hasAccess ) {
386309 return {
@@ -410,7 +333,7 @@ export async function checkChunkAccess(
410333
411334 const docData = doc [ 0 ] as DocumentData
412335
413- // Check if document processing is completed
336+ // Chunks are only accessible once the document has finished processing.
414337 if ( docData . processingStatus !== 'completed' ) {
415338 return {
416339 hasAccess : false ,
@@ -436,71 +359,31 @@ export async function checkChunkAccess(
436359 }
437360}
438361
362+ /**
363+ * Check if a user has read access to a chunk within a document and knowledge base.
364+ */
365+ export async function checkChunkAccess (
366+ knowledgeBaseId : string ,
367+ documentId : string ,
368+ chunkId : string ,
369+ userId : string
370+ ) : Promise < ChunkAccessCheck > {
371+ return resolveChunkAccess ( knowledgeBaseId , documentId , chunkId , userId , false )
372+ }
373+
439374/**
440375 * Check if a user has write access to a chunk.
441376 *
442- * Mirrors ` checkChunkAccess` but requires write/admin on the knowledge base's
443- * workspace (or KB ownership for legacy KBs), matching the permission needed to
444- * create chunks. Used for chunk mutation (update and delete) so those operations
445- * require the same permission as creation rather than read.
377+ * Mirrors { @link checkChunkAccess} but requires write/admin on the knowledge
378+ * base's workspace (or KB ownership for legacy KBs), matching the permission
379+ * needed to create chunks. Used for chunk mutation (update and delete) so those
380+ * operations require the same permission as creation rather than read.
446381 */
447382export async function checkChunkWriteAccess (
448383 knowledgeBaseId : string ,
449384 documentId : string ,
450385 chunkId : string ,
451386 userId : string
452387) : Promise < ChunkAccessCheck > {
453- const kbAccess = await checkKnowledgeBaseWriteAccess ( knowledgeBaseId , userId )
454-
455- if ( ! kbAccess . hasAccess ) {
456- return {
457- hasAccess : false ,
458- notFound : kbAccess . notFound ,
459- reason : kbAccess . notFound ? 'Knowledge base not found' : 'Unauthorized knowledge base access' ,
460- }
461- }
462-
463- const doc = await db
464- . select ( )
465- . from ( document )
466- . where (
467- and (
468- eq ( document . id , documentId ) ,
469- eq ( document . knowledgeBaseId , knowledgeBaseId ) ,
470- eq ( document . userExcluded , false ) ,
471- isNull ( document . archivedAt ) ,
472- isNull ( document . deletedAt )
473- )
474- )
475- . limit ( 1 )
476-
477- if ( doc . length === 0 ) {
478- return { hasAccess : false , notFound : true , reason : 'Document not found' }
479- }
480-
481- const docData = doc [ 0 ] as DocumentData
482-
483- if ( docData . processingStatus !== 'completed' ) {
484- return {
485- hasAccess : false ,
486- reason : `Document is not ready for access (status: ${ docData . processingStatus } )` ,
487- }
488- }
489-
490- const chunk = await db
491- . select ( )
492- . from ( embedding )
493- . where ( and ( eq ( embedding . id , chunkId ) , eq ( embedding . documentId , documentId ) ) )
494- . limit ( 1 )
495-
496- if ( chunk . length === 0 ) {
497- return { hasAccess : false , notFound : true , reason : 'Chunk not found' }
498- }
499-
500- return {
501- hasAccess : true ,
502- chunk : chunk [ 0 ] as EmbeddingData ,
503- document : docData ,
504- knowledgeBase : kbAccess . knowledgeBase ! ,
505- }
388+ return resolveChunkAccess ( knowledgeBaseId , documentId , chunkId , userId , true )
506389}
0 commit comments