mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-25 02:34:29 +00:00
All checks were successful
Read Version from pyproject.toml / read-version (push) Successful in 12s
* fix(factory.py): fix bedrock document url check Make check more generic - if starts with 'text' or 'application' assume it's a document and let it go through Fixes https://github.com/BerriAI/litellm/issues/7746 * feat(key_management_endpoints.py): support writing new key alias to aws secret manager - on key rotation adds rotation endpoint to aws key management hook - allows for rotated litellm virtual keys with new key alias to be written to it * feat(key_management_event_hooks.py): support rotating keys and updating secret manager * refactor(base_secret_manager.py): support rotate secret at the base level since it's just an abstraction function, it's easy to implement at the base manager level * style: cleanup unused imports
324 lines
13 KiB
Python
324 lines
13 KiB
Python
import asyncio
|
|
import json
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
from typing import Any, List, Optional
|
|
|
|
from fastapi import status
|
|
|
|
import litellm
|
|
from litellm._logging import verbose_proxy_logger
|
|
from litellm.proxy._types import (
|
|
GenerateKeyRequest,
|
|
GenerateKeyResponse,
|
|
KeyRequest,
|
|
LiteLLM_AuditLogs,
|
|
LiteLLM_VerificationToken,
|
|
LitellmTableNames,
|
|
ProxyErrorTypes,
|
|
ProxyException,
|
|
RegenerateKeyRequest,
|
|
UpdateKeyRequest,
|
|
UserAPIKeyAuth,
|
|
WebhookEvent,
|
|
)
|
|
|
|
# NOTE: This is the prefix for all virtual keys stored in AWS Secrets Manager
|
|
LITELLM_PREFIX_STORED_VIRTUAL_KEYS = "litellm/"
|
|
|
|
|
|
class KeyManagementEventHooks:
|
|
|
|
@staticmethod
|
|
async def async_key_generated_hook(
|
|
data: GenerateKeyRequest,
|
|
response: GenerateKeyResponse,
|
|
user_api_key_dict: UserAPIKeyAuth,
|
|
litellm_changed_by: Optional[str] = None,
|
|
):
|
|
"""
|
|
Hook that runs after a successful /key/generate request
|
|
|
|
Handles the following:
|
|
- Sending Email with Key Details
|
|
- Storing Audit Logs for key generation
|
|
- Storing Generated Key in DB
|
|
"""
|
|
from litellm.proxy.management_helpers.audit_logs import (
|
|
create_audit_log_for_update,
|
|
)
|
|
from litellm.proxy.proxy_server import litellm_proxy_admin_name
|
|
|
|
if data.send_invite_email is True:
|
|
await KeyManagementEventHooks._send_key_created_email(
|
|
response.model_dump(exclude_none=True)
|
|
)
|
|
|
|
# Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True
|
|
if litellm.store_audit_logs is True:
|
|
_updated_values = response.model_dump_json(exclude_none=True)
|
|
asyncio.create_task(
|
|
create_audit_log_for_update(
|
|
request_data=LiteLLM_AuditLogs(
|
|
id=str(uuid.uuid4()),
|
|
updated_at=datetime.now(timezone.utc),
|
|
changed_by=litellm_changed_by
|
|
or user_api_key_dict.user_id
|
|
or litellm_proxy_admin_name,
|
|
changed_by_api_key=user_api_key_dict.api_key,
|
|
table_name=LitellmTableNames.KEY_TABLE_NAME,
|
|
object_id=response.token_id or "",
|
|
action="created",
|
|
updated_values=_updated_values,
|
|
before_value=None,
|
|
)
|
|
)
|
|
)
|
|
# store the generated key in the secret manager
|
|
await KeyManagementEventHooks._store_virtual_key_in_secret_manager(
|
|
secret_name=data.key_alias or f"virtual-key-{response.token_id}",
|
|
secret_token=response.key,
|
|
)
|
|
|
|
@staticmethod
|
|
async def async_key_updated_hook(
|
|
data: UpdateKeyRequest,
|
|
existing_key_row: Any,
|
|
response: Any,
|
|
user_api_key_dict: UserAPIKeyAuth,
|
|
litellm_changed_by: Optional[str] = None,
|
|
):
|
|
"""
|
|
Post /key/update processing hook
|
|
|
|
Handles the following:
|
|
- Storing Audit Logs for key update
|
|
"""
|
|
from litellm.proxy.management_helpers.audit_logs import (
|
|
create_audit_log_for_update,
|
|
)
|
|
from litellm.proxy.proxy_server import litellm_proxy_admin_name
|
|
|
|
# Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True
|
|
if litellm.store_audit_logs is True:
|
|
_updated_values = json.dumps(data.json(exclude_none=True), default=str)
|
|
|
|
_before_value = existing_key_row.json(exclude_none=True)
|
|
_before_value = json.dumps(_before_value, default=str)
|
|
|
|
asyncio.create_task(
|
|
create_audit_log_for_update(
|
|
request_data=LiteLLM_AuditLogs(
|
|
id=str(uuid.uuid4()),
|
|
updated_at=datetime.now(timezone.utc),
|
|
changed_by=litellm_changed_by
|
|
or user_api_key_dict.user_id
|
|
or litellm_proxy_admin_name,
|
|
changed_by_api_key=user_api_key_dict.api_key,
|
|
table_name=LitellmTableNames.KEY_TABLE_NAME,
|
|
object_id=data.key,
|
|
action="updated",
|
|
updated_values=_updated_values,
|
|
before_value=_before_value,
|
|
)
|
|
)
|
|
)
|
|
|
|
@staticmethod
|
|
async def async_key_rotated_hook(
|
|
data: Optional[RegenerateKeyRequest],
|
|
existing_key_row: Any,
|
|
response: GenerateKeyResponse,
|
|
user_api_key_dict: UserAPIKeyAuth,
|
|
litellm_changed_by: Optional[str] = None,
|
|
):
|
|
# store the generated key in the secret manager
|
|
if data is not None and response.token_id is not None:
|
|
initial_secret_name = (
|
|
existing_key_row.key_alias or f"virtual-key-{existing_key_row.token}"
|
|
)
|
|
await KeyManagementEventHooks._rotate_virtual_key_in_secret_manager(
|
|
current_secret_name=initial_secret_name,
|
|
new_secret_name=data.key_alias or f"virtual-key-{response.token_id}",
|
|
new_secret_value=response.key,
|
|
)
|
|
|
|
@staticmethod
|
|
async def async_key_deleted_hook(
|
|
data: KeyRequest,
|
|
keys_being_deleted: List[LiteLLM_VerificationToken],
|
|
response: dict,
|
|
user_api_key_dict: UserAPIKeyAuth,
|
|
litellm_changed_by: Optional[str] = None,
|
|
):
|
|
"""
|
|
Post /key/delete processing hook
|
|
|
|
Handles the following:
|
|
- Storing Audit Logs for key deletion
|
|
"""
|
|
from litellm.proxy.management_helpers.audit_logs import (
|
|
create_audit_log_for_update,
|
|
)
|
|
from litellm.proxy.proxy_server import litellm_proxy_admin_name, prisma_client
|
|
|
|
# Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True
|
|
# we do this after the first for loop, since first for loop is for validation. we only want this inserted after validation passes
|
|
if litellm.store_audit_logs is True and data.keys is not None:
|
|
# make an audit log for each team deleted
|
|
for key in data.keys:
|
|
key_row = await prisma_client.get_data( # type: ignore
|
|
token=key, table_name="key", query_type="find_unique"
|
|
)
|
|
|
|
if key_row is None:
|
|
raise ProxyException(
|
|
message=f"Key {key} not found",
|
|
type=ProxyErrorTypes.bad_request_error,
|
|
param="key",
|
|
code=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
|
|
key_row = key_row.json(exclude_none=True)
|
|
_key_row = json.dumps(key_row, default=str)
|
|
|
|
asyncio.create_task(
|
|
create_audit_log_for_update(
|
|
request_data=LiteLLM_AuditLogs(
|
|
id=str(uuid.uuid4()),
|
|
updated_at=datetime.now(timezone.utc),
|
|
changed_by=litellm_changed_by
|
|
or user_api_key_dict.user_id
|
|
or litellm_proxy_admin_name,
|
|
changed_by_api_key=user_api_key_dict.api_key,
|
|
table_name=LitellmTableNames.KEY_TABLE_NAME,
|
|
object_id=key,
|
|
action="deleted",
|
|
updated_values="{}",
|
|
before_value=_key_row,
|
|
)
|
|
)
|
|
)
|
|
# delete the keys from the secret manager
|
|
await KeyManagementEventHooks._delete_virtual_keys_from_secret_manager(
|
|
keys_being_deleted=keys_being_deleted
|
|
)
|
|
pass
|
|
|
|
@staticmethod
|
|
async def _store_virtual_key_in_secret_manager(secret_name: str, secret_token: str):
|
|
"""
|
|
Store a virtual key in the secret manager
|
|
|
|
Args:
|
|
secret_name: Name of the virtual key
|
|
secret_token: Value of the virtual key (example: sk-1234)
|
|
"""
|
|
if litellm._key_management_settings is not None:
|
|
if litellm._key_management_settings.store_virtual_keys is True:
|
|
from litellm.secret_managers.base_secret_manager import (
|
|
BaseSecretManager,
|
|
)
|
|
|
|
# store the key in the secret manager
|
|
if isinstance(litellm.secret_manager_client, BaseSecretManager):
|
|
await litellm.secret_manager_client.async_write_secret(
|
|
secret_name=KeyManagementEventHooks._get_secret_name(
|
|
secret_name
|
|
),
|
|
secret_value=secret_token,
|
|
)
|
|
|
|
@staticmethod
|
|
async def _rotate_virtual_key_in_secret_manager(
|
|
current_secret_name: str, new_secret_name: str, new_secret_value: str
|
|
):
|
|
"""
|
|
Update a virtual key in the secret manager
|
|
|
|
Args:
|
|
secret_name: Name of the virtual key
|
|
secret_token: Value of the virtual key (example: sk-1234)
|
|
"""
|
|
if litellm._key_management_settings is not None:
|
|
if litellm._key_management_settings.store_virtual_keys is True:
|
|
from litellm.secret_managers.base_secret_manager import (
|
|
BaseSecretManager,
|
|
)
|
|
|
|
# store the key in the secret manager
|
|
if isinstance(litellm.secret_manager_client, BaseSecretManager):
|
|
await litellm.secret_manager_client.async_rotate_secret(
|
|
current_secret_name=KeyManagementEventHooks._get_secret_name(
|
|
current_secret_name
|
|
),
|
|
new_secret_name=KeyManagementEventHooks._get_secret_name(
|
|
new_secret_name
|
|
),
|
|
new_secret_value=new_secret_value,
|
|
)
|
|
|
|
@staticmethod
|
|
def _get_secret_name(secret_name: str) -> str:
|
|
if litellm._key_management_settings.prefix_for_stored_virtual_keys.endswith(
|
|
"/"
|
|
):
|
|
return f"{litellm._key_management_settings.prefix_for_stored_virtual_keys}{secret_name}"
|
|
else:
|
|
return f"{litellm._key_management_settings.prefix_for_stored_virtual_keys}/{secret_name}"
|
|
|
|
@staticmethod
|
|
async def _delete_virtual_keys_from_secret_manager(
|
|
keys_being_deleted: List[LiteLLM_VerificationToken],
|
|
):
|
|
"""
|
|
Deletes virtual keys from the secret manager
|
|
|
|
Args:
|
|
keys_being_deleted: List of keys being deleted, this is passed down from the /key/delete operation
|
|
"""
|
|
if litellm._key_management_settings is not None:
|
|
if litellm._key_management_settings.store_virtual_keys is True:
|
|
from litellm.secret_managers.base_secret_manager import (
|
|
BaseSecretManager,
|
|
)
|
|
|
|
if isinstance(litellm.secret_manager_client, BaseSecretManager):
|
|
for key in keys_being_deleted:
|
|
if key.key_alias is not None:
|
|
await litellm.secret_manager_client.async_delete_secret(
|
|
secret_name=KeyManagementEventHooks._get_secret_name(
|
|
key.key_alias
|
|
)
|
|
)
|
|
else:
|
|
verbose_proxy_logger.warning(
|
|
f"KeyManagementEventHooks._delete_virtual_key_from_secret_manager: Key alias not found for key {key.token}. Skipping deletion from secret manager."
|
|
)
|
|
|
|
@staticmethod
|
|
async def _send_key_created_email(response: dict):
|
|
from litellm.proxy.proxy_server import general_settings, proxy_logging_obj
|
|
|
|
if "email" not in general_settings.get("alerting", []):
|
|
raise ValueError(
|
|
"Email alerting not setup on config.yaml. Please set `alerting=['email']. \nDocs: https://docs.litellm.ai/docs/proxy/email`"
|
|
)
|
|
event = WebhookEvent(
|
|
event="key_created",
|
|
event_group="key",
|
|
event_message="API Key Created",
|
|
token=response.get("token", ""),
|
|
spend=response.get("spend", 0.0),
|
|
max_budget=response.get("max_budget", 0.0),
|
|
user_id=response.get("user_id", None),
|
|
team_id=response.get("team_id", "Default Team"),
|
|
key_alias=response.get("key_alias", None),
|
|
)
|
|
|
|
# If user configured email alerting - send an Email letting their end-user know the key was created
|
|
asyncio.create_task(
|
|
proxy_logging_obj.slack_alerting_instance.send_key_created_or_user_invited_email(
|
|
webhook_event=event,
|
|
)
|
|
)
|