fix: deny chained access to unregistered base models for non-admins (#26905)

A workspace model shared publicly could be used by any user even when its
base model was private. Unregistered base models (no row in the model
table) are admin-only for direct use — get_filtered_models hides them from
non-admins and check_model_access rejects them — but has_base_model_access
treated a missing row as "no ACL" and allowed the chained request through.

has_base_model_access now takes the caller's role and only allows an
unregistered base model hop for admins, so a shared preset can no longer
reach a base model the caller could not use directly. Registered base
models keep their existing grant-based enforcement.


Claude-Session: https://claude.ai/code/session_018toPfJW1hMXAhokGaL43Ep

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Classic298andClaude authored and GitHub committed 2026-07-24 01:32:32 -05:00
1 parent f7e7f32102
commit fe4b319428
2 files changed
+11 -6

No files matched your search

@@ -276,6 +276,7 @@ async def has_base_model_access(
user_id: str, user_id: str,
model_info, model_info,
*, *,
user_role: str | None = None,
user_group_ids: set[str] | None = None, user_group_ids: set[str] | None = None,
db=None, db=None,
) -> bool: ) -> bool:
@@ -283,9 +284,11 @@ async def has_base_model_access(
Walk the ``base_model_id`` chain and verify the caller has read access Walk the ``base_model_id`` chain and verify the caller has read access
at every hop. at every hop.
Returns ``True`` when access is granted (or the chain ends at a raw A base model without a ``model`` table row is admin-only, matching how
provider model that has no per-model ACL). Returns ``False`` the unregistered models are treated for direct use (``get_filtered_models``
moment a registered base model denies access. hides them from non-admins and ``check_model_access`` rejects them), so
a shared preset cannot be used to reach a base model the caller could
not use directly. Returns ``False`` the moment any hop denies access.
""" """
from open_webui.models.access_grants import AccessGrants from open_webui.models.access_grants import AccessGrants
from open_webui.models.models import Models from open_webui.models.models import Models
@@ -296,7 +299,7 @@ async def has_base_model_access(
seen.add(base_model_id) seen.add(base_model_id)
base_model_info = await Models.get_model_by_id(base_model_id, db=db) base_model_info = await Models.get_model_by_id(base_model_id, db=db)
if base_model_info is None: if base_model_info is None:
break # Raw provider model — no per-model ACL return user_role == 'admin'
if not ( if not (
user_id == base_model_info.user_id user_id == base_model_info.user_id
or await AccessGrants.has_access( or await AccessGrants.has_access(
@@ -355,7 +358,9 @@ async def check_model_access(
raise HTTPException(status_code=403, detail='Model not found') raise HTTPException(status_code=403, detail='Model not found')
# Enforce access on chained base models # Enforce access on chained base models
if not await has_base_model_access(user.id, model_info, user_group_ids=user_group_ids): if not await has_base_model_access(
user.id, model_info, user_role=user.role, user_group_ids=user_group_ids
):
raise HTTPException(status_code=403, detail='Model not found') raise HTTPException(status_code=403, detail='Model not found')
else: else:
if user.role != 'admin': if user.role != 'admin':
+1 -1
View File
@@ -456,7 +456,7 @@ async def check_model_access(user, model, db=None):
raise Exception('Model not found') raise Exception('Model not found')
# Enforce access on chained base models # Enforce access on chained base models
if not await has_base_model_access(user.id, model_info, db=db): if not await has_base_model_access(user.id, model_info, user_role=user.role, db=db):
raise Exception('Model not found') raise Exception('Model not found')