mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
a58f526992
* fix(presets): scope expand-prompt model resolution to owner /api/presets/expand resolved its model endpoint with no owner, so in a multi-user setup it could match another user's endpoint and use its URL and decrypted api_key. Pass effective_user(request) to _resolve_model so resolution is owner-scoped. Adds a regression test. * fix(presets): scope teacher and audit model resolution to owner Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Alex Little <alexwilliamlittle@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Kenny Van de Maele <kenny@kvandemaele.be>
87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
"""Route-level owner-scope test for POST /api/presets/expand.
|
|
|
|
`expand_character_prompt` resolves a model endpoint to run its LLM call. It must
|
|
scope that lookup to the calling user, otherwise it can resolve another owner's
|
|
ModelEndpoint (and its decrypted api_key) in a multi-user deployment. See #2283.
|
|
"""
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
from routes.preset_routes import setup_preset_routes
|
|
|
|
|
|
class _FakeRequest:
|
|
"""Minimal stand-in: an async ``json()`` body plus a ``state`` namespace."""
|
|
|
|
def __init__(self, body, **state):
|
|
self._body = body
|
|
self.state = SimpleNamespace(**state)
|
|
|
|
async def json(self):
|
|
return self._body
|
|
|
|
|
|
def _expand_endpoint():
|
|
router = setup_preset_routes(MagicMock())
|
|
for route in router.routes:
|
|
if getattr(route, "path", "") == "/api/presets/expand" and "POST" in getattr(route, "methods", set()):
|
|
return route.endpoint
|
|
raise AssertionError("POST /api/presets/expand route not registered")
|
|
|
|
|
|
def _patch_model_pipeline(monkeypatch):
|
|
"""Capture the owner passed to _resolve_model and stub the LLM call."""
|
|
seen = {}
|
|
|
|
def fake_resolve_model(spec, owner=None):
|
|
seen["spec"] = spec
|
|
seen["owner"] = owner
|
|
return ("http://endpoint.local/v1", "test-model", {})
|
|
|
|
async def fake_llm_call_async(url, model, messages, **kwargs):
|
|
return " expanded prompt "
|
|
|
|
monkeypatch.setattr("src.ai_interaction._resolve_model", fake_resolve_model)
|
|
monkeypatch.setattr("src.llm_core.llm_call_async", fake_llm_call_async)
|
|
return seen
|
|
|
|
|
|
def test_expand_scopes_model_resolution_to_cookie_user(monkeypatch):
|
|
seen = _patch_model_pipeline(monkeypatch)
|
|
endpoint = _expand_endpoint()
|
|
|
|
req = _FakeRequest({"name": "Pirate", "prompt": "talks like a pirate", "model": "test-model"},
|
|
current_user="alice")
|
|
result = asyncio.run(endpoint(req))
|
|
|
|
assert seen["owner"] == "alice"
|
|
assert seen["spec"] == "test-model"
|
|
assert result == {"success": True, "prompt": "expanded prompt"}
|
|
|
|
|
|
def test_expand_attributes_bearer_token_to_its_owner(monkeypatch):
|
|
# effective_user (not get_current_user) resolves a bearer ody_ caller to the
|
|
# token's real owner instead of the sandbox "api" pseudo-user.
|
|
seen = _patch_model_pipeline(monkeypatch)
|
|
endpoint = _expand_endpoint()
|
|
|
|
req = _FakeRequest({"name": "Pirate", "model": ""},
|
|
current_user="api", api_token=True, api_token_owner="bob")
|
|
asyncio.run(endpoint(req))
|
|
|
|
assert seen["owner"] == "bob"
|
|
|
|
|
|
def test_expand_short_circuits_without_input(monkeypatch):
|
|
seen = _patch_model_pipeline(monkeypatch)
|
|
endpoint = _expand_endpoint()
|
|
|
|
req = _FakeRequest({}, current_user="alice")
|
|
result = asyncio.run(endpoint(req))
|
|
|
|
# Nothing to expand: no model resolution attempted.
|
|
assert result["success"] is False
|
|
assert "owner" not in seen
|