mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(proxy): mask default_litellm_params secrets for non-admin callers of /get/config/callbacks
default_litellm_params is merged into every completion call's kwargs, so an operator can put a shared api_key or an Authorization header under extra_headers there. Router.get_settings() now returns it (needed so the Admin UI's Router Settings page can display/edit it), but /get/config/callbacks forwarded router_settings verbatim regardless of caller role - unlike the callback and alerting env vars on the same response, which already redact for non-full-admin callers (e.g. PROXY_ADMIN_VIEW_ONLY). That let a read-only admin read another admin's upstream provider credentials. Add the same role gate used for callback/alerting env vars, scoped to default_litellm_params via the existing SensitiveDataMasker so any key/secret/token/auth-shaped field is masked for non-full-admin callers while full admins keep seeing the real value.
This commit is contained in:
parent
750e849d12
commit
6eff7264c0
2 changed files with 74 additions and 1 deletions
|
|
@ -14620,6 +14620,23 @@ def _apply_webhook_role_gate(webhook_map, is_full_admin: bool):
|
|||
return {alert_type: "REDACTED" for alert_type in webhook_map}
|
||||
|
||||
|
||||
def _apply_router_settings_role_gate(router_settings: dict, is_full_admin: bool) -> dict:
|
||||
"""
|
||||
``default_litellm_params`` is an open-ended kwargs dict (Router merges it into
|
||||
every completion call), so an admin can put a shared ``api_key`` or an
|
||||
``extra_headers`` Authorization token there. Unlike the other fields on
|
||||
this page, nothing else masks it before it reaches non-full-admin callers
|
||||
of /get/config/callbacks.
|
||||
"""
|
||||
default_litellm_params = router_settings.get("default_litellm_params")
|
||||
if is_full_admin or not isinstance(default_litellm_params, dict):
|
||||
return router_settings
|
||||
return {
|
||||
**router_settings,
|
||||
"default_litellm_params": SENSITIVE_DATA_MASKER.mask_dict(default_litellm_params),
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/config/field/info",
|
||||
tags=["config.yaml"],
|
||||
|
|
@ -15224,7 +15241,7 @@ async def get_config(
|
|||
if llm_router is None:
|
||||
_router_settings = {}
|
||||
else:
|
||||
_router_settings = llm_router.get_settings()
|
||||
_router_settings = _apply_router_settings_role_gate(llm_router.get_settings(), is_full_admin)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
|
|
|
|||
|
|
@ -924,6 +924,62 @@ def test_get_config_custom_callback_api_env_vars(monkeypatch):
|
|||
}
|
||||
|
||||
|
||||
def _get_config_with_default_litellm_params(monkeypatch, user_role):
|
||||
"""
|
||||
default_litellm_params is merged into every completion call, so an admin
|
||||
can put a shared api_key/extra_headers Authorization token there. Router
|
||||
exposes it verbatim via get_settings(); /get/config/callbacks must not
|
||||
forward that verbatim to callers who aren't full proxy admins.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import app, proxy_config, user_api_key_auth
|
||||
|
||||
mock_router = MagicMock()
|
||||
mock_router.get_settings.return_value = {
|
||||
"default_litellm_params": {
|
||||
"api_key": "sk-super-secret-upstream-key",
|
||||
"timeout": 30,
|
||||
},
|
||||
}
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router)
|
||||
monkeypatch.setattr(
|
||||
proxy_config,
|
||||
"get_config",
|
||||
AsyncMock(return_value={"litellm_settings": {}, "general_settings": {}, "environment_variables": {}}),
|
||||
)
|
||||
|
||||
original_overrides = app.dependency_overrides.copy()
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=user_role, api_key="sk-1234"
|
||||
)
|
||||
|
||||
client = TestClient(app)
|
||||
try:
|
||||
return client.get("/get/config/callbacks")
|
||||
finally:
|
||||
app.dependency_overrides = original_overrides
|
||||
|
||||
|
||||
def test_get_config_masks_default_litellm_params_secrets_for_non_admin(monkeypatch):
|
||||
response = _get_config_with_default_litellm_params(
|
||||
monkeypatch, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
default_litellm_params = response.json()["router_settings"]["default_litellm_params"]
|
||||
assert "sk-super-secret-upstream-key" not in json.dumps(default_litellm_params)
|
||||
# non-sensitive keys are unaffected
|
||||
assert default_litellm_params["timeout"] == 30
|
||||
|
||||
|
||||
def test_get_config_returns_default_litellm_params_unmasked_for_full_admin(monkeypatch):
|
||||
response = _get_config_with_default_litellm_params(monkeypatch, LitellmUserRoles.PROXY_ADMIN)
|
||||
|
||||
assert response.status_code == 200
|
||||
default_litellm_params = response.json()["router_settings"]["default_litellm_params"]
|
||||
assert default_litellm_params["api_key"] == "sk-super-secret-upstream-key"
|
||||
assert default_litellm_params["timeout"] == 30
|
||||
|
||||
|
||||
def test_get_config_returns_email_settings(monkeypatch):
|
||||
"""
|
||||
Regression for https://github.com/BerriAI/litellm/issues/19221
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue