mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
4a9085d252
* Scope secondary endpoint lookups by owner * Reject unregistered image endpoint URLs for non-admins * Adjust owner-scope tests for rebased routes * Allow non-admins to compare endpoints they own The compare owner-scope guard called _reject_raw_endpoint_url_for_non_admin with endpoint_id=None, so it rejected every signed-in non-admin /api/compare/start request — even for endpoints the caller owns — because compare resolves endpoints by URL and carries no endpoint_id. That locked non-admins out of compare entirely. Resolve the owned ModelEndpoint first and pass its id, so a registered endpoint the caller owns is allowed while only truly raw, unregistered URLs are rejected (mirrors the gallery inpaint/harmonize checks in this PR). Replace the source-only reject test with deterministic reject + allow regressions that no longer depend on the dev DB contents. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Bind compare sessions to the resolved owner-scoped endpoint /api/compare/start created the [CMP] helper sessions with the raw caller-supplied endpoint URL and only used the owner-scoped lookup to decide whether to copy an API key. That stopped key borrowing but still let a non-admin inject an arbitrary raw endpoint URL into the compare session path. Now, when the supplied URL resolves to a registered endpoint visible to the caller, the session binds to that row's own normalized base URL (build_chat_url(normalize_base(ep.base_url))) plus its headers — the same registered-endpoint shape session_routes uses. The raw URL survives only when ep is None, which non-admins already hit a 403 on, leaving raw URLs reachable solely for admins / single-user mode with no borrowed key. Adds compare-specific behavior tests: another user's private endpoint is rejected (nothing created), the session binds to the stored URL rather than the raw input, and an admin raw URL is allowed but carries no inherited key. Addresses the review on #1511. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Validate both compare endpoints before creating any session start_comparison resolved + created each [CMP] session inside one loop, so a request pairing a valid owned endpoint A with an unregistered raw endpoint B raised 403 only after A's session was already created — and its Authorization header copied in. The rejected request left a partial compare session with that header behind. Split the flow into two phases: phase 1 resolves and owner-validates both endpoints (running the raw-URL reject helper) and stashes the session URL + headers; phase 2 creates the two sessions only once both passed. A 403 on either endpoint now aborts with nothing created and no header copied. Adds a regression test: owned endpoint A + unregistered/raw endpoint B -> 403 with no sessions created. Addresses the follow-up review on #1511. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Resolve compare credentials by endpoint id, not URL alone Two endpoints visible to a caller can share a base_url but hold different api_keys. _owned_endpoint_by_url returned whichever row sorted first, so /api/compare/start could copy the wrong key into the [CMP] session. Add _owned_endpoint_by_id (same owner scoping) and optional endpoint_a_id/ endpoint_b_id form fields. The id pins the exact registered endpoint; URL resolution remains only for legacy/admin raw-URL callers. An id the caller can't see 404s instead of falling back to a same-URL row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Loosen research-routes owner-scope assertion to the stable substring The rebased _resolve_research_endpoint generalized its owner derivation to honor an explicit owner arg first (owner = owner or getattr(sess, ...)), so the exact-line assertion broke CI. Assert the stable session-derivation substring instead of the full line. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
72 lines
2.9 KiB
Python
72 lines
2.9 KiB
Python
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _src(path: str) -> str:
|
|
return (ROOT / path).read_text(encoding="utf-8")
|
|
|
|
|
|
def test_registered_manual_compaction_uses_session_owner_for_utility_endpoint():
|
|
session_src = _src("routes/session_routes.py")
|
|
|
|
assert 'owner = getattr(session, "owner", None) or effective_user(request)' in session_src
|
|
assert 'resolve_endpoint("utility", owner=owner)' in session_src
|
|
|
|
|
|
def test_task_name_generation_uses_owner_scoped_session_endpoint():
|
|
src = _src("routes/task_routes.py")
|
|
|
|
assert "async def _generate_task_name(prompt: str, owner: Optional[str] = None)" in src
|
|
assert "q = q.filter(DbSession.owner == owner)" in src
|
|
assert "headers = recent.headers or {}" in src
|
|
assert "headers=headers" in src
|
|
assert "await _generate_task_name(req.prompt, owner=user)" in src
|
|
|
|
|
|
def test_auto_compaction_utility_endpoint_keeps_chat_owner():
|
|
helper_src = _src("routes/chat_helpers.py")
|
|
compact_src = _src("src/context_compactor.py")
|
|
|
|
assert "owner=user" in helper_src
|
|
assert "owner: Optional[str] = None" in compact_src
|
|
assert 'resolve_endpoint("utility", owner=owner)' in compact_src
|
|
|
|
|
|
def test_background_session_sort_uses_owner_task_endpoint():
|
|
src = _src("src/session_actions.py")
|
|
|
|
assert "resolve_task_endpoint(owner=owner or None)" in src
|
|
|
|
|
|
def test_scheduler_fallbacks_and_research_headers_are_owner_scoped():
|
|
src = _src("src/task_scheduler.py")
|
|
|
|
assert "resolve_utility_fallback_candidates(owner=task.owner or None)" in src
|
|
assert 'resolve_endpoint(\n "research",' in src
|
|
assert "owner=task.owner or None" in src
|
|
assert "headers_from_resolver = False" in src
|
|
assert "headers_from_resolver = True" in src
|
|
assert "from src.auth_helpers import owner_filter" in src
|
|
assert "owner_filter(ep_q, ModelEndpoint, task.owner or None)" in src
|
|
|
|
|
|
def test_research_routes_fallbacks_are_owner_scoped():
|
|
src = _src("routes/research_routes.py")
|
|
|
|
assert 'resolve_endpoint("research", owner=user)' in src
|
|
assert 'resolve_endpoint("utility", owner=user)' in src
|
|
assert 'resolve_endpoint("default", owner=user)' in src
|
|
assert 'resolve_endpoint("chat", owner=user)' in src
|
|
assert '_merge(*resolve_endpoint("chat", owner=user))' in src
|
|
assert '_merge(*resolve_endpoint("research", owner=user))' in src
|
|
assert '_merge(*resolve_endpoint("utility", owner=user))' in src
|
|
assert "ep = _owned_enabled_endpoint(db, user)" in src
|
|
assert "db.query(ModelEndpoint).filter(ModelEndpoint.is_enabled == True).first()" not in src
|
|
# _resolve_research_endpoint derives the scope from the session owner. The
|
|
# rebased code generalized this to honor an explicit `owner` argument first
|
|
# (``owner = owner or getattr(sess, "owner", None) or None``), so assert on
|
|
# the stable session-derivation substring rather than the exact line.
|
|
assert 'getattr(sess, "owner", None) or None' in src
|