diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 646779e6f8..1772da3d15 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -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"} diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 6e8a691ce9..8e800c4572 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -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 []