security: fail closed on bearer endpoint credentials

This commit is contained in:
RaresKeY
2026-08-30 22:27:35 +00:00
parent c473240131
commit aee6655fbd
8 changed files with 194 additions and 26 deletions
+5
View File
@@ -59,6 +59,9 @@ def _load_webhook_routes_for_test(monkeypatch):
core_pkg.__path__ = []
core_db = types.ModuleType("core.database")
core_db.SessionLocal = object
# The production sync-chat path reuses the central bearer session
# validator, which imports the durable Session model at invocation time.
core_db.Session = object
core_db.Webhook = object
core_db.ModelEndpoint = object
core_middleware = types.ModuleType("core.middleware")
@@ -242,10 +245,12 @@ def _install_sync_chat_stubs(monkeypatch):
endpoint_resolver.normalize_base = lambda url: (url or "").strip().rstrip("/")
endpoint_resolver.build_chat_url = lambda base_url: f"{base_url}/chat/completions"
endpoint_resolver.build_models_url = lambda base_url: f"{base_url}/models"
endpoint_resolver.resolve_endpoint = lambda *args, **kwargs: None
endpoint_resolver.build_headers = lambda api_key, base_url: {"Authorization": f"Bearer {api_key}"}
llm_core = types.ModuleType("src.llm_core")
llm_core.llm_call_async = _llm_call_async
llm_core.normalize_model_id = lambda *args, **kwargs: None
core_models.ChatMessage = _ChatMessage
monkeypatch.setitem(sys.modules, "python_multipart", python_multipart)
+56 -14
View File
@@ -346,7 +346,7 @@ def test_bearer_session_model_is_checked_against_endpoint_inventory(monkeypatch)
@pytest.mark.asyncio
async def test_sync_chat_uses_cached_model_and_skips_provider_runtime_resolution(monkeypatch):
async def test_sync_chat_uses_cached_provider_auth_without_live_refresh(monkeypatch):
from routes import webhook_routes
from src import chatgpt_subscription, llm_core
@@ -367,9 +367,10 @@ async def test_sync_chat_uses_cached_model_and_skips_provider_runtime_resolution
monkeypatch.setattr(
chatgpt_subscription,
"resolve_runtime_credentials",
lambda *args, **kwargs: runtime_calls.append((args, kwargs)) or pytest.fail(
"bearer sync resolved provider credentials"
),
lambda *args, **kwargs: runtime_calls.append((args, kwargs)) or {
"base_url": endpoint.base_url,
"api_key": "cached-access-token",
},
)
llm_calls = []
@@ -410,24 +411,58 @@ async def test_sync_chat_uses_cached_model_and_skips_provider_runtime_resolution
result = await route(request=_Request(), body=body)
assert result["model"] == "allowed-model"
assert runtime_calls == []
assert runtime_calls == [
(("provider-auth",), {"owner": "alice", "allow_live_probes": False})
]
assert llm_calls[0]["headers"]["Authorization"] == "Bearer cached-access-token"
assert llm_calls[0]["allow_live_probes"] is False
def test_provider_runtime_guard_is_no_live_without_opening_credentials_db(monkeypatch):
def test_provider_runtime_guard_uses_cached_credentials_without_refresh(monkeypatch):
from src import chatgpt_subscription
row = SimpleNamespace(
access_token="cached-access-token",
base_url="https://chatgpt.com/backend-api/codex",
auth_mode="chatgpt",
)
class _Query:
def filter(self, *args, **kwargs):
return self
def first(self):
return row
class _Db:
def query(self, _model):
return _Query()
def refresh(self, _row):
raise AssertionError("cache-only provider resolution refreshed credentials")
def close(self):
return None
monkeypatch.setattr(
chatgpt_subscription,
"_database_handles",
lambda: pytest.fail("no-live provider guard opened the credentials database"),
lambda: (cdb.ProviderAuthSession, lambda: _Db(), lambda: None),
)
with pytest.raises(chatgpt_subscription.ChatGPTSubscriptionReauthRequired):
chatgpt_subscription.resolve_runtime_credentials(
"provider-auth",
owner="alice",
allow_live_probes=False,
)
monkeypatch.setattr(
chatgpt_subscription, "access_token_is_expiring", lambda token: False
)
monkeypatch.setattr(
chatgpt_subscription,
"refresh_oauth_tokens",
lambda *args, **kwargs: pytest.fail("cache-only provider resolution refreshed credentials"),
)
result = chatgpt_subscription.resolve_runtime_credentials(
"provider-auth", owner="alice", allow_live_probes=False
)
assert result["api_key"] == "cached-access-token"
assert result["base_url"] == "https://chatgpt.com/backend-api/codex"
def test_foreground_descriptors_propagate_no_live_to_provider_endpoint_resolution(monkeypatch):
@@ -446,10 +481,14 @@ def test_foreground_descriptors_propagate_no_live_to_provider_endpoint_resolutio
hidden_models=None,
)
monkeypatch.setattr(endpoint_resolver, "SessionLocal", lambda: _EndpointDb(endpoint))
runtime_calls = []
monkeypatch.setattr(
chatgpt_subscription,
"resolve_runtime_credentials",
lambda *args, **kwargs: pytest.fail("foreground descriptor resolved provider credentials"),
lambda *args, **kwargs: runtime_calls.append((args, kwargs)) or {
"base_url": endpoint.base_url,
"api_key": "cached-access-token",
},
)
descriptors = foreground_model_routing.build_foreground_route_descriptors(
@@ -461,6 +500,9 @@ def test_foreground_descriptors_propagate_no_live_to_provider_endpoint_resolutio
allow_live_probes=False,
)
assert descriptors[0]["endpoint_label"] in {"Subscription", "Selected route"}
assert runtime_calls == [
(("provider-auth",), {"owner": "alice", "allow_live_probes": False})
]
@pytest.mark.asyncio
+94
View File
@@ -0,0 +1,94 @@
"""Cycle-8 regressions for bearer provider-auth session repair."""
import json
from types import SimpleNamespace
import pytest
from fastapi import HTTPException
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import core.database as cdb
_CODEX_BASE = "https://chatgpt.com/backend-api/codex"
def _provider_db(monkeypatch):
from routes import chat_helpers
from src import chatgpt_subscription
engine = create_engine("sqlite:///:memory:")
cdb.Base.metadata.create_all(bind=engine)
test_session_local = sessionmaker(bind=engine, autoflush=False)
monkeypatch.setattr(chat_helpers, "SessionLocal", test_session_local)
monkeypatch.setattr(
chatgpt_subscription,
"_database_handles",
lambda: (cdb.ProviderAuthSession, test_session_local, cdb.utcnow_naive),
)
db = test_session_local()
db.add(cdb.ProviderAuthSession(
id="auth-1",
provider="chatgpt-subscription",
owner="alice",
base_url=_CODEX_BASE,
access_token="cached-access-token",
refresh_token="refresh-token",
auth_mode="chatgpt",
))
db.add(cdb.ModelEndpoint(
id="endpoint-1",
name="ChatGPT Subscription",
base_url=_CODEX_BASE,
api_key=None,
provider_auth_id="auth-1",
owner="alice",
is_enabled=True,
endpoint_kind="api",
cached_models=json.dumps(["gpt-5.5"]),
pinned_models=json.dumps(["gpt-5.5"]),
))
db.commit()
db.close()
return test_session_local
def _registered_session():
return SimpleNamespace(
endpoint_url=f"{_CODEX_BASE}/responses",
model="gpt-5.5",
model_endpoint_id="endpoint-1",
endpoint_provenance="registered",
headers={"Authorization": "Bearer stale-token"},
)
def test_bearer_validator_uses_owner_cached_provider_auth_without_refresh(monkeypatch):
from routes.chat_helpers import _validate_bearer_session_model
from src import chatgpt_subscription
_provider_db(monkeypatch)
monkeypatch.setattr(chatgpt_subscription, "access_token_is_expiring", lambda token: False)
monkeypatch.setattr(
chatgpt_subscription,
"refresh_oauth_tokens",
lambda *args, **kwargs: pytest.fail("bearer validation refreshed provider credentials"),
)
session = _registered_session()
assert _validate_bearer_session_model(session, owner="alice") == "gpt-5.5"
assert session.headers["Authorization"] == "Bearer cached-access-token"
def test_bearer_validator_rejects_provider_auth_when_cache_is_unusable(monkeypatch):
from routes.chat_helpers import _validate_bearer_session_model
from src import chatgpt_subscription
_provider_db(monkeypatch)
monkeypatch.setattr(chatgpt_subscription, "access_token_is_expiring", lambda token: True)
with pytest.raises(HTTPException) as exc:
_validate_bearer_session_model(_registered_session(), owner="alice")
assert exc.value.status_code == 401
+4 -2
View File
@@ -162,8 +162,10 @@ def test_bearer_session_lifecycle_routes_do_not_emit_webhook_or_event(monkeypatc
assert result.model == "stored-model"
create_openai = _latest_endpoint(router, "/api/session/openai", "POST")
openai_result = create_openai(request, name="OpenAI", model="gpt-4o", rag="false")
assert openai_result["model"] == "gpt-4o"
with pytest.raises(HTTPException) as exc:
create_openai(request, name="OpenAI", model="gpt-4o", rag="false")
assert exc.value.status_code == 403
assert len(manager.sessions) == 1
webhook_manager.fire_and_forget.assert_not_called()
assert events == []