Merge pull request #26493 from BerriAI/litellm_key_route_caller_checks_addendum

[Fix] Extend caller-permission checks to service-account + tighten raw-body acceptance
This commit is contained in:
yuneng-jiang
2026-04-24 23:40:58 -07:00
committed by GitHub
@@ -462,22 +462,29 @@ _NON_ADMIN_SAFE_ALLOWED_ROUTES_PRESETS = frozenset({"llm_api_routes", "info_rout
def _check_allowed_routes_caller_permission(
allowed_routes: Optional[list],
user_api_key_dict: UserAPIKeyAuth,
*,
allow_safe_presets: bool = False,
) -> None:
"""
Only proxy admins may set `allowed_routes` on a key, except for the safe
presets produced by `handle_key_type` for non-elevated buckets
(`llm_api_routes`, `info_routes`).
Only proxy admins may set `allowed_routes` on a key.
`allowed_routes` overrides the standard role-based route gate in
RouteChecks.non_proxy_admin_allowed_routes_check, so the field is
restricted to admins outside of those safe presets.
restricted to admins. Non-admins must instead use `key_type` to pick a
preset bucket — that path goes through `handle_key_type` and re-enters
this function with `allow_safe_presets=True`, which lets the derived
`llm_api_routes` / `info_routes` values through. Raw-body call sites
leave `allow_safe_presets=False` so non-admins can't write those values
directly.
"""
# Empty list is the default on GenerateKeyRequest — treat as "not set".
if not allowed_routes:
return
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
return
if all(r in _NON_ADMIN_SAFE_ALLOWED_ROUTES_PRESETS for r in allowed_routes):
if allow_safe_presets and all(
r in _NON_ADMIN_SAFE_ALLOWED_ROUTES_PRESETS for r in allowed_routes
):
return
raise HTTPException(
status_code=403,
@@ -712,10 +719,13 @@ async def _common_key_generation_helper( # noqa: PLR0915
# Re-check allowed_routes after handle_key_type, since key_type can derive
# an elevated bucket (e.g. ["management_routes"]) that wasn't present in
# the original request body.
# the original request body. The safe presets produced by handle_key_type
# for non-elevated buckets are accepted here; the raw-body pre-checks at
# the entry of each handler keep their default strictness.
_check_allowed_routes_caller_permission(
allowed_routes=data_json.get("allowed_routes"),
user_api_key_dict=user_api_key_dict,
allow_safe_presets=True,
)
# if we get max_budget passed to /key/generate, then use it as key_max_budget. Since generate_key_helper_fn is used to make new users
@@ -1499,6 +1509,15 @@ async def generate_service_account_key_fn(
detail={"error": CommonProxyErrors.db_not_connected_error.value},
)
_check_allowed_routes_caller_permission(
allowed_routes=data.allowed_routes,
user_api_key_dict=user_api_key_dict,
)
_check_passthrough_routes_caller_permission(
data=data,
user_api_key_dict=user_api_key_dict,
)
await validate_team_id_used_in_service_account_request(
team_id=data.team_id,
prisma_client=prisma_client,
@@ -3950,6 +3969,20 @@ async def regenerate_key_fn( # noqa: PLR0915
data=data,
user_api_key_dict=user_api_key_dict,
)
# Mirror /key/generate's post-handle_key_type recheck so a
# non-admin can't elevate via a key_type preset that the
# regenerate flow would otherwise carry through unchecked.
# The empty dict is intentional — `handle_key_type` is reused
# purely as a side-effect-free lookup of the preset bucket, not
# to mutate an existing `data_json`. Do not pass a real
# `data_json` here; that path would write the derived routes
# into the DB update payload and is owned by
# `_common_key_generation_helper`.
_check_allowed_routes_caller_permission(
allowed_routes=handle_key_type(data, {}).get("allowed_routes"),
user_api_key_dict=user_api_key_dict,
allow_safe_presets=True,
)
is_master_key_regeneration = data and data.new_master_key is not None