refactor: scope knowledge directory lookups to their knowledge base

Caller-supplied directory ids are now resolved against the knowledge base in the URL before use, through one shared helper in the knowledge router, replacing the three hand-rolled copies of that check. The breadcrumb walk takes the knowledge base id and filters on it, and the upload auto-link path drops a directory id that does not name a directory of the target knowledge base.
This commit is contained in:
Classic298 2026-09-10 23:27:50 +02:00
parent 5e7e38c035
commit 8aaeb0affd
3 changed files with 56 additions and 25 deletions

View file

@ -625,6 +625,7 @@ class KnowledgeTable:
db=db,
),
breadcrumbs=await self.get_directory_breadcrumbs(
knowledge_id,
filter.get('directory_id') if filter else None,
db=db,
),
@ -908,6 +909,7 @@ class KnowledgeTable:
async def get_directory_breadcrumbs(
self,
knowledge_id: str,
directory_id: Optional[str],
db: Optional[AsyncSession] = None,
) -> list[KnowledgeDirectoryModel]:
@ -922,7 +924,10 @@ class KnowledgeTable:
while current_id and current_id not in seen:
seen.add(current_id)
result = await db.execute(select(KnowledgeDirectory).filter_by(id=current_id))
# Scoped by knowledge base so a caller-supplied id cannot walk another one's tree.
result = await db.execute(
select(KnowledgeDirectory).filter_by(id=current_id, knowledge_id=knowledge_id)
)
directory = result.scalars().first()
if not directory:
break

View file

@ -125,6 +125,19 @@ def _media_supported_for_extraction(
return bool(content_extraction_engine and _matches_configured_mime_type(supported, content_type))
async def _resolve_knowledge_directory_id(knowledge_id: str, directory_id: str | None, db) -> str | None:
"""Drop a client-supplied directory_id that does not name a directory of this knowledge base."""
if not directory_id:
return None
directory = await Knowledges.get_directory_by_id(directory_id, db=db)
if directory and directory.knowledge_id == knowledge_id:
return directory_id
log.warning('Ignoring directory %s: not a directory of knowledge %s', directory_id, knowledge_id)
return None
async def process_uploaded_file(
request,
file,
@ -218,6 +231,10 @@ async def process_uploaded_file(
db=db_session,
)
)
directory_id = await _resolve_knowledge_directory_id(
knowledge_id, file_metadata.get('directory_id'), db_session
)
if not can_write:
log.warning(
f'Refusing to auto-link file {file_item.id} to knowledge '
@ -237,7 +254,7 @@ async def process_uploaded_file(
knowledge_id=knowledge_id,
file_id=file_item.id,
user_id=user.id,
directory_id=file_metadata.get('directory_id'),
directory_id=directory_id,
db=db_session,
)
if not knowledge_file:

View file

@ -151,6 +151,25 @@ def external_knowledge_error():
)
async def _verify_directory_in_knowledge(
id: str,
directory_id: str | None,
db: AsyncSession,
detail: str = ERROR_MESSAGES.NOT_FOUND,
):
"""Verify a caller-supplied directory belongs to the knowledge base in the URL. Unset means the root level."""
if not directory_id:
return None
directory = await Knowledges.get_directory_by_id(directory_id, db=db)
if not directory or directory.knowledge_id != id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=detail,
)
return directory
@router.get('/', response_model=KnowledgeAccessListResponse)
async def get_knowledge_bases(
page: int | None = 1,
@ -1437,6 +1456,8 @@ async def add_file_to_knowledge_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
await _verify_directory_in_knowledge(id, form_data.directory_id, db, detail='Target directory not found.')
file = await Files.get_file_by_id(form_data.file_id, db=db)
if not file:
raise HTTPException(
@ -2049,6 +2070,9 @@ async def add_files_to_knowledge_batch(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
for directory_id in {form.directory_id for form in form_data if form.directory_id}:
await _verify_directory_in_knowledge(id, directory_id, db, detail='Target directory not found.')
# Batch-fetch all files to avoid N+1 queries
log.info('files/batch/add - %s files', len(form_data))
file_ids = [form.file_id for form in form_data]
@ -2237,6 +2261,8 @@ async def create_knowledge_directory(
):
await _verify_knowledge_write_access(id, user, db)
await _verify_directory_in_knowledge(id, form_data.parent_id, db, detail='Parent directory not found.')
directory = await Knowledges.create_directory(
knowledge_id=id,
name=form_data.name,
@ -2269,14 +2295,11 @@ async def update_knowledge_directory(
db: AsyncSession = Depends(get_async_session),
):
await _verify_knowledge_write_access(id, user, db)
await _verify_directory_in_knowledge(id, dir_id, db)
# Verify directory belongs to this knowledge base
directory = await Knowledges.get_directory_by_id(dir_id, db=db)
if not directory or directory.knowledge_id != id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
# '__unset__' leaves the parent alone, None moves the directory to the root
if form_data.parent_id not in (None, '__unset__'):
await _verify_directory_in_knowledge(id, form_data.parent_id, db, detail='Parent directory not found.')
result = await Knowledges.update_directory(
directory_id=dir_id,
@ -2309,14 +2332,7 @@ async def delete_knowledge_directory(
db: AsyncSession = Depends(get_async_session),
):
await _verify_knowledge_write_access(id, user, db)
# Verify directory belongs to this knowledge base
directory = await Knowledges.get_directory_by_id(dir_id, db=db)
if not directory or directory.knowledge_id != id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=ERROR_MESSAGES.NOT_FOUND,
)
await _verify_directory_in_knowledge(id, dir_id, db)
# Collect before delete_directory drops the KnowledgeFile rows
files = [] if move_files else await Knowledges.get_files_by_id_and_directory_id(id, dir_id, db=db)
@ -2375,14 +2391,7 @@ async def move_file_in_knowledge(
detail=ERROR_MESSAGES.NOT_FOUND,
)
# If target directory is set, verify it belongs to this knowledge base
if form_data.directory_id:
directory = await Knowledges.get_directory_by_id(form_data.directory_id, db=db)
if not directory or directory.knowledge_id != id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='Target directory not found.',
)
await _verify_directory_in_knowledge(id, form_data.directory_id, db, detail='Target directory not found.')
success = await Knowledges.move_file_to_directory(
knowledge_id=id,