mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-19 12:18:35 +00:00
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:
@@ -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/*
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user