mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-09 04:21:42 +00:00
Merge pull request #11417 from colesmcintosh/sso-config-ui
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .proxy_setting_endpoints import router as ui_crud_endpoints_router
|
||||
|
||||
__all__ = ["ui_crud_endpoints_router"]
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<string, string> = {
|
||||
@@ -97,7 +100,126 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
||||
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<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");
|
||||
}
|
||||
};
|
||||
|
||||
// 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<SSOModalsProps> = ({
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="Add SSO"
|
||||
title={ssoConfigured ? "Edit SSO Settings" : "Add SSO"}
|
||||
visible={isAddSSOModalVisible}
|
||||
width={800}
|
||||
footer={null}
|
||||
@@ -131,7 +253,7 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleShowInstructions}
|
||||
onFinish={handleFormSubmit}
|
||||
labelCol={{ span: 8 }}
|
||||
wrapperCol={{ span: 16 }}
|
||||
labelAlign="left"
|
||||
@@ -179,12 +301,52 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
</>
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<div style={{ textAlign: "right", marginTop: "10px", display: "flex", justifyContent: "flex-end", alignItems: "center", gap: "8px" }}>
|
||||
{ssoConfigured && (
|
||||
<Button2
|
||||
onClick={() => 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
|
||||
</Button2>
|
||||
)}
|
||||
<Button2 htmlType="submit">Save</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* Clear Confirmation Modal */}
|
||||
<Modal
|
||||
title="Confirm Clear SSO Settings"
|
||||
visible={isClearConfirmModalVisible}
|
||||
onOk={handleClearSSO}
|
||||
onCancel={() => setIsClearConfirmModalVisible(false)}
|
||||
okText="Yes, Clear"
|
||||
cancelText="Cancel"
|
||||
okButtonProps={{
|
||||
danger: true,
|
||||
style: {
|
||||
backgroundColor: '#dc2626',
|
||||
borderColor: '#dc2626'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p>Are you sure you want to clear all SSO settings? This action cannot be undone.</p>
|
||||
<p>Users will no longer be able to login using SSO after this change.</p>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="SSO Setup Instructions"
|
||||
visible={isInstructionsModalVisible}
|
||||
|
||||
@@ -62,12 +62,12 @@ import {
|
||||
Member,
|
||||
userGetAllUsersCall,
|
||||
User,
|
||||
setCallbacksCall,
|
||||
invitationCreateCall,
|
||||
getPossibleUserRoles,
|
||||
addAllowedIP,
|
||||
getAllowedIPs,
|
||||
deleteAllowedIP,
|
||||
getSSOSettings,
|
||||
} from "./networking";
|
||||
|
||||
const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
@@ -99,6 +99,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
const [isDeleteIPModalVisible, setIsDeleteIPModalVisible] = useState(false);
|
||||
const [allowedIPs, setAllowedIPs] = useState<string[]>([]);
|
||||
const [ipToDelete, setIPToDelete] = useState<string | null>(null);
|
||||
const [ssoConfigured, setSsoConfigured] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
const [possibleUIRoles, setPossibleUIRoles] = useState<null | Record<
|
||||
@@ -117,6 +118,30 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
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<AdminPanelProps> = ({
|
||||
const handleAddSSOOk = () => {
|
||||
setIsAddSSOModalVisible(false);
|
||||
form.resetFields();
|
||||
// Refresh SSO configuration status
|
||||
if (accessToken && premiumUser) {
|
||||
checkSSOConfiguration();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSSOCancel = () => {
|
||||
@@ -194,19 +223,24 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
};
|
||||
|
||||
const handleShowInstructions = (formValues: Record<string, any>) => {
|
||||
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<AdminPanelProps> = ({
|
||||
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<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">
|
||||
@@ -534,12 +546,22 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
<TabPanel>
|
||||
<Card>
|
||||
<Title level={4}> ✨ Security Settings</Title>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginTop: '1rem' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', marginTop: '1rem', marginLeft: '0.5rem' }}>
|
||||
<div>
|
||||
<Button onClick={() => premiumUser === true ? setIsAddSSOModalVisible(true) : message.error("Only premium users can add SSO")}>Add SSO</Button>
|
||||
<Button
|
||||
style={{ width: '150px' }}
|
||||
onClick={() => premiumUser === true ? setIsAddSSOModalVisible(true) : message.error("Only premium users can add SSO")}
|
||||
>
|
||||
{ssoConfigured ? "Edit SSO Settings" : "Add SSO"}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={handleShowAllowedIPs}>Allowed IPs</Button>
|
||||
<Button
|
||||
style={{ width: '150px' }}
|
||||
onClick={handleShowAllowedIPs}
|
||||
>
|
||||
Allowed IPs
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -554,6 +576,8 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
||||
handleInstructionsOk={handleInstructionsOk}
|
||||
handleInstructionsCancel={handleInstructionsCancel}
|
||||
form={form}
|
||||
accessToken={accessToken}
|
||||
ssoConfigured={ssoConfigured}
|
||||
/>
|
||||
<Modal
|
||||
title="Manage Allowed IP Addresses"
|
||||
|
||||
@@ -5357,6 +5357,72 @@ export const updateGuardrailCall = async (
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user