Repository: ansible/ansible

Issue:
user module should backup shadow to shadow-
##### SUMMARY
Tools that modify `/etc/shadow` (useradd, groupadd etc) all make a backup at `/etc/shadow-`. Ansible should also do the same. Thoughts?

##### ISSUE TYPE
 - Feature Idea

##### COMPONENT NAME
user

##### ANSIBLE VERSION
```
ansible 2.5.3
  config file = /home/lae/.ansible.cfg
  configured module search path = ['/home/lae/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.6.5 (default, May 11 2018, 04:00:52) [GCC 8.1.0]
```

##### OS / ENVIRONMENT
N/A


Discussion:
Files identified in the description:
* [lib/ansible/modules/system/user.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/user.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @sfromm
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
cc @samdoran
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I like this idea. We could use the existing `backup_local` method which would add a timestamped backup. We would have to detail in the docs that this is only for the shadow file to keep the scope and expectations focused.
The user module does not normally edit /etc/shadow directly, it actually uses useradd/usermod or the tool appropriate for the platform targeted.

Resolving patch:
diff --git a/lib/ansible/modules/system/user.py b/lib/ansible/modules/system/user.py
--- a/lib/ansible/modules/system/user.py
+++ b/lib/ansible/modules/system/user.py
@@ -22,6 +22,8 @@
     they generally come pre-installed with the system and Ansible will require they
     are present at runtime. If they are not, a descriptive error message will be shown.
   - For Windows targets, use the M(win_user) module instead.
+  - On SunOS platforms, the shadow file is backed up automatically since this module edits it directly.
+    On other platforms, the shadow file is backed up by the underlying tools used by this module.
 description:
     - Manage user accounts and user attributes.
     - For Windows targets, use the M(win_user) module instead.
@@ -438,6 +440,10 @@ def execute_command(self, cmd, use_unsafe_shell=False, data=None, obey_checkmode
             cmd = [str(x) for x in cmd]
             return self.module.run_command(cmd, use_unsafe_shell=use_unsafe_shell, data=data)
 
+    def backup_shadow(self):
+        if not self.module.check_mode and self.SHADOWFILE:
+            return self.module.backup_local(self.SHADOWFILE)
+
     def remove_user_userdel(self):
         if self.local:
             command_name = 'luserdel'
@@ -1558,6 +1564,7 @@ def create_user(self):
         if not self.module.check_mode:
             # we have to set the password by editing the /etc/shadow file
             if self.password is not None:
+                self.backup_shadow()
                 minweeks, maxweeks, warnweeks = self.get_password_defaults()
                 try:
                     lines = []
@@ -1662,6 +1669,7 @@ def modify_user_usermod(self):
 
         # we have to set the password by editing the /etc/shadow file
         if self.update_password == 'always' and self.password is not None and info[1] != self.password:
+            self.backup_shadow()
             (rc, out, err) = (0, '', '')
             if not self.module.check_mode:
                 minweeks, maxweeks, warnweeks = self.get_password_defaults()


Test patch:
diff --git a/test/integration/targets/user/tasks/main.yml b/test/integration/targets/user/tasks/main.yml
--- a/test/integration/targets/user/tasks/main.yml
+++ b/test/integration/targets/user/tasks/main.yml
@@ -299,3 +299,26 @@
         that:
           - bsd_account_expiration.stdout == '0'
   when: ansible_os_family == 'FreeBSD'
+
+
+## shadow backup
+- block:
+    - name: Create a user to test shadow file backup
+      user:
+        name: ansibulluser
+        state: present
+      register: result
+
+    - name: Find shadow backup files
+      find:
+        path: /etc
+        patterns: 'shadow\..*~$'
+        use_regex: yes
+      register: shadow_backups
+
+    - name: Assert that a backup file was created
+      assert:
+        that:
+          - result.bakup
+          - shadow_backups.files | map(attribute='path') | list | length > 0
+  when: ansible_os_family == 'Solaris'