Codex gate caught a cross-profile leak: _CREDENTIAL_POOL_CACHE was keyed by provider id alone, so a custom provider configured under profile A made profile B report has_key=True from a stale cache hit (then 401 at request). Key the cache by (active-profile auth-store path, provider id) at all get/set/evict sites via _credential_pool_profile_tag(). Also make the two new bare _entry.runtime_api_key reads getattr-safe (OAuth-only entries lack the attr), matching the guard the author already applied at providers.py. Adds a cross-profile isolation regression test. Co-authored-by: akrhin <akrhin@users.noreply.github.com>
84 lines
3.3 KiB
Python
84 lines
3.3 KiB
Python
"""Regression test: the credential-pool detection cache must be profile-scoped.
|
|
|
|
Guards the cross-profile leak Codex caught on #4247 — _CREDENTIAL_POOL_CACHE
|
|
was keyed by provider id alone, so a custom provider configured under profile A
|
|
made profile B (no pool entry) report has_key=True from a stale cache hit.
|
|
The cache key now includes the active profile's auth-store path.
|
|
|
|
Note: `_has_explicit_pool_credentials` does `from agent.credential_pool import
|
|
load_pool` at call time. The bundled `agent` package is importable locally but
|
|
NOT in CI's environment, so the test injects a stub `agent.credential_pool`
|
|
module into sys.modules rather than importing/monkeypatching the real one.
|
|
"""
|
|
import sys
|
|
import types
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
import api.config as config # noqa: E402
|
|
|
|
|
|
class _FakeEntry:
|
|
def __init__(self, source="config_yaml", label="custom:bothub", key_source="config_yaml"):
|
|
self.source = source
|
|
self.label = label
|
|
self.key_source = key_source
|
|
|
|
|
|
class _FakePool:
|
|
def __init__(self, entries):
|
|
self._entries = entries
|
|
|
|
def entries(self):
|
|
return list(self._entries)
|
|
|
|
|
|
def test_has_explicit_pool_credentials_is_profile_scoped(monkeypatch):
|
|
"""A pool loaded under profile A must not satisfy a lookup under profile B."""
|
|
config._CREDENTIAL_POOL_CACHE.clear()
|
|
|
|
# Profile A's auth store has a custom:bothub pool entry; profile B has none.
|
|
profile_pools = {
|
|
"/tmp/profileA/auth.json": _FakePool([_FakeEntry()]),
|
|
"/tmp/profileB/auth.json": _FakePool([]),
|
|
}
|
|
active = {"path": "/tmp/profileA/auth.json"}
|
|
|
|
monkeypatch.setattr(config, "_get_auth_store_path", lambda: active["path"])
|
|
monkeypatch.setattr(config, "_resolve_provider_alias", lambda p: p)
|
|
|
|
# Inject a stub `agent.credential_pool` so the in-function import resolves
|
|
# here regardless of whether the real bundled agent is installed (it is not
|
|
# on CI). Build a stub `agent` parent package too if needed.
|
|
def _fake_load_pool(provider):
|
|
return profile_pools[active["path"]]
|
|
|
|
fake_cp = types.ModuleType("agent.credential_pool")
|
|
fake_cp.load_pool = _fake_load_pool
|
|
fake_agent = sys.modules.get("agent")
|
|
created_agent = False
|
|
if fake_agent is None:
|
|
fake_agent = types.ModuleType("agent")
|
|
fake_agent.__path__ = [] # mark as package
|
|
monkeypatch.setitem(sys.modules, "agent", fake_agent)
|
|
created_agent = True
|
|
monkeypatch.setitem(sys.modules, "agent.credential_pool", fake_cp)
|
|
# If a real `agent` package exists, temporarily point its attribute too.
|
|
if not created_agent:
|
|
monkeypatch.setattr(fake_agent, "credential_pool", fake_cp, raising=False)
|
|
|
|
# Under profile A: custom:bothub IS configured (real entry, not ambient gh_cli).
|
|
active["path"] = "/tmp/profileA/auth.json"
|
|
assert config._has_explicit_pool_credentials("custom:bothub") is True
|
|
|
|
# Switch to profile B (no pool entry). It must NOT inherit A's cached pool.
|
|
active["path"] = "/tmp/profileB/auth.json"
|
|
assert config._has_explicit_pool_credentials("custom:bothub") is False
|
|
|
|
# Back to A — still configured (its own cache key survives).
|
|
active["path"] = "/tmp/profileA/auth.json"
|
|
assert config._has_explicit_pool_credentials("custom:bothub") is True
|
|
|
|
config._CREDENTIAL_POOL_CACHE.clear()
|