mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix(sso): keep the ID-JAG capture-gap diagnostic out of the unauthenticated debug page
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
4deedfb9df
commit
06bf85a0e0
2 changed files with 36 additions and 42 deletions
|
|
@ -1701,17 +1701,17 @@ async def warn_if_id_jag_assertion_uncaptured(assertion: SSOIdentityAssertion |
|
|||
)
|
||||
|
||||
|
||||
async def id_jag_capture_gap_to_surface() -> str | None:
|
||||
"""The gap worth showing an operator: a real capture gap and an ``oauth2_id_jag`` server
|
||||
registered for it to break."""
|
||||
async def warn_if_id_jag_capture_gap() -> None:
|
||||
gap: Final = id_jag_assertion_capture_gap()
|
||||
if gap is None:
|
||||
return None
|
||||
return
|
||||
try:
|
||||
return gap if await ema_assertion_retention_enabled() else None
|
||||
if not await ema_assertion_retention_enabled():
|
||||
return
|
||||
except Exception as exc: # noqa: BLE001 # diagnostics must never break the page they annotate
|
||||
verbose_proxy_logger.debug("Could not check for oauth2_id_jag MCP servers: %s", exc)
|
||||
return None
|
||||
return
|
||||
verbose_proxy_logger.warning("SSO debug callback ran with an ID-JAG capture gap: %s", gap)
|
||||
|
||||
|
||||
async def create_team_member_add_task(team_id, user_info):
|
||||
|
|
@ -4772,13 +4772,11 @@ async def debug_sso_callback(request: Request):
|
|||
safe_raw_claims: Final = {k: v for k, v in (received_response or {}).items() if k not in _OAUTH_TOKEN_FIELDS}
|
||||
safe_access_token_claims = {k: v for k, v in (access_token_payload or {}).items() if k not in _OAUTH_TOKEN_FIELDS}
|
||||
|
||||
gap: Final = await id_jag_capture_gap_to_surface()
|
||||
id_jag: Final = {"id_jag_assertion_capture": gap} if gap is not None else {} # mutable-ok: optional JSON member
|
||||
await warn_if_id_jag_capture_gap()
|
||||
sso_payload: Final = {
|
||||
"parsed_by_proxy": filtered_result,
|
||||
"raw_claims": safe_raw_claims,
|
||||
"access_token_claims": safe_access_token_claims,
|
||||
**id_jag,
|
||||
}
|
||||
|
||||
# Replace the placeholder in the template with the actual data
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from contextlib import ExitStack
|
||||
from contextlib import asynccontextmanager
|
||||
from contextlib import ExitStack, asynccontextmanager
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
from litellm._uuid import uuid
|
||||
|
||||
|
||||
import litellm
|
||||
from litellm._uuid import uuid
|
||||
from litellm.proxy._types import LiteLLM_UserTable, NewUserResponse
|
||||
from litellm.proxy.auth.handle_jwt import JWTHandler
|
||||
from litellm.proxy.management_endpoints.sso import CustomMicrosoftSSO
|
||||
|
|
@ -1616,8 +1613,8 @@ async def test_get_generic_sso_response_with_empty_headers():
|
|||
async def test_get_generic_sso_response_includes_token_claims_when_enabled(monkeypatch):
|
||||
import jwt as pyjwt
|
||||
|
||||
from litellm.proxy.management_endpoints.ui_sso import get_generic_sso_response
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
from litellm.proxy.management_endpoints.ui_sso import get_generic_sso_response
|
||||
|
||||
mock_request = MagicMock(spec=Request)
|
||||
mock_jwt_handler = MagicMock(spec=JWTHandler)
|
||||
|
|
@ -2322,10 +2319,10 @@ class TestCustomUISSO:
|
|||
async def test_handle_custom_ui_sso_sign_in_success(self):
|
||||
"""Test successful custom UI SSO sign-in with valid headers"""
|
||||
from fastapi_sso.sso.base import OpenID
|
||||
|
||||
from litellm_enterprise.proxy.auth.custom_sso_handler import (
|
||||
EnterpriseCustomSSOHandler,
|
||||
)
|
||||
|
||||
from litellm.integrations.custom_sso_handler import CustomSSOLoginHandler
|
||||
|
||||
# Mock request with custom headers
|
||||
|
|
@ -2401,6 +2398,7 @@ class TestCustomUISSO:
|
|||
from litellm_enterprise.proxy.auth.custom_sso_handler import (
|
||||
EnterpriseCustomSSOHandler,
|
||||
)
|
||||
|
||||
from litellm.integrations.custom_sso_handler import CustomSSOLoginHandler
|
||||
|
||||
mock_request = MagicMock(spec=Request)
|
||||
|
|
@ -2437,10 +2435,10 @@ class TestCustomUISSO:
|
|||
and its methods are called with the correct parameters
|
||||
"""
|
||||
from fastapi_sso.sso.base import OpenID
|
||||
|
||||
from litellm_enterprise.proxy.auth.custom_sso_handler import (
|
||||
EnterpriseCustomSSOHandler,
|
||||
)
|
||||
|
||||
from litellm.integrations.custom_sso_handler import CustomSSOLoginHandler
|
||||
|
||||
# Create a real custom handler class instance
|
||||
|
|
@ -8174,7 +8172,7 @@ _GOOGLE_DEBUG_CLIENT_ID = "debug-google-client-id"
|
|||
_GENERIC_DEBUG_CLIENT_ID = "debug-generic-client-id"
|
||||
|
||||
|
||||
async def _render_debug_page(provider_env, id_jag_registered, gap_override=None):
|
||||
async def _render_debug_page(provider_env, id_jag_registered, force_inert=False):
|
||||
"""Drive /sso/debug/callback and return the raw response body."""
|
||||
from litellm.proxy.management_endpoints.ui_sso import GoogleSSOHandler, debug_sso_callback
|
||||
|
||||
|
|
@ -8204,11 +8202,11 @@ async def _render_debug_page(provider_env, id_jag_registered, gap_override=None)
|
|||
patch("litellm.proxy.proxy_server.user_api_key_cache", MagicMock()),
|
||||
patch("litellm.proxy.proxy_server.jwt_handler", MagicMock(spec=JWTHandler)),
|
||||
]
|
||||
if gap_override is not None:
|
||||
if force_inert:
|
||||
stack.append(
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.ui_sso.id_jag_capture_gap_to_surface",
|
||||
AsyncMock(return_value=gap_override["value"]),
|
||||
"litellm.proxy.management_endpoints.ui_sso.warn_if_id_jag_capture_gap",
|
||||
AsyncMock(return_value=None),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -8224,23 +8222,17 @@ async def _render_debug_page(provider_env, id_jag_registered, gap_override=None)
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_debug_page_surfaces_the_capture_gap_to_the_operator_reading_it():
|
||||
"""This page is exactly where someone lands when ID-JAG is failing, so the reason has to be
|
||||
on it; a diagnostic that cannot reach the surface the operator is staring at has a hole."""
|
||||
body = await _render_debug_page({"GOOGLE_CLIENT_ID": _GOOGLE_DEBUG_CLIENT_ID}, id_jag_registered=True)
|
||||
async def test_debug_page_logs_the_capture_gap_but_never_renders_it():
|
||||
warning_mock = MagicMock()
|
||||
with patch("litellm.proxy.management_endpoints.ui_sso.verbose_proxy_logger.warning", warning_mock):
|
||||
body = await _render_debug_page({"GOOGLE_CLIENT_ID": _GOOGLE_DEBUG_CLIENT_ID}, id_jag_registered=True)
|
||||
|
||||
assert "id_jag_assertion_capture" in body
|
||||
assert "google" in body
|
||||
assert "GENERIC_CLIENT_ID" in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_debug_page_gap_text_carries_no_configuration_values():
|
||||
"""Only the provider name and the remedy belong in rendered HTML; the client id the operator
|
||||
configured is not ours to echo back onto a page."""
|
||||
body = await _render_debug_page({"GOOGLE_CLIENT_ID": _GOOGLE_DEBUG_CLIENT_ID}, id_jag_registered=True)
|
||||
|
||||
assert _GOOGLE_DEBUG_CLIENT_ID not in body
|
||||
warning_mock.assert_called_once()
|
||||
warning_args = " ".join(str(arg) for arg in warning_mock.call_args.args)
|
||||
assert "google" in warning_args
|
||||
assert "GENERIC_CLIENT_ID" in warning_args
|
||||
assert "id_jag" not in body
|
||||
assert "GENERIC_CLIENT_ID" not in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -8253,7 +8245,7 @@ async def test_debug_page_is_byte_identical_when_the_provider_captures():
|
|||
pre_change = await _render_debug_page(
|
||||
{"GENERIC_CLIENT_ID": _GENERIC_DEBUG_CLIENT_ID},
|
||||
id_jag_registered=True,
|
||||
gap_override={"value": None},
|
||||
force_inert=True,
|
||||
)
|
||||
|
||||
assert with_feature == pre_change
|
||||
|
|
@ -8270,7 +8262,7 @@ async def test_debug_page_is_byte_identical_when_no_id_jag_server_is_registered(
|
|||
pre_change = await _render_debug_page(
|
||||
{"GOOGLE_CLIENT_ID": _GOOGLE_DEBUG_CLIENT_ID},
|
||||
id_jag_registered=False,
|
||||
gap_override={"value": None},
|
||||
force_inert=True,
|
||||
)
|
||||
|
||||
assert with_feature == pre_change
|
||||
|
|
@ -8281,16 +8273,20 @@ async def test_debug_page_is_byte_identical_when_no_id_jag_server_is_registered(
|
|||
async def test_debug_page_survives_a_store_outage():
|
||||
"""The page's job is to render claims; an unreachable MCP table must cost it the annotation,
|
||||
not the page."""
|
||||
from litellm.proxy.management_endpoints.ui_sso import id_jag_capture_gap_to_surface
|
||||
from litellm.proxy.management_endpoints.ui_sso import warn_if_id_jag_capture_gap
|
||||
|
||||
warning_mock = MagicMock()
|
||||
with (
|
||||
patch.dict(os.environ, {"GOOGLE_CLIENT_ID": _GOOGLE_DEBUG_CLIENT_ID}, clear=False),
|
||||
patch(
|
||||
"litellm.proxy.management_endpoints.ui_sso.ema_assertion_retention_enabled",
|
||||
AsyncMock(side_effect=Exception("db down")),
|
||||
),
|
||||
patch("litellm.proxy.management_endpoints.ui_sso.verbose_proxy_logger.warning", warning_mock),
|
||||
):
|
||||
assert await id_jag_capture_gap_to_surface() is None
|
||||
assert await warn_if_id_jag_capture_gap() is None
|
||||
|
||||
warning_mock.assert_not_called()
|
||||
|
||||
|
||||
async def _render_legacy_login_page(env_overrides, general_settings):
|
||||
|
|
@ -8387,8 +8383,8 @@ async def test_saml_callback_enforces_free_sso_user_limit_after_validation():
|
|||
that /sso/key/generate enforces; the ACS re-checks it after validating the assertion,
|
||||
so the entitlement DB query never runs on unvalidated input."""
|
||||
from litellm.proxy._types import ProxyException
|
||||
from litellm.proxy.management_endpoints.ui_sso import saml_callback
|
||||
from litellm.proxy.management_endpoints.types import CustomOpenID
|
||||
from litellm.proxy.management_endpoints.ui_sso import saml_callback
|
||||
|
||||
call_order: list[str] = []
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue