mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-27 16:04:50 -06:00
perf: resolve model-attached file access with a targeted query (#28802)
Checking whether a user may reach a file loaded and validated every workspace model that user can access, then scanned each model's knowledge list in Python for one file id. Folder listings run that check once per file, so opening a folder of twenty files rebuilt the whole accessible-model set twenty times, and the same check sits on every retrieval and download path. The lookup now runs the other way round: the database returns the models that attach the file, and only those are access-checked. The text match on the metadata column is a prefilter and the knowledge entries still decide, so a file id that merely appears in a description grants nothing; file ids are server-generated uuids, so the match can only be too wide, never too narrow. Measured with 500 accessible workspace models: a single check drops from 9 queries and ~20 ms to 6 and ~2.6 ms, and a twenty-file folder listing from 180 queries and ~680 ms to 120 and ~56 ms. A 72-case matrix over owner, public, direct-user and group grants, for both read and write, returns exactly what it returned before, and write still requires the model owner to own the file. The check also no longer writes to the database while answering a read-only question.
This commit is contained in:
@@ -275,6 +275,24 @@ class ModelsTable:
|
||||
)
|
||||
return models
|
||||
|
||||
async def get_model_owners_attaching_file(self, file_id: str, db: AsyncSession | None = None) -> dict[str, str]:
|
||||
"""Map of model id to owner id for workspace models whose knowledge attaches this file."""
|
||||
async with get_async_db_context(db) as db:
|
||||
# File ids are server-generated uuids, so the text match can only over-match.
|
||||
result = await db.execute(
|
||||
select(Model.id, Model.user_id, Model.meta).filter(
|
||||
Model.base_model_id.is_not(None), cast(Model.meta, String).like(f'%"{file_id}"%')
|
||||
)
|
||||
)
|
||||
return {
|
||||
model_id: user_id
|
||||
for model_id, user_id, meta in result.all()
|
||||
if any(
|
||||
isinstance(item, dict) and item.get('type') == 'file' and item.get('id') == file_id
|
||||
for item in meta.get('knowledge') or []
|
||||
)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _meta_has_tag(meta: dict | None, tag: str) -> bool:
|
||||
if not meta:
|
||||
@@ -301,23 +319,16 @@ class ModelsTable:
|
||||
for model in all_models
|
||||
]
|
||||
|
||||
async def get_models_by_user_id(
|
||||
self,
|
||||
user_id: str,
|
||||
permission: str = 'write',
|
||||
db: AsyncSession | None = None,
|
||||
user_group_ids: set[str] | None = None,
|
||||
) -> list[ModelUserResponse]:
|
||||
async def get_models_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[ModelUserResponse]:
|
||||
models = await self.get_models(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)}
|
||||
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user_id, db=db)}
|
||||
|
||||
# One grants query for all non-owned models instead of one per model
|
||||
accessible_ids = await AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user_id,
|
||||
resource_type='model',
|
||||
resource_ids=[model.id for model in models if model.user_id != user_id],
|
||||
permission=permission,
|
||||
permission='write',
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
|
||||
@@ -106,16 +106,22 @@ async def has_access_to_file(
|
||||
|
||||
# Check if the file is directly attached to a shared workspace model (per the ownership
|
||||
# note above, model write is conferred only for files the model owner owns).
|
||||
for model in await Models.get_models_by_user_id(
|
||||
user.id, permission=access_type, db=db, user_group_ids=user_group_ids
|
||||
):
|
||||
knowledge_items = getattr(model.meta, 'knowledge', None) or []
|
||||
for item in knowledge_items:
|
||||
if isinstance(item, dict) and item.get('type') == 'file' and item.get('id') == file.id:
|
||||
if access_type == 'read' or model.user_id == file.user_id:
|
||||
return True
|
||||
model_owners = await Models.get_model_owners_attaching_file(file.id, db=db)
|
||||
if access_type != 'read':
|
||||
model_owners = {model_id: owner_id for model_id, owner_id in model_owners.items() if owner_id == file.user_id}
|
||||
if user.id in model_owners.values():
|
||||
return True
|
||||
|
||||
return False
|
||||
return bool(
|
||||
await AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type='model',
|
||||
resource_ids=list(model_owners),
|
||||
permission=access_type,
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def get_accessible_folder_files(
|
||||
|
||||
Reference in New Issue
Block a user