{"record":{"id":"8e019611cdab732c","repo":"BerriAI/litellm","slug":"only-proxy-admins-can-modify-user-roles","errorCode":null,"errorMessage":"Only proxy admins can modify user roles.","messagePattern":"Only proxy admins can modify user roles\\.","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"litellm/proxy/management_endpoints/internal_user_endpoints.py","lineNumber":1261,"sourceCode":"                    litellm_changed_by=litellm_changed_by or user_api_key_dict.user_id,\n                    user_api_key_dict=user_api_key_dict,\n                    litellm_proxy_admin_name=litellm_proxy_admin_name,\n                    before_value=(existing_user_row.model_dump_json(exclude_none=True) if existing_user_row else None),\n                    after_value=user_row_typed.model_dump_json(exclude_none=True),\n                )\n            )\n    except Exception as audit_error:\n        verbose_proxy_logger.warning(\"Failed to create audit log for user %s: %s\", response.get(\"user_id\"), audit_error)\n\n\ndef _check_user_update_authz(\n    user_request: UpdateUserRequest,\n    user_api_key_dict: UserAPIKeyAuth,\n    existing_user_row: BaseModel | None,\n) -> None:\n    \"\"\"Authorization checks for /user/update — raises HTTPException on failure.\"\"\"\n    if user_request.user_role is not None and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:\n        raise HTTPException(status_code=403, detail=\"Only proxy admins can modify user roles.\")\n\n    if existing_user_row is not None:\n        typed_row: Final = LiteLLM_UserTable.model_validate(existing_user_row.model_dump(exclude_none=True))\n        if not can_user_call_user_update(user_api_key_dict=user_api_key_dict, user_info=typed_row):\n            raise HTTPException(\n                status_code=403,\n                detail={\n                    \"error\": \"User does not have permission to update this user. Only PROXY_ADMIN can update other users.\"\n                },\n            )\n    elif user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:\n        # Silent-create guard: only PROXY_ADMIN may create via /user/update.\n        raise HTTPException(\n            status_code=404,\n            detail={\n                \"error\": \"User not found. Only PROXY_ADMIN can create users via /user/update; use /user/new instead.\"\n            },\n        )","sourceCodeStart":1243,"sourceCodeEnd":1279,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/internal_user_endpoints.py#L1243-L1279","documentation":"_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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use a proxy_admin key (master key or admin-bound key) for any payload that includes user_role","Strip user_role from the payload when a non-admin key only updates profile fields","Split the flow: non-admin updates profile fields, proxy admin applies the role change separately"],"exampleFix":"# before: internal-user key, role included\nPOST /user/update {\"user_id\": \"u1\", \"user_alias\": \"x\", \"user_role\": \"proxy_admin\"}  # 403\n\n# after: non-admin payload without role\nPOST /user/update {\"user_id\": \"u1\", \"user_alias\": \"x\"}  # 200","handlingStrategy":"validation","validationCode":"import requests\n\ndef safe_user_update_payload(base_url: str, key: str, payload: dict) -> dict:\n    if \"user_role\" in payload:\n        r = requests.get(f\"{base_url}/key/info\", params={\"key\": key}, timeout=10)\n        r.raise_for_status()\n        role = r.json()[\"info\"].get(\"user_role\") or \"internal_user\"\n        if role != \"proxy_admin\":\n            payload = {k: v for k, v in payload.items() if k != \"user_role\"}\n    return payload","typeGuard":null,"tryCatchPattern":"except requests.HTTPError as e:\n    if e.response is not None and e.response.status_code == 403 and \"modify user roles\" in e.response.text:\n        # strip user_role and retry, or escalate to an admin key\n        ...","preventionTips":["Never round-trip user_role from UI forms into /user/update for non-admins","Centralize role changes in an admin-only script","Assert key role via /key/info before sending privileged fields"],"tags":["litellm","rbac","user-management","admin"],"backgroundTag":"rbac-insufficient-permissions","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}