diff --git a/litellm/proxy/credential_endpoints/endpoints.py b/litellm/proxy/credential_endpoints/endpoints.py index 04698380c22..92479e8d8e9 100644 --- a/litellm/proxy/credential_endpoints/endpoints.py +++ b/litellm/proxy/credential_endpoints/endpoints.py @@ -307,7 +307,8 @@ async def get_credentials( try: is_proxy_admin = _is_proxy_admin(user_api_key_dict) - if is_proxy_admin: + is_admin_viewer = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY + if is_proxy_admin or is_admin_viewer: visible = list(litellm.credential_list) else: scope = await _caller_admin_scope(user_api_key_dict, prisma_client) diff --git a/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py b/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py index 0c484c29f1a..4cc65720590 100644 --- a/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/credential_endpoints/test_endpoints.py @@ -900,6 +900,49 @@ async def test_get_credentials_returns_all_for_proxy_admin(monkeypatch): assert generic["credential_values"]["otel_headers"] == raw_headers +@pytest.mark.asyncio +async def test_get_credentials_admin_viewer_gets_full_list_fully_masked(monkeypatch): + """PROXY_ADMIN_VIEW_ONLY keeps read parity with PROXY_ADMIN on this endpoint: + the full credential list, including provider credentials, with every stored + value constant-masked so the read-only role receives no usable secret.""" + raw_headers = "Authorization=Bearer collector-secret,x-api-key=api-secret" + monkeypatch.setattr( + litellm, + "credential_list", + [ + CredentialItem( + credential_name="openai", + credential_values={"api_key": "sk-secret"}, + credential_info={"custom_llm_provider": "openai"}, + ), + CredentialItem( + credential_name="generic-otel", + credential_values={"otel_headers": raw_headers}, + credential_info={ + "credential_type": "logging", + "description": "generic", + }, + ), + ], + ) + response = await endpoints.get_credentials( + request=MagicMock(), + fastapi_response=MagicMock(), + user_api_key_dict=UserAPIKeyAuth( + api_key="k", user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY + ), + ) + names = sorted(c["credential_name"] for c in response["credentials"]) + assert names == ["generic-otel", "openai"] + assert all( + value == "********" + for c in response["credentials"] + for value in c["credential_values"].values() + ) + assert "collector-secret" not in str(response) + assert "sk-secret" not in str(response) + + @pytest.mark.asyncio async def test_authorize_patch_malformed_stored_access_does_not_500( _patch_team_admin_lookup,