mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
fix: redact nested secrets in config field info for non-admins
/config/field/info returned structured general_settings fields verbatim to any admin-view caller, so a view-only admin reading database_args received the nested aws_web_identity_token (a DynamoDB role-assumption credential) in plaintext. Recurse into dict/list field values and redact secret leaves for non-PROXY_ADMIN callers, leaving non-secret siblings and full-admin reads unchanged
This commit is contained in:
parent
ad68c6b6a3
commit
686eb3c166
2 changed files with 86 additions and 6 deletions
|
|
@ -38,7 +38,7 @@ from typing import (
|
|||
import anyio
|
||||
import websockets
|
||||
import websockets.exceptions
|
||||
from pydantic import BaseModel, Json
|
||||
from pydantic import BaseModel, Json, JsonValue
|
||||
|
||||
from litellm._uuid import uuid
|
||||
from litellm.constants import (
|
||||
|
|
@ -15032,6 +15032,24 @@ def _is_secret_general_setting_field(field_name: str) -> bool:
|
|||
)
|
||||
|
||||
|
||||
def _redact_secret_values_in_obj(value: JsonValue) -> JsonValue:
|
||||
"""Recursively redact secret leaves inside a structured field so a nested
|
||||
credential (e.g. aws_web_identity_token under database_args) is never
|
||||
returned to a non-admin, while non-secret siblings stay visible"""
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: (
|
||||
"REDACTED"
|
||||
if _is_secret_general_setting_field(key)
|
||||
else _redact_secret_values_in_obj(sub)
|
||||
)
|
||||
for key, sub in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [_redact_secret_values_in_obj(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
@router.get(
|
||||
"/config/field/info",
|
||||
tags=["config.yaml"],
|
||||
|
|
@ -15087,11 +15105,11 @@ async def get_config_general_settings(
|
|||
# only a full PROXY_ADMIN sees raw secret-bearing fields; others
|
||||
# get them redacted
|
||||
field_value = general_settings[field_name]
|
||||
if (
|
||||
user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN
|
||||
and _is_secret_general_setting_field(field_name)
|
||||
):
|
||||
field_value = "REDACTED"
|
||||
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN:
|
||||
if _is_secret_general_setting_field(field_name):
|
||||
field_value = "REDACTED"
|
||||
elif isinstance(field_value, (dict, list)):
|
||||
field_value = _redact_secret_values_in_obj(field_value)
|
||||
return ConfigFieldInfo(field_name=field_name, field_value=field_value)
|
||||
else:
|
||||
raise HTTPException(
|
||||
|
|
|
|||
|
|
@ -248,6 +248,68 @@ def test_config_field_info_field_not_in_db(client, auth_as, mock_prisma, monkeyp
|
|||
assert "not in DB" in response.json().get("detail", {}).get("error", "")
|
||||
|
||||
|
||||
def test_config_field_info_redacts_nested_secret_for_view_only_admin(
|
||||
client, auth_as, mock_prisma, monkeypatch
|
||||
):
|
||||
"""A view-only admin reading a structured field must not receive nested
|
||||
credentials. database_args carries aws_web_identity_token (a DynamoDB
|
||||
role-assumption credential); it must come back redacted while non-secret
|
||||
siblings like region_name stay visible."""
|
||||
from litellm.proxy import proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
table = _install_litellm_config(mock_prisma)
|
||||
row = MagicMock()
|
||||
row.param_value = {
|
||||
"database_args": {
|
||||
"region_name": "us-east-1",
|
||||
"user_table_name": "LiteLLM_UserTable",
|
||||
"aws_web_identity_token": "sk-super-secret-token",
|
||||
}
|
||||
}
|
||||
table.find_first = AsyncMock(return_value=row)
|
||||
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
|
||||
|
||||
with auth_as(LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY):
|
||||
response = client.get(
|
||||
"/config/field/info", params={"field_name": "database_args"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
value = response.json()["field_value"]
|
||||
assert value["aws_web_identity_token"] == "REDACTED"
|
||||
assert value["region_name"] == "us-east-1"
|
||||
assert value["user_table_name"] == "LiteLLM_UserTable"
|
||||
|
||||
|
||||
def test_config_field_info_full_admin_sees_nested_secret(
|
||||
client, auth_as, mock_prisma, monkeypatch
|
||||
):
|
||||
"""The redaction must not over-redact for a full PROXY_ADMIN, who needs
|
||||
the real nested value to populate the edit form."""
|
||||
from litellm.proxy import proxy_server as ps
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
table = _install_litellm_config(mock_prisma)
|
||||
row = MagicMock()
|
||||
row.param_value = {
|
||||
"database_args": {
|
||||
"region_name": "us-east-1",
|
||||
"aws_web_identity_token": "sk-super-secret-token",
|
||||
}
|
||||
}
|
||||
table.find_first = AsyncMock(return_value=row)
|
||||
monkeypatch.setattr(ps, "prisma_client", mock_prisma)
|
||||
|
||||
with auth_as(LitellmUserRoles.PROXY_ADMIN):
|
||||
response = client.get(
|
||||
"/config/field/info", params={"field_name": "database_args"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
value = response.json()["field_value"]
|
||||
assert value["aws_web_identity_token"] == "sk-super-secret-token"
|
||||
assert value["region_name"] == "us-east-1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /config/list
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue