{"record":{"id":"1292a460df075508","repo":"ansible/ansible","slug":"cannot-change-group-ownership-of-s-to-s-as-user","errorCode":null,"errorMessage":"Cannot change group ownership of %s to %s, as user %s","messagePattern":"Cannot change group ownership of (.+?) to (.+?), as user (.+?)","errorType":"exception","errorClass":"UnarchiveError","httpStatus":null,"severity":"error","filePath":"lib/ansible/modules/unarchive.py","lineNumber":740,"sourceCode":"\n            if owner and owner != fut_owner:\n                change = True\n                err += 'Path %s is owned by user %s, not by user %s as expected\\n' % (path, owner, fut_owner)\n                itemized[6] = 'o'\n            elif uid and uid != fut_uid:\n                change = True\n                err += 'Path %s is owned by uid %s, not by uid %s as expected\\n' % (path, uid, fut_uid)\n                itemized[6] = 'o'\n\n            # Compare file group ownership\n            group = gid = None\n            try:\n                group = grp.getgrgid(st.st_gid).gr_name\n            except (KeyError, ValueError, OverflowError):\n                gid = st.st_gid\n\n            if run_uid != 0 and (fut_group != run_group or fut_gid != run_gid) and fut_gid not in groups:\n                raise UnarchiveError('Cannot change group ownership of %s to %s, as user %s' % (path, fut_group, run_owner))\n\n            if group and group != fut_group:\n                change = True\n                err += 'Path %s is owned by group %s, not by group %s as expected\\n' % (path, group, fut_group)\n                itemized[6] = 'g'\n            elif gid and gid != fut_gid:\n                change = True\n                err += 'Path %s is owned by gid %s, not by gid %s as expected\\n' % (path, gid, fut_gid)\n                itemized[6] = 'g'\n\n            # Register changed files and finalize diff output\n            if change:\n                if path not in self.includes:\n                    self.includes.append(path)\n                diff += '%s %s\\n' % (''.join(itemized), path)\n\n        if self.includes:\n            unarchived = False","sourceCodeStart":722,"sourceCodeEnd":758,"githubUrl":"https://github.com/ansible/ansible/blob/9cf16a4aca7898481c257f1e17ad28d0b67b1f85/lib/ansible/modules/unarchive.py#L722-L758","documentation":"Raised by the unarchive module when the archive contains files whose group ownership would need to change to a group that the executing (non-root) user does not belong to. The code first checks run_uid != 0, then verifies whether the future group/gid differs from the run group and is absent from the user's supplementary groups. It is an UnarchiveError that aborts the task before any extraction drift occurs.","triggerScenarios":"Running unarchive as a non-root user (no become) where the archive records a group different from the run group and the fut_gid is not in the groups obtained for the user; e.g. archive built with group 'apache' extracted by user 'deploy' that is only in 'deploy'.","commonSituations":"CI pipelines extracting tarballs as an unprivileged service user; archives created on a different host with different group names/gids; hardening setups that forbid become; check_mode runs still evaluate this ownership guard.","solutions":["Run the task with become: true (root can chgrp to any group), or delegate to a host where the user has the needed rights","Add the executing user to the target group (usermod -aG <group> <user>) and re-login so the supplementary group list includes fut_gid","Rebuild/normalize the archive so its group ownership matches the extracting user's group (tar --group= on creation, or chgrp -R the tree before archiving)","Set group ownership/sticky behaviors via the module's owner/group arguments consistent with the run user's memberships"],"exampleFix":"# before\n- name: extract app tarball\n  ansible.builtin.unarchive:\n    src: app.tar.gz\n    dest: /opt/app\n  # runs as non-root 'deploy', archive gid is 'apache'\n\n# after\n- name: extract app tarball\n  ansible.builtin.unarchive:\n    src: app.tar.gz\n    dest: /opt/app\n  become: true\n  # or: add deploy to apache group and re-run","handlingStrategy":"validation","validationCode":"# Before unarchive as non-root, confirm membership in the archive's group\nimport grp, os\n\ndef can_chgrp(path_gid, run_gid=None):\n    run_gid = run_gid if run_gid is not None else os.getgid()\n    groups = {g.gr_gid for g in grp.getgrall() if os.getlogin() in g.gr_mem}\n    groups.add(run_gid)\n    return path_gid in groups or os.getuid() == 0\n\n# in the task: fail fast with a clear message\n- ansible.builtin.stat: {path: /tmp/app.tar.gz}\n  register: st","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Run unarchive with become: true when archives carry foreign group ownership","Standardize archive creation with tar --group=<run_group> so ownership matches the extracting user","Document required group memberships in role meta and assert them with a pre-task checking `id -nG`","Avoid check_mode surprises: this ownership guard is evaluated even when nothing is extracted"],"tags":["unarchive","permissions","filesystem","ownership"],"backgroundTag":null,"analyzedSha":"9cf16a4aca7898481c257f1e17ad28d0b67b1f85","analyzedAt":"2026-08-15T00:15:47.100Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}