From 7d4a70bfe35a9c7d6325b6b9feb7547990b5ff11 Mon Sep 17 00:00:00 2001 From: Cole McIntosh Date: Wed, 4 Jun 2025 13:32:43 -0600 Subject: [PATCH 1/8] Enhance Admin Panel UI: Adjust button styles and layout for better accessibility and user experience --- ui/litellm-dashboard/src/components/admins.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index 2ea3e74bcb..bf36ffe35c 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -534,12 +534,22 @@ const AdminPanel: React.FC = ({ ✨ Security Settings -
+
- +
- +
From 9c481e3ba8ad6d45b2e262f32b2c4b8d1b8e4702 Mon Sep 17 00:00:00 2001 From: Cole McIntosh Date: Wed, 4 Jun 2025 14:09:19 -0600 Subject: [PATCH 2/8] Add SSO configuration endpoints and UI integration - Introduced new SSOConfig class to manage SSO settings. - Added endpoints for fetching and updating SSO settings in proxy_setting_endpoints.py. - Created a new __init__.py file to expose the SSO router. - Updated AdminPanel and SSOModals components to handle SSO settings retrieval and updates. - Removed deprecated SSO update logic from AdminPanel. - Enhanced error handling and logging for SSO operations. --- litellm/proxy/ui_crud_endpoints/__init__.py | 3 + .../proxy_setting_endpoints.py | 129 +++++++++++++++++- .../proxy/management_endpoints/ui_sso.py | 62 +++++++++ .../src/components/SSOModals.tsx | 81 ++++++++++- .../src/components/admins.tsx | 33 +---- .../src/components/networking.tsx | 66 +++++++++ 6 files changed, 338 insertions(+), 36 deletions(-) create mode 100644 litellm/proxy/ui_crud_endpoints/__init__.py diff --git a/litellm/proxy/ui_crud_endpoints/__init__.py b/litellm/proxy/ui_crud_endpoints/__init__.py new file mode 100644 index 0000000000..2af6220183 --- /dev/null +++ b/litellm/proxy/ui_crud_endpoints/__init__.py @@ -0,0 +1,3 @@ +from .proxy_setting_endpoints import router as ui_crud_endpoints_router + +__all__ = ["ui_crud_endpoints_router"] \ No newline at end of file diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 6f1b2bfb8c..23e929d57a 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -7,7 +7,7 @@ import litellm from litellm._logging import verbose_proxy_logger from litellm.proxy._types import * from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams +from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams, SSOConfig router = APIRouter() @@ -169,7 +169,7 @@ async def _get_settings_with_schema( tags=["SSO Settings"], dependencies=[Depends(user_api_key_auth)], ) -async def get_sso_settings(): +async def get_internal_user_settings(): """ Get all SSO settings from the litellm_settings configuration. Returns a structured object with values and descriptions for UI display. @@ -281,3 +281,128 @@ async def update_default_team_settings(settings: DefaultTeamSSOParams): in_memory_var=litellm.default_team_params, success_message="Default team settings updated successfully", ) + + +@router.get( + "/get/sso_settings", + tags=["SSO Settings"], + dependencies=[Depends(user_api_key_auth)], +) +async def get_sso_settings(): + """ + Get all SSO configuration settings from the environment variables. + Returns a structured object with values and descriptions for UI display. + """ + import os + from litellm.proxy.proxy_server import proxy_config + + # Load existing config to get both environment variables and general settings + config = await proxy_config.get_config() + general_settings = config.get("general_settings", {}) or {} + environment_variables = config.get("environment_variables", {}) or {} + + # Get user_email from general_settings + proxy_admin_email = general_settings.get("proxy_admin_email", None) + + # Helper function to get env var value (first from config, then from environment) + def get_env_value(env_var_name: str): + return environment_variables.get(env_var_name) or os.getenv(env_var_name) + + # Get current environment variables for SSO + sso_config = SSOConfig( + google_client_id=get_env_value("GOOGLE_CLIENT_ID"), + google_client_secret=get_env_value("GOOGLE_CLIENT_SECRET"), + microsoft_client_id=get_env_value("MICROSOFT_CLIENT_ID"), + microsoft_client_secret=get_env_value("MICROSOFT_CLIENT_SECRET"), + microsoft_tenant=get_env_value("MICROSOFT_TENANT"), + generic_client_id=get_env_value("GENERIC_CLIENT_ID"), + generic_client_secret=get_env_value("GENERIC_CLIENT_SECRET"), + generic_authorization_endpoint=get_env_value("GENERIC_AUTHORIZATION_ENDPOINT"), + generic_token_endpoint=get_env_value("GENERIC_TOKEN_ENDPOINT"), + generic_userinfo_endpoint=get_env_value("GENERIC_USERINFO_ENDPOINT"), + proxy_base_url=get_env_value("PROXY_BASE_URL"), + user_email=proxy_admin_email, # Get from config instead of environment + ) + + # Get the schema for UI display + from pydantic import TypeAdapter + schema = TypeAdapter(SSOConfig).json_schema(by_alias=True) + + # Convert to dict for response + sso_dict = sso_config.model_dump() + + # Add descriptions to the response + result = { + "values": sso_dict, + "schema": {"description": schema.get("description", ""), "properties": {}}, + } + + # Add property descriptions + for field_name, field_info in schema["properties"].items(): + result["schema"]["properties"][field_name] = { + "description": field_info.get("description", ""), + "type": field_info.get("type", "string"), + } + + return result + + +@router.patch( + "/update/sso_settings", + tags=["SSO Settings"], + dependencies=[Depends(user_api_key_auth)], +) +async def update_sso_settings(sso_config: SSOConfig): + """ + Update SSO configuration by saving to both environment variables and config file. + """ + from litellm.proxy.proxy_server import proxy_config + import os + + # Update environment variables + env_var_mapping = { + 'google_client_id': 'GOOGLE_CLIENT_ID', + 'google_client_secret': 'GOOGLE_CLIENT_SECRET', + 'microsoft_client_id': 'MICROSOFT_CLIENT_ID', + 'microsoft_client_secret': 'MICROSOFT_CLIENT_SECRET', + 'microsoft_tenant': 'MICROSOFT_TENANT', + 'generic_client_id': 'GENERIC_CLIENT_ID', + 'generic_client_secret': 'GENERIC_CLIENT_SECRET', + 'generic_authorization_endpoint': 'GENERIC_AUTHORIZATION_ENDPOINT', + 'generic_token_endpoint': 'GENERIC_TOKEN_ENDPOINT', + 'generic_userinfo_endpoint': 'GENERIC_USERINFO_ENDPOINT', + 'proxy_base_url': 'PROXY_BASE_URL', + } + + # Load existing config + config = await proxy_config.get_config() + + # Update config with new environment variables + if "environment_variables" not in config: + config["environment_variables"] = {} + + # Update general_settings for user_email (admin email) + if "general_settings" not in config: + config["general_settings"] = {} + + # Update environment variables in config and in memory + sso_data = sso_config.model_dump(exclude_none=True) + for field_name, value in sso_data.items(): + if field_name == 'user_email' and value is not None: + # Store user_email in general_settings instead of environment variables + config["general_settings"]["proxy_admin_email"] = value + elif field_name in env_var_mapping and value is not None: + env_var_name = env_var_mapping[field_name] + # Update in config + config["environment_variables"][env_var_name] = value + # Update in runtime environment + os.environ[env_var_name] = value + + # Save the updated config + await proxy_config.save_config(new_config=config) + + return { + "message": "SSO settings updated successfully", + "status": "success", + "settings": sso_data, + } diff --git a/litellm/types/proxy/management_endpoints/ui_sso.py b/litellm/types/proxy/management_endpoints/ui_sso.py index 0e6f8739fa..f6838b6170 100644 --- a/litellm/types/proxy/management_endpoints/ui_sso.py +++ b/litellm/types/proxy/management_endpoints/ui_sso.py @@ -31,6 +31,68 @@ class MicrosoftServicePrincipalTeam(TypedDict, total=False): principalId: Optional[str] +class SSOConfig(LiteLLMPydanticObjectBase): + """ + Configuration for SSO environment variables and settings + """ + + # Google SSO + google_client_id: Optional[str] = Field( + default=None, + description="Google OAuth Client ID for SSO authentication", + ) + google_client_secret: Optional[str] = Field( + default=None, + description="Google OAuth Client Secret for SSO authentication", + ) + + # Microsoft SSO + microsoft_client_id: Optional[str] = Field( + default=None, + description="Microsoft OAuth Client ID for SSO authentication", + ) + microsoft_client_secret: Optional[str] = Field( + default=None, + description="Microsoft OAuth Client Secret for SSO authentication", + ) + microsoft_tenant: Optional[str] = Field( + default=None, + description="Microsoft Azure Tenant ID for SSO authentication", + ) + + # Generic/Okta SSO + generic_client_id: Optional[str] = Field( + default=None, + description="Generic OAuth Client ID for SSO authentication (used for Okta and other providers)", + ) + generic_client_secret: Optional[str] = Field( + default=None, + description="Generic OAuth Client Secret for SSO authentication", + ) + generic_authorization_endpoint: Optional[str] = Field( + default=None, + description="Authorization endpoint URL for generic OAuth provider", + ) + generic_token_endpoint: Optional[str] = Field( + default=None, + description="Token endpoint URL for generic OAuth provider", + ) + generic_userinfo_endpoint: Optional[str] = Field( + default=None, + description="User info endpoint URL for generic OAuth provider", + ) + + # Common settings + proxy_base_url: Optional[str] = Field( + default=None, + description="Base URL of the proxy server for SSO redirects", + ) + user_email: Optional[str] = Field( + default=None, + description="Email of the proxy admin user", + ) + + class DefaultTeamSSOParams(LiteLLMPydanticObjectBase): """ Default parameters to apply when a new team is automatically created by LiteLLM via SSO Groups diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index 6e0c574016..47ece2fd3b 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.tsx @@ -1,6 +1,7 @@ -import React from "react"; -import { Modal, Form, Input, Button as Button2, Select } from "antd"; +import React, { useEffect } from "react"; +import { Modal, Form, Input, Button as Button2, Select, message } from "antd"; import { Text, TextInput } from "@tremor/react"; +import { getSSOSettings, updateSSOSettings } from "./networking"; interface SSOModalsProps { isAddSSOModalVisible: boolean; @@ -11,6 +12,7 @@ interface SSOModalsProps { handleInstructionsOk: () => void; handleInstructionsCancel: () => void; form: any; // Replace with proper Form type if available + accessToken: string | null; } const ssoProviderLogoMap: Record = { @@ -97,7 +99,80 @@ const SSOModals: React.FC = ({ handleInstructionsOk, handleInstructionsCancel, form, + accessToken, }) => { + // Load existing SSO settings when modal opens + useEffect(() => { + const loadSSOSettings = async () => { + if (isAddSSOModalVisible && accessToken) { + try { + const ssoData = await getSSOSettings(accessToken); + console.log("Raw SSO data received:", ssoData); // Debug log + if (ssoData && ssoData.values) { + console.log("SSO values:", ssoData.values); // Debug log + console.log("user_email from API:", ssoData.values.user_email); // Debug log + + // Determine which SSO provider is configured + let selectedProvider = null; + if (ssoData.values.google_client_id) { + selectedProvider = 'google'; + } else if (ssoData.values.microsoft_client_id) { + selectedProvider = 'microsoft'; + } else if (ssoData.values.generic_client_id) { + // Check if it looks like Okta based on endpoints + if (ssoData.values.generic_authorization_endpoint?.includes('okta') || + ssoData.values.generic_authorization_endpoint?.includes('auth0')) { + selectedProvider = 'okta'; + } else { + selectedProvider = 'generic'; + } + } + + // Set form values with existing data + const formValues = { + sso_provider: selectedProvider, + proxy_base_url: ssoData.values.proxy_base_url, + user_email: ssoData.values.user_email, + ...ssoData.values, + }; + + console.log("Setting form values:", formValues); // Debug log + + // Clear form first, then set values with a small delay to ensure proper initialization + form.resetFields(); + setTimeout(() => { + form.setFieldsValue(formValues); + console.log("Form values set, current form values:", form.getFieldsValue()); // Debug log + }, 100); + } + } catch (error) { + console.error("Failed to load SSO settings:", error); + } + } + }; + + loadSSOSettings(); + }, [isAddSSOModalVisible, accessToken, form]); + + // Enhanced form submission handler + const handleFormSubmit = async (formValues: Record) => { + if (!accessToken) { + message.error("No access token available"); + return; + } + + try { + // Save SSO settings using the new API + await updateSSOSettings(accessToken, formValues); + + // Continue with the original flow (show instructions) + handleShowInstructions(formValues); + } catch (error) { + console.error("Failed to save SSO settings:", error); + message.error("Failed to save SSO settings"); + } + }; + // Helper function to render provider fields const renderProviderFields = (provider: string) => { const config = ssoProviderConfigs[provider]; @@ -131,7 +206,7 @@ const SSOModals: React.FC = ({ >
= ({ }; const handleShowInstructions = (formValues: Record) => { - handleAdminCreate(formValues); - handleSSOUpdate(formValues); + console.log("Form submitted with values:", formValues); setIsAddSSOModalVisible(false); setIsInstructionsModalVisible(true); - // Optionally, you can call handleSSOUpdate here with the formValues }; const handleInstructionsOk = () => { @@ -493,33 +490,6 @@ const AdminPanel: React.FC = ({ } }; - const handleSSOUpdate = async (formValues: Record) => { - if (accessToken == null) { - return; - } - - const provider = formValues.sso_provider; - const config = ssoProviderConfigs[provider]; - - const envVars: Record = { - PROXY_BASE_URL: formValues.proxy_base_url, - }; - - // Add provider-specific environment variables using the configuration - if (config) { - Object.entries(config.envVarMap).forEach(([formKey, envKey]) => { - if (formValues[formKey]) { - envVars[envKey] = formValues[formKey]; - } - }); - } - - const payload = { - environment_variables: envVars, - }; - - setCallbacksCall(accessToken, payload); - }; console.log(`admins: ${admins?.length}`); return (
@@ -564,6 +534,7 @@ const AdminPanel: React.FC = ({ handleInstructionsOk={handleInstructionsOk} handleInstructionsCancel={handleInstructionsCancel} form={form} + accessToken={accessToken} /> { + try { + // Construct base URL + let url = proxyBaseUrl + ? `${proxyBaseUrl}/get/sso_settings` + : `/get/sso_settings`; + + console.log("Fetching SSO configuration from:", url); + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("Fetched SSO configuration:", data); + return data; + } catch (error) { + console.error("Failed to fetch SSO configuration:", error); + throw error; + } +}; + + +export const updateSSOSettings = async (accessToken: string, settings: Record) => { + try { + // Construct base URL + let url = proxyBaseUrl + ? `${proxyBaseUrl}/update/sso_settings` + : `/update/sso_settings`; + + console.log("Updating SSO configuration:", settings); + + const response = await fetch(url, { + method: "PATCH", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(settings), + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + console.log("Updated SSO configuration:", data); + return data; + } catch (error) { + console.error("Failed to update SSO configuration:", error); + throw error; + } +}; From 58b1f78ff024580c2d78c3a5c374f8bc8cdb6000 Mon Sep 17 00:00:00 2001 From: Cole McIntosh Date: Wed, 4 Jun 2025 14:25:57 -0600 Subject: [PATCH 3/8] Remove console log from handleShowInstructions in AdminPanel component --- ui/litellm-dashboard/src/components/admins.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index 8378134326..79c6b95ce7 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -193,7 +193,6 @@ const AdminPanel: React.FC = ({ }; const handleShowInstructions = (formValues: Record) => { - console.log("Form submitted with values:", formValues); setIsAddSSOModalVisible(false); setIsInstructionsModalVisible(true); }; From 72c7fd63bf43f65ffe8aae19b23fa553ef462111 Mon Sep 17 00:00:00 2001 From: Cole McIntosh Date: Wed, 4 Jun 2025 14:34:18 -0600 Subject: [PATCH 4/8] Implement SSO configuration check in AdminPanel and update SSOModals to reflect SSO status - Added logic to check SSO configuration and set state in AdminPanel. - Introduced a new function to handle SSO configuration checks. - Updated UI to conditionally render SSO button text based on configuration status. - Passed SSO configuration status as a prop to SSOModals for better integration. --- .../src/components/SSOModals.tsx | 4 +- .../src/components/admins.tsx | 46 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index 47ece2fd3b..9de42f8c01 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.tsx @@ -13,6 +13,7 @@ interface SSOModalsProps { handleInstructionsCancel: () => void; form: any; // Replace with proper Form type if available accessToken: string | null; + ssoConfigured?: boolean; // Add optional prop to indicate if SSO is configured } const ssoProviderLogoMap: Record = { @@ -100,6 +101,7 @@ const SSOModals: React.FC = ({ handleInstructionsCancel, form, accessToken, + ssoConfigured = false, // Default to false if not provided }) => { // Load existing SSO settings when modal opens useEffect(() => { @@ -197,7 +199,7 @@ const SSOModals: React.FC = ({ return ( <> = ({ @@ -98,6 +99,7 @@ const AdminPanel: React.FC = ({ const [isDeleteIPModalVisible, setIsDeleteIPModalVisible] = useState(false); const [allowedIPs, setAllowedIPs] = useState([]); const [ipToDelete, setIPToDelete] = useState(null); + const [ssoConfigured, setSsoConfigured] = useState(false); const router = useRouter(); const [possibleUIRoles, setPossibleUIRoles] = useState = ({ let nonSssoUrl = baseUrl; nonSssoUrl += "/fallback/login"; + // Extract the SSO configuration check logic into a separate function for reuse + const checkSSOConfiguration = async () => { + if (accessToken && premiumUser) { + try { + const ssoData = await getSSOSettings(accessToken); + console.log("SSO data:", ssoData); + + // Check if any SSO provider is configured + if (ssoData && ssoData.values) { + const hasGoogleSSO = ssoData.values.google_client_id && ssoData.values.google_client_secret; + const hasMicrosoftSSO = ssoData.values.microsoft_client_id && ssoData.values.microsoft_client_secret; + const hasGenericSSO = ssoData.values.generic_client_id && ssoData.values.generic_client_secret; + + setSsoConfigured(hasGoogleSSO || hasMicrosoftSSO || hasGenericSSO); + } else { + setSsoConfigured(false); + } + } catch (error) { + console.error("Error checking SSO configuration:", error); + setSsoConfigured(false); + } + } + }; + const handleShowAllowedIPs = async () => { try { if (premiumUser !== true) { @@ -185,6 +211,10 @@ const AdminPanel: React.FC = ({ const handleAddSSOOk = () => { setIsAddSSOModalVisible(false); form.resetFields(); + // Refresh SSO configuration status + if (accessToken && premiumUser) { + checkSSOConfiguration(); + } }; const handleAddSSOCancel = () => { @@ -199,10 +229,18 @@ const AdminPanel: React.FC = ({ const handleInstructionsOk = () => { setIsInstructionsModalVisible(false); + // Refresh SSO configuration status after instructions are closed + if (accessToken && premiumUser) { + checkSSOConfiguration(); + } }; const handleInstructionsCancel = () => { setIsInstructionsModalVisible(false); + // Refresh SSO configuration status after instructions are closed + if (accessToken && premiumUser) { + checkSSOConfiguration(); + } }; const roles = ["proxy_admin", "proxy_admin_viewer"]; @@ -264,6 +302,11 @@ const AdminPanel: React.FC = ({ fetchProxyAdminInfo(); }, [accessToken]); + // Add new useEffect to check SSO configuration + useEffect(() => { + checkSSOConfiguration(); + }, [accessToken, premiumUser]); + const handleMemberUpdateOk = () => { setIsUpdateModalModalVisible(false); memberForm.resetFields(); @@ -509,7 +552,7 @@ const AdminPanel: React.FC = ({ style={{ width: '150px' }} onClick={() => premiumUser === true ? setIsAddSSOModalVisible(true) : message.error("Only premium users can add SSO")} > - Add SSO + {ssoConfigured ? "Edit SSO Settings" : "Add SSO"}
@@ -534,6 +577,7 @@ const AdminPanel: React.FC = ({ handleInstructionsCancel={handleInstructionsCancel} form={form} accessToken={accessToken} + ssoConfigured={ssoConfigured} /> Date: Wed, 4 Jun 2025 14:39:26 -0600 Subject: [PATCH 5/8] Add clear SSO settings functionality in SSOModals component - Introduced a confirmation modal for clearing SSO settings. - Implemented handleClearSSO function to reset SSO settings and provide user feedback. - Updated UI to include a 'Clear' button for SSO settings, enhancing user experience. - Added state management for the confirmation modal visibility. --- .../src/components/SSOModals.tsx | 89 ++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index 9de42f8c01..0779edb89d 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { Modal, Form, Input, Button as Button2, Select, message } from "antd"; import { Text, TextInput } from "@tremor/react"; import { getSSOSettings, updateSSOSettings } from "./networking"; @@ -103,6 +103,8 @@ const SSOModals: React.FC = ({ accessToken, ssoConfigured = false, // Default to false if not provided }) => { + const [isClearConfirmModalVisible, setIsClearConfirmModalVisible] = useState(false); + // Load existing SSO settings when modal opens useEffect(() => { const loadSSOSettings = async () => { @@ -175,6 +177,49 @@ const SSOModals: React.FC = ({ } }; + // Handle clearing SSO settings + const handleClearSSO = async () => { + if (!accessToken) { + message.error("No access token available"); + return; + } + + try { + // Clear all SSO settings by sending empty values + const clearSettings = { + google_client_id: '', + google_client_secret: '', + microsoft_client_id: '', + microsoft_client_secret: '', + microsoft_tenant: '', + generic_client_id: '', + generic_client_secret: '', + generic_authorization_endpoint: '', + generic_token_endpoint: '', + generic_userinfo_endpoint: '', + proxy_base_url: '', + user_email: '', + sso_provider: '', + }; + + await updateSSOSettings(accessToken, clearSettings); + + // Clear the form + form.resetFields(); + + // Close the confirmation modal + setIsClearConfirmModalVisible(false); + + // Close the main SSO modal and trigger refresh + handleAddSSOOk(); + + message.success("SSO settings cleared successfully"); + } catch (error) { + console.error("Failed to clear SSO settings:", error); + message.error("Failed to clear SSO settings"); + } + }; + // Helper function to render provider fields const renderProviderFields = (provider: string) => { const config = ssoProviderConfigs[provider]; @@ -256,12 +301,52 @@ const SSOModals: React.FC = ({ -
+
+ {ssoConfigured && ( + setIsClearConfirmModalVisible(true)} + style={{ + backgroundColor: '#6366f1', + borderColor: '#6366f1', + color: 'white' + }} + onMouseEnter={(e) => { + e.currentTarget.style.backgroundColor = '#5558eb'; + e.currentTarget.style.borderColor = '#5558eb'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = '#6366f1'; + e.currentTarget.style.borderColor = '#6366f1'; + }} + > + Clear + + )} Save
+ {/* Clear Confirmation Modal */} + setIsClearConfirmModalVisible(false)} + okText="Yes, Clear" + cancelText="Cancel" + okButtonProps={{ + danger: true, + style: { + backgroundColor: '#dc2626', + borderColor: '#dc2626' + } + }} + > +

Are you sure you want to clear all SSO settings? This action cannot be undone.

+

Users will no longer be able to login using SSO after this change.

+
+ Date: Wed, 4 Jun 2025 14:55:29 -0600 Subject: [PATCH 6/8] Add SSO settings response model in proxy_setting_endpoints.py - Introduced SSOSettingsResponse model to encapsulate SSO configuration values and schema information. - Updated the get_sso_settings endpoint to utilize the new response model, enhancing API clarity and usability. --- .../ui_crud_endpoints/proxy_setting_endpoints.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 23e929d57a..c68cf603b4 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -1,5 +1,5 @@ #### CRUD ENDPOINTS for UI Settings ##### -from typing import Any, List, Union +from typing import Any, Dict, List, Union from fastapi import APIRouter, Depends, HTTPException @@ -16,6 +16,16 @@ class IPAddress(BaseModel): ip: str +class SSOSettingsResponse(BaseModel): + """Response model for SSO settings with values and schema information""" + + values: Dict[str, Any] + """The current SSO configuration values""" + + schema: Dict[str, Any] + """Schema information including descriptions and property types for UI display""" + + @router.get( "/get/allowed_ips", tags=["Budget & Spend Tracking"], @@ -287,6 +297,7 @@ async def update_default_team_settings(settings: DefaultTeamSSOParams): "/get/sso_settings", tags=["SSO Settings"], dependencies=[Depends(user_api_key_auth)], + response_model=SSOSettingsResponse, ) async def get_sso_settings(): """ From 3a946933ee1c7270ab89afecb9d4960e8baa4fde Mon Sep 17 00:00:00 2001 From: Cole McIntosh Date: Wed, 4 Jun 2025 15:08:05 -0600 Subject: [PATCH 7/8] Refactor settings response models in proxy_setting_endpoints.py - Renamed SSOSettingsResponse to inherit from a new base class SettingsResponse for better structure. - Introduced InternalUserSettingsResponse and DefaultTeamSettingsResponse models for internal user and default team settings. - Updated endpoint responses to use field_schema instead of schema for consistency. - Enhanced test cases to validate the new response structure and ensure proper functionality of SSO settings. --- .../proxy_setting_endpoints.py | 35 ++++-- .../test_proxy_setting_endpoints.py | 108 +++++++++++++++--- 2 files changed, 121 insertions(+), 22 deletions(-) diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index c68cf603b4..25991862fa 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -16,16 +16,31 @@ class IPAddress(BaseModel): ip: str -class SSOSettingsResponse(BaseModel): - """Response model for SSO settings with values and schema information""" +class SettingsResponse(BaseModel): + """Base response model for settings with values and schema information""" values: Dict[str, Any] - """The current SSO configuration values""" + """The current configuration values""" - schema: Dict[str, Any] + field_schema: Dict[str, Any] """Schema information including descriptions and property types for UI display""" +class SSOSettingsResponse(SettingsResponse): + """Response model for SSO settings""" + pass + + +class InternalUserSettingsResponse(SettingsResponse): + """Response model for internal user settings""" + pass + + +class DefaultTeamSettingsResponse(SettingsResponse): + """Response model for default team settings""" + pass + + @router.get( "/get/allowed_ips", tags=["Budget & Spend Tracking"], @@ -151,19 +166,19 @@ async def _get_settings_with_schema( # Add descriptions to the response result = { "values": settings_dict, - "schema": {"description": schema.get("description", ""), "properties": {}}, + "field_schema": {"description": schema.get("description", ""), "properties": {}}, } # Add property descriptions for field_name, field_info in schema["properties"].items(): - result["schema"]["properties"][field_name] = { + result["field_schema"]["properties"][field_name] = { "description": field_info.get("description", ""), "type": field_info.get("type", "string"), } # Add nested object descriptions for def_name, def_schema in schema.get("definitions", {}).items(): - result["schema"][def_name] = { + result["field_schema"][def_name] = { "description": def_schema.get("description", ""), "properties": { prop_name: {"description": prop_info.get("description", "")} @@ -178,6 +193,7 @@ async def _get_settings_with_schema( "/get/internal_user_settings", tags=["SSO Settings"], dependencies=[Depends(user_api_key_auth)], + response_model=InternalUserSettingsResponse, ) async def get_internal_user_settings(): """ @@ -200,6 +216,7 @@ async def get_internal_user_settings(): "/get/default_team_settings", tags=["SSO Settings"], dependencies=[Depends(user_api_key_auth)], + response_model=DefaultTeamSettingsResponse, ) async def get_default_team_settings(): """ @@ -345,12 +362,12 @@ async def get_sso_settings(): # Add descriptions to the response result = { "values": sso_dict, - "schema": {"description": schema.get("description", ""), "properties": {}}, + "field_schema": {"description": schema.get("description", ""), "properties": {}}, } # Add property descriptions for field_name, field_info in schema["properties"].items(): - result["schema"]["properties"][field_name] = { + result["field_schema"]["properties"][field_name] = { "description": field_info.get("description", ""), "type": field_info.get("type", "string"), } diff --git a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py index 543258640e..db7045aa0d 100644 --- a/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py +++ b/tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py @@ -11,7 +11,7 @@ sys.path.insert( from litellm.proxy._types import DefaultInternalUserParams, LitellmUserRoles from litellm.proxy.proxy_server import app -from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams +from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams, SSOConfig client = TestClient(app) @@ -34,6 +34,16 @@ def mock_proxy_config(monkeypatch): "tpm_limit": 100, "rpm_limit": 10, }, + }, + "general_settings": { + "proxy_admin_email": "admin@example.com" + }, + "environment_variables": { + "GOOGLE_CLIENT_ID": "test_google_client_id", + "GOOGLE_CLIENT_SECRET": "test_google_client_secret", + "MICROSOFT_CLIENT_ID": "test_microsoft_client_id", + "MICROSOFT_CLIENT_SECRET": "test_microsoft_client_secret", + "PROXY_BASE_URL": "https://example.com" } } @@ -84,9 +94,9 @@ class TestProxySettingEndpoints: assert response.status_code == 200 data = response.json() - # Check structure of response + # Check structure of response (updated to use field_schema) assert "values" in data - assert "schema" in data + assert "field_schema" in data # Check values match our mock config values = data["values"] @@ -98,10 +108,10 @@ class TestProxySettingEndpoints: assert values["budget_duration"] == mock_params["budget_duration"] assert values["models"] == mock_params["models"] - # Check schema contains descriptions - assert "properties" in data["schema"] - assert "user_role" in data["schema"]["properties"] - assert "description" in data["schema"]["properties"]["user_role"] + # Check field_schema contains descriptions (updated from schema to field_schema) + assert "properties" in data["field_schema"] + assert "user_role" in data["field_schema"]["properties"] + assert "description" in data["field_schema"]["properties"]["user_role"] def test_update_internal_user_settings( self, mock_proxy_config, mock_auth, monkeypatch @@ -153,9 +163,9 @@ class TestProxySettingEndpoints: assert response.status_code == 200 data = response.json() - # Check structure of response + # Check structure of response (updated to use field_schema) assert "values" in data - assert "schema" in data + assert "field_schema" in data # Check values match our mock config values = data["values"] @@ -168,10 +178,10 @@ class TestProxySettingEndpoints: assert values["tpm_limit"] == mock_params["tpm_limit"] assert values["rpm_limit"] == mock_params["rpm_limit"] - # Check schema contains descriptions - assert "properties" in data["schema"] - assert "models" in data["schema"]["properties"] - assert "description" in data["schema"]["properties"]["models"] + # Check field_schema contains descriptions (updated from schema to field_schema) + assert "properties" in data["field_schema"] + assert "models" in data["field_schema"]["properties"] + assert "description" in data["field_schema"]["properties"]["models"] def test_update_default_team_settings( self, mock_proxy_config, mock_auth, monkeypatch @@ -218,3 +228,75 @@ class TestProxySettingEndpoints: # Verify save_config was called exactly once assert mock_proxy_config["save_call_count"]() == 1 + + def test_get_sso_settings(self, mock_proxy_config, mock_auth): + """Test getting the SSO settings""" + response = client.get("/get/sso_settings") + + assert response.status_code == 200 + data = response.json() + + # Check structure of response + assert "values" in data + assert "field_schema" in data + + # Check values contain SSO configuration + values = data["values"] + assert "google_client_id" in values + assert "google_client_secret" in values + assert "microsoft_client_id" in values + assert "microsoft_client_secret" in values + assert "proxy_base_url" in values + assert "user_email" in values + + # Verify values match our mock config + assert values["google_client_id"] == "test_google_client_id" + assert values["google_client_secret"] == "test_google_client_secret" + assert values["microsoft_client_id"] == "test_microsoft_client_id" + assert values["microsoft_client_secret"] == "test_microsoft_client_secret" + assert values["proxy_base_url"] == "https://example.com" + assert values["user_email"] == "admin@example.com" + + # Check field_schema contains descriptions + assert "properties" in data["field_schema"] + assert "google_client_id" in data["field_schema"]["properties"] + assert "description" in data["field_schema"]["properties"]["google_client_id"] + + def test_update_sso_settings(self, mock_proxy_config, mock_auth): + """Test updating the SSO settings""" + # New SSO settings to update + new_sso_settings = { + "google_client_id": "new_google_client_id", + "google_client_secret": "new_google_client_secret", + "microsoft_client_id": "new_microsoft_client_id", + "microsoft_client_secret": "new_microsoft_client_secret", + "proxy_base_url": "https://newexample.com", + "user_email": "newadmin@example.com" + } + + response = client.patch("/update/sso_settings", json=new_sso_settings) + + assert response.status_code == 200 + data = response.json() + + # Check response structure + assert data["status"] == "success" + assert "settings" in data + + # Verify settings were updated + settings = data["settings"] + assert settings["google_client_id"] == new_sso_settings["google_client_id"] + assert settings["google_client_secret"] == new_sso_settings["google_client_secret"] + assert settings["microsoft_client_id"] == new_sso_settings["microsoft_client_id"] + assert settings["microsoft_client_secret"] == new_sso_settings["microsoft_client_secret"] + assert settings["proxy_base_url"] == new_sso_settings["proxy_base_url"] + assert settings["user_email"] == new_sso_settings["user_email"] + + # Verify the config was updated + updated_config = mock_proxy_config["config"] + assert updated_config["environment_variables"]["GOOGLE_CLIENT_ID"] == new_sso_settings["google_client_id"] + assert updated_config["environment_variables"]["GOOGLE_CLIENT_SECRET"] == new_sso_settings["google_client_secret"] + assert updated_config["general_settings"]["proxy_admin_email"] == new_sso_settings["user_email"] + + # Verify save_config was called exactly once + assert mock_proxy_config["save_call_count"]() == 1 From 65b28826a615e0692107d50ad3dcffd262f4767e Mon Sep 17 00:00:00 2001 From: Cole McIntosh Date: Wed, 4 Jun 2025 18:19:53 -0600 Subject: [PATCH 8/8] Add uiAuditLogsCall function --- .../src/components/networking.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 7853baf704..49dab8b819 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -5422,3 +5422,49 @@ export const updateSSOSettings = async (accessToken: string, settings: Record { + try { + // Construct base URL + let url = proxyBaseUrl ? `${proxyBaseUrl}/audit` : `/audit`; + + // Add query parameters if they exist + const queryParams = new URLSearchParams(); + // if (start_date) queryParams.append('start_date', start_date); + // if (end_date) queryParams.append('end_date', end_date); + if (page) queryParams.append('page', page.toString()); + if (page_size) queryParams.append('page_size', page_size.toString()); + + // Append query parameters to URL if any exist + const queryString = queryParams.toString(); + if (queryString) { + url += `?${queryString}`; + } + + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.text(); + handleError(errorData); + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error("Failed to fetch audit logs:", error); + throw error; + } +};