mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
feat(security): encrypt logging callback_vars before DB write
This commit is contained in:
parent
251023b5e0
commit
7add825f38
4 changed files with 140 additions and 81 deletions
|
|
@ -363,17 +363,17 @@ def get_remaining_tokens_and_requests_from_request_data(data: Dict) -> Dict[str,
|
|||
remaining_requests_variable_name = f"litellm-key-remaining-requests-{model_group}"
|
||||
remaining_requests = _metadata.get(remaining_requests_variable_name, None)
|
||||
if remaining_requests:
|
||||
headers[
|
||||
f"x-litellm-key-remaining-requests-{h11_model_group_name}"
|
||||
] = remaining_requests
|
||||
headers[f"x-litellm-key-remaining-requests-{h11_model_group_name}"] = (
|
||||
remaining_requests
|
||||
)
|
||||
|
||||
# Remaining Tokens
|
||||
remaining_tokens_variable_name = f"litellm-key-remaining-tokens-{model_group}"
|
||||
remaining_tokens = _metadata.get(remaining_tokens_variable_name, None)
|
||||
if remaining_tokens:
|
||||
headers[
|
||||
f"x-litellm-key-remaining-tokens-{h11_model_group_name}"
|
||||
] = remaining_tokens
|
||||
headers[f"x-litellm-key-remaining-tokens-{h11_model_group_name}"] = (
|
||||
remaining_tokens
|
||||
)
|
||||
|
||||
return headers
|
||||
|
||||
|
|
@ -473,9 +473,9 @@ def add_guardrail_response_to_standard_logging_object(
|
|||
):
|
||||
if litellm_logging_obj is None:
|
||||
return
|
||||
standard_logging_object: Optional[
|
||||
StandardLoggingPayload
|
||||
] = litellm_logging_obj.model_call_details.get("standard_logging_object")
|
||||
standard_logging_object: Optional[StandardLoggingPayload] = (
|
||||
litellm_logging_obj.model_call_details.get("standard_logging_object")
|
||||
)
|
||||
if standard_logging_object is None:
|
||||
return
|
||||
guardrail_information = standard_logging_object.get("guardrail_information", [])
|
||||
|
|
@ -527,6 +527,44 @@ def normalize_callback_names(callbacks: Iterable[Any]) -> List[Any]:
|
|||
return [c.lower() if isinstance(c, str) else c for c in callbacks]
|
||||
|
||||
|
||||
def encrypt_logging_callback_vars(metadata: Optional[Dict]) -> Optional[Dict]:
|
||||
"""
|
||||
Encrypt credential values inside ``metadata["logging"][*]["callback_vars"]``
|
||||
before persisting to the database.
|
||||
|
||||
Values that are environment-variable references (``os.environ/…``) are
|
||||
left untouched — they are pointers, not secrets, and do not need
|
||||
to be stored encrypted.
|
||||
|
||||
Returns the *same* dict (mutated in-place) so the caller can assign the
|
||||
result back to ``metadata`` if convenient.
|
||||
"""
|
||||
if not metadata:
|
||||
return metadata
|
||||
|
||||
logging_configs = metadata.get("logging")
|
||||
if not isinstance(logging_configs, list):
|
||||
return metadata
|
||||
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import encrypt_value_helper
|
||||
|
||||
for entry in logging_configs:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
callback_vars = entry.get("callback_vars")
|
||||
if not isinstance(callback_vars, dict):
|
||||
continue
|
||||
for key, value in callback_vars.items():
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
# Leave env-var pointers as-is; encrypt everything else
|
||||
if value.startswith("os.environ/"):
|
||||
continue
|
||||
callback_vars[key] = encrypt_value_helper(value)
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def redact_sensitive_logging_metadata(metadata: Optional[Dict]) -> Optional[Dict]:
|
||||
"""
|
||||
Return a copy of `metadata` with credential values inside
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from litellm.proxy._types import (
|
|||
TeamCallbackMetadata,
|
||||
UserAPIKeyAuth,
|
||||
)
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
|
||||
from litellm.proxy.common_utils.http_parsing_utils import _safe_get_request_headers
|
||||
|
||||
# Cache special headers as a frozenset for O(1) lookup performance
|
||||
|
|
@ -43,6 +44,8 @@ def _sanitize_for_log(value: Any) -> str:
|
|||
text = repr(value)
|
||||
# Strip CR/LF characters commonly used for log injection
|
||||
return text.replace("\r", "").replace("\n", "")
|
||||
|
||||
|
||||
from litellm.router import Router
|
||||
from litellm.secret_managers.main import get_secret_bool
|
||||
from litellm.types.llms.anthropic import ANTHROPIC_API_HEADERS
|
||||
|
|
@ -185,9 +188,18 @@ def convert_key_logging_metadata_to_callback(
|
|||
for var, value in data.callback_vars.items():
|
||||
if team_callback_settings_obj.callback_vars is None:
|
||||
team_callback_settings_obj.callback_vars = {}
|
||||
team_callback_settings_obj.callback_vars[var] = str(
|
||||
litellm.utils.get_secret(value, default_value=value) or value
|
||||
)
|
||||
resolved = str(litellm.utils.get_secret(value, default_value=value) or value)
|
||||
# If not an env-var pointer the stored value may be encrypted — decrypt it
|
||||
if not value.startswith("os.environ/"):
|
||||
resolved = str(
|
||||
decrypt_value_helper(
|
||||
value=resolved,
|
||||
key=var,
|
||||
exception_type="debug",
|
||||
return_original_value=True,
|
||||
)
|
||||
)
|
||||
team_callback_settings_obj.callback_vars[var] = resolved
|
||||
|
||||
return team_callback_settings_obj
|
||||
|
||||
|
|
@ -220,12 +232,12 @@ def _get_dynamic_logging_metadata(
|
|||
user_api_key_dict: UserAPIKeyAuth, proxy_config: ProxyConfig
|
||||
) -> Optional[TeamCallbackMetadata]:
|
||||
callback_settings_obj: Optional[TeamCallbackMetadata] = None
|
||||
key_dynamic_logging_settings: Optional[
|
||||
dict
|
||||
] = KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings(user_api_key_dict)
|
||||
team_dynamic_logging_settings: Optional[
|
||||
dict
|
||||
] = KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings(user_api_key_dict)
|
||||
key_dynamic_logging_settings: Optional[dict] = (
|
||||
KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings(user_api_key_dict)
|
||||
)
|
||||
team_dynamic_logging_settings: Optional[dict] = (
|
||||
KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings(user_api_key_dict)
|
||||
)
|
||||
#########################################################################################
|
||||
# Key-based callbacks
|
||||
#########################################################################################
|
||||
|
|
@ -779,11 +791,11 @@ class LiteLLMProxyRequestSetup:
|
|||
|
||||
## KEY-LEVEL SPEND LOGS / TAGS
|
||||
if "tags" in key_metadata and key_metadata["tags"] is not None:
|
||||
data[_metadata_variable_name][
|
||||
"tags"
|
||||
] = LiteLLMProxyRequestSetup._merge_tags(
|
||||
request_tags=data[_metadata_variable_name].get("tags"),
|
||||
tags_to_add=key_metadata["tags"],
|
||||
data[_metadata_variable_name]["tags"] = (
|
||||
LiteLLMProxyRequestSetup._merge_tags(
|
||||
request_tags=data[_metadata_variable_name].get("tags"),
|
||||
tags_to_add=key_metadata["tags"],
|
||||
)
|
||||
)
|
||||
if "disable_global_guardrails" in key_metadata and isinstance(
|
||||
key_metadata["disable_global_guardrails"], bool
|
||||
|
|
@ -1079,9 +1091,9 @@ async def add_litellm_data_to_request( # noqa: PLR0915
|
|||
data[_metadata_variable_name]["litellm_api_version"] = version
|
||||
|
||||
if general_settings is not None:
|
||||
data[_metadata_variable_name][
|
||||
"global_max_parallel_requests"
|
||||
] = general_settings.get("global_max_parallel_requests", None)
|
||||
data[_metadata_variable_name]["global_max_parallel_requests"] = (
|
||||
general_settings.get("global_max_parallel_requests", None)
|
||||
)
|
||||
|
||||
### KEY-LEVEL Controls
|
||||
key_metadata = user_api_key_dict.metadata
|
||||
|
|
@ -1881,7 +1893,9 @@ async def move_guardrails_to_metadata(
|
|||
)
|
||||
|
||||
# Only check policy engine if no local config (avoid import + registry lookup)
|
||||
if not (has_key_config or has_team_config or has_project_config or has_request_config):
|
||||
if not (
|
||||
has_key_config or has_team_config or has_project_config or has_request_config
|
||||
):
|
||||
from litellm.proxy.policy_engine.policy_registry import get_policy_registry
|
||||
|
||||
if not get_policy_registry().is_initialized():
|
||||
|
|
|
|||
|
|
@ -49,7 +49,10 @@ from litellm.proxy.auth.auth_checks import (
|
|||
)
|
||||
from litellm.proxy.auth.auth_utils import abbreviate_api_key
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_utils.callback_utils import redact_sensitive_logging_metadata
|
||||
from litellm.proxy.common_utils.callback_utils import (
|
||||
encrypt_logging_callback_vars,
|
||||
redact_sensitive_logging_metadata,
|
||||
)
|
||||
from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time
|
||||
from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks
|
||||
from litellm.proxy.management_endpoints.common_utils import (
|
||||
|
|
@ -769,9 +772,9 @@ async def _common_key_generation_helper( # noqa: PLR0915
|
|||
request_type="key", **data_json, table_name="key"
|
||||
)
|
||||
|
||||
response[
|
||||
"soft_budget"
|
||||
] = data.soft_budget # include the user-input soft budget in the response
|
||||
response["soft_budget"] = (
|
||||
data.soft_budget
|
||||
) # include the user-input soft budget in the response
|
||||
|
||||
response = GenerateKeyResponse(**response)
|
||||
|
||||
|
|
@ -1541,6 +1544,7 @@ def prepare_metadata_fields(
|
|||
)
|
||||
|
||||
non_default_values["metadata"] = casted_metadata
|
||||
encrypt_logging_callback_vars(non_default_values["metadata"])
|
||||
return non_default_values
|
||||
|
||||
|
||||
|
|
@ -2952,6 +2956,7 @@ async def generate_key_helper_fn( # noqa: PLR0915
|
|||
metadata = metadata or {}
|
||||
metadata["prompts"] = prompts
|
||||
|
||||
encrypt_logging_callback_vars(metadata)
|
||||
metadata_json = json.dumps(metadata)
|
||||
validate_model_max_budget(model_max_budget)
|
||||
model_max_budget_json = json.dumps(model_max_budget)
|
||||
|
|
@ -3287,10 +3292,10 @@ async def delete_verification_tokens(
|
|||
try:
|
||||
if prisma_client:
|
||||
tokens = [_hash_token_if_needed(token=key) for key in tokens]
|
||||
_keys_being_deleted: List[
|
||||
LiteLLM_VerificationToken
|
||||
] = await prisma_client.db.litellm_verificationtoken.find_many(
|
||||
where={"token": {"in": tokens}}
|
||||
_keys_being_deleted: List[LiteLLM_VerificationToken] = (
|
||||
await prisma_client.db.litellm_verificationtoken.find_many(
|
||||
where={"token": {"in": tokens}}
|
||||
)
|
||||
)
|
||||
|
||||
if len(_keys_being_deleted) == 0:
|
||||
|
|
@ -3490,9 +3495,9 @@ async def _rotate_master_key( # noqa: PLR0915
|
|||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
try:
|
||||
models: Optional[
|
||||
List
|
||||
] = await prisma_client.db.litellm_proxymodeltable.find_many()
|
||||
models: Optional[List] = (
|
||||
await prisma_client.db.litellm_proxymodeltable.find_many()
|
||||
)
|
||||
except Exception:
|
||||
models = None
|
||||
# 2. process model table
|
||||
|
|
@ -4132,11 +4137,11 @@ async def validate_key_list_check(
|
|||
param="user_id",
|
||||
code=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
complete_user_info_db_obj: Optional[
|
||||
BaseModel
|
||||
] = await prisma_client.db.litellm_usertable.find_unique(
|
||||
where={"user_id": user_api_key_dict.user_id},
|
||||
include={"organization_memberships": True},
|
||||
complete_user_info_db_obj: Optional[BaseModel] = (
|
||||
await prisma_client.db.litellm_usertable.find_unique(
|
||||
where={"user_id": user_api_key_dict.user_id},
|
||||
include={"organization_memberships": True},
|
||||
)
|
||||
)
|
||||
|
||||
if complete_user_info_db_obj is None:
|
||||
|
|
@ -4219,10 +4224,10 @@ async def _fetch_user_team_objects(
|
|||
if complete_user_info is None or not complete_user_info.teams:
|
||||
return []
|
||||
|
||||
teams: Optional[
|
||||
List[BaseModel]
|
||||
] = await prisma_client.db.litellm_teamtable.find_many(
|
||||
where={"team_id": {"in": complete_user_info.teams}}
|
||||
teams: Optional[List[BaseModel]] = (
|
||||
await prisma_client.db.litellm_teamtable.find_many(
|
||||
where={"team_id": {"in": complete_user_info.teams}}
|
||||
)
|
||||
)
|
||||
if teams is None:
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -69,7 +69,10 @@ from litellm.proxy.auth.auth_checks import (
|
|||
get_user_object,
|
||||
)
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.common_utils.callback_utils import redact_sensitive_logging_metadata
|
||||
from litellm.proxy.common_utils.callback_utils import (
|
||||
encrypt_logging_callback_vars,
|
||||
redact_sensitive_logging_metadata,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.common_utils import (
|
||||
_is_user_org_admin_for_team,
|
||||
_is_user_team_admin,
|
||||
|
|
@ -121,6 +124,7 @@ def _sanitize_for_log(value: Any) -> str:
|
|||
text = repr(value)
|
||||
return text.replace("\r", "").replace("\n", "")
|
||||
|
||||
|
||||
async def _verify_team_access(
|
||||
team_obj: LiteLLM_TeamTable,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
|
|
@ -315,10 +319,8 @@ class TeamMemberBudgetHandler:
|
|||
return
|
||||
|
||||
# Batch-fetch existing memberships for this team (avoids N+1 queries)
|
||||
existing_memberships = (
|
||||
await prisma_client.db.litellm_teammembership.find_many(
|
||||
where={"team_id": team_id}
|
||||
)
|
||||
existing_memberships = await prisma_client.db.litellm_teammembership.find_many(
|
||||
where={"team_id": team_id}
|
||||
)
|
||||
existing_user_ids = {m.user_id for m in existing_memberships}
|
||||
|
||||
|
|
@ -1082,6 +1084,7 @@ async def new_team( # noqa: PLR0915
|
|||
complete_team_data.members_with_roles = []
|
||||
|
||||
complete_team_data_dict = complete_team_data.model_dump(exclude_none=True)
|
||||
encrypt_logging_callback_vars(complete_team_data_dict.get("metadata"))
|
||||
|
||||
# Serialize router_settings to JSON (matching key creation pattern)
|
||||
router_settings_value = getattr(data, "router_settings", None)
|
||||
|
|
@ -1639,6 +1642,7 @@ async def update_team( # noqa: PLR0915
|
|||
|
||||
# update team metadata fields
|
||||
_update_metadata_fields(updated_kv=updated_kv)
|
||||
encrypt_logging_callback_vars(updated_kv.get("metadata"))
|
||||
|
||||
if "model_aliases" in updated_kv:
|
||||
updated_kv.pop("model_aliases")
|
||||
|
|
@ -1660,12 +1664,12 @@ async def update_team( # noqa: PLR0915
|
|||
updated_kv["router_settings"] = safe_dumps(updated_kv["router_settings"])
|
||||
|
||||
updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv)
|
||||
team_row: Optional[
|
||||
LiteLLM_TeamTable
|
||||
] = await prisma_client.db.litellm_teamtable.update(
|
||||
where={"team_id": data.team_id},
|
||||
data=updated_kv,
|
||||
include={"litellm_model_table": True}, # type: ignore
|
||||
team_row: Optional[LiteLLM_TeamTable] = (
|
||||
await prisma_client.db.litellm_teamtable.update(
|
||||
where={"team_id": data.team_id},
|
||||
data=updated_kv,
|
||||
include={"litellm_model_table": True}, # type: ignore
|
||||
)
|
||||
)
|
||||
|
||||
if team_row is None or team_row.team_id is None:
|
||||
|
|
@ -2412,13 +2416,13 @@ async def team_member_delete(
|
|||
)
|
||||
|
||||
# Fetch keys before deletion to persist them
|
||||
keys_to_delete: List[
|
||||
LiteLLM_VerificationToken
|
||||
] = await prisma_client.db.litellm_verificationtoken.find_many(
|
||||
where={
|
||||
"user_id": {"in": list(user_ids_to_delete)},
|
||||
"team_id": data.team_id,
|
||||
}
|
||||
keys_to_delete: List[LiteLLM_VerificationToken] = (
|
||||
await prisma_client.db.litellm_verificationtoken.find_many(
|
||||
where={
|
||||
"user_id": {"in": list(user_ids_to_delete)},
|
||||
"team_id": data.team_id,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if keys_to_delete:
|
||||
|
|
@ -2802,10 +2806,10 @@ async def delete_team(
|
|||
team_rows: List[LiteLLM_TeamTable] = []
|
||||
for team_id in data.team_ids:
|
||||
try:
|
||||
team_row_base: Optional[
|
||||
BaseModel
|
||||
] = await prisma_client.db.litellm_teamtable.find_unique(
|
||||
where={"team_id": team_id}
|
||||
team_row_base: Optional[BaseModel] = (
|
||||
await prisma_client.db.litellm_teamtable.find_unique(
|
||||
where={"team_id": team_id}
|
||||
)
|
||||
)
|
||||
if team_row_base is None:
|
||||
raise Exception
|
||||
|
|
@ -2871,10 +2875,10 @@ async def delete_team(
|
|||
_persist_deleted_verification_tokens,
|
||||
)
|
||||
|
||||
keys_to_delete: List[
|
||||
LiteLLM_VerificationToken
|
||||
] = await prisma_client.db.litellm_verificationtoken.find_many(
|
||||
where={"team_id": {"in": data.team_ids}}
|
||||
keys_to_delete: List[LiteLLM_VerificationToken] = (
|
||||
await prisma_client.db.litellm_verificationtoken.find_many(
|
||||
where={"team_id": {"in": data.team_ids}}
|
||||
)
|
||||
)
|
||||
|
||||
if keys_to_delete:
|
||||
|
|
@ -3111,11 +3115,11 @@ async def team_info(
|
|||
)
|
||||
|
||||
try:
|
||||
team_info: Optional[
|
||||
BaseModel
|
||||
] = await prisma_client.db.litellm_teamtable.find_unique(
|
||||
where={"team_id": team_id},
|
||||
include={"object_permission": True},
|
||||
team_info: Optional[BaseModel] = (
|
||||
await prisma_client.db.litellm_teamtable.find_unique(
|
||||
where={"team_id": team_id},
|
||||
include={"object_permission": True},
|
||||
)
|
||||
)
|
||||
if team_info is None:
|
||||
raise Exception
|
||||
|
|
@ -3982,9 +3986,7 @@ async def list_team(
|
|||
except Exception as e:
|
||||
team_exception = """Invalid team object for team_id: {}. team_object={}.
|
||||
Error: {}
|
||||
""".format(
|
||||
team.team_id, team.model_dump(), str(e)
|
||||
)
|
||||
""".format(team.team_id, team.model_dump(), str(e))
|
||||
verbose_proxy_logger.exception(team_exception)
|
||||
continue
|
||||
# Sort the responses by team_alias
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue