BerriAI/litellm · error · HTTPException

f"Only proxy admins can create administrative users (proxy_a

Error message

f"Only proxy admins can create administrative users (proxy_admin, proxy_admin_viewer). Attempted to create user with role: {data.user_role}. Your role: {user_api_key_dict.user_role}"

What it means

POST /user/new refuses to create administrative users unless the caller is a proxy admin. If the request body sets user_role to proxy_admin or proxy_admin_viewer while the authenticated key's role (UserAPIKeyAuth.user_role) is not proxy_admin, LiteLLM returns 403. The guard blocks privilege escalation: team admins and internal users must not be able to mint new admins. (The isinstance(user_api_key_dict, UserAPIKeyAuth) condition only relaxes the check for direct test calls that pass a Depends object.)

Source

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

        await _check_duplicate_user_email(data.user_email, prisma_client)

        # Check if license is over limit
        billable_users: Final = await UserRepository(prisma_client).count_billable_users()
        if billable_users and _license_check.is_over_limit(total_users=billable_users):
            raise HTTPException(
                status_code=403,
                detail="License is over limit. Please contact support@berri.ai to upgrade your license.",
            )

        # Only proxy admins can create administrative users
        # Check if user_api_key_dict is actually a UserAPIKeyAuth instance (not a Depends object)
        # This can happen when the function is called directly in tests
        if (
            data.user_role in [LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY]
            and isinstance(user_api_key_dict, UserAPIKeyAuth)
            and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN
        ):
            raise HTTPException(
                status_code=403,
                detail=f"Only proxy admins can create administrative users (proxy_admin, proxy_admin_viewer). Attempted to create user with role: {data.user_role}. Your role: {user_api_key_dict.user_role}",
            )

        _check_permissions_caller_permission(
            data=data,
            user_api_key_dict=user_api_key_dict,
        )

        data_json = data.json()
        data_json = _update_internal_new_user_params(data_json, data)
        # Persist the requested grants as their own row and link it, mirroring key/team creation.
        # generate_key_helper_fn only forwards object_permission_id, so without this the entitlement
        # the caller sent would be dropped on the floor.
        data_json = await _set_object_permission(data_json=data_json, prisma_client=prisma_client)
        _hash_password_in_dict(data_json)
        teams = data.teams
        if teams is None:

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Authenticate with a proxy_admin key (the master key or a key bound to an admin user) for the create call
  2. Create the user without user_role (defaults to internal_user) and have a proxy admin promote them afterwards via POST /user/update
  3. Drop or change user_role in the payload if an administrative user was not intended

Example fix

# before: team-admin key -> 403
curl -X POST http://localhost:4000/user/new -H 'Authorization: Bearer sk-team-admin' -d '{"user_id": "u1", "user_role": "proxy_admin"}'

# after: proxy-admin key
curl -X POST http://localhost:4000/user/new -H 'Authorization: Bearer sk-master-key' -d '{"user_id": "u1", "user_role": "proxy_admin"}'  # 200
Defensive patterns

Strategy: validation

Validate before calling

import requests

def assert_can_create_admins(base_url: str, key: str) -> None:
    r = requests.get(f"{base_url}/key/info", params={"key": key}, timeout=10)
    r.raise_for_status()
    info = r.json()["info"]
    role = info.get("user_role") or "internal_user"
    if role != "proxy_admin":
        raise PermissionError(f"key role is {role}; proxy_admin required to create admin users")

Try / catch

except requests.HTTPError as e:
    if e.response is not None and e.response.status_code == 403 and "Only proxy admins" in e.response.text:
        # retry with an admin key, or re-submit without user_role
        ...

Prevention

When it happens

Trigger: POST /user/new with body containing "user_role": "proxy_admin" or "proxy_admin_viewer", authenticated with a virtual key whose user is internal_user or team_admin; provisioning scripts that try to create admins with a non-admin service key.

Common situations: Automation run with team-admin keys; copy-pasted payloads from docs that include user_role; upgrading LiteLLM to a version where this escalation check was added and previously-working calls now fail with 403.

Related errors


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