mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(vault): cache the secret body per url so mutations evict every field
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
4694bd0c63
commit
8691a1e190
2 changed files with 31 additions and 17 deletions
|
|
@ -39,7 +39,6 @@ class _VaultSecretTarget(TypedDict):
|
|||
url: ReadOnly[str]
|
||||
data_key: ReadOnly[str]
|
||||
secret_name: ReadOnly[str]
|
||||
cache_key: ReadOnly[str]
|
||||
|
||||
|
||||
class _VaultSecretDataBlock(TypedDict, total=False):
|
||||
|
|
@ -369,7 +368,6 @@ class HashicorpSecretManager(BaseSecretManager):
|
|||
"url": url,
|
||||
"data_key": data_key,
|
||||
"secret_name": secret_name,
|
||||
"cache_key": f"{url}#{data_key}",
|
||||
}
|
||||
|
||||
def _get_request_headers(self) -> dict:
|
||||
|
|
@ -408,17 +406,16 @@ class HashicorpSecretManager(BaseSecretManager):
|
|||
)
|
||||
try:
|
||||
target: Final = self._build_secret_target(secret_name, optional_params)
|
||||
cached_value: Final = self.cache.get_cache(target["cache_key"])
|
||||
if cached_value is not None:
|
||||
return cached_value
|
||||
cached_body: Final = self.cache.get_cache(target["url"])
|
||||
if cached_body is not None:
|
||||
return self._get_secret_value_from_json_response(cached_body, target["data_key"])
|
||||
|
||||
response: Final = await async_client.get(target["url"], headers=self._get_request_headers())
|
||||
response.raise_for_status()
|
||||
|
||||
json_resp: Final = _json_object_body(response)
|
||||
_value: Final = self._get_secret_value_from_json_response(json_resp, target["data_key"])
|
||||
self.cache.set_cache(target["cache_key"], _value)
|
||||
return _value
|
||||
self.cache.set_cache(target["url"], json_resp)
|
||||
return self._get_secret_value_from_json_response(json_resp, target["data_key"])
|
||||
|
||||
except Exception as e:
|
||||
verbose_logger.exception("Error reading secret from Hashicorp Vault: %s", e)
|
||||
|
|
@ -438,17 +435,16 @@ class HashicorpSecretManager(BaseSecretManager):
|
|||
sync_client: Final = _get_httpx_client()
|
||||
try:
|
||||
target: Final = self._build_secret_target(secret_name, optional_params)
|
||||
cached_value: Final = self.cache.get_cache(target["cache_key"])
|
||||
if cached_value is not None:
|
||||
return cached_value
|
||||
cached_body: Final = self.cache.get_cache(target["url"])
|
||||
if cached_body is not None:
|
||||
return self._get_secret_value_from_json_response(cached_body, target["data_key"])
|
||||
|
||||
response: Final = sync_client.get(target["url"], headers=self._get_request_headers())
|
||||
response.raise_for_status()
|
||||
|
||||
json_resp: Final = _json_object_body(response)
|
||||
_value: Final = self._get_secret_value_from_json_response(json_resp, target["data_key"])
|
||||
self.cache.set_cache(target["cache_key"], _value)
|
||||
return _value
|
||||
self.cache.set_cache(target["url"], json_resp)
|
||||
return self._get_secret_value_from_json_response(json_resp, target["data_key"])
|
||||
|
||||
except Exception as e:
|
||||
verbose_logger.exception("Error reading secret from Hashicorp Vault: %s", e)
|
||||
|
|
@ -637,10 +633,10 @@ class HashicorpSecretManager(BaseSecretManager):
|
|||
)
|
||||
else:
|
||||
# Clear cache for the old secret only if deletion was successful
|
||||
self.cache.delete_cache(current_target["cache_key"])
|
||||
self.cache.delete_cache(current_target["url"])
|
||||
|
||||
# Clear cache for the new secret (or updated secret if names are the same)
|
||||
self.cache.delete_cache(new_target["cache_key"])
|
||||
self.cache.delete_cache(new_target["url"])
|
||||
|
||||
return create_response
|
||||
|
||||
|
|
@ -681,7 +677,7 @@ class HashicorpSecretManager(BaseSecretManager):
|
|||
response: Final = await async_client.delete(url=target["url"], headers=self._get_request_headers())
|
||||
response.raise_for_status()
|
||||
|
||||
self.cache.delete_cache(target["cache_key"])
|
||||
self.cache.delete_cache(target["url"])
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
|
|
|
|||
|
|
@ -132,6 +132,24 @@ def test_sync_read_caches_per_data_key_for_the_same_secret_path(monkeypatch: pyt
|
|||
assert manager.sync_read_secret("DB_CREDS") == "sk-from-vault"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_async_delete_evicts_every_cached_field_of_the_secret_path(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
manager: Final = _build_manager(monkeypatch, {"HCP_VAULT_SECRET_NAMESPACE": "teams/team-a"})
|
||||
respx.post(f"{VAULT_ADDR}/v1/auth/approle/login").respond(json=LOGIN_RESPONSE)
|
||||
secret_url: Final = f"{VAULT_ADDR}/v1/teams/team-a/secret/data/DB_CREDS"
|
||||
read_route: Final = respx.get(secret_url).respond(json=SECRET_RESPONSE)
|
||||
respx.delete(secret_url).respond(status_code=204)
|
||||
password_params: Final = {"secret_manager_settings": {"data": "password"}}
|
||||
|
||||
assert await manager.async_read_secret("DB_CREDS", optional_params=password_params) == "pw-from-vault"
|
||||
assert await manager.async_delete_secret("DB_CREDS")
|
||||
assert await manager.async_read_secret("DB_CREDS", optional_params=password_params) == "pw-from-vault"
|
||||
|
||||
assert read_route.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@respx.mock
|
||||
async def test_async_read_uses_secret_namespace_and_login_namespace(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue