fix: per-user model cache used static key= (cross-user model exposure) (#25783)

routers/openai.py and routers/ollama.py decorated get_all_models with
`@cached(key=lambda _, user: ...)`. In aiocache 0.12, `key=` is a STATIC
cache key: get_cache_key returns `self.key` verbatim (the lambda object)
without calling it, so every caller collides to ONE shared entry within
the TTL. The intended per-user namespacing never happened — one user's
permission-filtered model list could be served to another user (or an
anonymous caller) during the cache window.

The per-call hook is `key_builder=` (called as key_builder(func, *args,
**kwargs)). Switch both sites to key_builder with a (func, request,
user=None) signature so the key is built per call. user=None mirrors
ollama's optional-user signature and stays correct whether user is passed
positionally, as a kwarg, or omitted.

Verified offline: old form returns the same key object for distinct users
(collision); new form yields distinct openai_all_models_<id> /
ollama_all_models_<id> keys, and the unauthenticated base key when no user.
These two were the only @cached(key=lambda ...) sites in the backend.
This commit is contained in:
Classic298
2026-06-29 10:51:02 +02:00
committed by GitHub
parent ee11069ef2
commit 0fc630b34b
2 changed files with 6 additions and 2 deletions

View File

@@ -336,7 +336,9 @@ def resolve_api_config(api_configs: dict, idx: int, url: str) -> dict:
@cached(
ttl=MODELS_CACHE_TTL,
key=lambda _, user: f'ollama_all_models_{user.id}' if user else 'ollama_all_models',
# key_builder (not key) is the per-call hook in aiocache 0.12; `key=` is a
# static key, so a `key=lambda` collapsed every caller to one shared entry.
key_builder=lambda _func, request, user=None: f'ollama_all_models_{user.id}' if user else 'ollama_all_models',
)
async def get_all_models(request: Request, user: UserModel | None = None):
"""Aggregate model tags from every enabled Ollama backend."""

View File

@@ -522,7 +522,9 @@ async def get_filtered_models(models, user, db=None):
@cached(
ttl=MODELS_CACHE_TTL,
key=lambda _, user: f'openai_all_models_{user.id}' if user else 'openai_all_models',
# key_builder (not key) is the per-call hook in aiocache 0.12; `key=` is a
# static key, so a `key=lambda` collapsed every caller to one shared entry.
key_builder=lambda _func, request, user=None: f'openai_all_models_{user.id}' if user else 'openai_all_models',
)
async def get_all_models(request: Request, user: UserModel) -> dict[str, list]:
log.info('get_all_models()')