mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(auth): keep blanket wildcards and team-less jwts out of the passthrough grant
A team_allowed_routes entry that names no path segment, such as * or /*, is a blanket grant like a named route group, so it no longer opens auth=true passthroughs. The grant in the shared route check now also requires a team on the JWT token, because team_allowed_routes should not apply to a JWT that resolved no team
This commit is contained in:
parent
a8003102b2
commit
dac88cc6d0
3 changed files with 27 additions and 15 deletions
|
|
@ -1693,7 +1693,7 @@ class JWTAuthManager:
|
|||
team_object=team_object,
|
||||
route=route,
|
||||
request_method=request_method,
|
||||
team_allowed_routes=jwt_handler.litellm_jwtauth.team_allowed_routes or (),
|
||||
team_allowed_routes=jwt_handler.litellm_jwtauth.team_allowed_routes,
|
||||
):
|
||||
is_allowed = False
|
||||
denied_auth_enforced_pass_through_route = True
|
||||
|
|
@ -2589,7 +2589,7 @@ class JWTAuthManager:
|
|||
team_object=team_object,
|
||||
route=route,
|
||||
request_method=request_method,
|
||||
team_allowed_routes=handler.litellm_jwtauth.team_allowed_routes or (),
|
||||
team_allowed_routes=handler.litellm_jwtauth.team_allowed_routes,
|
||||
):
|
||||
JWTAuthManager._raise_team_passthrough_route_denial(route=route)
|
||||
|
||||
|
|
@ -2659,7 +2659,7 @@ class JWTAuthManager:
|
|||
team_object=team_object,
|
||||
route=route,
|
||||
request_method=request_method,
|
||||
team_allowed_routes=handler.litellm_jwtauth.team_allowed_routes or (),
|
||||
team_allowed_routes=handler.litellm_jwtauth.team_allowed_routes,
|
||||
):
|
||||
JWTAuthManager._raise_team_passthrough_route_denial(route=route)
|
||||
elif team_id is None:
|
||||
|
|
|
|||
|
|
@ -686,23 +686,25 @@ class RouteChecks:
|
|||
@staticmethod
|
||||
def jwt_team_routes_grant_pass_through(route: str, team_allowed_routes: Collection[str]) -> bool:
|
||||
"""
|
||||
Explicit paths and trailing-wildcard prefixes grant auth=true pass-through. Entries are only ever compared
|
||||
as paths, so a named route group like ``openai_routes`` never grants.
|
||||
Explicit paths and trailing-wildcard prefixes grant auth=true pass-through. Blanket grants never do:
|
||||
a named route group like ``openai_routes`` is only ever compared as a path, and an entry that names
|
||||
no path segment (``*``, ``/*``) is skipped.
|
||||
"""
|
||||
return any(
|
||||
RouteChecks.route_matches_wildcard_pattern(route=route, pattern=allowed_route)
|
||||
for allowed_route in team_allowed_routes
|
||||
if allowed_route.rstrip("*").strip("/")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _jwt_team_allowed_routes(valid_token: UserAPIKeyAuth) -> Collection[str]:
|
||||
"""``team_allowed_routes`` for tokens built by JWT auth; JWT-mapped virtual keys stay key-scoped."""
|
||||
if valid_token.jwt_claims is None or valid_token.token is not None:
|
||||
"""``team_allowed_routes`` for team tokens built by JWT auth; JWT-mapped virtual keys stay key-scoped."""
|
||||
if valid_token.jwt_claims is None or valid_token.token is not None or valid_token.team_id is None:
|
||||
return ()
|
||||
|
||||
from litellm.proxy.proxy_server import jwt_handler
|
||||
|
||||
return jwt_handler.litellm_jwtauth.team_allowed_routes or ()
|
||||
return jwt_handler.litellm_jwtauth.team_allowed_routes
|
||||
|
||||
@staticmethod
|
||||
def _require_auth_pass_through_access(
|
||||
|
|
|
|||
|
|
@ -1272,6 +1272,8 @@ def test_non_proxy_admin_allows_auth_pass_through_with_team_allowlist():
|
|||
("/model-host/v1/extractor/predict", ["/model-host/v1/extractor"], False),
|
||||
("/other/v1/extractor", ["/model-host/*"], False),
|
||||
("/model-host/v1/extractor", ["openai_routes", "llm_api_routes", "mapped_pass_through_routes"], False),
|
||||
("/model-host/v1/extractor", ["*"], False),
|
||||
("/model-host/v1/extractor", ["/*"], False),
|
||||
("/model-host/v1/extractor", [], False),
|
||||
],
|
||||
)
|
||||
|
|
@ -1350,20 +1352,28 @@ def test_non_proxy_admin_denies_auth_pass_through_for_jwt_when_only_route_groups
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"jwt_claims",
|
||||
[None, {"sub": "test_user"}],
|
||||
ids=["plain_virtual_key", "jwt_mapped_virtual_key"],
|
||||
"api_key, team_id, jwt_claims",
|
||||
[
|
||||
("sk-test-key", "team-a", None),
|
||||
("sk-test-key", "team-a", {"sub": "test_user"}),
|
||||
(None, "team-a", None),
|
||||
(None, None, {"sub": "test_user"}),
|
||||
],
|
||||
ids=["plain_virtual_key", "jwt_mapped_virtual_key", "keyless_non_jwt_caller", "jwt_without_team"],
|
||||
)
|
||||
def test_non_proxy_admin_jwt_team_allowed_routes_never_grant_pass_through_to_virtual_keys(jwt_claims):
|
||||
virtual_key: Final = UserAPIKeyAuth(
|
||||
api_key="sk-test-key",
|
||||
def test_non_proxy_admin_jwt_team_allowed_routes_grant_pass_through_only_to_jwt_team_callers(
|
||||
api_key, team_id, jwt_claims
|
||||
):
|
||||
caller: Final = UserAPIKeyAuth(
|
||||
api_key=api_key,
|
||||
user_id="test_user",
|
||||
user_role=LitellmUserRoles.INTERNAL_USER.value,
|
||||
team_id=team_id,
|
||||
jwt_claims=jwt_claims,
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
_check_model_host_route_as(virtual_key, team_allowed_routes=["openai_routes", "/model-host/*"])
|
||||
_check_model_host_route_as(caller, team_allowed_routes=["openai_routes", "/model-host/*"])
|
||||
|
||||
assert exc_info.value.status_code == 403, exc_info.value.detail
|
||||
assert "allowed_passthrough_routes" in exc_info.value.detail
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue