Skip to content

Commit 1585a1b

Browse files
committed
fix: SSO clear button now properly clears settings instead of creating empty encrypted values
1 parent 8a012f9 commit 1585a1b

File tree

3 files changed

+238
-23
lines changed

3 files changed

+238
-23
lines changed

litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -493,21 +493,32 @@ async def update_sso_settings(sso_config: SSOConfig):
493493
config["general_settings"] = {}
494494

495495
# Update environment variables in config and in memory
496-
sso_data = sso_config.model_dump(exclude_none=True)
496+
sso_data = sso_config.model_dump()
497497
for field_name, value in sso_data.items():
498-
499-
if field_name == "user_email" and value is not None:
500-
# Store user_email in general_settings instead of environment variables
501-
config["general_settings"]["proxy_admin_email"] = value
502-
elif field_name == "ui_access_mode" and value is not None:
503-
504-
config["general_settings"]["ui_access_mode"] = value
505-
elif field_name in env_var_mapping and value is not None:
498+
if field_name == "user_email":
499+
if value:
500+
# Store user_email in general_settings instead of environment variables
501+
config["general_settings"]["proxy_admin_email"] = value
502+
else:
503+
# Clear user_email if null/empty
504+
config["general_settings"].pop("proxy_admin_email", None)
505+
elif field_name == "ui_access_mode":
506+
if value:
507+
config["general_settings"]["ui_access_mode"] = value
508+
else:
509+
# Clear ui_access_mode if null/empty
510+
config["general_settings"].pop("ui_access_mode", None)
511+
elif field_name in env_var_mapping and value:
506512
env_var_name = env_var_mapping[field_name]
507513
# Update in config
508514
config["environment_variables"][env_var_name] = value
509515
# Update in runtime environment
510516
os.environ[env_var_name] = value
517+
elif field_name in env_var_mapping:
518+
# Clear environment variable if value is null/empty
519+
env_var_name = env_var_mapping[field_name]
520+
config["environment_variables"].pop(env_var_name, None)
521+
os.environ.pop(env_var_name, None)
511522

512523
stored_config = config
513524
if len(config["environment_variables"]) > 0:

tests/test_litellm/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,3 +322,207 @@ def test_update_sso_settings(self, mock_proxy_config, mock_auth, monkeypatch):
322322

323323
# Verify save_config was called exactly once
324324
assert mock_proxy_config["save_call_count"]() == 1
325+
326+
def test_update_sso_settings_with_null_values_clears_env_vars(
327+
self, mock_proxy_config, mock_auth, monkeypatch
328+
):
329+
"""Test that updating SSO settings with null values clears environment variables"""
330+
monkeypatch.setenv("LITELLM_SALT_KEY", "test_salt_key")
331+
332+
# First, verify we have existing environment variables
333+
initial_config = mock_proxy_config["config"]
334+
assert "GOOGLE_CLIENT_ID" in initial_config["environment_variables"]
335+
assert "MICROSOFT_CLIENT_ID" in initial_config["environment_variables"]
336+
337+
# Set some initial environment variables for runtime testing
338+
monkeypatch.setenv("GOOGLE_CLIENT_ID", "test_existing_google_id")
339+
monkeypatch.setenv("MICROSOFT_CLIENT_ID", "test_existing_microsoft_id")
340+
341+
# Send SSO settings with null values to clear them
342+
clear_sso_settings = {
343+
"google_client_id": None,
344+
"google_client_secret": None,
345+
"microsoft_client_id": None,
346+
"microsoft_client_secret": None,
347+
"microsoft_tenant": None,
348+
"generic_client_id": None,
349+
"generic_client_secret": None,
350+
"generic_authorization_endpoint": None,
351+
"generic_token_endpoint": None,
352+
"generic_userinfo_endpoint": None,
353+
"proxy_base_url": None,
354+
"user_email": None,
355+
"sso_provider": None,
356+
}
357+
358+
response = client.patch("/update/sso_settings", json=clear_sso_settings)
359+
360+
assert response.status_code == 200
361+
data = response.json()
362+
assert data["status"] == "success"
363+
364+
# Verify that environment variables were cleared from config
365+
updated_config = mock_proxy_config["config"]
366+
367+
# These should be removed from environment_variables
368+
assert "GOOGLE_CLIENT_ID" not in updated_config["environment_variables"]
369+
assert "GOOGLE_CLIENT_SECRET" not in updated_config["environment_variables"]
370+
assert "MICROSOFT_CLIENT_ID" not in updated_config["environment_variables"]
371+
assert "MICROSOFT_CLIENT_SECRET" not in updated_config["environment_variables"]
372+
assert "MICROSOFT_TENANT" not in updated_config["environment_variables"]
373+
assert "PROXY_BASE_URL" not in updated_config["environment_variables"]
374+
375+
# Verify that runtime environment variables were cleared
376+
assert "GOOGLE_CLIENT_ID" not in os.environ
377+
assert "MICROSOFT_CLIENT_ID" not in os.environ
378+
379+
# Verify user_email was cleared from general_settings
380+
assert updated_config["general_settings"].get("proxy_admin_email") is None
381+
382+
# Verify save_config was called
383+
assert mock_proxy_config["save_call_count"]() == 1
384+
385+
def test_update_sso_settings_with_empty_strings_clears_env_vars(
386+
self, mock_proxy_config, mock_auth, monkeypatch
387+
):
388+
"""Test that updating SSO settings with empty strings also clears environment variables"""
389+
monkeypatch.setenv("LITELLM_SALT_KEY", "test_salt_key")
390+
391+
# Set some initial environment variables for runtime testing
392+
monkeypatch.setenv("GOOGLE_CLIENT_ID", "test_existing_google_id")
393+
monkeypatch.setenv("MICROSOFT_CLIENT_SECRET", "test_existing_microsoft_secret")
394+
395+
# Send SSO settings with empty strings to clear them
396+
clear_sso_settings = {
397+
"google_client_id": "",
398+
"google_client_secret": "",
399+
"microsoft_client_secret": "",
400+
"proxy_base_url": "",
401+
"user_email": "",
402+
}
403+
404+
response = client.patch("/update/sso_settings", json=clear_sso_settings)
405+
406+
assert response.status_code == 200
407+
data = response.json()
408+
assert data["status"] == "success"
409+
410+
# Verify that environment variables with empty strings were cleared from config
411+
updated_config = mock_proxy_config["config"]
412+
assert "GOOGLE_CLIENT_ID" not in updated_config["environment_variables"]
413+
assert "GOOGLE_CLIENT_SECRET" not in updated_config["environment_variables"]
414+
assert "MICROSOFT_CLIENT_SECRET" not in updated_config["environment_variables"]
415+
assert "PROXY_BASE_URL" not in updated_config["environment_variables"]
416+
417+
# Verify that runtime environment variables were cleared
418+
assert "GOOGLE_CLIENT_ID" not in os.environ
419+
assert "MICROSOFT_CLIENT_SECRET" not in os.environ
420+
421+
# Verify user_email was cleared from general_settings
422+
assert updated_config["general_settings"].get("proxy_admin_email") is None
423+
424+
# Verify save_config was called
425+
assert mock_proxy_config["save_call_count"]() == 1
426+
427+
def test_update_sso_settings_mixed_null_and_valid_values(
428+
self, mock_proxy_config, mock_auth, monkeypatch
429+
):
430+
"""Test updating SSO settings with mix of null and valid values"""
431+
monkeypatch.setenv("LITELLM_SALT_KEY", "test_salt_key")
432+
433+
# Set some initial environment variables
434+
monkeypatch.setenv("GOOGLE_CLIENT_ID", "old_google_id")
435+
monkeypatch.setenv("MICROSOFT_CLIENT_ID", "old_microsoft_id")
436+
monkeypatch.setenv("PROXY_BASE_URL", "old_proxy_url")
437+
438+
# Send mixed SSO settings - some null, some valid
439+
mixed_sso_settings = {
440+
"google_client_id": "new_google_client_id", # Valid value
441+
"google_client_secret": None, # Null to clear
442+
"microsoft_client_id": None, # Null to clear
443+
"microsoft_client_secret": "new_microsoft_secret", # Valid value
444+
"proxy_base_url": "https://newproxy.com", # Valid value
445+
"user_email": None, # Null to clear
446+
}
447+
448+
response = client.patch("/update/sso_settings", json=mixed_sso_settings)
449+
450+
assert response.status_code == 200
451+
data = response.json()
452+
assert data["status"] == "success"
453+
454+
# Verify the config was updated correctly
455+
updated_config = mock_proxy_config["config"]
456+
457+
# Valid values should be set
458+
assert (
459+
updated_config["environment_variables"]["GOOGLE_CLIENT_ID"]
460+
!= "new_google_client_id"
461+
) # Encrypted
462+
assert (
463+
updated_config["environment_variables"]["MICROSOFT_CLIENT_SECRET"]
464+
!= "new_microsoft_secret"
465+
) # Encrypted
466+
assert (
467+
updated_config["environment_variables"]["PROXY_BASE_URL"]
468+
!= "https://newproxy.com"
469+
) # Encrypted
470+
471+
# Null values should be cleared
472+
assert "GOOGLE_CLIENT_SECRET" not in updated_config["environment_variables"]
473+
assert "MICROSOFT_CLIENT_ID" not in updated_config["environment_variables"]
474+
475+
# Verify runtime environment variables
476+
assert os.environ.get("GOOGLE_CLIENT_ID") == "new_google_client_id"
477+
assert os.environ.get("MICROSOFT_CLIENT_SECRET") == "new_microsoft_secret"
478+
assert "GOOGLE_CLIENT_SECRET" not in os.environ
479+
assert "MICROSOFT_CLIENT_ID" not in os.environ
480+
481+
# Verify user_email was cleared from general_settings
482+
assert updated_config["general_settings"].get("proxy_admin_email") is None
483+
484+
# Verify save_config was called
485+
assert mock_proxy_config["save_call_count"]() == 1
486+
487+
def test_update_sso_settings_ui_access_mode_handling(
488+
self, mock_proxy_config, mock_auth, monkeypatch
489+
):
490+
"""Test that ui_access_mode is handled correctly in general_settings"""
491+
monkeypatch.setenv("LITELLM_SALT_KEY", "test_salt_key")
492+
493+
# Test setting ui_access_mode
494+
sso_settings_with_ui_mode = {
495+
"ui_access_mode": "admin_only",
496+
"user_email": "[email protected]",
497+
}
498+
499+
response = client.patch("/update/sso_settings", json=sso_settings_with_ui_mode)
500+
501+
assert response.status_code == 200
502+
data = response.json()
503+
assert data["status"] == "success"
504+
505+
# Verify ui_access_mode was set in general_settings (not environment_variables)
506+
updated_config = mock_proxy_config["config"]
507+
assert updated_config["general_settings"]["ui_access_mode"] == "admin_only"
508+
assert (
509+
updated_config["general_settings"]["proxy_admin_email"] == "[email protected]"
510+
)
511+
512+
# Verify ui_access_mode is NOT in environment_variables
513+
assert "ui_access_mode" not in updated_config["environment_variables"]
514+
515+
# Test clearing ui_access_mode
516+
clear_ui_mode = {"ui_access_mode": None, "user_email": None}
517+
518+
response = client.patch("/update/sso_settings", json=clear_ui_mode)
519+
520+
assert response.status_code == 200
521+
522+
# Verify ui_access_mode and user_email were cleared
523+
updated_config = mock_proxy_config["config"]
524+
assert updated_config["general_settings"].get("ui_access_mode") is None
525+
assert updated_config["general_settings"].get("proxy_admin_email") is None
526+
527+
# Verify save_config was called twice (once for each update)
528+
assert mock_proxy_config["save_call_count"]() == 2

ui/litellm-dashboard/src/components/SSOModals.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -186,21 +186,21 @@ const SSOModals: React.FC<SSOModalsProps> = ({
186186
}
187187

188188
try {
189-
// Clear all SSO settings by sending empty values
189+
// Clear all SSO settings
190190
const clearSettings = {
191-
google_client_id: '',
192-
google_client_secret: '',
193-
microsoft_client_id: '',
194-
microsoft_client_secret: '',
195-
microsoft_tenant: '',
196-
generic_client_id: '',
197-
generic_client_secret: '',
198-
generic_authorization_endpoint: '',
199-
generic_token_endpoint: '',
200-
generic_userinfo_endpoint: '',
201-
proxy_base_url: '',
202-
user_email: '',
203-
sso_provider: '',
191+
google_client_id: null,
192+
google_client_secret: null,
193+
microsoft_client_id: null,
194+
microsoft_client_secret: null,
195+
microsoft_tenant: null,
196+
generic_client_id: null,
197+
generic_client_secret: null,
198+
generic_authorization_endpoint: null,
199+
generic_token_endpoint: null,
200+
generic_userinfo_endpoint: null,
201+
proxy_base_url: null,
202+
user_email: null,
203+
sso_provider: null,
204204
};
205205

206206
await updateSSOSettings(accessToken, clearSettings);

0 commit comments

Comments
 (0)