mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(auth): match templated info routes by pattern
is_info_route compared the request path against info_routes with a bare `in`, so an entry
carrying a path parameter could never match: the incoming route holds a resolved id, not the
`{key_id}` template. /key/{key_id}/budgets was therefore refused for the view-only, team and
customer roles that /key/info serves, and its info_routes entry matched nothing at all.
Route it through check_route_access, the way is_management_route already does. It is the only
templated entry in the list today, so no other route changes reachability.
This commit is contained in:
parent
3b5ab06a95
commit
dd720b6385
2 changed files with 49 additions and 1 deletions
|
|
@ -440,8 +440,11 @@ class RouteChecks:
|
|||
def is_info_route(route: str) -> bool:
|
||||
"""
|
||||
Check if route is an info route
|
||||
|
||||
Pattern-aware, like ``is_management_route``, so an info route carrying a path parameter is as
|
||||
reachable as one without: the incoming route holds a resolved id, never the ``{...}`` template.
|
||||
"""
|
||||
return route in LiteLLMRoutes.info_routes.value
|
||||
return RouteChecks.check_route_access(route=route, allowed_routes=LiteLLMRoutes.info_routes.value)
|
||||
|
||||
@staticmethod
|
||||
def _is_azure_openai_route(route: str) -> bool:
|
||||
|
|
|
|||
|
|
@ -149,3 +149,48 @@ def test_v2_user_info_route_access():
|
|||
valid_token=valid_token,
|
||||
request_data={},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
[
|
||||
LitellmUserRoles.INTERNAL_USER,
|
||||
LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
|
||||
LitellmUserRoles.TEAM,
|
||||
LitellmUserRoles.CUSTOMER,
|
||||
None,
|
||||
],
|
||||
)
|
||||
def test_templated_info_route_is_as_reachable_as_a_plain_one(role):
|
||||
"""
|
||||
A resolved path parameter must not make an info route less reachable than one without.
|
||||
|
||||
`is_info_route` compared the request path against the route list by exact string, so
|
||||
`/key/{key_id}/budgets` never matched a real request and callers who could read /key/info
|
||||
were refused on the equivalent budgets route.
|
||||
"""
|
||||
user_obj = LiteLLM_UserTable(
|
||||
user_id="test_user",
|
||||
user_email="test@example.com",
|
||||
user_role=role.value if role is not None else None,
|
||||
max_budget=None,
|
||||
spend=0.0,
|
||||
models=[],
|
||||
)
|
||||
|
||||
for route in ("/key/info", "/key/some-resolved-key-hash/budgets", "/key/budgets"):
|
||||
RouteChecks.non_proxy_admin_allowed_routes_check(
|
||||
user_obj=user_obj,
|
||||
_user_role=role,
|
||||
route=route,
|
||||
request=MagicMock(spec=Request),
|
||||
valid_token=UserAPIKeyAuth(api_key="sk-test"),
|
||||
request_data={},
|
||||
)
|
||||
|
||||
|
||||
def test_templated_route_matching_does_not_widen_unrelated_key_routes():
|
||||
"""Pattern matching must stay scoped to the entries in the list, not every /key/<x>/<y> path."""
|
||||
assert RouteChecks.is_info_route("/key/some-hash/regenerate") is False
|
||||
assert RouteChecks.is_info_route("/key/some-hash/delete") is False
|
||||
assert RouteChecks.is_info_route("/key/some-hash/budgets") is True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue