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..25991862fa 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 @@ -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() @@ -16,6 +16,31 @@ class IPAddress(BaseModel): ip: str +class SettingsResponse(BaseModel): + """Base response model for settings with values and schema information""" + + values: Dict[str, Any] + """The current configuration values""" + + 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"], @@ -141,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", "")} @@ -168,8 +193,9 @@ 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_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. @@ -190,6 +216,7 @@ async def get_sso_settings(): "/get/default_team_settings", tags=["SSO Settings"], dependencies=[Depends(user_api_key_auth)], + response_model=DefaultTeamSettingsResponse, ) async def get_default_team_settings(): """ @@ -281,3 +308,129 @@ 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)], + response_model=SSOSettingsResponse, +) +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, + "field_schema": {"description": schema.get("description", ""), "properties": {}}, + } + + # Add property descriptions + for field_name, field_info in schema["properties"].items(): + result["field_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/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 diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index 6e0c574016..0779edb89d 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, 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"; interface SSOModalsProps { isAddSSOModalVisible: boolean; @@ -11,6 +12,8 @@ interface SSOModalsProps { handleInstructionsOk: () => void; 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 = { @@ -97,7 +100,126 @@ const SSOModals: React.FC = ({ handleInstructionsOk, handleInstructionsCancel, form, + 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 () => { + 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"); + } + }; + + // 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]; @@ -122,7 +244,7 @@ const SSOModals: React.FC = ({ return ( <> = ({ >
= ({ -
+
+ {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.

+
+ = ({ @@ -99,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) { @@ -186,6 +211,10 @@ const AdminPanel: React.FC = ({ const handleAddSSOOk = () => { setIsAddSSOModalVisible(false); form.resetFields(); + // Refresh SSO configuration status + if (accessToken && premiumUser) { + checkSSOConfiguration(); + } }; const handleAddSSOCancel = () => { @@ -194,19 +223,24 @@ const AdminPanel: React.FC = ({ }; const handleShowInstructions = (formValues: Record) => { - handleAdminCreate(formValues); - handleSSOUpdate(formValues); setIsAddSSOModalVisible(false); setIsInstructionsModalVisible(true); - // Optionally, you can call handleSSOUpdate here with the formValues }; 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"]; @@ -268,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(); @@ -493,33 +532,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 (
@@ -534,12 +546,22 @@ const AdminPanel: React.FC = ({ ✨ Security Settings -
+
- +
- +
@@ -554,6 +576,8 @@ const AdminPanel: React.FC = ({ handleInstructionsOk={handleInstructionsOk} handleInstructionsCancel={handleInstructionsCancel} form={form} + accessToken={accessToken} + ssoConfigured={ssoConfigured} /> { + 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; + } +}; + export const uiAuditLogsCall = async ( accessToken: String, start_date?: string, @@ -5398,7 +5464,7 @@ export const uiAuditLogsCall = async ( const data = await response.json(); return data; } catch (error) { - console.error("Failed to fetch spend logs:", error); + console.error("Failed to fetch audit logs:", error); throw error; } -}; \ No newline at end of file +};