BerriAI/litellm · error · HTTPException

Only proxy admins can modify user roles.

Error message

Only proxy admins can modify user roles.

What it means

_check_user_update_authz runs for POST /user/update: if the payload sets user_role and the calling key's role is not proxy_admin, LiteLLM returns 403 'Only proxy admins can modify user roles.' Role changes are admin-only so keys cannot escalate privileges. The /user/bulk_update endpoint raises the same message when a bulk payload contains user_role and the caller is not a proxy admin.

Source

Thrown at litellm/proxy/management_endpoints/internal_user_endpoints.py:1261

                    litellm_changed_by=litellm_changed_by or user_api_key_dict.user_id,
                    user_api_key_dict=user_api_key_dict,
                    litellm_proxy_admin_name=litellm_proxy_admin_name,
                    before_value=(existing_user_row.model_dump_json(exclude_none=True) if existing_user_row else None),
                    after_value=user_row_typed.model_dump_json(exclude_none=True),
                )
            )
    except Exception as audit_error:
        verbose_proxy_logger.warning("Failed to create audit log for user %s: %s", response.get("user_id"), audit_error)


def _check_user_update_authz(
    user_request: UpdateUserRequest,
    user_api_key_dict: UserAPIKeyAuth,
    existing_user_row: BaseModel | None,
) -> None:
    """Authorization checks for /user/update — raises HTTPException on failure."""
    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.")

    if existing_user_row is not None:
        typed_row: Final = LiteLLM_UserTable.model_validate(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=typed_row):
            raise HTTPException(
                status_code=403,
                detail={
                    "error": "User does not have permission to update this user. Only PROXY_ADMIN can update other users."
                },
            )
    elif user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:
        # Silent-create guard: only PROXY_ADMIN may create via /user/update.
        raise HTTPException(
            status_code=404,
            detail={
                "error": "User not found. Only PROXY_ADMIN can create users via /user/update; use /user/new instead."
            },
        )

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Use a proxy_admin key (master key or admin-bound key) for any payload that includes user_role
  2. Strip user_role from the payload when a non-admin key only updates profile fields
  3. Split the flow: non-admin updates profile fields, proxy admin applies the role change separately

Example fix

# before: internal-user key, role included
POST /user/update {"user_id": "u1", "user_alias": "x", "user_role": "proxy_admin"}  # 403

# after: non-admin payload without role
POST /user/update {"user_id": "u1", "user_alias": "x"}  # 200
Defensive patterns

Strategy: validation

Validate before calling

import requests

def safe_user_update_payload(base_url: str, key: str, payload: dict) -> dict:
    if "user_role" in payload:
        r = requests.get(f"{base_url}/key/info", params={"key": key}, timeout=10)
        r.raise_for_status()
        role = r.json()["info"].get("user_role") or "internal_user"
        if role != "proxy_admin":
            payload = {k: v for k, v in payload.items() if k != "user_role"}
    return payload

Try / catch

except requests.HTTPError as e:
    if e.response is not None and e.response.status_code == 403 and "modify user roles" in e.response.text:
        # strip user_role and retry, or escalate to an admin key
        ...

Prevention

When it happens

Trigger: POST /user/update with any user_role in the body using an internal_user or team_admin key; POST /user/bulk_update whose user_updates or users entries contain user_role with a non-admin key.

Common situations: Self-service profile flows that forward the whole user object (including user_role) from the client; promotion scripts run with non-admin keys; payloads copied from admin docs.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/8e019611cdab732c. Report an issue: GitHub.