diff --git a/backend/open_webui/retrieval/utils.py b/backend/open_webui/retrieval/utils.py index 35a72f4df..721d7263a 100644 --- a/backend/open_webui/retrieval/utils.py +++ b/backend/open_webui/retrieval/utils.py @@ -932,6 +932,44 @@ def get_reranking_function(reranking_engine, reranking_model, reranking_function ) +async def filter_accessible_collections( + collection_names: set[str], + user: UserModel, + access_type: str = 'read', +) -> set[str]: + """ + Return only the collection names the user is allowed to access. + Admins bypass all checks. For non-admins: + - file-* → validated via has_access_to_file + - user-memory-* → must match user's own memory collection + - knowledge-bases → always denied (meta-collection) + - known KB ids → validated via Knowledges.check_access_by_user_id + - everything else → allowed (ephemeral collections like web-search-*) + """ + if user.role == 'admin': + return collection_names + + validated = set() + for name in collection_names: + if name == 'knowledge-bases': + continue + elif name.startswith('file-'): + file_id = name[len('file-'):] + if await has_access_to_file(file_id=file_id, access_type=access_type, user=user): + validated.add(name) + elif name.startswith('user-memory-'): + if name == f'user-memory-{user.id}': + validated.add(name) + else: + # May be a knowledge-base ID or an ephemeral collection + if await Knowledges.check_access_by_user_id(name, user.id, permission=access_type): + validated.add(name) + elif not await Knowledges.get_knowledge_by_id(name): + # Not a KB at all — ephemeral collection (e.g. web-search-*), allow + validated.add(name) + return validated + + async def get_sources_from_items( request, items, @@ -1147,6 +1185,13 @@ async def get_sources_from_items( log.debug(f'skipping {item} as it has already been extracted') continue + # Filter out collections the user cannot read + if user: + collection_names = await filter_accessible_collections(collection_names, user) + if not collection_names: + log.debug(f'access denied for all collections in item {item}') + continue + try: if full_context: # Sync helper makes blocking VECTOR_DB_CLIENT calls; diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index af61d7f38..85686c4fc 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -82,6 +82,7 @@ from open_webui.retrieval.web.yandex import search_yandex from open_webui.retrieval.web.ydc import search_youcom from open_webui.retrieval.utils import ( + filter_accessible_collections, get_content_from_url, get_embedding_function, get_reranking_function, @@ -2350,50 +2351,19 @@ async def process_web_search(request: Request, form_data: SearchForm, user=Depen async def _validate_collection_access(collection_names: list[str], user, access_type: str = 'read') -> None: """ - Prevent users from accessing collections they don't own. - Enforces ownership on user-memory-*, file-*, and knowledge-base collections. - Admins bypass this check. + Raise 403 if the user lacks access to any of the requested collections. + Delegates to the shared filter_accessible_collections utility so the + access rules stay in one place. """ - if user.role == 'admin': - return + requested = set(collection_names) + allowed = await filter_accessible_collections(requested, user, access_type=access_type) + denied = requested - allowed + if denied: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) - for name in collection_names: - # The 'knowledge-bases' meta-collection stores embedded metadata - # (names, descriptions, UUIDs) for every KB in the instance. - # Querying it would let any user enumerate all knowledge bases. - if name == 'knowledge-bases': - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - elif name.startswith('user-memory-') and name != f'user-memory-{user.id}': - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - elif name.startswith('file-'): - file_id = name[len('file-') :] - if not await has_access_to_file( - file_id=file_id, - access_type=access_type, - user=user, - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) - else: - # Non-prefixed collection names may be knowledge base IDs. - # Verify the caller has the required permission on the knowledge base. - knowledge = await Knowledges.get_knowledge_by_id(name) - if knowledge is not None: - if not await Knowledges.check_access_by_user_id( - name, user.id, permission=access_type - ): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=ERROR_MESSAGES.ACCESS_PROHIBITED, - ) class QueryDocForm(BaseModel):