From 7cf6051a74d36fffe07827aab91c5bd94966eec9 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:16:29 +0200 Subject: [PATCH] perf: resolve group membership once per folder listing instead of once per entry (#28810) Listing a user's folders re-checks which entries they may still see, and it resolved their group membership again for every folder, then again inside the collection and note branches for every entry. A comment in that helper claims one membership fetch for the whole listing, but the caller invokes it once per folder, so the claim never held. The listing now resolves membership once, and only when some folder actually carries entries, then threads it through the file, collection and note checks. Callers that do not supply it are unchanged and still resolve for themselves. Measured with twenty folders holding six files, two knowledge bases and two notes each: 245 queries and ~145 ms before, 186 and ~117 ms after. The folders returned, and the entries the integrity pass writes back, are unchanged. That was checked against entries the caller owns, entries shared through a group, entries shared with nobody, another user's files, and an unrecognised entry type. --- backend/open_webui/models/knowledge.py | 14 +++++++++++--- backend/open_webui/routers/folders.py | 8 +++++++- backend/open_webui/utils/access_control/files.py | 10 +++++++--- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/backend/open_webui/models/knowledge.py b/backend/open_webui/models/knowledge.py index 401265a1c6..848232d6f2 100644 --- a/backend/open_webui/models/knowledge.py +++ b/backend/open_webui/models/knowledge.py @@ -467,14 +467,22 @@ class KnowledgeTable: print('search_knowledge_files error:', e) return KnowledgeFileListResponse(items=[], total=0) - async def check_access_by_user_id(self, id, user_id, permission='write', db: Optional[AsyncSession] = None) -> bool: + async def check_access_by_user_id( + self, + id, + user_id, + permission='write', + db: Optional[AsyncSession] = None, + user_group_ids: set[str] | None = None, + ) -> bool: knowledge = await self.get_knowledge_by_id(id, db=db) if not knowledge: return False if knowledge.user_id == user_id: return True - user_groups = await Groups.get_groups_by_member_id(user_id, db=db) - user_group_ids = {group.id for group in user_groups} + if user_group_ids is None: + user_groups = await Groups.get_groups_by_member_id(user_id, db=db) + user_group_ids = {group.id for group in user_groups} return await AccessGrants.has_access( user_id=user_id, resource_type='knowledge', diff --git a/backend/open_webui/routers/folders.py b/backend/open_webui/routers/folders.py index c4ed8d0d4d..9a6ff1c4df 100644 --- a/backend/open_webui/routers/folders.py +++ b/backend/open_webui/routers/folders.py @@ -99,6 +99,10 @@ async def get_folders( folders = await Folders.get_folders_by_user_id(user.id, db=db) folder_ids = {folder.id for folder in folders} + user_group_ids = None + if user.role != 'admin' and any(folder.data and 'files' in folder.data for folder in folders): + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + # Verify folder data integrity folder_list = [] for folder in folders: @@ -106,7 +110,9 @@ async def get_folders( folder = await Folders.update_folder_parent_id_by_id_and_user_id(folder.id, user.id, None, db=db) if folder.data and 'files' in folder.data: - accessible_files = await get_accessible_folder_files(folder.data['files'], user, db=db) + accessible_files = await get_accessible_folder_files( + folder.data['files'], user, db=db, user_group_ids=user_group_ids + ) if len(accessible_files) != len(folder.data.get('files', [])): folder.data['files'] = accessible_files await Folders.update_folder_by_id_and_user_id( diff --git a/backend/open_webui/utils/access_control/files.py b/backend/open_webui/utils/access_control/files.py index 5d192db603..efc8b11222 100644 --- a/backend/open_webui/utils/access_control/files.py +++ b/backend/open_webui/utils/access_control/files.py @@ -128,6 +128,7 @@ async def get_accessible_folder_files( entries: list[dict] | None, user: UserModel, db: AsyncSession | None = None, + user_group_ids: set[str] | None = None, ) -> list[dict]: """Filter folder.data['files'] entries to those the caller can read. @@ -144,8 +145,8 @@ async def get_accessible_folder_files( if user.role == 'admin': return entries - # One group-membership fetch for the whole folder listing - user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} + if user_group_ids is None: + user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} accessible: list[dict] = [] for entry in entries: @@ -155,7 +156,9 @@ async def get_accessible_folder_files( if await has_access_to_file(entry_id, 'read', user, db=db, user_group_ids=user_group_ids): accessible.append(entry) elif entry_type == 'collection': - if await Knowledges.check_access_by_user_id(entry_id, user.id, 'read', db=db): + if await Knowledges.check_access_by_user_id( + entry_id, user.id, 'read', db=db, user_group_ids=user_group_ids + ): accessible.append(entry) elif entry_type == 'note': # Owner has no self-grant (notes are private by default), so check ownership too. @@ -169,6 +172,7 @@ async def get_accessible_folder_files( resource_type='note', resource_id=entry_id, permission='read', + user_group_ids=user_group_ids, db=db, ) ):