First, smallest cut of a LAN companion bridge (split out of #855 per review): a thin, additive, read-only layer so a LAN client can discover what a server offers. No new LLM logic; auth is enforced by the existing AuthMiddleware. - GET /api/companion/ping -- cheap auth-validated health check - GET /api/companion/info -- server identity + capability flags - GET /api/companion/models -- the CALLER's own model endpoints /models scopes to the caller's real owner (the token's owner for bearer callers) plus legacy null-owner shared rows, mirroring owner_filter, and never returns api_key material. The owner rule lives in two pure helpers (token_owner, owner_can_see) with direct tests proving a token for owner A cannot see owner B's rows and that null-owner rows don't widen access.
79 lines
2.7 KiB
Python
79 lines
2.7 KiB
Python
"""Owner-scope tests for the read-only companion bridge.
|
|
|
|
Mirrors the direct-helper style of tests/test_null_owner_gates.py: exercise the
|
|
small pure helpers against mock request state and owner values, so the scoping
|
|
rule can't silently regress. A bearer token for owner A must never see owner B's
|
|
rows, and legacy null-owner rows must not widen a token's access.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import types
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
# core.database instantiates SQLAlchemy declarative classes at import time, which
|
|
# blows up under conftest's sqlalchemy MagicMock stubs. companion.routes only
|
|
# imports it lazily inside the /models handler, but stub it defensively so the
|
|
# import is robust regardless of collection order.
|
|
if "core.database" not in sys.modules:
|
|
_db = types.ModuleType("core.database")
|
|
_db.SessionLocal = MagicMock()
|
|
_db.ModelEndpoint = MagicMock()
|
|
sys.modules["core.database"] = _db
|
|
|
|
from companion.routes import token_owner, owner_can_see
|
|
|
|
|
|
def _request(**state):
|
|
return SimpleNamespace(state=SimpleNamespace(**state))
|
|
|
|
|
|
# --- token_owner: who a request is attributed to ---------------------------
|
|
|
|
def test_token_owner_bearer_resolves_to_token_owner():
|
|
# A paired bearer caller runs as the "api" pseudo-user, but must attribute
|
|
# to the token's real owner.
|
|
req = _request(api_token=True, api_token_owner="alice", current_user="api")
|
|
assert token_owner(req) == "alice"
|
|
|
|
|
|
def test_token_owner_cookie_uses_logged_in_user():
|
|
req = _request(api_token=False, current_user="alice")
|
|
assert token_owner(req) == "alice"
|
|
|
|
|
|
def test_token_owner_none_when_unresolved():
|
|
req = _request(api_token=True, api_token_owner=None, current_user="api")
|
|
assert token_owner(req) is None
|
|
|
|
|
|
# --- owner_can_see: the read-scope rule ------------------------------------
|
|
|
|
def test_owner_sees_their_own_rows():
|
|
assert owner_can_see("alice", "alice") is True
|
|
|
|
|
|
def test_null_owner_shared_rows_are_visible():
|
|
# Legacy shared rows (owner is None) are visible to everyone by design...
|
|
assert owner_can_see(None, "alice") is True
|
|
|
|
|
|
def test_null_owner_does_not_widen_access_to_others_rows():
|
|
# ...but a null-owner row must not be a backdoor to another OWNER's rows.
|
|
assert owner_can_see("bob", "alice") is False
|
|
|
|
|
|
def test_cross_owner_is_blocked():
|
|
assert owner_can_see("bob", "alice") is False
|
|
assert owner_can_see("alice", "bob") is False
|
|
|
|
|
|
def test_unauthenticated_owner_sees_only_shared_rows():
|
|
# owner=None (no resolved caller): only null-owner shared rows are visible,
|
|
# never any owned row.
|
|
assert owner_can_see(None, None) is True
|
|
assert owner_can_see("alice", None) is False
|