mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-26 06:03:19 +00:00
Merge 08f7628cf3 into d7620c77a1
This commit is contained in:
commit
58b1397fab
4 changed files with 149 additions and 1 deletions
|
|
@ -1,10 +1,51 @@
|
|||
"""Utils for accessing credentials."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.router import holds_secret_pointer
|
||||
from litellm.types.utils import CredentialItem
|
||||
|
||||
_ENV_REF_PREFIX: Final = "os.environ/"
|
||||
|
||||
|
||||
def _resolved_credential_value(credential_name: str, key: str, value: object) -> object:
|
||||
"""The environment variable's value for an ``os.environ/<VAR>`` reference, or the value itself.
|
||||
|
||||
An unresolvable reference is kept and logged rather than dropped, so a provider with optional
|
||||
params cannot silently fall back to another account's ambient credentials. ``*_ref`` federation
|
||||
fields are pointers their identity source dereferences at use time, so they stay as written.
|
||||
"""
|
||||
if not isinstance(value, str) or not value.startswith(_ENV_REF_PREFIX):
|
||||
return value
|
||||
if holds_secret_pointer(key):
|
||||
return value
|
||||
resolved: Final = get_secret_str(value)
|
||||
if resolved is not None:
|
||||
return resolved
|
||||
verbose_logger.warning(
|
||||
"credential '%s' value '%s' references unset environment variable %s; keeping the reference",
|
||||
credential_name,
|
||||
key,
|
||||
value,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def expand_credential_env_refs(
|
||||
credential_name: str, values: Mapping[str, object]
|
||||
) -> dict[str, object]: # mutable-ok: CredentialItem.credential_values is a mutable dict by contract
|
||||
"""Resolve ``os.environ/<VAR>`` values against the environment.
|
||||
|
||||
Mirrors the config loader: references resolve once, when the credential enters the process, so
|
||||
runtime reads stay plain and a rotated env var needs the credential to be re-saved or the proxy
|
||||
restarted.
|
||||
"""
|
||||
return {key: _resolved_credential_value(credential_name, key, value) for key, value in values.items()}
|
||||
|
||||
|
||||
class CredentialAccessor:
|
||||
@staticmethod
|
||||
|
|
@ -27,6 +68,11 @@ class CredentialAccessor:
|
|||
def upsert_credentials(credentials: list[CredentialItem]):
|
||||
"""Add a credential to the list of credentials."""
|
||||
|
||||
for credential in credentials:
|
||||
credential.credential_values = expand_credential_env_refs(
|
||||
credential.credential_name, credential.credential_values
|
||||
)
|
||||
|
||||
credential_names: Final = [cred.credential_name for cred in litellm.credential_list]
|
||||
|
||||
for credential in credentials:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from types import MappingProxyType
|
|||
from typing import Final, cast # noqa: TID251 # narrows untyped credential values at the decrypt boundary
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.credential_accessor import expand_credential_env_refs
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
|
||||
from litellm.proxy.utils import PrismaClient
|
||||
from litellm.repositories.credentials_repository import CredentialsRepository
|
||||
|
|
@ -78,7 +79,7 @@ def _decrypted(db_credential: CredentialItem) -> CredentialItem:
|
|||
)
|
||||
return CredentialItem(
|
||||
credential_name=db_credential.credential_name,
|
||||
credential_values=decrypted_values, # pyright: ignore[reportArgumentType] # declared dict[str, str], and pydantic copies this mapping into one on validation; LIT002 rules out building that dict here
|
||||
credential_values=expand_credential_env_refs(db_credential.credential_name, decrypted_values), # pyright: ignore[reportArgumentType] # declared dict[str, str], and pydantic copies this mapping into one on validation; LIT002 rules out building that dict here
|
||||
credential_info=db_credential.credential_info,
|
||||
)
|
||||
|
||||
|
|
|
|||
71
tests/unit/litellm_core_utils/test_credential_accessor.py
Normal file
71
tests/unit/litellm_core_utils/test_credential_accessor.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
from typing import Final
|
||||
from unittest.mock import patch
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.credential_accessor import (
|
||||
CredentialAccessor,
|
||||
expand_credential_env_refs,
|
||||
)
|
||||
from litellm.models.credentials import CredentialItem
|
||||
|
||||
CREDENTIAL_NAME: Final = "bedrock-account-b"
|
||||
|
||||
|
||||
def _credential(values: dict[str, object]) -> CredentialItem:
|
||||
return CredentialItem(
|
||||
credential_name=CREDENTIAL_NAME,
|
||||
credential_values=values,
|
||||
credential_info={"custom_llm_provider": "bedrock"},
|
||||
)
|
||||
|
||||
|
||||
def test_upsert_resolves_env_refs_so_deployments_read_effective_values(monkeypatch):
|
||||
monkeypatch.setenv("AWS_ACCESS_KEY_ID_CRED_TEST", "ak-account-b")
|
||||
with patch.object(litellm, "credential_list", []): # test-quality-ok: isolate the global list
|
||||
CredentialAccessor.upsert_credentials(
|
||||
[
|
||||
_credential(
|
||||
{
|
||||
"aws_access_key_id": "os.environ/AWS_ACCESS_KEY_ID_CRED_TEST",
|
||||
"aws_region_name": "us-east-1",
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert CredentialAccessor.get_credential_values(CREDENTIAL_NAME) == {
|
||||
"aws_access_key_id": "ak-account-b",
|
||||
"aws_region_name": "us-east-1",
|
||||
}
|
||||
|
||||
|
||||
def test_upsert_keeps_the_reference_when_the_variable_is_unset(monkeypatch):
|
||||
monkeypatch.delenv("AWS_SECRET_ACCESS_KEY_CRED_TEST", raising=False)
|
||||
with patch.object(litellm, "credential_list", []): # test-quality-ok: isolate the global list
|
||||
CredentialAccessor.upsert_credentials(
|
||||
[_credential({"aws_secret_access_key": "os.environ/AWS_SECRET_ACCESS_KEY_CRED_TEST"})]
|
||||
)
|
||||
|
||||
assert CredentialAccessor.get_credential_values(CREDENTIAL_NAME) == {
|
||||
"aws_secret_access_key": "os.environ/AWS_SECRET_ACCESS_KEY_CRED_TEST"
|
||||
}
|
||||
|
||||
|
||||
def test_expand_leaves_non_reference_values_alone():
|
||||
values: Final = {
|
||||
"aws_region_name": "us-east-1",
|
||||
"aws_session_tags": ["team", "prod"],
|
||||
"max_retries": 4,
|
||||
}
|
||||
|
||||
assert expand_credential_env_refs(CREDENTIAL_NAME, values) == values
|
||||
|
||||
|
||||
def test_expand_leaves_federation_pointer_fields_as_written(monkeypatch):
|
||||
monkeypatch.setenv("JWKS_CRED_TEST_SIGNING_KEY", "pem-bytes")
|
||||
values: Final = {
|
||||
"anthropic_identity_source": "internal_issuer",
|
||||
"anthropic_issuer_signing_key_ref": "os.environ/JWKS_CRED_TEST_SIGNING_KEY",
|
||||
}
|
||||
|
||||
assert expand_credential_env_refs(CREDENTIAL_NAME, values) == values
|
||||
|
|
@ -7,6 +7,36 @@ from litellm.proxy.common_utils.credential_hydration import hydrate_named_creden
|
|||
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authoritative_hydrate_resolves_env_refs_in_stored_values(monkeypatch):
|
||||
monkeypatch.setenv("LITELLM_SALT_KEY", "sk-hydration-test-salt")
|
||||
monkeypatch.setenv("BEDROCK_HYDRATION_TEST_KEY", "ak-account-b")
|
||||
monkeypatch.delenv("BEDROCK_HYDRATION_TEST_MISSING", raising=False)
|
||||
row = {
|
||||
"credential_name": "bedrock-account-b",
|
||||
"credential_values": {
|
||||
"aws_access_key_id": encrypt_value_helper("os.environ/BEDROCK_HYDRATION_TEST_KEY"),
|
||||
"aws_secret_access_key": encrypt_value_helper("literal-secret"),
|
||||
"aws_session_token": encrypt_value_helper("os.environ/BEDROCK_HYDRATION_TEST_MISSING"),
|
||||
"anthropic_issuer_signing_key_ref": encrypt_value_helper("os.environ/BEDROCK_HYDRATION_TEST_KEY"),
|
||||
},
|
||||
"credential_info": {"custom_llm_provider": "bedrock"},
|
||||
}
|
||||
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("bedrock-account-b", prisma)
|
||||
|
||||
assert resolved is not None
|
||||
assert resolved.credential_values == {
|
||||
"aws_access_key_id": "ak-account-b",
|
||||
"aws_secret_access_key": "literal-secret",
|
||||
"aws_session_token": "os.environ/BEDROCK_HYDRATION_TEST_MISSING",
|
||||
"anthropic_issuer_signing_key_ref": "os.environ/BEDROCK_HYDRATION_TEST_KEY",
|
||||
}
|
||||
|
||||
|
||||
@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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue