From 62fcc52eb625c240d1df83bfb33be05906df5ebe Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Mon, 1 Sep 2025 19:48:29 -0700 Subject: [PATCH 1/2] fix(internal_user_endpoints.py): ensure only proxy admin can update values for other users prevents proxy_admin_viewer from updating other user passwords --- litellm/proxy/_new_secret_config.yaml | 22 +--------- litellm/proxy/auth/route_checks.py | 43 +++++++++++++++---- .../internal_user_endpoints.py | 25 +++++++++++ 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index b38272560d..c785dd05c4 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -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/* diff --git a/litellm/proxy/auth/route_checks.py b/litellm/proxy/auth/route_checks.py index 3774cdfb81..56218b3345 100644 --- a/litellm/proxy/auth/route_checks.py +++ b/litellm/proxy/auth/route_checks.py @@ -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, diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 3876230f5a..68e6342d6b 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -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, From 7cbf91cf3618f39c2fab53959b270ac648b80af7 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Tue, 9 Sep 2025 19:44:25 -0700 Subject: [PATCH 2/2] fix(proxy_server.py): fix api call --- litellm/proxy/proxy_server.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index a5dcf0b8e4..4ed6569f85 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -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(