mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
Merge pull request #24884 from BerriAI/worktree-velvety-seeking-treehouse
feat(key-rotation): add key_rotation_email field for rotation notifications
This commit is contained in:
commit
438653df7b
13 changed files with 99 additions and 11 deletions
|
|
@ -596,14 +596,32 @@ curl 'http://0.0.0.0:4000/key/generate' \
|
|||
}'
|
||||
```
|
||||
|
||||
**Set a custom rotation notification email (useful for service accounts)**
|
||||
|
||||
By default, rotation emails go to the key owner. Use `key_rotation_email` to send notifications to a specific address instead — useful when the key is owned by a service account with no real inbox.
|
||||
|
||||
```bash
|
||||
curl 'http://0.0.0.0:4000/key/generate' \
|
||||
-H 'Authorization: Bearer <your-master-key>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"models": ["gpt-4o"],
|
||||
"auto_rotate": true,
|
||||
"rotation_interval": "30d",
|
||||
"key_rotation_email": "platform-alerts@yourcompany.com"
|
||||
}'
|
||||
```
|
||||
|
||||
**LiteLLM UI**
|
||||
|
||||
On the LiteLLM UI, Navigate to the Keys page and click on `Generate Key` > `Key Lifecycle` > `Enable Auto Rotation`
|
||||
<Image
|
||||
<Image
|
||||
img={require('../../img/key_r.png')}
|
||||
style={{width: '30%', display: 'block', margin: '0'}}
|
||||
/>
|
||||
|
||||
When auto-rotation is enabled, a **Rotation Notification Email** field appears. Leave it empty to use the key owner's email, or enter a specific address (e.g. a team alias or alert inbox).
|
||||
|
||||
**Valid rotation_interval formats:**
|
||||
- `"30s"` - 30 seconds
|
||||
- `"30m"` - 30 minutes
|
||||
|
|
@ -622,7 +640,8 @@ curl 'http://0.0.0.0:4000/key/update' \
|
|||
-d '{
|
||||
"key": "sk-existing-key",
|
||||
"auto_rotate": true,
|
||||
"rotation_interval": "90d"
|
||||
"rotation_interval": "90d",
|
||||
"key_rotation_email": "platform-alerts@yourcompany.com"
|
||||
}'
|
||||
```
|
||||
|
||||
|
|
@ -630,7 +649,7 @@ curl 'http://0.0.0.0:4000/key/update' \
|
|||
|
||||
On the LiteLLM UI, Navigate to the Keys page. Select the key you want to update and click on `Edit Settings` > `Auto-Rotation Settings`
|
||||
|
||||
<Image
|
||||
<Image
|
||||
img={require('../../img/key_u.png')}
|
||||
style={{width: '30%', display: 'block', margin: '0'}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -158,7 +158,8 @@ class BaseEmailLogger(CustomLogger):
|
|||
"""
|
||||
email_params = await self._get_email_params(
|
||||
user_id=send_key_rotated_email_event.user_id,
|
||||
user_email=send_key_rotated_email_event.user_email,
|
||||
user_email=send_key_rotated_email_event.key_rotation_email
|
||||
or send_key_rotated_email_event.user_email,
|
||||
email_event=EmailEvent.virtual_key_rotated,
|
||||
event_message=send_key_rotated_email_event.event_message,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ class SendKeyCreatedEmailEvent(WebhookEvent):
|
|||
class SendKeyRotatedEmailEvent(WebhookEvent):
|
||||
virtual_key: str
|
||||
key_alias: Optional[str] = None
|
||||
key_rotation_email: Optional[str] = None
|
||||
"""
|
||||
The virtual key that was rotated
|
||||
this will be sk-123xxx, since we will be emailing this to the user to start using the new key
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
-- Add key_rotation_email to LiteLLM_VerificationToken and LiteLLM_DeletedVerificationToken
|
||||
-- AlterTable
|
||||
ALTER TABLE "LiteLLM_VerificationToken"
|
||||
ADD COLUMN IF NOT EXISTS "key_rotation_email" TEXT;
|
||||
|
||||
ALTER TABLE "LiteLLM_DeletedVerificationToken"
|
||||
ADD COLUMN IF NOT EXISTS "key_rotation_email" TEXT;
|
||||
|
|
@ -955,6 +955,10 @@ class GenerateKeyRequest(KeyRequestBase):
|
|||
default=None,
|
||||
description="How often to rotate this key (e.g., '30d', '90d'). Required if auto_rotate=True",
|
||||
)
|
||||
key_rotation_email: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Email to notify when key is rotated. Overrides key owner email. Useful for service accounts.",
|
||||
)
|
||||
organization_id: Optional[str] = None
|
||||
project_id: Optional[str] = None
|
||||
|
||||
|
|
@ -1009,6 +1013,7 @@ class UpdateKeyRequest(KeyRequestBase):
|
|||
temp_budget_expiry: Optional[datetime] = None
|
||||
auto_rotate: Optional[bool] = None
|
||||
rotation_interval: Optional[str] = None
|
||||
key_rotation_email: Optional[str] = None
|
||||
organization_id: Optional[str] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
|
@ -2361,6 +2366,9 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase):
|
|||
rotation_interval: Optional[str] = None # How often to rotate (e.g., "30d", "90d")
|
||||
last_rotation_at: Optional[datetime] = None # When this key was last rotated
|
||||
key_rotation_at: Optional[datetime] = None # When this key should next be rotated
|
||||
key_rotation_email: Optional[str] = (
|
||||
None # Override email for rotation notifications
|
||||
)
|
||||
router_settings: Optional[dict] = None
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
|
|
|
|||
|
|
@ -178,6 +178,9 @@ class KeyManagementEventHooks:
|
|||
await KeyManagementEventHooks._send_key_rotated_email(
|
||||
response=response.model_dump(exclude_none=True),
|
||||
existing_key_alias=existing_key_row.key_alias,
|
||||
key_rotation_email=getattr(
|
||||
existing_key_row, "key_rotation_email", None
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.warning(f"Failed to send key rotated email: {e}")
|
||||
|
|
@ -364,10 +367,10 @@ class KeyManagementEventHooks:
|
|||
if key.key_alias is not None:
|
||||
team_id = getattr(key, "team_id", None)
|
||||
if team_id not in team_settings_cache:
|
||||
team_settings_cache[
|
||||
team_id
|
||||
] = await KeyManagementEventHooks._get_secret_manager_optional_params(
|
||||
team_id
|
||||
team_settings_cache[team_id] = (
|
||||
await KeyManagementEventHooks._get_secret_manager_optional_params(
|
||||
team_id
|
||||
)
|
||||
)
|
||||
optional_params = team_settings_cache[team_id]
|
||||
await litellm.secret_manager_client.async_delete_secret(
|
||||
|
|
@ -542,7 +545,9 @@ class KeyManagementEventHooks:
|
|||
|
||||
@staticmethod
|
||||
async def _send_key_rotated_email(
|
||||
response: dict, existing_key_alias: Optional[str]
|
||||
response: dict,
|
||||
existing_key_alias: Optional[str],
|
||||
key_rotation_email: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Send key rotated email if email sending is enabled.
|
||||
|
|
@ -589,6 +594,7 @@ class KeyManagementEventHooks:
|
|||
user_id=response.get("user_id", None),
|
||||
team_id=response.get("team_id", "Default Team"),
|
||||
key_alias=response.get("key_alias", existing_key_alias),
|
||||
key_rotation_email=key_rotation_email,
|
||||
)
|
||||
|
||||
##########################
|
||||
|
|
|
|||
|
|
@ -2798,6 +2798,7 @@ async def generate_key_helper_fn( # noqa: PLR0915
|
|||
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None,
|
||||
auto_rotate: Optional[bool] = None,
|
||||
rotation_interval: Optional[str] = None,
|
||||
key_rotation_email: Optional[str] = None,
|
||||
router_settings: Optional[dict] = None,
|
||||
access_group_ids: Optional[list] = None,
|
||||
):
|
||||
|
|
@ -2928,6 +2929,9 @@ async def generate_key_helper_fn( # noqa: PLR0915
|
|||
rotation_interval=rotation_interval,
|
||||
)
|
||||
|
||||
if key_rotation_email is not None:
|
||||
key_data["key_rotation_email"] = key_rotation_email
|
||||
|
||||
if (
|
||||
get_secret("DISABLE_KEY_NAME", False) is True
|
||||
): # allow user to disable storing abbreviated key name (shown in UI, to help figure out which key spent how much)
|
||||
|
|
|
|||
|
|
@ -387,6 +387,7 @@ model LiteLLM_VerificationToken {
|
|||
rotation_interval String? // How often to rotate (e.g., "30d", "90d")
|
||||
last_rotation_at DateTime? // When this key was last rotated
|
||||
key_rotation_at DateTime? // When this key should next be rotated
|
||||
key_rotation_email String? // Override email for rotation notifications (useful for service accounts)
|
||||
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
|
||||
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
|
||||
litellm_project_table LiteLLM_ProjectTable? @relation(fields: [project_id], references: [project_id])
|
||||
|
|
@ -481,7 +482,8 @@ model LiteLLM_DeletedVerificationToken {
|
|||
rotation_interval String?
|
||||
last_rotation_at DateTime?
|
||||
key_rotation_at DateTime?
|
||||
|
||||
key_rotation_email String?
|
||||
|
||||
// Deletion metadata
|
||||
deleted_at DateTime @default(now()) @map("deleted_at")
|
||||
deleted_by String? @map("deleted_by") // User who deleted the key
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"ignore": [],
|
||||
"exclude": ["**/node_modules", "**/__pycache__", "litellm/types/utils.py", "litellm/proxy/_types.py"],
|
||||
"extraPaths": ["enterprise"],
|
||||
"reportMissingImports": false,
|
||||
"reportPrivateImportUsage": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ interface KeyLifecycleSettingsProps {
|
|||
onAutoRotationChange: (enabled: boolean) => void;
|
||||
rotationInterval: string;
|
||||
onRotationIntervalChange: (interval: string) => void;
|
||||
keyRotationEmail?: string;
|
||||
onKeyRotationEmailChange?: (email: string) => void;
|
||||
isCreateMode?: boolean; // If true, shows "leave empty to never expire" instead of "-1 to never expire"
|
||||
neverExpire?: boolean;
|
||||
onNeverExpireChange?: (checked: boolean) => void;
|
||||
|
|
@ -22,6 +24,8 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
onAutoRotationChange,
|
||||
rotationInterval,
|
||||
onRotationIntervalChange,
|
||||
keyRotationEmail = "",
|
||||
onKeyRotationEmailChange,
|
||||
isCreateMode = false,
|
||||
neverExpire = false,
|
||||
onNeverExpireChange,
|
||||
|
|
@ -169,9 +173,27 @@ const KeyLifecycleSettings: React.FC<KeyLifecycleSettingsProps> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
{autoRotationEnabled && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700 flex items-center space-x-1">
|
||||
<span>Rotation Notification Email</span>
|
||||
<Tooltip title="Email to notify when this key is rotated. Leave empty to use the key owner's email. Useful for service accounts.">
|
||||
<InfoCircleOutlined className="text-gray-400 cursor-help text-xs" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<TextInput
|
||||
name="key_rotation_email"
|
||||
placeholder="alerts@yourteam.com (optional)"
|
||||
className="w-full"
|
||||
value={keyRotationEmail}
|
||||
onValueChange={(val) => onKeyRotationEmailChange?.(val)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{autoRotationEnabled && (
|
||||
<div className="bg-blue-50 p-3 rounded-md text-sm text-blue-700">
|
||||
When rotation occurs, you'll receive a notification with the new key. The old key will be deactivated
|
||||
When rotation occurs, a notification with the new key will be sent to the configured email (or the key owner if none set). The old key will be deactivated
|
||||
after a brief grace period.
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ export interface KeyResponse {
|
|||
access_group_ids?: string[];
|
||||
auto_rotate?: boolean;
|
||||
rotation_interval?: string;
|
||||
key_rotation_email?: string;
|
||||
last_rotation_at?: string;
|
||||
key_rotation_at?: string;
|
||||
next_rotation_at?: string;
|
||||
|
|
|
|||
|
|
@ -200,6 +200,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
const [modelAliases, setModelAliases] = useState<{ [key: string]: string }>({});
|
||||
const [autoRotationEnabled, setAutoRotationEnabled] = useState<boolean>(false);
|
||||
const [rotationInterval, setRotationInterval] = useState<string>("30d");
|
||||
const [keyRotationEmail, setKeyRotationEmail] = useState<string>("");
|
||||
const [routerSettings, setRouterSettings] = useState<RouterSettingsAccordionValue | null>(null);
|
||||
const [routerSettingsKey, setRouterSettingsKey] = useState<number>(0);
|
||||
const [agentsList, setAgentsList] = useState<{ agent_id: string; agent_name: string }[]>([]);
|
||||
|
|
@ -424,6 +425,9 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
if (autoRotationEnabled) {
|
||||
formValues.auto_rotate = true;
|
||||
formValues.rotation_interval = rotationInterval;
|
||||
if (keyRotationEmail.trim()) {
|
||||
formValues.key_rotation_email = keyRotationEmail.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle duration field for key expiry - convert empty string to null
|
||||
|
|
@ -1532,6 +1536,8 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
onAutoRotationChange={setAutoRotationEnabled}
|
||||
rotationInterval={rotationInterval}
|
||||
onRotationIntervalChange={setRotationInterval}
|
||||
keyRotationEmail={keyRotationEmail}
|
||||
onKeyRotationEmailChange={setKeyRotationEmail}
|
||||
isCreateMode={true}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ export function KeyEditView({
|
|||
const [selectedOrganizationId, setSelectedOrganizationId] = useState<string | null>(keyData.organization_id || null);
|
||||
const [autoRotationEnabled, setAutoRotationEnabled] = useState<boolean>(keyData.auto_rotate || false);
|
||||
const [rotationInterval, setRotationInterval] = useState<string>(keyData.rotation_interval || "");
|
||||
const [keyRotationEmail, setKeyRotationEmail] = useState<string>(keyData.key_rotation_email || "");
|
||||
const [neverExpire, setNeverExpire] = useState<boolean>(!keyData.expires);
|
||||
const [isKeySaving, setIsKeySaving] = useState(false);
|
||||
const { data: organizations, isLoading: isOrganizationsLoading } = useOrganizations();
|
||||
|
|
@ -238,6 +239,10 @@ export function KeyEditView({
|
|||
}
|
||||
}, [rotationInterval, form]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldValue("key_rotation_email", keyRotationEmail || null);
|
||||
}, [keyRotationEmail, form]);
|
||||
|
||||
// Fetch tags for selector
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
|
|
@ -707,6 +712,8 @@ export function KeyEditView({
|
|||
onAutoRotationChange={setAutoRotationEnabled}
|
||||
rotationInterval={rotationInterval}
|
||||
onRotationIntervalChange={setRotationInterval}
|
||||
keyRotationEmail={keyRotationEmail}
|
||||
onKeyRotationEmailChange={setKeyRotationEmail}
|
||||
neverExpire={neverExpire}
|
||||
onNeverExpireChange={setNeverExpire}
|
||||
/>
|
||||
|
|
@ -732,6 +739,9 @@ export function KeyEditView({
|
|||
<Form.Item name="rotation_interval" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="key_rotation_email" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<div className="sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]">
|
||||
<div className="flex justify-end items-center gap-2">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue