Merge pull request #25541 from BerriAI/litellm_field_level_checks

[Fix] Align field-level checks in user and key update endpoints
This commit is contained in:
yuneng-jiang
2026-04-11 20:50:10 -07:00
committed by GitHub
2 changed files with 60 additions and 22 deletions
@@ -1131,6 +1131,16 @@ async def _update_single_user_helper(
if prisma_client is None:
raise Exception("Not connected to DB!")
# Only proxy admins can modify user_role
if (
user_request.user_role is not None
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
):
raise HTTPException(
status_code=403,
detail="Only proxy admins can modify user roles.",
)
# Validate user identifier
if not user_request.user_id and not user_request.user_email:
raise ValueError("Either user_id or user_email must be provided")
@@ -1508,12 +1518,35 @@ async def bulk_user_update(
detail={"error": "Database not connected"},
)
# Only proxy admins can modify user_role in bulk updates
_bulk_role = (
getattr(data.user_updates, "user_role", None) if data.user_updates else None
)
if _bulk_role is None and data.users:
_bulk_role = next(
(u.user_role for u in data.users if u.user_role is not None), None
)
if (
_bulk_role is not None
and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
):
raise HTTPException(
status_code=403,
detail="Only proxy admins can modify user roles.",
)
# Determine the list of users to update
users_to_update: Union[
List[UpdateUserRequest], List[UpdateUserRequestNoUserIDorEmail]
] = []
if data.all_users and data.user_updates:
# Only proxy admins can update all users at once
if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:
raise HTTPException(
status_code=403,
detail="Only proxy admins can update all users at once.",
)
# Optimized path for updating all users directly in database
all_users_in_db = await prisma_client.db.litellm_usertable.find_many(
order={"created_at": "desc"}
@@ -768,9 +768,9 @@ async def _common_key_generation_helper( # noqa: PLR0915
request_type="key", **data_json, table_name="key"
)
response["soft_budget"] = (
data.soft_budget
) # include the user-input soft budget in the response
response[
"soft_budget"
] = data.soft_budget # include the user-input soft budget in the response
response = GenerateKeyResponse(**response)
@@ -1961,8 +1961,13 @@ async def _validate_update_key_data(
user_api_key_cache=user_api_key_cache,
)
# Admin-only: only proxy admins, team admins, or org admins can modify max_budget
if data.max_budget is not None and data.max_budget != existing_key_row.max_budget:
# Admin-only: only proxy admins, team admins, or org admins can modify max_budget or spend
if (
data.max_budget is not None and data.max_budget != existing_key_row.max_budget
) or (
data.spend is not None
and data.spend != getattr(existing_key_row, "spend", None)
):
if prisma_client is not None:
hashed_key = existing_key_row.token
await _check_key_admin_access(
@@ -1970,7 +1975,7 @@ async def _validate_update_key_data(
hashed_token=hashed_key,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
route="/key/update (max_budget)",
route="/key/update (max_budget/spend)",
)
# Check team limits if key has a team_id (from request or existing key)
@@ -3272,10 +3277,10 @@ async def delete_verification_tokens(
try:
if prisma_client:
tokens = [_hash_token_if_needed(token=key) for key in tokens]
_keys_being_deleted: List[LiteLLM_VerificationToken] = (
await prisma_client.db.litellm_verificationtoken.find_many(
where={"token": {"in": tokens}}
)
_keys_being_deleted: List[
LiteLLM_VerificationToken
] = await prisma_client.db.litellm_verificationtoken.find_many(
where={"token": {"in": tokens}}
)
if len(_keys_being_deleted) == 0:
@@ -3475,9 +3480,9 @@ async def _rotate_master_key( # noqa: PLR0915
from litellm.proxy.proxy_server import proxy_config
try:
models: Optional[List] = (
await prisma_client.db.litellm_proxymodeltable.find_many()
)
models: Optional[
List
] = await prisma_client.db.litellm_proxymodeltable.find_many()
except Exception:
models = None
# 2. process model table
@@ -4117,11 +4122,11 @@ async def validate_key_list_check(
param="user_id",
code=status.HTTP_403_FORBIDDEN,
)
complete_user_info_db_obj: Optional[BaseModel] = (
await prisma_client.db.litellm_usertable.find_unique(
where={"user_id": user_api_key_dict.user_id},
include={"organization_memberships": True},
)
complete_user_info_db_obj: Optional[
BaseModel
] = await prisma_client.db.litellm_usertable.find_unique(
where={"user_id": user_api_key_dict.user_id},
include={"organization_memberships": True},
)
if complete_user_info_db_obj is None:
@@ -4204,10 +4209,10 @@ async def _fetch_user_team_objects(
if complete_user_info is None or not complete_user_info.teams:
return []
teams: Optional[List[BaseModel]] = (
await prisma_client.db.litellm_teamtable.find_many(
where={"team_id": {"in": complete_user_info.teams}}
)
teams: Optional[
List[BaseModel]
] = await prisma_client.db.litellm_teamtable.find_many(
where={"team_id": {"in": complete_user_info.teams}}
)
if teams is None:
return []