Merge pull request #14387 from BerriAI/litellm_dev_09_09_2025_p1

fix(internal_user_endpoints.py): ensure only proxy admin can update v…
This commit is contained in:
Krish Dholakia
2025-09-09 19:54:13 -07:00
committed by GitHub
4 changed files with 69 additions and 38 deletions
+2 -20
View File
@@ -4,24 +4,6 @@ model_list:
model: openai/fake
api_key: fake-key
api_base: https://exampleopenaiendpoint-production.up.railway.app/
- model_name: gpt-5-mini
- model_name: wildcard_models/*
litellm_params:
model: azure/gpt-5-mini
api_base: os.environ/AZURE_GPT_5_MINI_API_BASE # runs os.getenv("AZURE_API_BASE")
api_key: os.environ/AZURE_GPT_5_MINI_API_KEY # runs os.getenv("AZURE_API_KEY")
stream_timeout: 60
merge_reasoning_content_in_choices: true
model_info:
mode: chat
- model_name: ollama-deepseek-r1
litellm_params:
model: ollama/deepseek-r1:1.5b
model_info:
mode: chat
router_settings:
model_group_alias: {"my-fake-gpt-4": "fake-openai-endpoint"}
litellm_settings:
callbacks: ["otel"]
success_callback: ["braintrust"]
model: openai/*
+35 -8
View File
@@ -300,7 +300,7 @@ class RouteChecks:
if re.match(pattern, route):
return True
return False
@staticmethod
def _is_wildcard_pattern(pattern: str) -> bool:
"""
@@ -354,16 +354,22 @@ class RouteChecks:
#########################################################
if route in allowed_routes:
return True
#########################################################
# wildcard match route is in allowed_routes
# e.g calling /anthropic/v1/messages is allowed if allowed_routes has /anthropic/*
#########################################################
wildcard_allowed_routes = [route for route in allowed_routes if RouteChecks._is_wildcard_pattern(pattern=route)]
wildcard_allowed_routes = [
route
for route in allowed_routes
if RouteChecks._is_wildcard_pattern(pattern=route)
]
for allowed_route in wildcard_allowed_routes:
if RouteChecks._route_matches_wildcard_pattern(route=route, pattern=allowed_route):
if RouteChecks._route_matches_wildcard_pattern(
route=route, pattern=allowed_route
):
return True
#########################################################
# pattern match route is in allowed_routes
# pattern: "/threads/{thread_id}"
@@ -375,7 +381,7 @@ class RouteChecks:
for allowed_route in allowed_routes
):
return True
return False
@staticmethod
@@ -420,7 +426,7 @@ class RouteChecks:
status_code=status.HTTP_403_FORBIDDEN,
detail=f"user not allowed to access this OpenAI routes, role= {_user_role}",
)
# Check if this is a write operation on management routes
if RouteChecks.check_route_access(
route=route, allowed_routes=LiteLLMRoutes.management_routes.value
@@ -436,7 +442,28 @@ class RouteChecks:
status_code=status.HTTP_403_FORBIDDEN,
detail=f"user not allowed to access this route, role= {_user_role}. Trying to access: {route} and updating invalid param: {param}. only user_email and password can be updated",
)
elif route in ["/user/new", "/user/delete", "/team/new", "/team/update", "/team/delete", "/model/new", "/model/update", "/model/delete", "/key/generate", "/key/delete", "/key/update", "/key/regenerate", "/key/service-account/generate", "/key/block", "/key/unblock"] or route.startswith("/key/") and route.endswith("/regenerate"):
elif (
route
in [
"/user/new",
"/user/delete",
"/team/new",
"/team/update",
"/team/delete",
"/model/new",
"/model/update",
"/model/delete",
"/key/generate",
"/key/delete",
"/key/update",
"/key/regenerate",
"/key/service-account/generate",
"/key/block",
"/key/unblock",
]
or route.startswith("/key/")
and route.endswith("/regenerate")
):
# Block write operations for PROXY_ADMIN_VIEW_ONLY
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -835,6 +835,16 @@ async def _update_single_user_helper(
existing_user_row = LiteLLM_UserTable(
**existing_user_row.model_dump(exclude_none=True)
)
if not can_user_call_user_update(
user_api_key_dict=user_api_key_dict,
user_info=existing_user_row,
):
raise HTTPException(
status_code=403,
detail={
"error": "User does not have permission to update this user. Only PROXY_ADMIN can update other users."
},
)
existing_metadata = (
cast(Dict, getattr(existing_user_row, "metadata", {}) or {})
@@ -929,6 +939,20 @@ async def _update_single_user_helper(
return response
def can_user_call_user_update(
user_api_key_dict: UserAPIKeyAuth,
user_info: LiteLLM_UserTable,
) -> bool:
"""
Helper to check if the user has access to the key's info
"""
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
return True
elif user_api_key_dict.user_id == user_info.user_id:
return True
return False
@router.post(
"/user/update",
tags=["Internal User management"],
@@ -988,6 +1012,7 @@ async def user_update(
"""
try:
verbose_proxy_logger.debug("/user/update: Received data = %s", data)
response = await _update_single_user_helper(
user_request=data,
user_api_key_dict=user_api_key_dict,
+7 -10
View File
@@ -248,9 +248,7 @@ from litellm.proxy.management_endpoints.customer_endpoints import (
from litellm.proxy.management_endpoints.internal_user_endpoints import (
router as internal_user_router,
)
from litellm.proxy.management_endpoints.internal_user_endpoints import (
user_update,
)
from litellm.proxy.management_endpoints.internal_user_endpoints import user_update
from litellm.proxy.management_endpoints.key_management_endpoints import (
delete_verification_tokens,
duration_in_seconds,
@@ -297,9 +295,7 @@ from litellm.proxy.middleware.prometheus_auth_middleware import PrometheusAuthMi
from litellm.proxy.openai_files_endpoints.files_endpoints import (
router as openai_files_router,
)
from litellm.proxy.openai_files_endpoints.files_endpoints import (
set_files_config,
)
from litellm.proxy.openai_files_endpoints.files_endpoints import set_files_config
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
passthrough_endpoint_router,
)
@@ -3812,9 +3808,7 @@ class ProxyStartupEvent:
# CloudZero Background Job
########################################################
from litellm.integrations.cloudzero.cloudzero import CloudZeroLogger
from litellm.proxy.spend_tracking.cloudzero_endpoints import (
is_cloudzero_setup,
)
from litellm.proxy.spend_tracking.cloudzero_endpoints import is_cloudzero_setup
if await is_cloudzero_setup():
await CloudZeroLogger.init_cloudzero_background_job(scheduler=scheduler)
@@ -7601,7 +7595,10 @@ async def login(request: Request): # noqa: PLR0915
data=UpdateUserRequest(
user_id=key_user_id,
user_role=user_role,
)
),
user_api_key_dict=UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN,
),
)
if os.getenv("DATABASE_URL") is not None:
response = await generate_key_helper_fn(