mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-13 01:02:25 -06:00
perf: stop refetching the model row and user groups in the completion access check (#27378)
The chat completion entry point fetched the model row and then check_model_access immediately fetched the exact same row again. Inside the check, the direct grant lookup and every hop of the base-model chain each refetched the caller's group memberships, because neither call passed user_group_ids even though both AccessGrants.has_access and has_base_model_access already accept it. check_model_access now takes an optional prefetched model_info (used only when its id matches the requested model, so stale callers cannot bypass the lookup) and resolves the caller's group ids once, sharing them across the direct check and the whole base-model chain. The group fetch is skipped entirely for the owner-with-no-base-chain case, which previously needed no groups either. DB round trips for one completion-entry access check (non-owner model with one base-model hop): | queries | before | after | | --- | --- | --- | | model row SELECTs | 3 | 2 | | group membership SELECTs | 2 | 1 | For deeper base-model chains the before column grows by one group SELECT per hop; the after column stays at one. Functionally verified with stubbed model, group and grant accessors: owner fast path issues no group or grant queries; a non-owner with a base chain resolves groups once and passes the same set to every hop; a prefetched matching model_info skips the duplicate row fetch while a mismatched one is refetched; denial and unknown-model cases still raise; the arena path is unchanged.
This commit is contained in:
@@ -1071,7 +1071,7 @@ async def chat_completion(
|
||||
# Check if user has access to the model
|
||||
if not BYPASS_MODEL_ACCESS_CONTROL and (user.role != 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL):
|
||||
try:
|
||||
await check_model_access(user, model)
|
||||
await check_model_access(user, model, model_info=model_info)
|
||||
except Exception as e:
|
||||
raise e
|
||||
else:
|
||||
|
||||
@@ -428,7 +428,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None)
|
||||
return models
|
||||
|
||||
|
||||
async def check_model_access(user, model, db=None):
|
||||
async def check_model_access(user, model, model_info=None, db=None):
|
||||
if model.get('arena'):
|
||||
meta = model.get('info', {}).get('meta', {})
|
||||
access_grants = meta.get('access_grants', [])
|
||||
@@ -440,23 +440,35 @@ async def check_model_access(user, model, db=None):
|
||||
):
|
||||
raise Exception('Model not found')
|
||||
else:
|
||||
model_info = await Models.get_model_by_id(model.get('id'), db=db)
|
||||
# Callers that already fetched the row (chat completion entry) pass it in
|
||||
if model_info is None or model_info.id != model.get('id'):
|
||||
model_info = await Models.get_model_by_id(model.get('id'), db=db)
|
||||
if not model_info:
|
||||
raise Exception('Model not found')
|
||||
elif not (
|
||||
|
||||
# One group-membership fetch shared by the direct check and every
|
||||
# base-model hop; skipped when no check below needs it.
|
||||
user_group_ids = None
|
||||
if user.id != model_info.user_id or model_info.base_model_id:
|
||||
user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)}
|
||||
|
||||
if not (
|
||||
user.id == model_info.user_id
|
||||
or await AccessGrants.has_access(
|
||||
user_id=user.id,
|
||||
resource_type='model',
|
||||
resource_id=model_info.id,
|
||||
permission='read',
|
||||
user_group_ids=user_group_ids,
|
||||
db=db,
|
||||
)
|
||||
):
|
||||
raise Exception('Model not found')
|
||||
|
||||
# Enforce access on chained base models
|
||||
if not await has_base_model_access(user.id, model_info, user_role=user.role, db=db):
|
||||
if not await has_base_model_access(
|
||||
user.id, model_info, user_role=user.role, user_group_ids=user_group_ids, db=db
|
||||
):
|
||||
raise Exception('Model not found')
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user