mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
fix(proxy): hide default credentials login hint when UI_PASSWORD is set
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
cab1e113f7
commit
ba171d9eb4
7 changed files with 87 additions and 13 deletions
|
|
@ -1446,6 +1446,18 @@ def has_user_setup_sso() -> bool:
|
|||
)
|
||||
|
||||
|
||||
def should_hide_default_credentials_hint(general_settings: Mapping[str, object]) -> bool:
|
||||
"""
|
||||
Whether login pages hide the "admin / MASTER_KEY" hint: explicit opt-in, or a
|
||||
non-empty UI_PASSWORD, which makes that hint wrong. UI_USERNAME alone keeps it.
|
||||
"""
|
||||
return (
|
||||
os.getenv("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", "false").lower() == "true"
|
||||
or general_settings.get("hide_default_credentials_hint", False) is True
|
||||
or bool(os.getenv("UI_PASSWORD"))
|
||||
)
|
||||
|
||||
|
||||
def _is_google_ready() -> bool:
|
||||
return bool(os.getenv("GOOGLE_CLIENT_ID")) and bool(os.getenv("GOOGLE_CLIENT_SECRET"))
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ router: Final = APIRouter()
|
|||
@router.get("/.well-known/litellm-ui-config", response_model=UiDiscoveryEndpoints)
|
||||
@router.get("/litellm/.well-known/litellm-ui-config", response_model=UiDiscoveryEndpoints) # if mounted at root path
|
||||
async def get_ui_config():
|
||||
from litellm.proxy.auth.auth_utils import has_user_setup_sso
|
||||
from litellm.proxy.auth.auth_utils import has_user_setup_sso, should_hide_default_credentials_hint
|
||||
from litellm.proxy.proxy_server import general_settings
|
||||
from litellm.proxy.utils import get_proxy_base_url, get_server_root_path
|
||||
|
||||
|
|
@ -23,10 +23,7 @@ async def get_ui_config():
|
|||
or general_settings.get("auto_redirect_ui_login_to_sso", False) is True
|
||||
)
|
||||
admin_ui_disabled: Final = os.getenv("DISABLE_ADMIN_UI", "false").lower() == "true"
|
||||
hide_default_credentials_hint: Final = bool(
|
||||
os.getenv("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", "false").lower() == "true"
|
||||
or general_settings.get("hide_default_credentials_hint", False) is True
|
||||
)
|
||||
hide_default_credentials_hint: Final = should_hide_default_credentials_hint(general_settings)
|
||||
|
||||
sso_configured: Final = has_user_setup_sso()
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ from litellm.proxy.auth.auth_checks import ExperimentalUIJWTToken, get_user_obje
|
|||
from litellm.proxy.auth.auth_utils import (
|
||||
_get_request_ip_address,
|
||||
has_user_setup_sso,
|
||||
should_hide_default_credentials_hint,
|
||||
)
|
||||
from litellm.proxy.auth.handle_jwt import JWTHandler
|
||||
from litellm.proxy.auth.ip_address_utils import IPAddressUtils
|
||||
|
|
@ -1110,10 +1111,7 @@ async def google_login(
|
|||
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
hide_default_credentials_hint: Final = (
|
||||
os.getenv("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", "false").lower() == "true"
|
||||
or general_settings.get("hide_default_credentials_hint", False) is True
|
||||
)
|
||||
hide_default_credentials_hint: Final = should_hide_default_credentials_hint(general_settings)
|
||||
form_response: Final = HTMLResponse(
|
||||
content=build_ui_login_form(
|
||||
show_deprecation_banner=True,
|
||||
|
|
|
|||
|
|
@ -318,6 +318,7 @@ from litellm.proxy.auth.auth_utils import (
|
|||
check_response_size_is_safe,
|
||||
is_request_body_safe,
|
||||
log_once_if_budget_reservation_disabled,
|
||||
should_hide_default_credentials_hint,
|
||||
warn_once_if_custom_auth_skips_common_checks,
|
||||
)
|
||||
from litellm.proxy.auth.fallback_model_access import router_fallback_access_check
|
||||
|
|
@ -15816,10 +15817,7 @@ async def fallback_login(request: Request):
|
|||
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
hide_default_credentials_hint: Final = (
|
||||
os.getenv("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", "false").lower() == "true"
|
||||
or general_settings.get("hide_default_credentials_hint", False) is True
|
||||
)
|
||||
hide_default_credentials_hint: Final = should_hide_default_credentials_hint(general_settings)
|
||||
return HTMLResponse(
|
||||
content=build_ui_login_form(
|
||||
show_deprecation_banner=False,
|
||||
|
|
|
|||
|
|
@ -340,6 +340,7 @@ def test_ui_discovery_endpoints_hide_default_credentials_hint_default_false():
|
|||
patch.dict(os.environ, {"DISABLE_ADMIN_UI": "false"}, clear=False),
|
||||
):
|
||||
os.environ.pop("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", None)
|
||||
os.environ.pop("UI_PASSWORD", None)
|
||||
|
||||
response = client.get("/.well-known/litellm-ui-config")
|
||||
|
||||
|
|
@ -348,6 +349,45 @@ def test_ui_discovery_endpoints_hide_default_credentials_hint_default_false():
|
|||
assert data["hide_default_credentials_hint"] is False
|
||||
|
||||
|
||||
def test_ui_discovery_endpoints_hide_default_credentials_hint_when_ui_password_set():
|
||||
"""A non-empty UI_PASSWORD hides the card: 'admin / MASTER_KEY' is no longer the login."""
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
client = TestClient(app)
|
||||
|
||||
with patch.dict(os.environ, {"UI_PASSWORD": "s3cret-pass", "DISABLE_ADMIN_UI": "false"}, clear=False):
|
||||
os.environ.pop("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", None)
|
||||
|
||||
response = client.get("/.well-known/litellm-ui-config")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["hide_default_credentials_hint"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"env_overrides",
|
||||
[
|
||||
pytest.param({"UI_USERNAME": "opsadmin"}, id="username_only_keeps_master_key_password"),
|
||||
pytest.param({"UI_PASSWORD": ""}, id="empty_password_is_not_set"),
|
||||
],
|
||||
)
|
||||
def test_ui_discovery_endpoints_keeps_default_credentials_hint_without_real_ui_password(env_overrides):
|
||||
"""Only a non-empty UI_PASSWORD counts as custom credentials; the hint stays accurate otherwise."""
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
client = TestClient(app)
|
||||
|
||||
with patch.dict(os.environ, {"DISABLE_ADMIN_UI": "false", **env_overrides}, clear=False):
|
||||
os.environ.pop("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", None)
|
||||
if "UI_PASSWORD" not in env_overrides:
|
||||
os.environ.pop("UI_PASSWORD", None)
|
||||
|
||||
response = client.get("/.well-known/litellm-ui-config")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["hide_default_credentials_hint"] is False
|
||||
|
||||
|
||||
def test_ui_discovery_endpoints_hide_default_credentials_hint_via_env_var():
|
||||
"""LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT=true hides the login-page credentials card."""
|
||||
app = FastAPI()
|
||||
|
|
|
|||
|
|
@ -8334,6 +8334,7 @@ async def _render_legacy_login_page(env_overrides, general_settings):
|
|||
"GOOGLE_CLIENT_ID",
|
||||
"GENERIC_CLIENT_ID",
|
||||
"LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT",
|
||||
"UI_PASSWORD",
|
||||
):
|
||||
os.environ.pop(var, None)
|
||||
os.environ.update(env_overrides)
|
||||
|
|
@ -8386,6 +8387,22 @@ async def test_legacy_login_page_hides_credentials_hint_via_general_settings():
|
|||
assert "MASTER_KEY" not in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_login_page_hides_credentials_hint_when_ui_password_set():
|
||||
"""Regression: the legacy page shares the rule with the discovery endpoint, so a non-empty
|
||||
UI_PASSWORD hides the now-inaccurate 'admin / MASTER_KEY' hint here too."""
|
||||
response = await _render_legacy_login_page(
|
||||
env_overrides={"UI_PASSWORD": "s3cret-pass"},
|
||||
general_settings={},
|
||||
)
|
||||
|
||||
body = response.body.decode()
|
||||
assert response.status_code == 200
|
||||
assert "Default Credentials" not in body
|
||||
assert "MASTER_KEY" not in body
|
||||
assert 'name="username"' in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_saml_callback_blocked_when_admin_ui_disabled():
|
||||
"""An IdP-initiated assertion must not mint a UI session when the admin UI is
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ def test_fallback_login_returns_html_form_with_ui_username_set(client, monkeypat
|
|||
def test_fallback_login_shows_credentials_hint_by_default(client, monkeypatch):
|
||||
"""Control: without the flag, /fallback/login still renders the hint."""
|
||||
monkeypatch.delenv("UI_USERNAME", raising=False)
|
||||
monkeypatch.delenv("UI_PASSWORD", raising=False)
|
||||
monkeypatch.delenv("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", raising=False)
|
||||
response = client.get("/fallback/login")
|
||||
assert response.status_code == 200
|
||||
|
|
@ -104,6 +105,17 @@ def test_fallback_login_shows_credentials_hint_by_default(client, monkeypatch):
|
|||
assert "MASTER_KEY" in response.text
|
||||
|
||||
|
||||
def test_fallback_login_hides_credentials_hint_when_ui_password_set(client, monkeypatch):
|
||||
"""Regression: a non-empty UI_PASSWORD means 'admin / MASTER_KEY' is wrong, so the hint must go."""
|
||||
monkeypatch.setenv("UI_PASSWORD", "s3cret-pass")
|
||||
monkeypatch.delenv("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", raising=False)
|
||||
response = client.get("/fallback/login")
|
||||
assert response.status_code == 200
|
||||
assert "Default Credentials" not in response.text
|
||||
assert "MASTER_KEY" not in response.text
|
||||
assert 'name="username"' in response.text
|
||||
|
||||
|
||||
def test_fallback_login_hides_credentials_hint_via_env_flag(client, monkeypatch):
|
||||
"""Pin: LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT removes the hint on /fallback/login."""
|
||||
monkeypatch.delenv("UI_USERNAME", raising=False)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue