mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
fix(auth_v2): complete the freeze batch (re-exports, SCIM error helper, bounded SAML login state)
- Re-export the test/public helpers from the sub-package __init__s so existing imports resolve: oidc exposes _provider_key/_user_from_userinfo, saml exposes _map_attributes/_metadata_source/_user_from_mapped. - S7: add errors.scim_error_response(exc) rendering the RFC 7644 SCIM Error schema; a host registers it as the exception handler for the SCIM routes (noted on the AuthSecurity docstring). - veria-ai MEDIUM: bound the SAML outstanding-request map with a TTL (300s) and max-size eviction, the same treatment as the session store, so unauthenticated /auth/saml/login traffic can no longer accumulate login state unbounded. - Silence a fastapi/starlette generic-Request override quirk on the SCIM route's get_route_handler so mypy is clean at the freeze sha.
This commit is contained in:
parent
75509686f7
commit
fec8e0a039
6 changed files with 79 additions and 11 deletions
|
|
@ -3,6 +3,9 @@ from __future__ import annotations
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
SCIM_ERROR_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:Error"
|
||||
|
||||
|
||||
class AuthError(HTTPException):
|
||||
|
|
@ -52,3 +55,16 @@ def forbidden_permission() -> AuthError:
|
|||
|
||||
def account_disabled() -> AuthError:
|
||||
return AuthError(403, "Account disabled")
|
||||
|
||||
|
||||
def scim_error_response(exc: Exception) -> JSONResponse:
|
||||
status_code = exc.status_code if isinstance(exc, HTTPException) else 500
|
||||
detail = exc.detail if isinstance(exc, HTTPException) else "Internal server error"
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={
|
||||
"schemas": [SCIM_ERROR_SCHEMA],
|
||||
"status": str(status_code),
|
||||
"detail": str(detail),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
from .config import OIDCProviderConfig
|
||||
from .router import build_oidc_router
|
||||
from .router import _provider_key, _user_from_userinfo, build_oidc_router
|
||||
|
||||
__all__ = ["OIDCProviderConfig", "build_oidc_router"]
|
||||
__all__ = [
|
||||
"OIDCProviderConfig",
|
||||
"build_oidc_router",
|
||||
"_provider_key",
|
||||
"_user_from_userinfo",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
from .config import SAMLConfig
|
||||
from .router import build_saml_router
|
||||
from .router import (
|
||||
_map_attributes,
|
||||
_metadata_source,
|
||||
_user_from_mapped,
|
||||
build_saml_router,
|
||||
)
|
||||
|
||||
__all__ = ["SAMLConfig", "build_saml_router"]
|
||||
__all__ = [
|
||||
"SAMLConfig",
|
||||
"build_saml_router",
|
||||
"_map_attributes",
|
||||
"_metadata_source",
|
||||
"_user_from_mapped",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, cast
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse, Response
|
||||
|
|
@ -129,13 +129,44 @@ def build_sp_client(config: SAMLConfig) -> Saml2Client:
|
|||
|
||||
|
||||
class SAMLProtocolStore:
|
||||
def __init__(self, replay_ttl_seconds: int) -> None:
|
||||
self.outstanding: Dict[str, str] = {}
|
||||
def __init__(
|
||||
self,
|
||||
replay_ttl_seconds: int,
|
||||
outstanding_ttl_seconds: int = 300,
|
||||
max_outstanding: int = 10000,
|
||||
) -> None:
|
||||
self._outstanding: Dict[str, Tuple[float, str]] = {}
|
||||
self._seen_assertions: Dict[str, float] = {}
|
||||
self._replay_ttl = replay_ttl_seconds
|
||||
self._outstanding_ttl = outstanding_ttl_seconds
|
||||
self._max_outstanding = max_outstanding
|
||||
|
||||
def remember_request(self, request_id: str, relay_state: str) -> None:
|
||||
self.outstanding[request_id] = relay_state
|
||||
now = time.time()
|
||||
self._evict_outstanding(now)
|
||||
self._outstanding[request_id] = (now + self._outstanding_ttl, relay_state)
|
||||
|
||||
def outstanding_relays(self) -> Dict[str, str]:
|
||||
now = time.time()
|
||||
return {
|
||||
rid: relay for rid, (exp, relay) in self._outstanding.items() if exp >= now
|
||||
}
|
||||
|
||||
def consume_request(self, request_id: str) -> Optional[str]:
|
||||
entry = self._outstanding.pop(request_id, None)
|
||||
if entry is None:
|
||||
return None
|
||||
expires_at, relay = entry
|
||||
return relay if expires_at >= time.time() else None
|
||||
|
||||
def _evict_outstanding(self, now: float) -> None:
|
||||
for rid in [r for r, (exp, _) in self._outstanding.items() if exp < now]:
|
||||
self._outstanding.pop(rid, None)
|
||||
overflow = len(self._outstanding) - self._max_outstanding + 1
|
||||
if overflow > 0:
|
||||
oldest = sorted(self._outstanding, key=lambda r: self._outstanding[r][0])
|
||||
for rid in oldest[:overflow]:
|
||||
self._outstanding.pop(rid, None)
|
||||
|
||||
def consume_assertion(self, assertion_id: str) -> bool:
|
||||
now = time.time()
|
||||
|
|
@ -185,7 +216,7 @@ def build_saml_router(auth: "AuthSecurity") -> APIRouter:
|
|||
authn_response = client.parse_authn_request_response(
|
||||
saml_response,
|
||||
BINDING_HTTP_POST,
|
||||
outstanding=protocol.outstanding or None,
|
||||
outstanding=protocol.outstanding_relays() or None,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
|
|
@ -196,7 +227,7 @@ def build_saml_router(auth: "AuthSecurity") -> APIRouter:
|
|||
|
||||
in_response_to = getattr(authn_response, "in_response_to", None)
|
||||
bound_relay = (
|
||||
protocol.outstanding.pop(in_response_to, None) if in_response_to else None
|
||||
protocol.consume_request(in_response_to) if in_response_to else None
|
||||
)
|
||||
|
||||
assertion = getattr(authn_response, "assertion", None)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ def _error(status_code: int, detail: str) -> JSONResponse:
|
|||
class _ScimRoute(APIRoute):
|
||||
"""Render authentication failures with the SCIM Error schema (RFC 7644)."""
|
||||
|
||||
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
||||
def get_route_handler( # type: ignore[override]
|
||||
self,
|
||||
) -> Callable[[Request], Coroutine[Any, Any, Response]]:
|
||||
handler = super().get_route_handler()
|
||||
|
||||
async def scim_handler(request: Request) -> Response:
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ class AuthSecurity:
|
|||
uvicorn with ``--no-proxy-headers`` and let this module resolve the client IP,
|
||||
or leave ``trusted_proxy_cidrs`` empty and rely on uvicorn's
|
||||
``--forwarded-allow-ips``. Do not enable both.
|
||||
|
||||
To return SCIM-shaped error bodies, register ``errors.scim_error_response`` as
|
||||
the host app's exception handler for the SCIM routes.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue