diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]/route.ts index 08c02d508b6..aae50974041 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/[chunkId]/route.ts @@ -95,6 +95,16 @@ export async function PUT( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + if (accessCheck.document?.connectorId) { + logger.warn( + `[${requestId}] User ${session.user.id} attempted to update chunk on connector-synced document: Doc=${documentId}` + ) + return NextResponse.json( + { error: 'Chunks from connector-synced documents are read-only' }, + { status: 403 } + ) + } + const body = await req.json() try { @@ -167,6 +177,16 @@ export async function DELETE( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + if (accessCheck.document?.connectorId) { + logger.warn( + `[${requestId}] User ${session.user.id} attempted to delete chunk on connector-synced document: Doc=${documentId}` + ) + return NextResponse.json( + { error: 'Chunks from connector-synced documents are read-only' }, + { status: 403 } + ) + } + await deleteChunk(chunkId, documentId, requestId) logger.info( diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts index c7979d41b00..762f9be66cf 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts @@ -158,6 +158,16 @@ export async function POST( return NextResponse.json({ error: 'Document not found' }, { status: 404 }) } + if (doc.connectorId) { + logger.warn( + `[${requestId}] User ${userId} attempted to create chunk on connector-synced document: Doc=${documentId}` + ) + return NextResponse.json( + { error: 'Chunks from connector-synced documents are read-only' }, + { status: 403 } + ) + } + // Allow manual chunk creation even if document is not fully processed // but it should exist and not be in failed state if (doc.processingStatus === 'failed') { @@ -283,6 +293,16 @@ export async function PATCH( return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + if (accessCheck.document?.connectorId) { + logger.warn( + `[${requestId}] User ${userId} attempted batch chunk operation on connector-synced document: Doc=${documentId}` + ) + return NextResponse.json( + { error: 'Chunks from connector-synced documents are read-only' }, + { status: 403 } + ) + } + const body = await req.json() try { diff --git a/apps/sim/app/api/knowledge/utils.ts b/apps/sim/app/api/knowledge/utils.ts index 7a2f82d071c..8b6d89fdde7 100644 --- a/apps/sim/app/api/knowledge/utils.ts +++ b/apps/sim/app/api/knowledge/utils.ts @@ -56,6 +56,8 @@ export interface DocumentData { boolean1?: boolean | null boolean2?: boolean | null boolean3?: boolean | null + // Connector fields + connectorId?: string | null } export interface EmbeddingData { @@ -283,6 +285,8 @@ export async function checkDocumentWriteAccess( boolean1: document.boolean1, boolean2: document.boolean2, boolean3: document.boolean3, + // Connector fields + connectorId: document.connectorId, }) .from(document) .where(and(eq(document.id, documentId), isNull(document.deletedAt))) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx index 4586b3306fd..4c0067fc1ff 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/chunk-context-menu/chunk-context-menu.tsx @@ -45,6 +45,10 @@ interface ChunkContextMenuProps { * Whether add chunk is disabled */ disableAddChunk?: boolean + /** + * Whether the document is synced from a connector (chunks are read-only) + */ + isConnectorDocument?: boolean /** * Number of selected chunks (for batch operations) */ @@ -80,6 +84,7 @@ export function ChunkContextMenu({ disableToggleEnabled = false, disableDelete = false, disableAddChunk = false, + isConnectorDocument = false, selectedCount = 1, enabledCount = 0, disabledCount = 0, @@ -134,7 +139,7 @@ export function ChunkContextMenu({ onClose() }} > - Edit + {isConnectorDocument ? 'View' : 'Edit'} )} {!isMultiSelect && onCopyContent && ( diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx index 643faa53a3c..0534ba5f9e3 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/components/edit-chunk-modal/edit-chunk-modal.tsx @@ -50,6 +50,8 @@ export function EditChunkModal({ maxChunkSize, }: EditChunkModalProps) { const userPermissions = useUserPermissionsContext() + const isConnectorDocument = Boolean(document?.connectorId) + const canEditChunk = userPermissions.canEdit && !isConnectorDocument const { mutate: updateChunk, isPending: isSaving, @@ -186,7 +188,9 @@ export function EditChunkModal({
- Edit Chunk #{chunk.chunkIndex} + + {canEditChunk ? 'Edit' : 'View'} Chunk #{chunk.chunkIndex} + {/* Navigation Controls */}
@@ -270,11 +274,15 @@ export function EditChunkModal({ value={editedContent} onChange={(e) => setEditedContent(e.target.value)} placeholder={ - userPermissions.canEdit ? 'Enter chunk content...' : 'Read-only view' + canEditChunk + ? 'Enter chunk content...' + : isConnectorDocument + ? 'This chunk is synced from a connector and cannot be edited' + : 'Read-only view' } rows={20} - disabled={isSaving || isNavigating || !userPermissions.canEdit} - readOnly={!userPermissions.canEdit} + disabled={isSaving || isNavigating || !canEditChunk} + readOnly={!canEditChunk} /> )}
@@ -306,7 +314,7 @@ export function EditChunkModal({ > Cancel - {userPermissions.canEdit && ( + {canEditChunk && (
@@ -830,7 +838,8 @@ export function Document({ onCheckedChange={handleSelectAll} disabled={ documentData?.processingStatus !== 'completed' || - !userPermissions.canEdit + !userPermissions.canEdit || + isConnectorDocument } aria-label='Select all chunks' /> @@ -917,7 +926,7 @@ export function Document({ onCheckedChange={(checked) => handleSelectChunk(chunk.id, checked as boolean) } - disabled={!userPermissions.canEdit} + disabled={!userPermissions.canEdit || isConnectorDocument} aria-label={`Select chunk ${chunk.chunkIndex}`} onClick={(e) => e.stopPropagation()} /> @@ -957,7 +966,7 @@ export function Document({ e.stopPropagation() handleToggleEnabled(chunk.id) }} - disabled={!userPermissions.canEdit} + disabled={!userPermissions.canEdit || isConnectorDocument} className='h-[28px] w-[28px] p-0 text-[var(--text-muted)] hover:text-[var(--text-primary)] disabled:opacity-50' > {chunk.enabled ? ( @@ -970,9 +979,11 @@ export function Document({ {!userPermissions.canEdit ? 'Write permission required to modify chunks' - : chunk.enabled - ? 'Disable Chunk' - : 'Enable Chunk'} + : isConnectorDocument + ? 'Connector-synced chunks are read-only' + : chunk.enabled + ? 'Disable Chunk' + : 'Enable Chunk'} @@ -983,7 +994,7 @@ export function Document({ e.stopPropagation() handleDeleteChunk(chunk.id) }} - disabled={!userPermissions.canEdit} + disabled={!userPermissions.canEdit || isConnectorDocument} className='h-[28px] w-[28px] p-0 text-[var(--text-muted)] hover:text-[var(--text-error)] disabled:opacity-50' > @@ -992,7 +1003,9 @@ export function Document({ {!userPermissions.canEdit ? 'Write permission required to delete chunks' - : 'Delete Chunk'} + : isConnectorDocument + ? 'Connector-synced chunks are read-only' + : 'Delete Chunk'} @@ -1114,9 +1127,9 @@ export function Document({ {/* Bulk Action Bar */} 0 ? handleBulkEnable : undefined} - onDisable={enabledCount > 0 ? handleBulkDisable : undefined} - onDelete={handleBulkDelete} + onEnable={disabledCount > 0 && !isConnectorDocument ? handleBulkEnable : undefined} + onDisable={enabledCount > 0 && !isConnectorDocument ? handleBulkDisable : undefined} + onDelete={!isConnectorDocument ? handleBulkDelete : undefined} enabledCount={enabledCount} disabledCount={disabledCount} isLoading={isBulkOperating} @@ -1197,7 +1210,7 @@ export function Document({ : undefined } onToggleEnabled={ - contextMenuChunk && userPermissions.canEdit + contextMenuChunk && userPermissions.canEdit && !isConnectorDocument ? selectedChunks.size > 1 ? () => { if (disabledCount > 0) { @@ -1210,20 +1223,27 @@ export function Document({ : undefined } onDelete={ - contextMenuChunk && userPermissions.canEdit + contextMenuChunk && userPermissions.canEdit && !isConnectorDocument ? selectedChunks.size > 1 ? handleBulkDelete : () => handleDeleteChunk(contextMenuChunk.id) : undefined } onAddChunk={ - userPermissions.canEdit && documentData?.processingStatus !== 'failed' + userPermissions.canEdit && + documentData?.processingStatus !== 'failed' && + !isConnectorDocument ? () => setIsCreateChunkModalOpen(true) : undefined } - disableToggleEnabled={!userPermissions.canEdit} - disableDelete={!userPermissions.canEdit} - disableAddChunk={!userPermissions.canEdit || documentData?.processingStatus === 'failed'} + disableToggleEnabled={!userPermissions.canEdit || isConnectorDocument} + disableDelete={!userPermissions.canEdit || isConnectorDocument} + disableAddChunk={ + !userPermissions.canEdit || + documentData?.processingStatus === 'failed' || + isConnectorDocument + } + isConnectorDocument={isConnectorDocument} /> )