Merge pull request #26362 from BerriAI/litellm_fix_proxy_test_master_key_leak

[Fix] Tests - Proxy: Isolate master_key/prisma_client module globals between tests
This commit is contained in:
yuneng-jiang 2026-04-24 10:04:09 -07:00 committed by GitHub
commit 09f0a3380f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 39 deletions

View File

@ -65,12 +65,18 @@ _GIT_SUBDIR_SOURCE = {
}
@pytest.fixture(autouse=True)
def _patch_proxy_globals(monkeypatch):
"""Scope prisma_client/master_key mutations to each test via monkeypatch."""
monkeypatch.setattr(
litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma()
)
monkeypatch.setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
@pytest.mark.asyncio
async def test_register_plugin_git_subdir_success():
"""git-subdir with both url and path fields registers successfully."""
setattr(litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma())
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
request = RegisterPluginRequest(
name="my-monorepo-plugin", source=_GIT_SUBDIR_SOURCE
)
@ -86,9 +92,6 @@ async def test_register_plugin_git_subdir_success():
@pytest.mark.asyncio
async def test_register_plugin_git_subdir_update():
"""Registering the same git-subdir plugin twice returns action=updated."""
setattr(litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma())
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
request = RegisterPluginRequest(
name="my-monorepo-plugin", source=_GIT_SUBDIR_SOURCE, version="1.0.0"
)
@ -106,9 +109,6 @@ async def test_register_plugin_git_subdir_update():
@pytest.mark.asyncio
async def test_register_plugin_git_subdir_missing_url():
"""git-subdir without url field raises HTTP 400."""
setattr(litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma())
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
request = RegisterPluginRequest(
name="bad-plugin",
source={"source": "git-subdir", "path": "plugins/my-plugin"},
@ -124,9 +124,6 @@ async def test_register_plugin_git_subdir_missing_url():
@pytest.mark.asyncio
async def test_register_plugin_git_subdir_empty_url():
"""git-subdir with empty url raises HTTP 400."""
setattr(litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma())
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
request = RegisterPluginRequest(
name="bad-plugin",
source={"source": "git-subdir", "url": "", "path": "plugins/my-plugin"},
@ -142,9 +139,6 @@ async def test_register_plugin_git_subdir_empty_url():
@pytest.mark.asyncio
async def test_register_plugin_git_subdir_missing_path():
"""git-subdir without path field raises HTTP 400."""
setattr(litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma())
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
request = RegisterPluginRequest(
name="bad-plugin",
source={"source": "git-subdir", "url": "https://github.com/org/monorepo.git"},
@ -160,9 +154,6 @@ async def test_register_plugin_git_subdir_missing_path():
@pytest.mark.asyncio
async def test_register_plugin_git_subdir_empty_path():
"""git-subdir with empty path raises HTTP 400."""
setattr(litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma())
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
request = RegisterPluginRequest(
name="bad-plugin",
source={
@ -182,9 +173,6 @@ async def test_register_plugin_git_subdir_empty_path():
@pytest.mark.asyncio
async def test_register_plugin_git_subdir_path_traversal():
"""git-subdir with path traversal segments raises HTTP 400."""
setattr(litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma())
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
for bad_path in [
"../../etc/passwd",
"../secrets",
@ -213,9 +201,6 @@ async def test_register_plugin_git_subdir_path_traversal():
@pytest.mark.asyncio
async def test_register_plugin_unknown_source_type():
"""Unknown source type raises HTTP 400 listing all valid types."""
setattr(litellm.proxy.proxy_server, "prisma_client", _make_mock_prisma())
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
request = RegisterPluginRequest(
name="bad-plugin",
source={"source": "ftp", "url": "ftp://example.com/repo"},

View File

@ -15,6 +15,40 @@ import yaml
from fastapi.testclient import TestClient
_PROXY_MODULE_GLOBALS_TO_ISOLATE = (
"master_key",
"prisma_client",
)
@pytest.fixture(autouse=True)
def _isolate_proxy_module_globals():
"""
Snapshot and restore module-level globals on litellm.proxy.proxy_server
that tests sometimes mutate via raw setattr (not monkeypatch).
Without this, a leaked value e.g. master_key set by a sibling test
flips the auth short-circuit in user_api_key_auth and causes unrelated
tests in the same xdist worker to return 401 instead of 200.
"""
from litellm.proxy import proxy_server
sentinel = object()
snapshot = {
name: getattr(proxy_server, name, sentinel)
for name in _PROXY_MODULE_GLOBALS_TO_ISOLATE
}
try:
yield
finally:
for name, value in snapshot.items():
if value is sentinel:
if hasattr(proxy_server, name):
delattr(proxy_server, name)
else:
setattr(proxy_server, name, value)
def build_cache_config(enable_cache: bool = True) -> Optional[Dict]:
"""
Build Redis cache configuration from environment variables.

View File

@ -113,19 +113,11 @@ def test_decode_realtime_token_payload_ephemeral_key_not_string():
@pytest.fixture
def proxy_app():
def proxy_app(monkeypatch):
from litellm.proxy import proxy_server
# master_key is a module-global — restore it on teardown so this fixture
# doesn't leak state into unrelated tests that share the same xdist worker
# (e.g. tests that assume master_key is None and send unauthenticated
# requests to the shared FastAPI app).
original_master_key = proxy_server.master_key
proxy_server.master_key = "sk-test-master-key"
try:
yield proxy_server.app
finally:
proxy_server.master_key = original_master_key
monkeypatch.setattr(proxy_server, "master_key", "sk-test-master-key")
return proxy_server.app
@pytest.fixture
@ -284,10 +276,6 @@ async def test_realtime_calls_success_with_valid_encrypted_token(
mock_pre_call_hook,
):
"""POST /v1/realtime/calls returns 201 with valid encrypted token from client_secrets."""
from litellm.proxy import proxy_server
proxy_server.master_key = "sk-test-master-key"
# Build a valid encrypted token (same format as client_secrets returns)
future_expires_at = int(time.time()) + 3600
token_payload = _encode_realtime_token_payload(