fix(proxy): keep an empty credential value empty after decryption

This commit is contained in:
mateo-berri 2026-09-03 17:39:01 -07:00
parent 56cf0cd223
commit 1caa2fa9c1
4 changed files with 54 additions and 5 deletions

View file

@ -23,13 +23,16 @@ from litellm.types.router import (
from litellm.types.utils import CredentialItem
def decrypted_or_stored(key: str, value: str) -> str:
"""The stored value decrypted, or as stored when it was never encrypted (a config.yaml value)."""
decrypted: Final = decrypt_value_helper(value=value, key=key)
return value if decrypted is None else decrypted
def _decrypted(db_credential: CredentialItem) -> CredentialItem:
"""The stored credential with every value decrypted, leaving already-plaintext values alone."""
decrypted_values: Final = MappingProxyType(
{
key: decrypt_value_helper(value=value, key=key) or value
for key, value in db_credential.credential_values.items()
}
{key: decrypted_or_stored(key, value) for key, value in db_credential.credential_values.items()}
)
return CredentialItem(
credential_name=db_credential.credential_name,

View file

@ -335,6 +335,7 @@ from litellm.proxy.common_utils.auth_cache_invalidation_pubsub import (
)
from litellm.proxy.common_utils.callback_utils import initialize_callbacks_on_proxy
from litellm.proxy.common_utils.config_sync_pubsub import ConfigSyncSubscriber
from litellm.proxy.common_utils.credential_hydration import decrypted_or_stored
from litellm.proxy.common_utils.debug_utils import init_verbose_loggers
from litellm.proxy.common_utils.debug_utils import router as debugging_endpoints_router
from litellm.proxy.common_utils.encrypt_decrypt_utils import (
@ -7933,7 +7934,7 @@ class ProxyConfig:
decrypted_credential_values: Final = {}
for k, v in credential_object.credential_values.items():
decrypted_credential_values[k] = decrypt_value_helper(value=v, key=k) or v
decrypted_credential_values[k] = decrypted_or_stored(k, v)
credential_object.credential_values = decrypted_credential_values
return credential_object

View file

@ -0,0 +1,28 @@
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import litellm
from litellm.proxy.common_utils.credential_hydration import hydrate_named_credential_authoritative
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
@pytest.mark.asyncio
async def test_authoritative_hydrate_returns_an_encrypted_empty_value_as_empty(monkeypatch):
monkeypatch.setenv("LITELLM_SALT_KEY", "sk-hydration-test-salt")
row = {
"credential_name": "openai-wif",
"credential_values": {
"api_base": encrypt_value_helper(""),
"openai_service_account_id": encrypt_value_helper("user-1"),
},
"credential_info": {"custom_llm_provider": "openai"},
}
prisma = MagicMock()
prisma.db.litellm_credentialstable.find_unique = AsyncMock(return_value=row)
with patch.object(litellm, "credential_list", []): # test-quality-ok: the row under test must win over memory
resolved = await hydrate_named_credential_authoritative("openai-wif", prisma)
assert resolved is not None
assert resolved.credential_values == {"api_base": "", "openai_service_account_id": "user-1"}

View file

@ -2345,6 +2345,23 @@ def test_ProxyConfig__add_deployment_resolves_env_refs_on_arbitrary_field(monkey
# ---------------------------------------------------------------------------
def test_ProxyConfig_decrypt_credentials_returns_an_encrypted_empty_value_as_empty(monkeypatch):
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
monkeypatch.setenv("LITELLM_SALT_KEY", "sk-decrypt-credentials-test-salt")
decrypted = ProxyConfig().decrypt_credentials(
{
"credential_name": "openai-wif",
"credential_values": {
"api_base": encrypt_value_helper(""),
"openai_service_account_id": encrypt_value_helper("user-1"),
},
"credential_info": {"custom_llm_provider": "openai"},
}
)
assert decrypted.credential_values == {"api_base": "", "openai_service_account_id": "user-1"}
def test_ProxyConfig_decrypt_model_list_from_db_returns_decrypted(monkeypatch):
monkeypatch.setattr(
"litellm.proxy.proxy_server.decrypt_value_helper",