diff --git a/litellm/proxy/credential_endpoints/endpoints.py b/litellm/proxy/credential_endpoints/endpoints.py index dd369554544..165b1b9e666 100644 --- a/litellm/proxy/credential_endpoints/endpoints.py +++ b/litellm/proxy/credential_endpoints/endpoints.py @@ -306,7 +306,8 @@ async def get_credentials( from litellm.proxy.proxy_server import prisma_client try: - if _is_proxy_admin(user_api_key_dict): + is_proxy_admin = _is_proxy_admin(user_api_key_dict) + if is_proxy_admin: visible = list(litellm.credential_list) else: scope = await _caller_admin_scope(user_api_key_dict, prisma_client) @@ -331,7 +332,14 @@ async def get_credentials( masked_credentials = [ { "credential_name": credential.credential_name, - "credential_values": _get_masked_values(credential.credential_values), + "credential_values": ( + { + **_get_masked_values(credential.credential_values), + "otel_headers": "********", + } + if not is_proxy_admin and "otel_headers" in credential.credential_values + else _get_masked_values(credential.credential_values) + ), "credential_info": credential.credential_info, } for credential in visible diff --git a/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py b/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py index a62e991391d..2874dedd3d4 100644 --- a/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py @@ -617,6 +617,44 @@ async def test_get_credentials_shows_only_in_scope_destinations_for_non_admin( assert names == ["poc-langfuse"] +@pytest.mark.asyncio +async def test_get_credentials_masks_otel_headers_only_for_non_admin( + monkeypatch, _patch_team_admin_lookup +): + raw_headers = "Authorization=Bearer collector-secret,x-api-key=api-secret" + monkeypatch.setattr( + litellm, + "credential_list", + [ + CredentialItem( + credential_name="generic-otel", + credential_values={ + "otel_endpoint": "https://collector.example.com/v1/traces", + "otel_headers": raw_headers, + }, + credential_info={ + "credential_type": "logging", + "description": "generic", + "access": {"teams": ["team-existing"]}, + }, + ), + ], + ) + _patch_team_admin_lookup["ids"] = frozenset({"team-existing"}) + + response = await endpoints.get_credentials( + request=MagicMock(), + fastapi_response=MagicMock(), + user_api_key_dict=_team_admin_of(["team-existing"]), + ) + + values = response["credentials"][0]["credential_values"] + assert values == { + "otel_endpoint": "https://collector.example.com/v1/traces", + "otel_headers": "********", + } + + @pytest.mark.asyncio async def test_get_credentials_hides_out_of_scope_destination( monkeypatch, _patch_team_admin_lookup @@ -822,6 +860,7 @@ async def test_patch_credentials_echoes_foreign_team_id_to_legit_team_admin( @pytest.mark.asyncio async def test_get_credentials_returns_all_for_proxy_admin(monkeypatch): + raw_headers = "Authorization=Bearer collector-secret,x-api-key=api-secret" monkeypatch.setattr( litellm, "credential_list", @@ -836,6 +875,14 @@ async def test_get_credentials_returns_all_for_proxy_admin(monkeypatch): credential_values={"public_key": "pk-1"}, credential_info=_DEST_WITH_TEAMS, ), + CredentialItem( + credential_name="generic-otel", + credential_values={"otel_headers": raw_headers}, + credential_info={ + "credential_type": "logging", + "description": "generic", + }, + ), ], ) response = await endpoints.get_credentials( @@ -844,7 +891,11 @@ async def test_get_credentials_returns_all_for_proxy_admin(monkeypatch): user_api_key_dict=_admin(), ) names = sorted(c["credential_name"] for c in response["credentials"]) - assert names == ["openai", "poc-langfuse"] + assert names == ["generic-otel", "openai", "poc-langfuse"] + generic = next( + c for c in response["credentials"] if c["credential_name"] == "generic-otel" + ) + assert generic["credential_values"]["otel_headers"] == raw_headers @pytest.mark.asyncio