{"record":{"id":"2326cfc10a720c6f","repo":"BerriAI/litellm","slug":"user-does-not-have-permission-to-update-this-user","errorCode":null,"errorMessage":"User does not have permission to update this user. Only PROXY_ADMIN can update other users.","messagePattern":"User does not have permission to update this user\\. Only PROXY_ADMIN can update other users\\.","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"litellm/proxy/management_endpoints/internal_user_endpoints.py","lineNumber":1266,"sourceCode":"                )\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        )\n\n\nasync def _invalidate_user_spend_counter_if_changed(\n    non_default_values: Mapping[str, object],\n) -> None:","sourceCodeStart":1248,"sourceCodeEnd":1284,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/internal_user_endpoints.py#L1248-L1284","documentation":"On /user/update, when the target user row exists, can_user_call_user_update allows the call only if the caller is proxy_admin or the target is the caller's own user_id; anything else returns 403. Note the detail is a JSON object {'error': ...} rather than a plain string. Team admins are NOT exempt on this user-management endpoint - only self-updates and proxy admins pass.","triggerScenarios":"POST /user/update with the user_id or user_email of another user while authenticated as internal_user or team_admin; scripts that enumerate and update many users with a single member key.","commonSituations":"Team admins assuming they can edit team members through /user/update; frontends reusing one member's key for everyone's profile saves; shared service keys used for HR-style bulk edits.","solutions":["Authenticate with a proxy_admin key to update other users","Non-admins: only send updates for their own user_id (or omit user_id to target self)","For team-scoped management, use the team member management endpoints or have an admin perform user updates"],"exampleFix":"# before: member key updating a teammate\nPOST /user/update -H 'Authorization: Bearer sk-member1' {\"user_id\": \"member2\", \"max_budget\": 10}  # 403\n\n# after: admin key\nPOST /user/update -H 'Authorization: Bearer sk-admin' {\"user_id\": \"member2\", \"max_budget\": 10}  # 200","handlingStrategy":"validation","validationCode":"import requests\n\ndef can_update_target(base_url: str, key: str, target_user_id: str) -> bool:\n    r = requests.get(f\"{base_url}/key/info\", params={\"key\": key}, timeout=10)\n    r.raise_for_status()\n    info = r.json()[\"info\"]\n    return info.get(\"user_role\") == \"proxy_admin\" or info.get(\"user_id\") == target_user_id","typeGuard":"def updateWillPassAuthz(keyRole: string, keyUserId: string, targetUserId: string): boolean {\n  return keyRole === \"proxy_admin\" || keyUserId === targetUserId;\n}","tryCatchPattern":"except requests.HTTPError as e:\n    body = e.response.text if e.response is not None else \"\"\n    if e.response is not None and e.response.status_code == 403 and \"does not have permission\" in body:\n        # target is another user: re-issue with admin key or skip\n        ...","preventionTips":["Per-user operations should use that user's own key; batch edits should use an admin key","Do not assume team-admin rights extend to /user/update - they do not","Cache the caller's identity from /key/info and route updates accordingly"],"tags":["litellm","rbac","user-management","authorization"],"backgroundTag":"rbac-insufficient-permissions","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}