From 149d136ab76efdd3f7475b49d0bec6a972b73bbc Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Wed, 4 Mar 2026 17:37:11 -0800 Subject: [PATCH] Merge sensitive fields from env vars when no DB record exists --- .../config_override_endpoints.py | 8 ++- .../test_config_override_endpoints.py | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/config_override_endpoints.py b/litellm/proxy/management_endpoints/config_override_endpoints.py index 2fd759b052b..8cee4d3ffc5 100644 --- a/litellm/proxy/management_endpoints/config_override_endpoints.py +++ b/litellm/proxy/management_endpoints/config_override_endpoints.py @@ -178,7 +178,7 @@ async def update_hashicorp_vault_config( config_data = config.model_dump(exclude_none=True) - # Merge with existing DB record: preserve sensitive fields the user didn't re-enter + # Merge sensitive fields the user didn't re-enter: try DB first, fall back to env vars existing_record = await prisma_client.db.litellm_configoverrides.find_unique( where={"config_type": "hashicorp_vault"} ) @@ -190,6 +190,12 @@ async def update_hashicorp_vault_config( for field in HASHICORP_SENSITIVE_FIELDS: if field not in config_data and existing_decrypted.get(field): config_data[field] = existing_decrypted[field] + else: + # No DB record yet — merge from current env vars + env_values = _get_current_env_values(HASHICORP_ENV_VAR_MAPPING) + for field in HASHICORP_SENSITIVE_FIELDS: + if field not in config_data and env_values.get(field): + config_data[field] = env_values[field] # Validate that the config has enough fields to initialize has_vault_addr = bool(config_data.get("vault_addr")) diff --git a/tests/test_litellm/proxy/management_endpoints/test_config_override_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_config_override_endpoints.py index ca0175bc2be..dd5b5edaa9b 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_config_override_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_config_override_endpoints.py @@ -348,6 +348,57 @@ async def test_update_hashicorp_config_preserves_existing_sensitive_fields( os.environ.pop("HCP_VAULT_APPROLE_ROLE_ID", None) +@pytest.mark.asyncio +async def test_update_hashicorp_config_merges_env_vars_when_no_db_record( + client, monkeypatch +): + """POST without sensitive fields and no DB record should merge from env vars.""" + mock_configoverrides = MagicMock() + mock_configoverrides.find_unique = AsyncMock(return_value=None) + mock_configoverrides.upsert = AsyncMock(return_value=None) + + mock_prisma = MagicMock() + mock_prisma.db = MagicMock() + mock_prisma.db.litellm_configoverrides = mock_configoverrides + + mock_proxy_config = MagicMock() + mock_proxy_config.initialize_secret_manager = MagicMock() + mock_proxy_config._last_hashicorp_vault_config = None + + monkeypatch.setattr(ps, "prisma_client", mock_prisma) + monkeypatch.setattr(ps, "proxy_config", mock_proxy_config) + + # Simulate existing env var token (set before UI was used) + monkeypatch.setenv("HCP_VAULT_TOKEN", "env-token") + + with patch( + "litellm.proxy.management_endpoints.config_override_endpoints.encrypt_value_helper" + ) as mock_encrypt: + mock_encrypt.side_effect = lambda v: f"enc_{v}" + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin_user" + ) + + try: + # Only send vault_addr — token should merge from env var + response = client.post( + "/config_overrides/hashicorp_vault", + json={"vault_addr": "https://vault.new.com"}, + ) + assert response.status_code == 200 + + upsert_call = mock_configoverrides.upsert.call_args + create_data = json.loads( + upsert_call.kwargs["data"]["create"]["config_value"] + ) + assert create_data["vault_addr"] == "https://vault.new.com" + assert create_data["vault_token"] == "enc_env-token" + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + os.environ.pop("HCP_VAULT_ADDR", None) + + @pytest.mark.asyncio async def test_update_hashicorp_config_init_failure_restores_env_vars( client, monkeypatch