{"record":{"id":"05f783104747c2fe","repo":"getredash/redash","slug":"must-be-admin-to-change-groups-membership","errorCode":null,"errorMessage":"Must be admin to change groups membership.","messagePattern":"Must be admin to change groups membership\\.","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"redash/handlers/users.py","lineNumber":226,"sourceCode":"        user = models.User.get_by_id_and_org(user_id, self.current_org)\n\n        req = request.get_json(True)\n\n        params = project(req, (\"email\", \"name\", \"password\", \"old_password\", \"group_ids\"))\n\n        if \"password\" in params and \"old_password\" not in params:\n            abort(403, message=\"Must provide current password to update password.\")\n\n        if \"old_password\" in params and not user.verify_password(params[\"old_password\"]):\n            abort(403, message=\"Incorrect current password.\")\n\n        if \"password\" in params:\n            user.hash_password(params.pop(\"password\"))\n            params.pop(\"old_password\")\n\n        if \"group_ids\" in params:\n            if not self.current_user.has_permission(\"admin\"):\n                abort(403, message=\"Must be admin to change groups membership.\")\n\n            for group_id in params[\"group_ids\"]:\n                try:\n                    models.Group.get_by_id_and_org(group_id, self.current_org)\n                except NoResultFound:\n                    abort(400, message=\"Group id {} is invalid.\".format(group_id))\n\n            if len(params[\"group_ids\"]) == 0:\n                params.pop(\"group_ids\")\n\n        if \"email\" in params:\n            require_allowed_email(params[\"email\"])\n\n        email_address_changed = \"email\" in params and params[\"email\"] != user.email\n        needs_to_verify_email = email_address_changed and settings.email_server_is_configured()\n        if needs_to_verify_email:\n            user.is_email_verified = False\n","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/getredash/redash/blob/ca79fe988d81cdac9675b412f3dfcab107bc1fbc/redash/handlers/users.py#L208-L244","documentation":"Raised by Redash's user update endpoint (POST /api/users/<id>) when the request body contains group_ids but the authenticated user lacks the admin permission. Only admins may change a user's group memberships, so the handler aborts with HTTP 403 before applying any updates.","triggerScenarios":"POST (or PUT) to /api/users/<user_id> with a JSON body containing a \"group_ids\" key while authenticated as a non-admin user (e.g. a regular user editing their own profile and including group_ids, or a disabled/limited API key without admin access).","commonSituations":"Client UI sending the full user object (including group_ids it previously read) on profile save; automation scripts reusing a personal (non-admin) API key for user management; org members assuming self-edit covers group membership.","solutions":["Remove group_ids from the request payload if you only intend to update profile fields (name, email, password)","Authenticate with an account/API key that belongs to the admin group of the target organization","If self-service group management is intended, have an admin perform the change or add the caller to the admin group"],"exampleFix":"# before\nrequests.post(f\"{url}/api/users/{uid}\", json={\"name\": \"x\", \"group_ids\": [1,2]}, headers=hdrs)  # 403\n# after\nrequests.post(f\"{url}/api/users/{uid}\", json={\"name\": \"x\"}, headers=hdrs)","handlingStrategy":"validation","validationCode":"me = requests.get(f\"{url}/api/session\", headers=hdrs).json()\n# or fetch current user and check group membership\nis_admin = any(g[\"type\"] == \"admin\" or \"admin\" in g.get(\"name\", \"\").lower()\n               for g in requests.get(f\"{url}/api/groups\", headers=hdrs).json())\nif not is_admin:\n    payload.pop(\"group_ids\", None)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only include group_ids in user-update payloads when strictly necessary","Keep a dedicated admin API key for user-management scripts and a personal key for reads","Check the caller's groups via /api/groups before attempting membership changes"],"tags":["redash","authorization","http-403","users","groups"],"backgroundTag":"permission-denied-403","analyzedSha":"ca79fe988d81cdac9675b412f3dfcab107bc1fbc","analyzedAt":"2026-08-28T18:32:34.637Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}