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.
This commit is contained in:
Cole McIntosh 2025-06-04 14:09:19 -06:00
parent 7d4a70bfe3
commit 9c481e3ba8
6 changed files with 338 additions and 36 deletions

View file

@ -0,0 +1,3 @@
from .proxy_setting_endpoints import router as ui_crud_endpoints_router
__all__ = ["ui_crud_endpoints_router"]

View file

@ -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,
}

View file

@ -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

View file

@ -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<string, string> = {
@ -97,7 +99,80 @@ const SSOModals: React.FC<SSOModalsProps> = ({
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<string, any>) => {
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<SSOModalsProps> = ({
>
<Form
form={form}
onFinish={handleShowInstructions}
onFinish={handleFormSubmit}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"

View file

@ -62,7 +62,6 @@ import {
Member,
userGetAllUsersCall,
User,
setCallbacksCall,
invitationCreateCall,
getPossibleUserRoles,
addAllowedIP,
@ -194,11 +193,9 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
};
const handleShowInstructions = (formValues: Record<string, any>) => {
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<AdminPanelProps> = ({
}
};
const handleSSOUpdate = async (formValues: Record<string, any>) => {
if (accessToken == null) {
return;
}
const provider = formValues.sso_provider;
const config = ssoProviderConfigs[provider];
const envVars: Record<string, string> = {
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 (
<div className="w-full m-2 mt-2 p-8">
@ -564,6 +534,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
handleInstructionsOk={handleInstructionsOk}
handleInstructionsCancel={handleInstructionsCancel}
form={form}
accessToken={accessToken}
/>
<Modal
title="Manage Allowed IP Addresses"

View file

@ -5356,3 +5356,69 @@ export const updateGuardrailCall = async (
throw error;
}
};
export const getSSOSettings = async (accessToken: string) => {
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<string, any>) => {
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;
}
};