mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
fix(models): scope API-token model listing (#4292)
This commit is contained in:
+11
-8
@@ -26,7 +26,7 @@ from src.endpoint_resolver import (
|
|||||||
build_models_url,
|
build_models_url,
|
||||||
build_headers,
|
build_headers,
|
||||||
)
|
)
|
||||||
from src.auth_helpers import _auth_disabled, owner_filter
|
from src.auth_helpers import _auth_disabled, effective_user, owner_filter
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -1255,13 +1255,16 @@ def setup_model_routes(model_discovery):
|
|||||||
# Require auth; "" is the unconfigured single-user mode, treated as
|
# Require auth; "" is the unconfigured single-user mode, treated as
|
||||||
# "see everything" by _fetch_models.
|
# "see everything" by _fetch_models.
|
||||||
try:
|
try:
|
||||||
from src.auth_helpers import get_current_user as _gcu
|
if getattr(request.state, "api_token", False):
|
||||||
owner = _gcu(request) or ""
|
scopes = set(getattr(request.state, "api_token_scopes", []) or [])
|
||||||
except Exception:
|
if "chat" not in scopes:
|
||||||
owner = ""
|
raise HTTPException(403, "API token is not scoped for chat")
|
||||||
# Reject anonymous in configured deployments — no leaking the model
|
if not getattr(request.state, "api_token_owner", None):
|
||||||
# list to unauthenticated callers.
|
raise HTTPException(403, "API token has no owner")
|
||||||
try:
|
owner = effective_user(request) or ""
|
||||||
|
|
||||||
|
# Reject anonymous in configured deployments — no leaking the model
|
||||||
|
# list to unauthenticated callers.
|
||||||
auth_mgr = getattr(request.app.state, "auth_manager", None)
|
auth_mgr = getattr(request.app.state, "auth_manager", None)
|
||||||
if not owner and not _auth_disabled() and auth_mgr is not None and getattr(auth_mgr, "is_configured", False):
|
if not owner and not _auth_disabled() and auth_mgr is not None and getattr(auth_mgr, "is_configured", False):
|
||||||
raise HTTPException(401, "Not authenticated")
|
raise HTTPException(401, "Not authenticated")
|
||||||
|
|||||||
@@ -1286,6 +1286,14 @@ class _ImmediateThread:
|
|||||||
self.target()
|
self.target()
|
||||||
|
|
||||||
|
|
||||||
|
class _NoopThread:
|
||||||
|
def __init__(self, target, daemon=None):
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _wait_for(predicate, timeout=2.0):
|
def _wait_for(predicate, timeout=2.0):
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
@@ -1313,6 +1321,7 @@ def _route_ep(
|
|||||||
pinned_models=None,
|
pinned_models=None,
|
||||||
refresh_mode="auto",
|
refresh_mode="auto",
|
||||||
refresh_timeout=None,
|
refresh_timeout=None,
|
||||||
|
owner=None,
|
||||||
):
|
):
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
id=id,
|
id=id,
|
||||||
@@ -1329,7 +1338,7 @@ def _route_ep(
|
|||||||
model_refresh_interval=None,
|
model_refresh_interval=None,
|
||||||
model_refresh_timeout=refresh_timeout,
|
model_refresh_timeout=refresh_timeout,
|
||||||
supports_tools=None,
|
supports_tools=None,
|
||||||
owner=None,
|
owner=owner,
|
||||||
created_at=None,
|
created_at=None,
|
||||||
updated_at=None,
|
updated_at=None,
|
||||||
)
|
)
|
||||||
@@ -1342,6 +1351,72 @@ def _route_request():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_models_rejects_api_token_without_chat_scope(monkeypatch):
|
||||||
|
router = model_routes.setup_model_routes(model_discovery=None)
|
||||||
|
|
||||||
|
def fail_session():
|
||||||
|
raise AssertionError("model DB should not be queried without chat scope")
|
||||||
|
|
||||||
|
monkeypatch.setattr(model_routes, "SessionLocal", fail_session)
|
||||||
|
|
||||||
|
request = SimpleNamespace(
|
||||||
|
state=SimpleNamespace(
|
||||||
|
current_user="api",
|
||||||
|
api_token=True,
|
||||||
|
api_token_owner="alice",
|
||||||
|
api_token_scopes=["documents:read"],
|
||||||
|
),
|
||||||
|
app=SimpleNamespace(
|
||||||
|
state=SimpleNamespace(
|
||||||
|
auth_manager=SimpleNamespace(is_configured=True, is_admin=lambda user: False),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
_route_endpoint(router, "/api/models")(request)
|
||||||
|
|
||||||
|
assert exc.value.status_code == 403
|
||||||
|
assert "chat" in str(exc.value.detail)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_models_scopes_api_token_to_token_owner(monkeypatch):
|
||||||
|
rows = [
|
||||||
|
_route_ep("alice", "http://alice.example/v1", cached_models=["alice-model"], owner="alice"),
|
||||||
|
_route_ep("shared", "http://shared.example/v1", cached_models=["shared-model"], owner=None),
|
||||||
|
_route_ep("bob", "http://bob.example/v1", cached_models=["bob-model"], owner="bob"),
|
||||||
|
]
|
||||||
|
db = _RouteDb(rows)
|
||||||
|
router = model_routes.setup_model_routes(model_discovery=None)
|
||||||
|
admin_checks = []
|
||||||
|
|
||||||
|
monkeypatch.setattr(model_routes, "ModelEndpoint", _RouteModelEndpoint)
|
||||||
|
monkeypatch.setattr(model_routes, "SessionLocal", lambda: db)
|
||||||
|
monkeypatch.setattr(threading, "Thread", _NoopThread)
|
||||||
|
|
||||||
|
request = SimpleNamespace(
|
||||||
|
state=SimpleNamespace(
|
||||||
|
current_user="api",
|
||||||
|
api_token=True,
|
||||||
|
api_token_owner="alice",
|
||||||
|
api_token_scopes=["chat"],
|
||||||
|
),
|
||||||
|
app=SimpleNamespace(
|
||||||
|
state=SimpleNamespace(
|
||||||
|
auth_manager=SimpleNamespace(
|
||||||
|
is_configured=True,
|
||||||
|
is_admin=lambda user: admin_checks.append(user) or False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _route_endpoint(router, "/api/models")(request)
|
||||||
|
|
||||||
|
assert [item["endpoint_name"] for item in result["items"]] == ["alice", "shared"]
|
||||||
|
assert admin_checks == ["alice"]
|
||||||
|
|
||||||
|
|
||||||
def test_api_models_returns_cached_proxy_models_without_refresh_probe(monkeypatch):
|
def test_api_models_returns_cached_proxy_models_without_refresh_probe(monkeypatch):
|
||||||
row = _route_ep(
|
row = _route_ep(
|
||||||
"proxy",
|
"proxy",
|
||||||
|
|||||||
Reference in New Issue
Block a user