mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-23 00:41:40 +00:00
fix(security): redact SSO client secrets in GET /get/sso_settings response
Read-only admins could previously retrieve plaintext okta_client_secret, google_client_secret, microsoft_client_secret, and generic_client_secret via GET /get/sso_settings. Now these fields are replaced with "**redacted**" in the response. PATCH /update/sso_settings preserves the stored secret when the sentinel value is echoed back unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b7f93a8913
commit
daa6ab6cdb
2 changed files with 51 additions and 4 deletions
|
|
@ -633,6 +633,20 @@ async def update_default_team_settings(settings: DefaultTeamSSOParams):
|
|||
)
|
||||
|
||||
|
||||
# Sentinel returned in place of secret values in GET /get/sso_settings so callers
|
||||
# can tell a secret is configured without receiving the plaintext value.
|
||||
_REDACTED_SECRET = "**redacted**"
|
||||
# SSO fields that contain secrets and must never be returned in plaintext.
|
||||
_SSO_SECRET_FIELDS = frozenset(
|
||||
{
|
||||
"google_client_secret",
|
||||
"microsoft_client_secret",
|
||||
"okta_client_secret",
|
||||
"generic_client_secret",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/get/sso_settings",
|
||||
tags=["SSO Settings"],
|
||||
|
|
@ -743,6 +757,12 @@ async def get_sso_settings():
|
|||
# Convert to dict for response
|
||||
sso_dict = sso_config.model_dump()
|
||||
|
||||
# Replace secret values with a redacted sentinel so callers can confirm a
|
||||
# secret is set without receiving the plaintext value.
|
||||
for _secret_field in _SSO_SECRET_FIELDS:
|
||||
if sso_dict.get(_secret_field):
|
||||
sso_dict[_secret_field] = _REDACTED_SECRET
|
||||
|
||||
# Add descriptions to the response
|
||||
result = {
|
||||
"values": sso_dict,
|
||||
|
|
@ -826,8 +846,26 @@ async def update_sso_settings(sso_config: SSOConfig):
|
|||
if "general_settings" not in config:
|
||||
config["general_settings"] = {}
|
||||
|
||||
# Load existing SSO record so we can restore secrets that were not changed.
|
||||
# The GET response returns _REDACTED_SECRET for secret fields; if a caller
|
||||
# echoes that sentinel back we must not overwrite the real stored value.
|
||||
existing_sso_record = await prisma_client.db.litellm_ssoconfig.find_unique(
|
||||
where={"id": "sso_config"}
|
||||
)
|
||||
existing_plain: dict = {}
|
||||
if existing_sso_record and existing_sso_record.sso_settings:
|
||||
existing_plain = proxy_config._decrypt_and_set_db_env_variables(
|
||||
environment_variables=dict(existing_sso_record.sso_settings)
|
||||
)
|
||||
|
||||
# Update environment variables in config and in memory
|
||||
sso_data = sso_config.model_dump()
|
||||
|
||||
# Restore original secret values when the sentinel is echoed back.
|
||||
for _secret_field in _SSO_SECRET_FIELDS:
|
||||
if sso_data.get(_secret_field) == _REDACTED_SECRET:
|
||||
sso_data[_secret_field] = existing_plain.get(_secret_field)
|
||||
|
||||
for field_name, value in sso_data.items():
|
||||
if field_name in env_var_mapping:
|
||||
env_var_name = env_var_mapping[field_name]
|
||||
|
|
|
|||
|
|
@ -362,9 +362,9 @@ class TestProxySettingEndpoints:
|
|||
|
||||
# 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["google_client_secret"] == "**redacted**"
|
||||
assert values["microsoft_client_id"] == "test_microsoft_client_id"
|
||||
assert values["microsoft_client_secret"] == "test_microsoft_client_secret"
|
||||
assert values["microsoft_client_secret"] == "**redacted**"
|
||||
assert values["proxy_base_url"] == "https://example.com"
|
||||
assert values["user_email"] == "admin@example.com"
|
||||
|
||||
|
|
@ -393,6 +393,7 @@ class TestProxySettingEndpoints:
|
|||
# Mock the prisma client
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig.upsert = AsyncMock()
|
||||
mock_prisma.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config = MagicMock()
|
||||
mock_prisma.db.litellm_config.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config.update = AsyncMock()
|
||||
|
|
@ -475,6 +476,7 @@ class TestProxySettingEndpoints:
|
|||
# Mock the prisma client
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig.upsert = AsyncMock()
|
||||
mock_prisma.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config = MagicMock()
|
||||
|
||||
env_var_entry = MagicMock()
|
||||
|
|
@ -554,6 +556,7 @@ class TestProxySettingEndpoints:
|
|||
# Mock the prisma client
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig.upsert = AsyncMock()
|
||||
mock_prisma.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config = MagicMock()
|
||||
env_var_entry = MagicMock()
|
||||
env_var_entry.param_value = json.dumps(
|
||||
|
|
@ -624,6 +627,7 @@ class TestProxySettingEndpoints:
|
|||
# Mock the prisma client
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig.upsert = AsyncMock()
|
||||
mock_prisma.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config = MagicMock()
|
||||
|
||||
env_var_entry = MagicMock()
|
||||
|
|
@ -701,6 +705,7 @@ class TestProxySettingEndpoints:
|
|||
# Mock the prisma client
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig.upsert = AsyncMock()
|
||||
mock_prisma.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config = MagicMock()
|
||||
mock_prisma.db.litellm_config.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config.update = AsyncMock()
|
||||
|
|
@ -1321,10 +1326,10 @@ class TestProxySettingEndpoints:
|
|||
assert "values" in data
|
||||
assert "field_schema" in data
|
||||
|
||||
# Verify decrypted values are returned
|
||||
# Verify decrypted values are returned (secrets are redacted in GET response)
|
||||
values = data["values"]
|
||||
assert values["google_client_id"] == "decrypted_google_id"
|
||||
assert values["google_client_secret"] == "decrypted_google_secret"
|
||||
assert values["google_client_secret"] == "**redacted**"
|
||||
assert values["microsoft_client_id"] == "decrypted_microsoft_id"
|
||||
assert values["proxy_base_url"] == "https://decrypted.example.com"
|
||||
|
||||
|
|
@ -1345,6 +1350,7 @@ class TestProxySettingEndpoints:
|
|||
mock_prisma = MagicMock()
|
||||
upsert_mock = AsyncMock()
|
||||
mock_prisma.db.litellm_ssoconfig.upsert = upsert_mock
|
||||
mock_prisma.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config = MagicMock()
|
||||
mock_prisma.db.litellm_config.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config.update = AsyncMock()
|
||||
|
|
@ -1424,6 +1430,7 @@ class TestProxySettingEndpoints:
|
|||
mock_prisma.db = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig.upsert = AsyncMock()
|
||||
mock_prisma.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None)
|
||||
|
||||
env_var_entry = MagicMock()
|
||||
env_var_entry.param_value = json.dumps(
|
||||
|
|
@ -1477,6 +1484,7 @@ class TestProxySettingEndpoints:
|
|||
mock_prisma.db = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig.upsert = AsyncMock()
|
||||
mock_prisma.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None)
|
||||
|
||||
env_var_entry = MagicMock()
|
||||
env_var_entry.param_value = {
|
||||
|
|
@ -1648,6 +1656,7 @@ class TestProxySettingEndpoints:
|
|||
# Mock the prisma client
|
||||
mock_prisma = MagicMock()
|
||||
mock_prisma.db.litellm_ssoconfig.upsert = AsyncMock()
|
||||
mock_prisma.db.litellm_ssoconfig.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config = MagicMock()
|
||||
mock_prisma.db.litellm_config.find_unique = AsyncMock(return_value=None)
|
||||
mock_prisma.db.litellm_config.update = AsyncMock()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue