mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(security): encrypt callback_vars on POST and redact on GET in team callback endpoint
This commit is contained in:
parent
74a7b9c9a2
commit
844c43bb63
2 changed files with 145 additions and 1 deletions
|
|
@ -19,6 +19,10 @@ from litellm.proxy._types import (
|
|||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_utils.callback_utils import (
|
||||
encrypt_logging_callback_vars,
|
||||
redact_sensitive_logging_metadata,
|
||||
)
|
||||
from litellm.proxy.management_helpers.utils import management_endpoint_wrapper
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -126,6 +130,7 @@ async def add_team_callbacks(
|
|||
team_callback_settings.append(data.model_dump())
|
||||
|
||||
team_metadata["logging"] = team_callback_settings
|
||||
encrypt_logging_callback_vars(team_metadata)
|
||||
team_metadata_json = json.dumps(team_metadata) # update team_metadata
|
||||
|
||||
new_team_row = await prisma_client.db.litellm_teamtable.update(
|
||||
|
|
@ -312,13 +317,22 @@ async def get_team_callbacks(
|
|||
# Convert to TeamCallbackMetadata object for consistent structure
|
||||
team_callback_settings_obj = TeamCallbackMetadata(**team_callback_settings)
|
||||
|
||||
redacted_metadata = redact_sensitive_logging_metadata(
|
||||
{"logging": [{"callback_vars": team_callback_settings_obj.callback_vars}]}
|
||||
)
|
||||
redacted_vars = (
|
||||
redacted_metadata["logging"][0]["callback_vars"]
|
||||
if redacted_metadata
|
||||
else team_callback_settings_obj.callback_vars
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"team_id": team_id,
|
||||
"success_callbacks": team_callback_settings_obj.success_callback,
|
||||
"failure_callbacks": team_callback_settings_obj.failure_callback,
|
||||
"callback_vars": team_callback_settings_obj.callback_vars,
|
||||
"callback_vars": redacted_vars,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../.."))
|
||||
|
||||
from litellm.proxy.management_endpoints.team_callback_endpoints import (
|
||||
add_team_callbacks,
|
||||
get_team_callbacks,
|
||||
)
|
||||
from litellm.proxy._types import AddTeamCallback, UserAPIKeyAuth
|
||||
|
||||
|
||||
def _make_team(metadata: dict) -> MagicMock:
|
||||
team = MagicMock()
|
||||
team.metadata = metadata
|
||||
return team
|
||||
|
||||
|
||||
def _make_request() -> MagicMock:
|
||||
req = MagicMock()
|
||||
req.headers = {}
|
||||
return req
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /team/{team_id}/callback — encrypt on write
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("litellm.proxy.proxy_server.prisma_client")
|
||||
async def test_add_team_callback_encrypts_callback_vars(mock_prisma, monkeypatch):
|
||||
"""callback_vars must be encrypted before writing to DB."""
|
||||
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
|
||||
|
||||
plaintext_secret = "sk-lf-supersecret99"
|
||||
existing_team = _make_team({"logging": []})
|
||||
mock_prisma.get_data = AsyncMock(return_value=existing_team)
|
||||
|
||||
captured = {}
|
||||
|
||||
async def fake_update(where, data):
|
||||
captured["data"] = data
|
||||
row = MagicMock()
|
||||
row.metadata = data.get("metadata", "{}")
|
||||
return row
|
||||
|
||||
mock_prisma.db = MagicMock()
|
||||
mock_prisma.db.litellm_teamtable = MagicMock()
|
||||
mock_prisma.db.litellm_teamtable.update = fake_update
|
||||
|
||||
data = AddTeamCallback(
|
||||
callback_name="langfuse",
|
||||
callback_type="success",
|
||||
callback_vars={"langfuse_secret_key": plaintext_secret},
|
||||
)
|
||||
|
||||
await add_team_callbacks(
|
||||
data=data,
|
||||
http_request=_make_request(),
|
||||
team_id="team-123",
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
)
|
||||
|
||||
written_metadata = json.loads(captured["data"]["metadata"])
|
||||
stored_secret = written_metadata["logging"][0]["callback_vars"]["langfuse_secret_key"]
|
||||
assert stored_secret != plaintext_secret, "plaintext secret must be encrypted before DB write"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /team/{team_id}/callback — redact on read
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("litellm.proxy.proxy_server.prisma_client")
|
||||
async def test_get_team_callbacks_redacts_callback_vars(mock_prisma, monkeypatch):
|
||||
"""callback_vars must be redacted (last-3-chars) in the GET response."""
|
||||
monkeypatch.setenv("LITELLM_SALT_KEY", "test-salt-key-1234567890123456")
|
||||
|
||||
existing_team = _make_team(
|
||||
{
|
||||
"callback_settings": {
|
||||
"success_callback": ["langfuse"],
|
||||
"callback_vars": {"langfuse_secret_key": "sk-lf-supersecret99"},
|
||||
}
|
||||
}
|
||||
)
|
||||
mock_prisma.get_data = AsyncMock(return_value=existing_team)
|
||||
|
||||
result = await get_team_callbacks(
|
||||
http_request=_make_request(),
|
||||
team_id="team-123",
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
)
|
||||
|
||||
secret = result["data"]["callback_vars"]["langfuse_secret_key"]
|
||||
assert secret != "sk-lf-supersecret99", "plaintext secret must not be returned"
|
||||
assert secret.startswith("..."), "redacted value must use ...XYZ format"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("litellm.proxy.proxy_server.prisma_client")
|
||||
async def test_get_team_callbacks_keeps_env_var_pointers(mock_prisma):
|
||||
"""os.environ/ references must pass through unredacted."""
|
||||
existing_team = _make_team(
|
||||
{
|
||||
"callback_settings": {
|
||||
"callback_vars": {
|
||||
"langfuse_secret_key": "os.environ/LANGFUSE_SECRET_KEY"
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
mock_prisma.get_data = AsyncMock(return_value=existing_team)
|
||||
|
||||
result = await get_team_callbacks(
|
||||
http_request=_make_request(),
|
||||
team_id="team-123",
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
)
|
||||
|
||||
assert (
|
||||
result["data"]["callback_vars"]["langfuse_secret_key"]
|
||||
== "os.environ/LANGFUSE_SECRET_KEY"
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue