From b4b1bf993269e32056bc5d94c75641757df9d466 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Mon, 11 Jan 2021 14:11:26 -0500 Subject: [PATCH] [stable-2.10] user - properly handle password and password lock when used together (#73016) (#73177) Do the right thing on Linux when password lock and a password hash are provided by writing out the password hash prepended by the appropriate lock string rather than using -U and -L. This is the correct way to set and lock the account in one command. On BSD, run separate commands as appropriate since locking and setting the password cannot be done in a single action. FreeBSD requires running several commands to get the account in the desired state. As a result, the rc, output, and error from all commands need to be combined and evaluated so an accurate and complete summary can be given at the end of module execution. * Improve integration tests to cover this scenario. * Break up user integration tests into smaller files * Properly lock account when creating a new account and password is supplied * Simplify rc collection in FreeBSD class Since the _handle_lock() method was added, the rc would be set to None, which could make task change reporting incorrect. My first attempt to solve this used a set and was a bit too complicated. Simplify it my comparing the rc from _handle_lock() and the current value of rc. * Improve the Linux password hash and locking behavior If password lock and hash are provided, set the hash and lock the account by using a password hash since -L cannot be used with -p. * Ensure -U and -L are not combined with -p since they are mutually exclusive to usermod. * Clarify password_lock behavior.. (cherry picked from commit 264e08f21a15213a4db76339888d3dfa2f2d6abb) Co-authored-by: Sam Doran --- ...72992-user-account-lock-always-changes.yml | 4 + lib/ansible/modules/user.py | 104 +- test/integration/targets/user/tasks/main.yml | 1132 +---------------- .../user/tasks/test_create_system_user.yml | 12 + .../targets/user/tasks/test_create_user.yml | 67 + .../user/tasks/test_create_user_home.yml | 136 ++ .../user/tasks/test_create_user_password.yml | 90 ++ .../user/tasks/test_create_user_uid.yml | 26 + .../targets/user/tasks/test_expires.yml | 147 +++ .../user/tasks/test_expires_new_account.yml | 55 + ...est_expires_new_account_epoch_negative.yml | 112 ++ .../targets/user/tasks/test_local.yml | 169 +++ ...pires_local.yml => test_local_expires.yml} | 0 .../user/tasks/test_no_home_fallback.yml | 106 ++ .../targets/user/tasks/test_password_lock.yml | 140 ++ .../tasks/test_password_lock_new_user.yml | 63 + .../targets/user/tasks/test_remove_user.yml | 19 + .../targets/user/tasks/test_shadow_backup.yml | 21 + .../user/tasks/test_ssh_key_passphrase.yml | 29 + 19 files changed, 1283 insertions(+), 1149 deletions(-) create mode 100644 changelogs/fragments/72992-user-account-lock-always-changes.yml create mode 100644 test/integration/targets/user/tasks/test_create_system_user.yml create mode 100644 test/integration/targets/user/tasks/test_create_user.yml create mode 100644 test/integration/targets/user/tasks/test_create_user_home.yml create mode 100644 test/integration/targets/user/tasks/test_create_user_password.yml create mode 100644 test/integration/targets/user/tasks/test_create_user_uid.yml create mode 100644 test/integration/targets/user/tasks/test_expires.yml create mode 100644 test/integration/targets/user/tasks/test_expires_new_account.yml create mode 100644 test/integration/targets/user/tasks/test_expires_new_account_epoch_negative.yml create mode 100644 test/integration/targets/user/tasks/test_local.yml rename test/integration/targets/user/tasks/{expires_local.yml => test_local_expires.yml} (100%) create mode 100644 test/integration/targets/user/tasks/test_no_home_fallback.yml create mode 100644 test/integration/targets/user/tasks/test_password_lock.yml create mode 100644 test/integration/targets/user/tasks/test_password_lock_new_user.yml create mode 100644 test/integration/targets/user/tasks/test_remove_user.yml create mode 100644 test/integration/targets/user/tasks/test_shadow_backup.yml create mode 100644 test/integration/targets/user/tasks/test_ssh_key_passphrase.yml diff --git a/changelogs/fragments/72992-user-account-lock-always-changes.yml b/changelogs/fragments/72992-user-account-lock-always-changes.yml new file mode 100644 index 00000000000..dea289863c2 --- /dev/null +++ b/changelogs/fragments/72992-user-account-lock-always-changes.yml @@ -0,0 +1,4 @@ +bugfixes: + - > + user - do the right thing when ``password_lock=True`` and ``password`` + are used together (https://github.com/ansible/ansible/issues/72992) diff --git a/lib/ansible/modules/user.py b/lib/ansible/modules/user.py index c57fadc99e1..0e37cbf5310 100644 --- a/lib/ansible/modules/user.py +++ b/lib/ansible/modules/user.py @@ -192,9 +192,10 @@ options: version_added: "1.9" password_lock: description: - - Lock the password (usermod -L, pw lock, usermod -C). - - BUT implementation differs on different platforms, this option does not always mean the user cannot login via other methods. - - This option does not disable the user, only lock the password. Do not change the password in the same task. + - Lock the password (C(usermod -L), C(usermod -U), C(pw lock)). + - Implementation differs by platform. This option does not always mean the user cannot login using other methods. + - This option does not disable the user, only lock the password. + - This must be set to C(False) in order to unlock a currently locked password. The absence of this parameter will not unlock a password. - Currently supported on Linux, FreeBSD, DragonFlyBSD, NetBSD, OpenBSD. type: bool version_added: "2.6" @@ -658,7 +659,10 @@ class User(object): if self.password is not None: cmd.append('-p') - cmd.append(self.password) + if self.password_lock: + cmd.append('!%s' % self.password) + else: + cmd.append(self.password) if self.create_home: if not self.local: @@ -844,9 +848,15 @@ class User(object): # usermod will refuse to unlock a user with no password, module shows 'changed' regardless cmd.append('-U') - if self.update_password == 'always' and self.password is not None and info[1] != self.password: + if self.update_password == 'always' and self.password is not None and info[1].lstrip('!') != self.password.lstrip('!'): + # Remove options that are mutually exclusive with -p + cmd = [c for c in cmd if c not in ['-U', '-L']] cmd.append('-p') - cmd.append(self.password) + if self.password_lock: + # Lock the account and set the hash in a single command + cmd.append('!%s' % self.password) + else: + cmd.append(self.password) (rc, out, err) = (None, '', '') @@ -1208,6 +1218,31 @@ class FreeBsdUser(User): SHADOWFILE_EXPIRE_INDEX = 6 DATE_FORMAT = '%d-%b-%Y' + def _handle_lock(self): + info = self.user_info() + if self.password_lock and not info[1].startswith('*LOCKED*'): + cmd = [ + self.module.get_bin_path('pw', True), + 'lock', + self.name + ] + if self.uid is not None and info[2] != int(self.uid): + cmd.append('-u') + cmd.append(self.uid) + return self.execute_command(cmd) + elif self.password_lock is False and info[1].startswith('*LOCKED*'): + cmd = [ + self.module.get_bin_path('pw', True), + 'unlock', + self.name + ] + if self.uid is not None and info[2] != int(self.uid): + cmd.append('-u') + cmd.append(self.uid) + return self.execute_command(cmd) + + return (None, '', '') + def remove_user(self): cmd = [ self.module.get_bin_path('pw', True), @@ -1279,6 +1314,7 @@ class FreeBsdUser(User): # system cannot be handled currently - should we error if its requested? # create the user (rc, out, err) = self.execute_command(cmd) + if rc is not None and rc != 0: self.module.fail_json(name=self.name, msg=err, rc=rc) @@ -1290,7 +1326,18 @@ class FreeBsdUser(User): self.password, self.name ] - return self.execute_command(cmd) + _rc, _out, _err = self.execute_command(cmd) + if rc is None: + rc = _rc + out += _out + err += _err + + # we have to lock/unlock the password in a distinct command + _rc, _out, _err = self._handle_lock() + if rc is None: + rc = _rc + out += _out + err += _err return (rc, out, err) @@ -1394,45 +1441,38 @@ class FreeBsdUser(User): cmd.append('-e') cmd.append(str(calendar.timegm(self.expires))) + (rc, out, err) = (None, '', '') + # modify the user if cmd will do anything if cmd_len != len(cmd): - (rc, out, err) = self.execute_command(cmd) + (rc, _out, _err) = self.execute_command(cmd) + out += _out + err += _err + if rc is not None and rc != 0: self.module.fail_json(name=self.name, msg=err, rc=rc) - else: - (rc, out, err) = (None, '', '') # we have to set the password in a second command - if self.update_password == 'always' and self.password is not None and info[1] != self.password: + if self.update_password == 'always' and self.password is not None and info[1].lstrip('*LOCKED*') != self.password.lstrip('*LOCKED*'): cmd = [ self.module.get_bin_path('chpass', True), '-p', self.password, self.name ] - return self.execute_command(cmd) + _rc, _out, _err = self.execute_command(cmd) + if rc is None: + rc = _rc + out += _out + err += _err # we have to lock/unlock the password in a distinct command - if self.password_lock and not info[1].startswith('*LOCKED*'): - cmd = [ - self.module.get_bin_path('pw', True), - 'lock', - self.name - ] - if self.uid is not None and info[2] != int(self.uid): - cmd.append('-u') - cmd.append(self.uid) - return self.execute_command(cmd) - elif self.password_lock is False and info[1].startswith('*LOCKED*'): - cmd = [ - self.module.get_bin_path('pw', True), - 'unlock', - self.name - ] - if self.uid is not None and info[2] != int(self.uid): - cmd.append('-u') - cmd.append(self.uid) - return self.execute_command(cmd) + _rc, _out, _err = self._handle_lock() + if rc is None: + rc = _rc + out += _out + err += _err + return (rc, out, err) diff --git a/test/integration/targets/user/tasks/main.yml b/test/integration/targets/user/tasks/main.yml index 19b12742f14..3b8ff377d91 100644 --- a/test/integration/targets/user/tasks/main.yml +++ b/test/integration/targets/user/tasks/main.yml @@ -17,1120 +17,18 @@ # along with Ansible. If not, see . # -## user add - -- name: remove the test user - user: - name: ansibulluser - state: absent - -- name: try to create a user - user: - name: ansibulluser - state: present - register: user_test0_0 - -- name: create the user again - user: - name: ansibulluser - state: present - register: user_test0_1 - -- debug: - var: user_test0 - verbosity: 2 - -- name: make a list of users - script: userlist.sh {{ ansible_facts.distribution }} - register: user_names - -- debug: - var: user_names - verbosity: 2 - -- name: validate results for testcase 0 - assert: - that: - - user_test0_0 is changed - - user_test0_1 is not changed - - '"ansibulluser" in user_names.stdout_lines' - -# create system user - -- name: remove user - user: - name: ansibulluser - state: absent - -- name: create system user - user: - name: ansibulluser - state: present - system: yes - -# test adding user with uid -# https://github.com/ansible/ansible/issues/62969 -- name: remove the test user - user: - name: ansibulluser - state: absent - -- name: try to create a user with uid - user: - name: ansibulluser - state: present - uid: 572 - register: user_test01_0 - -- name: create the user again - user: - name: ansibulluser - state: present - uid: 572 - register: user_test01_1 - -- name: validate results for testcase 0 - assert: - that: - - user_test01_0 is changed - - user_test01_1 is not changed - -# test user add with password -- name: add an encrypted password for user - user: - name: ansibulluser - password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." - state: present - update_password: always - register: test_user_encrypt0 - -- name: there should not be warnings - assert: - that: "'warnings' not in test_user_encrypt0" - -# https://github.com/ansible/ansible/issues/65711 -- name: Test updating password only on creation - user: - name: ansibulluser - password: '*' - update_password: on_create - register: test_user_update_password - -- name: Ensure password was not changed - assert: - that: - - test_user_update_password is not changed - -- name: Verify password hash for Linux - when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] - block: - - name: LINUX | Get shadow entry for ansibulluser - getent: - database: shadow - key: ansibulluser - - - name: LINUX | Ensure password hash was not removed - assert: - that: - - getent_shadow['ansibulluser'][1] != '*' - -- block: - - name: add an plaintext password for user - user: - name: ansibulluser - password: "plaintextpassword" - state: present - update_password: always - register: test_user_encrypt1 - - - name: there should be a warning complains that the password is plaintext - assert: - that: "'warnings' in test_user_encrypt1" - - - name: add an invalid hashed password - user: - name: ansibulluser - password: "$6$rounds=656000$tgK3gYTyRLUmhyv2$lAFrYUQwn7E6VsjPOwQwoSx30lmpiU9r/E0Al7tzKrR9mkodcMEZGe9OXD0H/clOn6qdsUnaL4zefy5fG+++++" - state: present - update_password: always - register: test_user_encrypt2 - - - name: there should be a warning complains about the character set of password - assert: - that: "'warnings' in test_user_encrypt2" - - - name: change password to '!' - user: - name: ansibulluser - password: '!' - register: test_user_encrypt3 - - - name: change password to '*' - user: - name: ansibulluser - password: '*' - register: test_user_encrypt4 - - - name: change password to '*************' - user: - name: ansibulluser - password: '*************' - register: test_user_encrypt5 - - - name: there should be no warnings when setting the password to '!', '*' or '*************' - assert: - that: - - "'warnings' not in test_user_encrypt3" - - "'warnings' not in test_user_encrypt4" - - "'warnings' not in test_user_encrypt5" - when: ansible_facts.system != 'Darwin' - - -# https://github.com/ansible/ansible/issues/42484 -# Skipping macOS for now since there is a bug when changing home directory -- block: - - name: create user specifying home - user: - name: ansibulluser - state: present - home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser" - register: user_test3_0 - - - name: create user again specifying home - user: - name: ansibulluser - state: present - home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser" - register: user_test3_1 - - - name: change user home - user: - name: ansibulluser - state: present - home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser-mod" - register: user_test3_2 - - - name: change user home back - user: - name: ansibulluser - state: present - home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser" - register: user_test3_3 - - - name: validate results for testcase 3 - assert: - that: - - user_test3_0 is not changed - - user_test3_1 is not changed - - user_test3_2 is changed - - user_test3_3 is changed - when: ansible_facts.system != 'Darwin' - -# https://github.com/ansible/ansible/issues/41393 -# Create a new user account with a path that has parent directories that do not exist -- name: Create user with home path that has parents that do not exist - user: - name: ansibulluser2 - state: present - home: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2" - register: create_home_with_no_parent_1 - -- name: Create user with home path that has parents that do not exist again - user: - name: ansibulluser2 - state: present - home: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2" - register: create_home_with_no_parent_2 - -- name: Check the created home directory - stat: - path: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2" - register: home_with_no_parent_3 - -- name: Ensure user with non-existing parent paths was created successfully - assert: - that: - - create_home_with_no_parent_1 is changed - - create_home_with_no_parent_1.home == user_home_prefix[ansible_facts.system] ~ '/in2deep/ansibulluser2' - - create_home_with_no_parent_2 is not changed - - home_with_no_parent_3.stat.uid == create_home_with_no_parent_1.uid - - home_with_no_parent_3.stat.gr_name == default_user_group[ansible_facts.distribution] | default('ansibulluser2') - -- name: Cleanup test account - user: - name: ansibulluser2 - home: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2" - state: absent - remove: yes - -- name: Remove testing dir - file: - path: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/" - state: absent - - -# https://github.com/ansible/ansible/issues/60307 -# Make sure we can create a user when the home directory is missing -- name: Create user with home path that does not exist - user: - name: ansibulluser3 - state: present - home: "{{ user_home_prefix[ansible_facts.system] }}/nosuchdir" - createhome: no - -- name: Cleanup test account - user: - name: ansibulluser3 - state: absent - remove: yes - -# https://github.com/ansible/ansible/issues/70589 -# Create user with create_home: no and parent directory does not exist. -- name: "Check if parent dir for home dir for user exists (before)" - stat: - path: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir" - register: create_user_no_create_home_with_no_parent_parent_dir_before - -- name: "Create user with create_home == no and home path parent dir does not exist" - user: - name: randomuser - state: present - create_home: false - home: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir/randomuser" - register: create_user_no_create_home_with_no_parent - -- name: "Check if parent dir for home dir for user exists (after)" - stat: - path: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir" - register: create_user_no_create_home_with_no_parent_parent_dir_after - -- name: "Check if home for user is created" - stat: - path: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir/randomuser" - register: create_user_no_create_home_with_no_parent_home_dir - -- name: "Ensure user with non-existing parent paths with create_home: no was created successfully" - assert: - that: - - not create_user_no_create_home_with_no_parent_parent_dir_before.stat.exists - - not create_user_no_create_home_with_no_parent_parent_dir_after.stat.isdir is defined - - not create_user_no_create_home_with_no_parent_home_dir.stat.exists - -- name: Cleanup test account - user: - name: randomuser - state: absent - remove: yes - -## user check - -- name: run existing user check tests - user: - name: "{{ user_names.stdout_lines | random }}" - state: present - create_home: no - loop: "{{ range(1, 5+1) | list }}" - register: user_test1 - -- debug: - var: user_test1 - verbosity: 2 - -- name: validate results for testcase 1 - assert: - that: - - user_test1.results is defined - - user_test1.results | length == 5 - -- name: validate changed results for testcase 1 - assert: - that: - - "user_test1.results[0] is not changed" - - "user_test1.results[1] is not changed" - - "user_test1.results[2] is not changed" - - "user_test1.results[3] is not changed" - - "user_test1.results[4] is not changed" - - "user_test1.results[0]['state'] == 'present'" - - "user_test1.results[1]['state'] == 'present'" - - "user_test1.results[2]['state'] == 'present'" - - "user_test1.results[3]['state'] == 'present'" - - "user_test1.results[4]['state'] == 'present'" - - -## user remove - -- name: try to delete the user - user: - name: ansibulluser - state: absent - force: true - register: user_test2 - -- name: make a new list of users - script: userlist.sh {{ ansible_facts.distribution }} - register: user_names2 - -- debug: - var: user_names2 - verbosity: 2 - -- name: validate results for testcase 2 - assert: - that: - - '"ansibulluser" not in user_names2.stdout_lines' - - -## create user without home and test fallback home dir create - -- block: - - name: create the user - user: - name: ansibulluser - - - name: delete the user and home dir - user: - name: ansibulluser - state: absent - force: true - remove: true - - - name: create the user without home - user: - name: ansibulluser - create_home: no - - - name: create the user home dir - user: - name: ansibulluser - register: user_create_home_fallback - - - name: stat home dir - stat: - path: '{{ user_create_home_fallback.home }}' - register: user_create_home_fallback_dir - - - name: read UMASK from /etc/login.defs and return mode - shell: | - import re - import os - try: - for line in open('/etc/login.defs').readlines(): - m = re.match(r'^UMASK\s+(\d+)$', line) - if m: - umask = int(m.group(1), 8) - except: - umask = os.umask(0) - mode = oct(0o777 & ~umask) - print(str(mode).replace('o', '')) - args: - executable: "{{ ansible_python_interpreter }}" - register: user_login_defs_umask - - - name: validate that user home dir is created - assert: - that: - - user_create_home_fallback is changed - - user_create_home_fallback_dir.stat.exists - - user_create_home_fallback_dir.stat.isdir - - user_create_home_fallback_dir.stat.pw_name == 'ansibulluser' - - user_create_home_fallback_dir.stat.mode == user_login_defs_umask.stdout - when: ansible_facts.system != 'Darwin' - -- block: - - name: create non-system user on macOS to test the shell is set to /bin/bash - user: - name: macosuser - register: macosuser_output - - - name: validate the shell is set to /bin/bash - assert: - that: - - 'macosuser_output.shell == "/bin/bash"' - - - name: cleanup - user: - name: macosuser - state: absent - - - name: create system user on macos to test the shell is set to /usr/bin/false - user: - name: macosuser - system: yes - register: macosuser_output - - - name: validate the shell is set to /usr/bin/false - assert: - that: - - 'macosuser_output.shell == "/usr/bin/false"' - - - name: cleanup - user: - name: macosuser - state: absent - - - name: create non-system user on macos and set the shell to /bin/sh - user: - name: macosuser - shell: /bin/sh - register: macosuser_output - - - name: validate the shell is set to /bin/sh - assert: - that: - - 'macosuser_output.shell == "/bin/sh"' - - - name: cleanup - user: - name: macosuser - state: absent - when: ansible_facts.distribution == "MacOSX" - - -## user expires -# Date is March 3, 2050 -- name: Set user expiration - user: - name: ansibulluser - state: present - expires: 2529881062 - register: user_test_expires1 - tags: - - timezone - -- name: Set user expiration again to ensure no change is made - user: - name: ansibulluser - state: present - expires: 2529881062 - register: user_test_expires2 - tags: - - timezone - -- name: Ensure that account with expiration was created and did not change on subsequent run - assert: - that: - - user_test_expires1 is changed - - user_test_expires2 is not changed - -- name: Verify expiration date for Linux - block: - - name: LINUX | Get expiration date for ansibulluser - getent: - database: shadow - key: ansibulluser - - - name: LINUX | Ensure proper expiration date was set - assert: - that: - - getent_shadow['ansibulluser'][6] == '29281' - when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] - - -- name: Verify expiration date for BSD - block: - - name: BSD | Get expiration date for ansibulluser - shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7' - changed_when: no - register: bsd_account_expiration - - - name: BSD | Ensure proper expiration date was set - assert: - that: - - bsd_account_expiration.stdout == '2529881062' - when: ansible_facts.os_family == 'FreeBSD' - -- name: Change timezone - timezone: - name: America/Denver - register: original_timezone - tags: - - timezone - -- name: Change system timezone to make sure expiration comparison works properly - block: - - name: Create user with expiration again to ensure no change is made in a new timezone - user: - name: ansibulluser - state: present - expires: 2529881062 - register: user_test_different_tz - tags: - - timezone - - - name: Ensure that no change was reported - assert: - that: - - user_test_different_tz is not changed - tags: - - timezone - - always: - - name: Restore original timezone - {{ original_timezone.diff.before.name }} - timezone: - name: "{{ original_timezone.diff.before.name }}" - when: original_timezone.diff.before.name != "n/a" - tags: - - timezone - - - name: Restore original timezone when n/a - file: - path: /etc/sysconfig/clock - state: absent - when: - - original_timezone.diff.before.name == "n/a" - - "'/etc/sysconfig/clock' in original_timezone.msg" - tags: - - timezone - - -- name: Unexpire user - user: - name: ansibulluser - state: present - expires: -1 - register: user_test_expires3 - -- name: Verify un expiration date for Linux - block: - - name: LINUX | Get expiration date for ansibulluser - getent: - database: shadow - key: ansibulluser - - - name: LINUX | Ensure proper expiration date was set - assert: - msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}" - that: - - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0 - when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] - -- name: Verify un expiration date for Linux/BSD - block: - - name: Unexpire user again to check for change - user: - name: ansibulluser - state: present - expires: -1 - register: user_test_expires4 - - - name: Ensure first expiration reported a change and second did not - assert: - msg: The second run of the expiration removal task reported a change when it should not - that: - - user_test_expires3 is changed - - user_test_expires4 is not changed - when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse', 'FreeBSD'] - -- name: Verify un expiration date for BSD - block: - - name: BSD | Get expiration date for ansibulluser - shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7' - changed_when: no - register: bsd_account_expiration - - - name: BSD | Ensure proper expiration date was set - assert: - msg: "expiry is supposed to be '0', not {{ bsd_account_expiration.stdout }}" - that: - - bsd_account_expiration.stdout == '0' - when: ansible_facts.os_family == 'FreeBSD' - -# Test setting no expiration when creating a new account -# https://github.com/ansible/ansible/issues/44155 -- name: Remove ansibulluser - user: - name: ansibulluser - state: absent - -- name: Create user account without expiration - user: - name: ansibulluser - state: present - expires: -1 - register: user_test_create_no_expires_1 - -- name: Create user account without expiration again - user: - name: ansibulluser - state: present - expires: -1 - register: user_test_create_no_expires_2 - -- name: Ensure changes were made appropriately - assert: - msg: Setting 'expires='-1 resulted in incorrect changes - that: - - user_test_create_no_expires_1 is changed - - user_test_create_no_expires_2 is not changed - -- name: Verify un expiration date for Linux - block: - - name: LINUX | Get expiration date for ansibulluser - getent: - database: shadow - key: ansibulluser - - - name: LINUX | Ensure proper expiration date was set - assert: - msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}" - that: - - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0 - when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] - -- name: Verify un expiration date for BSD - block: - - name: BSD | Get expiration date for ansibulluser - shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7' - changed_when: no - register: bsd_account_expiration - - - name: BSD | Ensure proper expiration date was set - assert: - msg: "expiry is supposed to be '0', not {{ bsd_account_expiration.stdout }}" - that: - - bsd_account_expiration.stdout == '0' - when: ansible_facts.os_family == 'FreeBSD' - -# Test setting epoch 0 expiration when creating a new account, then removing the expiry -# https://github.com/ansible/ansible/issues/47114 -- name: Remove ansibulluser - user: - name: ansibulluser - state: absent - -- name: Create user account with epoch 0 expiration - user: - name: ansibulluser - state: present - expires: 0 - register: user_test_expires_create0_1 - -- name: Create user account with epoch 0 expiration again - user: - name: ansibulluser - state: present - expires: 0 - register: user_test_expires_create0_2 - -- name: Change the user account to remove the expiry time - user: - name: ansibulluser - expires: -1 - register: user_test_remove_expires_1 - -- name: Change the user account to remove the expiry time again - user: - name: ansibulluser - expires: -1 - register: user_test_remove_expires_2 - - -- name: Verify un expiration date for Linux - block: - - name: LINUX | Ensure changes were made appropriately - assert: - msg: Creating an account with 'expries=0' then removing that expriation with 'expires=-1' resulted in incorrect changes - that: - - user_test_expires_create0_1 is changed - - user_test_expires_create0_2 is not changed - - user_test_remove_expires_1 is changed - - user_test_remove_expires_2 is not changed - - - name: LINUX | Get expiration date for ansibulluser - getent: - database: shadow - key: ansibulluser - - - name: LINUX | Ensure proper expiration date was set - assert: - msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}" - that: - - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0 - when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] - - -- name: Verify proper expiration behavior for BSD - block: - - name: BSD | Ensure changes were made appropriately - assert: - msg: Creating an account with 'expries=0' then removing that expriation with 'expires=-1' resulted in incorrect changes - that: - - user_test_expires_create0_1 is changed - - user_test_expires_create0_2 is not changed - - user_test_remove_expires_1 is not changed - - user_test_remove_expires_2 is not changed - when: ansible_facts.os_family == 'FreeBSD' - -# Test expiration with a very large negative number. This should have the same -# result as setting -1. -- name: Set expiration date using very long negative number - user: - name: ansibulluser - state: present - expires: -2529881062 - register: user_test_expires5 - -- name: Ensure no change was made - assert: - that: - - user_test_expires5 is not changed - -- name: Verify un expiration date for Linux - block: - - name: LINUX | Get expiration date for ansibulluser - getent: - database: shadow - key: ansibulluser - - - name: LINUX | Ensure proper expiration date was set - assert: - msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}" - that: - - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0 - when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] - -- name: Verify un expiration date for BSD - block: - - name: BSD | Get expiration date for ansibulluser - shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7' - changed_when: no - register: bsd_account_expiration - - - name: BSD | Ensure proper expiration date was set - assert: - msg: "expiry is supposed to be '0', not {{ bsd_account_expiration.stdout }}" - that: - - bsd_account_expiration.stdout == '0' - when: ansible_facts.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_facts.os_family == 'Solaris' - - -# Test creating ssh key with passphrase -- name: Remove ansibulluser - user: - name: ansibulluser - state: absent - -- name: Create user with ssh key - user: - name: ansibulluser - state: present - generate_ssh_key: yes - force: yes - ssh_key_file: "{{ output_dir }}/test_id_rsa" - ssh_key_passphrase: secret_passphrase - -- name: Unlock ssh key - command: "ssh-keygen -y -f {{ output_dir }}/test_id_rsa -P secret_passphrase" - register: result - -- name: Check that ssh key was unlocked successfully - assert: - that: - - result.rc == 0 - -- name: Clean ssh key - file: - path: "{{ output_dir }}/test_id_rsa" - state: absent - when: ansible_os_family == 'FreeBSD' - - -## password lock -- block: - - name: Set password for ansibulluser - user: - name: ansibulluser - password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." - - - name: Lock account - user: - name: ansibulluser - password_lock: yes - register: password_lock_1 - - - name: Lock account again - user: - name: ansibulluser - password_lock: yes - register: password_lock_2 - - - name: Unlock account - user: - name: ansibulluser - password_lock: no - register: password_lock_3 - - - name: Unlock account again - user: - name: ansibulluser - password_lock: no - register: password_lock_4 - - - name: Ensure task reported changes appropriately - assert: - msg: The password_lock tasks did not make changes appropriately - that: - - password_lock_1 is changed - - password_lock_2 is not changed - - password_lock_3 is changed - - password_lock_4 is not changed - - - name: Lock account - user: - name: ansibulluser - password_lock: yes - - - name: Verify account lock for BSD - block: - - name: BSD | Get account status - shell: "{{ status_command[ansible_facts['system']] }}" - register: account_status_locked - - - name: Unlock account - user: - name: ansibulluser - password_lock: no - - - name: BSD | Get account status - shell: "{{ status_command[ansible_facts['system']] }}" - register: account_status_unlocked - - - name: FreeBSD | Ensure account is locked - assert: - that: - - "'LOCKED' in account_status_locked.stdout" - - "'LOCKED' not in account_status_unlocked.stdout" - when: ansible_facts['system'] == 'FreeBSD' - - when: ansible_facts['system'] in ['FreeBSD', 'OpenBSD'] - - - name: Verify account lock for Linux - block: - - name: LINUX | Get account status - getent: - database: shadow - key: ansibulluser - - - name: LINUX | Ensure account is locked - assert: - that: - - getent_shadow['ansibulluser'][0].startswith('!') - - - name: Unlock account - user: - name: ansibulluser - password_lock: no - - - name: LINUX | Get account status - getent: - database: shadow - key: ansibulluser - - - name: LINUX | Ensure account is unlocked - assert: - that: - - not getent_shadow['ansibulluser'][0].startswith('!') - - when: ansible_facts['system'] == 'Linux' - - always: - - name: Unlock account - user: - name: ansibulluser - password_lock: no - - when: ansible_facts['system'] in ['FreeBSD', 'OpenBSD', 'Linux'] - - - ## Check local mode - # Even if we don't have a system that is bound to a directory, it's useful - # to run with local: true to exercise the code path that reads through the local - # user database file. - # https://github.com/ansible/ansible/issues/50947 - -- name: Create /etc/gshadow - file: - path: /etc/gshadow - state: touch - when: ansible_facts.os_family == 'Suse' - tags: - - user_test_local_mode - -- name: Create /etc/libuser.conf - file: - path: /etc/libuser.conf - state: touch - when: - - ansible_facts.distribution == 'Ubuntu' - - ansible_facts.distribution_major_version is version_compare('16', '==') - tags: - - user_test_local_mode - -- name: Ensure luseradd is present - action: "{{ ansible_facts.pkg_mgr }}" - args: - name: libuser - state: present - when: ansible_facts.system in ['Linux'] - tags: - - user_test_local_mode - -- name: Create local account that already exists to check for warning - user: - name: root - local: yes - register: local_existing - tags: - - user_test_local_mode - -- name: Create local_ansibulluser - user: - name: local_ansibulluser - state: present - local: yes - register: local_user_test_1 - tags: - - user_test_local_mode - -- name: Create local_ansibulluser again - user: - name: local_ansibulluser - state: present - local: yes - register: local_user_test_2 - tags: - - user_test_local_mode - -- name: Remove local_ansibulluser - user: - name: local_ansibulluser - state: absent - remove: yes - local: yes - register: local_user_test_remove_1 - tags: - - user_test_local_mode - -- name: Remove local_ansibulluser again - user: - name: local_ansibulluser - state: absent - remove: yes - local: yes - register: local_user_test_remove_2 - tags: - - user_test_local_mode - -- name: Create test groups - group: - name: "{{ item }}" - loop: - - testgroup1 - - testgroup2 - - testgroup3 - - testgroup4 - tags: - - user_test_local_mode - -- name: Create local_ansibulluser with groups - user: - name: local_ansibulluser - state: present - local: yes - groups: ['testgroup1', 'testgroup2'] - register: local_user_test_3 - ignore_errors: yes - tags: - - user_test_local_mode - -- name: Append groups for local_ansibulluser - user: - name: local_ansibulluser - state: present - local: yes - groups: ['testgroup3', 'testgroup4'] - append: yes - register: local_user_test_4 - ignore_errors: yes - tags: - - user_test_local_mode - -- name: Test append without groups for local_ansibulluser - user: - name: local_ansibulluser - state: present - append: yes - register: local_user_test_5 - ignore_errors: yes - tags: - - user_test_local_mode - -- name: Remove local_ansibulluser again - user: - name: local_ansibulluser - state: absent - remove: yes - local: yes - tags: - - user_test_local_mode - -- name: Remove test groups - group: - name: "{{ item }}" - state: absent - loop: - - testgroup1 - - testgroup2 - - testgroup3 - - testgroup4 - tags: - - user_test_local_mode - -- name: Ensure local user accounts were created and removed properly - assert: - that: - - local_user_test_1 is changed - - local_user_test_2 is not changed - - local_user_test_3 is changed - - local_user_test_4 is changed - - local_user_test_remove_1 is changed - - local_user_test_remove_2 is not changed - tags: - - user_test_local_mode - -- name: Ensure warnings were displayed properly - assert: - that: - - local_user_test_1['warnings'] | length > 0 - - local_user_test_1['warnings'] | first is search('The local user account may already exist') - - local_user_test_5['warnings'] is search("'append' is set, but no 'groups' are specified. Use 'groups'") - - local_existing['warnings'] is not defined - when: ansible_facts.system in ['Linux'] - tags: - - user_test_local_mode - -- name: Test expires for local users - import_tasks: expires_local.yml +- import_tasks: test_create_user.yml +- import_tasks: test_create_system_user.yml +- import_tasks: test_create_user_uid.yml +- import_tasks: test_create_user_password.yml +- import_tasks: test_create_user_home.yml +- import_tasks: test_remove_user.yml +- import_tasks: test_no_home_fallback.yml +- import_tasks: test_expires.yml +- import_tasks: test_expires_new_account.yml +- import_tasks: test_expires_new_account_epoch_negative.yml +- import_tasks: test_shadow_backup.yml +- import_tasks: test_ssh_key_passphrase.yml +- import_tasks: test_password_lock.yml +- import_tasks: test_password_lock_new_user.yml +- import_tasks: test_local.yml diff --git a/test/integration/targets/user/tasks/test_create_system_user.yml b/test/integration/targets/user/tasks/test_create_system_user.yml new file mode 100644 index 00000000000..da746c506d6 --- /dev/null +++ b/test/integration/targets/user/tasks/test_create_system_user.yml @@ -0,0 +1,12 @@ +# create system user + +- name: remove user + user: + name: ansibulluser + state: absent + +- name: create system user + user: + name: ansibulluser + state: present + system: yes diff --git a/test/integration/targets/user/tasks/test_create_user.yml b/test/integration/targets/user/tasks/test_create_user.yml new file mode 100644 index 00000000000..bced79056e5 --- /dev/null +++ b/test/integration/targets/user/tasks/test_create_user.yml @@ -0,0 +1,67 @@ +- name: remove the test user + user: + name: ansibulluser + state: absent + +- name: try to create a user + user: + name: ansibulluser + state: present + register: user_test0_0 + +- name: create the user again + user: + name: ansibulluser + state: present + register: user_test0_1 + +- debug: + var: user_test0 + verbosity: 2 + +- name: make a list of users + script: userlist.sh {{ ansible_facts.distribution }} + register: user_names + +- debug: + var: user_names + verbosity: 2 + +- name: validate results for testcase 0 + assert: + that: + - user_test0_0 is changed + - user_test0_1 is not changed + - '"ansibulluser" in user_names.stdout_lines' + +- name: run existing user check tests + user: + name: "{{ user_names.stdout_lines | random }}" + state: present + create_home: no + loop: "{{ range(1, 5+1) | list }}" + register: user_test1 + +- debug: + var: user_test1 + verbosity: 2 + +- name: validate results for testcase 1 + assert: + that: + - user_test1.results is defined + - user_test1.results | length == 5 + +- name: validate changed results for testcase 1 + assert: + that: + - "user_test1.results[0] is not changed" + - "user_test1.results[1] is not changed" + - "user_test1.results[2] is not changed" + - "user_test1.results[3] is not changed" + - "user_test1.results[4] is not changed" + - "user_test1.results[0]['state'] == 'present'" + - "user_test1.results[1]['state'] == 'present'" + - "user_test1.results[2]['state'] == 'present'" + - "user_test1.results[3]['state'] == 'present'" + - "user_test1.results[4]['state'] == 'present'" diff --git a/test/integration/targets/user/tasks/test_create_user_home.yml b/test/integration/targets/user/tasks/test_create_user_home.yml new file mode 100644 index 00000000000..1b529f76be3 --- /dev/null +++ b/test/integration/targets/user/tasks/test_create_user_home.yml @@ -0,0 +1,136 @@ +# https://github.com/ansible/ansible/issues/42484 +# Skipping macOS for now since there is a bug when changing home directory +- name: Test home directory creation + when: ansible_facts.system != 'Darwin' + block: + - name: create user specifying home + user: + name: ansibulluser + state: present + home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser" + register: user_test3_0 + + - name: create user again specifying home + user: + name: ansibulluser + state: present + home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser" + register: user_test3_1 + + - name: change user home + user: + name: ansibulluser + state: present + home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser-mod" + register: user_test3_2 + + - name: change user home back + user: + name: ansibulluser + state: present + home: "{{ user_home_prefix[ansible_facts.system] }}/ansibulluser" + register: user_test3_3 + + - name: validate results for testcase 3 + assert: + that: + - user_test3_0 is not changed + - user_test3_1 is not changed + - user_test3_2 is changed + - user_test3_3 is changed + +# https://github.com/ansible/ansible/issues/41393 +# Create a new user account with a path that has parent directories that do not exist +- name: Create user with home path that has parents that do not exist + user: + name: ansibulluser2 + state: present + home: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2" + register: create_home_with_no_parent_1 + +- name: Create user with home path that has parents that do not exist again + user: + name: ansibulluser2 + state: present + home: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2" + register: create_home_with_no_parent_2 + +- name: Check the created home directory + stat: + path: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2" + register: home_with_no_parent_3 + +- name: Ensure user with non-existing parent paths was created successfully + assert: + that: + - create_home_with_no_parent_1 is changed + - create_home_with_no_parent_1.home == user_home_prefix[ansible_facts.system] ~ '/in2deep/ansibulluser2' + - create_home_with_no_parent_2 is not changed + - home_with_no_parent_3.stat.uid == create_home_with_no_parent_1.uid + - home_with_no_parent_3.stat.gr_name == default_user_group[ansible_facts.distribution] | default('ansibulluser2') + +- name: Cleanup test account + user: + name: ansibulluser2 + home: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/ansibulluser2" + state: absent + remove: yes + +- name: Remove testing dir + file: + path: "{{ user_home_prefix[ansible_facts.system] }}/in2deep/" + state: absent + + +# https://github.com/ansible/ansible/issues/60307 +# Make sure we can create a user when the home directory is missing +- name: Create user with home path that does not exist + user: + name: ansibulluser3 + state: present + home: "{{ user_home_prefix[ansible_facts.system] }}/nosuchdir" + createhome: no + +- name: Cleanup test account + user: + name: ansibulluser3 + state: absent + remove: yes + +# https://github.com/ansible/ansible/issues/70589 +# Create user with create_home: no and parent directory does not exist. +- name: "Check if parent dir for home dir for user exists (before)" + stat: + path: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir" + register: create_user_no_create_home_with_no_parent_parent_dir_before + +- name: "Create user with create_home == no and home path parent dir does not exist" + user: + name: randomuser + state: present + create_home: false + home: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir/randomuser" + register: create_user_no_create_home_with_no_parent + +- name: "Check if parent dir for home dir for user exists (after)" + stat: + path: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir" + register: create_user_no_create_home_with_no_parent_parent_dir_after + +- name: "Check if home for user is created" + stat: + path: "{{ user_home_prefix[ansible_facts.system] }}/thereisnodir/randomuser" + register: create_user_no_create_home_with_no_parent_home_dir + +- name: "Ensure user with non-existing parent paths with create_home: no was created successfully" + assert: + that: + - not create_user_no_create_home_with_no_parent_parent_dir_before.stat.exists + - not create_user_no_create_home_with_no_parent_parent_dir_after.stat.isdir is defined + - not create_user_no_create_home_with_no_parent_home_dir.stat.exists + +- name: Cleanup test account + user: + name: randomuser + state: absent + remove: yes diff --git a/test/integration/targets/user/tasks/test_create_user_password.yml b/test/integration/targets/user/tasks/test_create_user_password.yml new file mode 100644 index 00000000000..02aae003999 --- /dev/null +++ b/test/integration/targets/user/tasks/test_create_user_password.yml @@ -0,0 +1,90 @@ +# test user add with password +- name: add an encrypted password for user + user: + name: ansibulluser + password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." + state: present + update_password: always + register: test_user_encrypt0 + +- name: there should not be warnings + assert: + that: "'warnings' not in test_user_encrypt0" + +# https://github.com/ansible/ansible/issues/65711 +- name: Test updating password only on creation + user: + name: ansibulluser + password: '*' + update_password: on_create + register: test_user_update_password + +- name: Ensure password was not changed + assert: + that: + - test_user_update_password is not changed + +- name: Verify password hash for Linux + when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] + block: + - name: LINUX | Get shadow entry for ansibulluser + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure password hash was not removed + assert: + that: + - getent_shadow['ansibulluser'][1] != '*' + +- name: Test plaintext warning + when: ansible_facts.system != 'Darwin' + block: + - name: add an plaintext password for user + user: + name: ansibulluser + password: "plaintextpassword" + state: present + update_password: always + register: test_user_encrypt1 + + - name: there should be a warning complains that the password is plaintext + assert: + that: "'warnings' in test_user_encrypt1" + + - name: add an invalid hashed password + user: + name: ansibulluser + password: "$6$rounds=656000$tgK3gYTyRLUmhyv2$lAFrYUQwn7E6VsjPOwQwoSx30lmpiU9r/E0Al7tzKrR9mkodcMEZGe9OXD0H/clOn6qdsUnaL4zefy5fG+++++" + state: present + update_password: always + register: test_user_encrypt2 + + - name: there should be a warning complains about the character set of password + assert: + that: "'warnings' in test_user_encrypt2" + + - name: change password to '!' + user: + name: ansibulluser + password: '!' + register: test_user_encrypt3 + + - name: change password to '*' + user: + name: ansibulluser + password: '*' + register: test_user_encrypt4 + + - name: change password to '*************' + user: + name: ansibulluser + password: '*************' + register: test_user_encrypt5 + + - name: there should be no warnings when setting the password to '!', '*' or '*************' + assert: + that: + - "'warnings' not in test_user_encrypt3" + - "'warnings' not in test_user_encrypt4" + - "'warnings' not in test_user_encrypt5" diff --git a/test/integration/targets/user/tasks/test_create_user_uid.yml b/test/integration/targets/user/tasks/test_create_user_uid.yml new file mode 100644 index 00000000000..9ac8a96fa79 --- /dev/null +++ b/test/integration/targets/user/tasks/test_create_user_uid.yml @@ -0,0 +1,26 @@ +# test adding user with uid +# https://github.com/ansible/ansible/issues/62969 +- name: remove the test user + user: + name: ansibulluser + state: absent + +- name: try to create a user with uid + user: + name: ansibulluser + state: present + uid: 572 + register: user_test01_0 + +- name: create the user again + user: + name: ansibulluser + state: present + uid: 572 + register: user_test01_1 + +- name: validate results for testcase 0 + assert: + that: + - user_test01_0 is changed + - user_test01_1 is not changed diff --git a/test/integration/targets/user/tasks/test_expires.yml b/test/integration/targets/user/tasks/test_expires.yml new file mode 100644 index 00000000000..8c238934b04 --- /dev/null +++ b/test/integration/targets/user/tasks/test_expires.yml @@ -0,0 +1,147 @@ +# Date is March 3, 2050 +- name: Set user expiration + user: + name: ansibulluser + state: present + expires: 2529881062 + register: user_test_expires1 + tags: + - timezone + +- name: Set user expiration again to ensure no change is made + user: + name: ansibulluser + state: present + expires: 2529881062 + register: user_test_expires2 + tags: + - timezone + +- name: Ensure that account with expiration was created and did not change on subsequent run + assert: + that: + - user_test_expires1 is changed + - user_test_expires2 is not changed + +- name: Verify expiration date for Linux + block: + - name: LINUX | Get expiration date for ansibulluser + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure proper expiration date was set + assert: + that: + - getent_shadow['ansibulluser'][6] == '29281' + when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] + + +- name: Verify expiration date for BSD + block: + - name: BSD | Get expiration date for ansibulluser + shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7' + changed_when: no + register: bsd_account_expiration + + - name: BSD | Ensure proper expiration date was set + assert: + that: + - bsd_account_expiration.stdout == '2529881062' + when: ansible_facts.os_family == 'FreeBSD' + +- name: Change timezone + timezone: + name: America/Denver + register: original_timezone + tags: + - timezone + +- name: Change system timezone to make sure expiration comparison works properly + block: + - name: Create user with expiration again to ensure no change is made in a new timezone + user: + name: ansibulluser + state: present + expires: 2529881062 + register: user_test_different_tz + tags: + - timezone + + - name: Ensure that no change was reported + assert: + that: + - user_test_different_tz is not changed + tags: + - timezone + + always: + - name: Restore original timezone - {{ original_timezone.diff.before.name }} + timezone: + name: "{{ original_timezone.diff.before.name }}" + when: original_timezone.diff.before.name != "n/a" + tags: + - timezone + + - name: Restore original timezone when n/a + file: + path: /etc/sysconfig/clock + state: absent + when: + - original_timezone.diff.before.name == "n/a" + - "'/etc/sysconfig/clock' in original_timezone.msg" + tags: + - timezone + + +- name: Unexpire user + user: + name: ansibulluser + state: present + expires: -1 + register: user_test_expires3 + +- name: Verify un expiration date for Linux + block: + - name: LINUX | Get expiration date for ansibulluser + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure proper expiration date was set + assert: + msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}" + that: + - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0 + when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] + +- name: Verify un expiration date for Linux/BSD + block: + - name: Unexpire user again to check for change + user: + name: ansibulluser + state: present + expires: -1 + register: user_test_expires4 + + - name: Ensure first expiration reported a change and second did not + assert: + msg: The second run of the expiration removal task reported a change when it should not + that: + - user_test_expires3 is changed + - user_test_expires4 is not changed + when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse', 'FreeBSD'] + +- name: Verify un expiration date for BSD + block: + - name: BSD | Get expiration date for ansibulluser + shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7' + changed_when: no + register: bsd_account_expiration + + - name: BSD | Ensure proper expiration date was set + assert: + msg: "expiry is supposed to be '0', not {{ bsd_account_expiration.stdout }}" + that: + - bsd_account_expiration.stdout == '0' + when: ansible_facts.os_family == 'FreeBSD' diff --git a/test/integration/targets/user/tasks/test_expires_new_account.yml b/test/integration/targets/user/tasks/test_expires_new_account.yml new file mode 100644 index 00000000000..b77d137f82a --- /dev/null +++ b/test/integration/targets/user/tasks/test_expires_new_account.yml @@ -0,0 +1,55 @@ +# Test setting no expiration when creating a new account +# https://github.com/ansible/ansible/issues/44155 +- name: Remove ansibulluser + user: + name: ansibulluser + state: absent + +- name: Create user account without expiration + user: + name: ansibulluser + state: present + expires: -1 + register: user_test_create_no_expires_1 + +- name: Create user account without expiration again + user: + name: ansibulluser + state: present + expires: -1 + register: user_test_create_no_expires_2 + +- name: Ensure changes were made appropriately + assert: + msg: Setting 'expires='-1 resulted in incorrect changes + that: + - user_test_create_no_expires_1 is changed + - user_test_create_no_expires_2 is not changed + +- name: Verify un expiration date for Linux + block: + - name: LINUX | Get expiration date for ansibulluser + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure proper expiration date was set + assert: + msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}" + that: + - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0 + when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] + +- name: Verify un expiration date for BSD + block: + - name: BSD | Get expiration date for ansibulluser + shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7' + changed_when: no + register: bsd_account_expiration + + - name: BSD | Ensure proper expiration date was set + assert: + msg: "expiry is supposed to be '0', not {{ bsd_account_expiration.stdout }}" + that: + - bsd_account_expiration.stdout == '0' + when: ansible_facts.os_family == 'FreeBSD' diff --git a/test/integration/targets/user/tasks/test_expires_new_account_epoch_negative.yml b/test/integration/targets/user/tasks/test_expires_new_account_epoch_negative.yml new file mode 100644 index 00000000000..77a07c4a3c2 --- /dev/null +++ b/test/integration/targets/user/tasks/test_expires_new_account_epoch_negative.yml @@ -0,0 +1,112 @@ +# Test setting epoch 0 expiration when creating a new account, then removing the expiry +# https://github.com/ansible/ansible/issues/47114 +- name: Remove ansibulluser + user: + name: ansibulluser + state: absent + +- name: Create user account with epoch 0 expiration + user: + name: ansibulluser + state: present + expires: 0 + register: user_test_expires_create0_1 + +- name: Create user account with epoch 0 expiration again + user: + name: ansibulluser + state: present + expires: 0 + register: user_test_expires_create0_2 + +- name: Change the user account to remove the expiry time + user: + name: ansibulluser + expires: -1 + register: user_test_remove_expires_1 + +- name: Change the user account to remove the expiry time again + user: + name: ansibulluser + expires: -1 + register: user_test_remove_expires_2 + + +- name: Verify un expiration date for Linux + block: + - name: LINUX | Ensure changes were made appropriately + assert: + msg: Creating an account with 'expries=0' then removing that expriation with 'expires=-1' resulted in incorrect changes + that: + - user_test_expires_create0_1 is changed + - user_test_expires_create0_2 is not changed + - user_test_remove_expires_1 is changed + - user_test_remove_expires_2 is not changed + + - name: LINUX | Get expiration date for ansibulluser + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure proper expiration date was set + assert: + msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}" + that: + - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0 + when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] + + +- name: Verify proper expiration behavior for BSD + block: + - name: BSD | Ensure changes were made appropriately + assert: + msg: Creating an account with 'expries=0' then removing that expriation with 'expires=-1' resulted in incorrect changes + that: + - user_test_expires_create0_1 is changed + - user_test_expires_create0_2 is not changed + - user_test_remove_expires_1 is not changed + - user_test_remove_expires_2 is not changed + when: ansible_facts.os_family == 'FreeBSD' + + +# Test expiration with a very large negative number. This should have the same +# result as setting -1. +- name: Set expiration date using very long negative number + user: + name: ansibulluser + state: present + expires: -2529881062 + register: user_test_expires5 + +- name: Ensure no change was made + assert: + that: + - user_test_expires5 is not changed + +- name: Verify un expiration date for Linux + block: + - name: LINUX | Get expiration date for ansibulluser + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure proper expiration date was set + assert: + msg: "expiry is supposed to be empty or -1, not {{ getent_shadow['ansibulluser'][6] }}" + that: + - not getent_shadow['ansibulluser'][6] or getent_shadow['ansibulluser'][6] | int < 0 + when: ansible_facts.os_family in ['RedHat', 'Debian', 'Suse'] + +- name: Verify un expiration date for BSD + block: + - name: BSD | Get expiration date for ansibulluser + shell: 'grep ansibulluser /etc/master.passwd | cut -d: -f 7' + changed_when: no + register: bsd_account_expiration + + - name: BSD | Ensure proper expiration date was set + assert: + msg: "expiry is supposed to be '0', not {{ bsd_account_expiration.stdout }}" + that: + - bsd_account_expiration.stdout == '0' + when: ansible_facts.os_family == 'FreeBSD' diff --git a/test/integration/targets/user/tasks/test_local.yml b/test/integration/targets/user/tasks/test_local.yml new file mode 100644 index 00000000000..16c79c57812 --- /dev/null +++ b/test/integration/targets/user/tasks/test_local.yml @@ -0,0 +1,169 @@ +## Check local mode +# Even if we don't have a system that is bound to a directory, it's useful +# to run with local: true to exercise the code path that reads through the local +# user database file. +# https://github.com/ansible/ansible/issues/50947 + +- name: Create /etc/gshadow + file: + path: /etc/gshadow + state: touch + when: ansible_facts.os_family == 'Suse' + tags: + - user_test_local_mode + +- name: Create /etc/libuser.conf + file: + path: /etc/libuser.conf + state: touch + when: + - ansible_facts.distribution == 'Ubuntu' + - ansible_facts.distribution_major_version is version_compare('16', '==') + tags: + - user_test_local_mode + +- name: Ensure luseradd is present + action: "{{ ansible_facts.pkg_mgr }}" + args: + name: libuser + state: present + when: ansible_facts.system in ['Linux'] + tags: + - user_test_local_mode + +- name: Create local account that already exists to check for warning + user: + name: root + local: yes + register: local_existing + tags: + - user_test_local_mode + +- name: Create local_ansibulluser + user: + name: local_ansibulluser + state: present + local: yes + register: local_user_test_1 + tags: + - user_test_local_mode + +- name: Create local_ansibulluser again + user: + name: local_ansibulluser + state: present + local: yes + register: local_user_test_2 + tags: + - user_test_local_mode + +- name: Remove local_ansibulluser + user: + name: local_ansibulluser + state: absent + remove: yes + local: yes + register: local_user_test_remove_1 + tags: + - user_test_local_mode + +- name: Remove local_ansibulluser again + user: + name: local_ansibulluser + state: absent + remove: yes + local: yes + register: local_user_test_remove_2 + tags: + - user_test_local_mode + +- name: Create test groups + group: + name: "{{ item }}" + loop: + - testgroup1 + - testgroup2 + - testgroup3 + - testgroup4 + tags: + - user_test_local_mode + +- name: Create local_ansibulluser with groups + user: + name: local_ansibulluser + state: present + local: yes + groups: ['testgroup1', 'testgroup2'] + register: local_user_test_3 + ignore_errors: yes + tags: + - user_test_local_mode + +- name: Append groups for local_ansibulluser + user: + name: local_ansibulluser + state: present + local: yes + groups: ['testgroup3', 'testgroup4'] + append: yes + register: local_user_test_4 + ignore_errors: yes + tags: + - user_test_local_mode + +- name: Test append without groups for local_ansibulluser + user: + name: local_ansibulluser + state: present + append: yes + register: local_user_test_5 + ignore_errors: yes + tags: + - user_test_local_mode + +- name: Remove local_ansibulluser again + user: + name: local_ansibulluser + state: absent + remove: yes + local: yes + tags: + - user_test_local_mode + +- name: Remove test groups + group: + name: "{{ item }}" + state: absent + loop: + - testgroup1 + - testgroup2 + - testgroup3 + - testgroup4 + tags: + - user_test_local_mode + +- name: Ensure local user accounts were created and removed properly + assert: + that: + - local_user_test_1 is changed + - local_user_test_2 is not changed + - local_user_test_3 is changed + - local_user_test_4 is changed + - local_user_test_remove_1 is changed + - local_user_test_remove_2 is not changed + tags: + - user_test_local_mode + +- name: Ensure warnings were displayed properly + assert: + that: + - local_user_test_1['warnings'] | length > 0 + - local_user_test_1['warnings'] | first is search('The local user account may already exist') + - local_user_test_5['warnings'] is search("'append' is set, but no 'groups' are specified. Use 'groups'") + - local_existing['warnings'] is not defined + when: ansible_facts.system in ['Linux'] + tags: + - user_test_local_mode + +- name: Test expires for local users + import_tasks: test_local_expires.yml diff --git a/test/integration/targets/user/tasks/expires_local.yml b/test/integration/targets/user/tasks/test_local_expires.yml similarity index 100% rename from test/integration/targets/user/tasks/expires_local.yml rename to test/integration/targets/user/tasks/test_local_expires.yml diff --git a/test/integration/targets/user/tasks/test_no_home_fallback.yml b/test/integration/targets/user/tasks/test_no_home_fallback.yml new file mode 100644 index 00000000000..f7627fae1e3 --- /dev/null +++ b/test/integration/targets/user/tasks/test_no_home_fallback.yml @@ -0,0 +1,106 @@ +## create user without home and test fallback home dir create + +- name: Test home directory creation + when: ansible_facts.system != 'Darwin' + block: + - name: create the user + user: + name: ansibulluser + + - name: delete the user and home dir + user: + name: ansibulluser + state: absent + force: true + remove: true + + - name: create the user without home + user: + name: ansibulluser + create_home: no + + - name: create the user home dir + user: + name: ansibulluser + register: user_create_home_fallback + + - name: stat home dir + stat: + path: '{{ user_create_home_fallback.home }}' + register: user_create_home_fallback_dir + + - name: read UMASK from /etc/login.defs and return mode + shell: | + import re + import os + try: + for line in open('/etc/login.defs').readlines(): + m = re.match(r'^UMASK\s+(\d+)$', line) + if m: + umask = int(m.group(1), 8) + except: + umask = os.umask(0) + mode = oct(0o777 & ~umask) + print(str(mode).replace('o', '')) + args: + executable: "{{ ansible_python_interpreter }}" + register: user_login_defs_umask + + - name: validate that user home dir is created + assert: + that: + - user_create_home_fallback is changed + - user_create_home_fallback_dir.stat.exists + - user_create_home_fallback_dir.stat.isdir + - user_create_home_fallback_dir.stat.pw_name == 'ansibulluser' + - user_create_home_fallback_dir.stat.mode == user_login_defs_umask.stdout + +- name: Create non-system user + when: ansible_facts.distribution == "MacOSX" + block: + - name: create non-system user on macOS to test the shell is set to /bin/bash + user: + name: macosuser + register: macosuser_output + + - name: validate the shell is set to /bin/bash + assert: + that: + - 'macosuser_output.shell == "/bin/bash"' + + - name: cleanup + user: + name: macosuser + state: absent + + - name: create system user on macOS to test the shell is set to /usr/bin/false + user: + name: macosuser + system: yes + register: macosuser_output + + - name: validate the shell is set to /usr/bin/false + assert: + that: + - 'macosuser_output.shell == "/usr/bin/false"' + + - name: cleanup + user: + name: macosuser + state: absent + + - name: create non-system user on macos and set the shell to /bin/sh + user: + name: macosuser + shell: /bin/sh + register: macosuser_output + + - name: validate the shell is set to /bin/sh + assert: + that: + - 'macosuser_output.shell == "/bin/sh"' + + - name: cleanup + user: + name: macosuser + state: absent diff --git a/test/integration/targets/user/tasks/test_password_lock.yml b/test/integration/targets/user/tasks/test_password_lock.yml new file mode 100644 index 00000000000..dde374ee015 --- /dev/null +++ b/test/integration/targets/user/tasks/test_password_lock.yml @@ -0,0 +1,140 @@ +- name: Test password lock + when: ansible_facts.system in ['FreeBSD', 'OpenBSD', 'Linux'] + block: + - name: Remove ansibulluser + user: + name: ansibulluser + state: absent + remove: yes + + - name: Create ansibulluser with password + user: + name: ansibulluser + password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." + + - name: Lock account without password parameter + user: + name: ansibulluser + password_lock: yes + register: password_lock_1 + + - name: Lock account without password parameter again + user: + name: ansibulluser + password_lock: yes + register: password_lock_2 + + - name: Unlock account without password parameter + user: + name: ansibulluser + password_lock: no + register: password_lock_3 + + - name: Unlock account without password parameter again + user: + name: ansibulluser + password_lock: no + register: password_lock_4 + + - name: Lock account with password parameter + user: + name: ansibulluser + password_lock: yes + password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." + register: password_lock_5 + + - name: Lock account with password parameter again + user: + name: ansibulluser + password_lock: yes + password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." + register: password_lock_6 + + - name: Unlock account with password parameter + user: + name: ansibulluser + password_lock: no + password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." + register: password_lock_7 + + - name: Unlock account with password parameter again + user: + name: ansibulluser + password_lock: no + password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." + register: password_lock_8 + + - name: Ensure task reported changes appropriately + assert: + msg: The password_lock tasks did not make changes appropriately + that: + - password_lock_1 is changed + - password_lock_2 is not changed + - password_lock_3 is changed + - password_lock_4 is not changed + - password_lock_5 is changed + - password_lock_6 is not changed + - password_lock_7 is changed + - password_lock_8 is not changed + + - name: Lock account + user: + name: ansibulluser + password_lock: yes + + - name: Verify account lock for BSD + when: ansible_facts.system in ['FreeBSD', 'OpenBSD'] + block: + - name: BSD | Get account status + shell: "{{ status_command[ansible_facts['system']] }}" + register: account_status_locked + + - name: Unlock account + user: + name: ansibulluser + password_lock: no + + - name: BSD | Get account status + shell: "{{ status_command[ansible_facts['system']] }}" + register: account_status_unlocked + + - name: FreeBSD | Ensure account is locked + assert: + that: + - "'LOCKED' in account_status_locked.stdout" + - "'LOCKED' not in account_status_unlocked.stdout" + when: ansible_facts['system'] == 'FreeBSD' + + - name: Verify account lock for Linux + when: ansible_facts.system == 'Linux' + block: + - name: LINUX | Get account status + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure account is locked + assert: + that: + - getent_shadow['ansibulluser'][0].startswith('!') + + - name: Unlock account + user: + name: ansibulluser + password_lock: no + + - name: LINUX | Get account status + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure account is unlocked + assert: + that: + - not getent_shadow['ansibulluser'][0].startswith('!') + + always: + - name: Unlock account + user: + name: ansibulluser + password_lock: no diff --git a/test/integration/targets/user/tasks/test_password_lock_new_user.yml b/test/integration/targets/user/tasks/test_password_lock_new_user.yml new file mode 100644 index 00000000000..dd4f23dae95 --- /dev/null +++ b/test/integration/targets/user/tasks/test_password_lock_new_user.yml @@ -0,0 +1,63 @@ +- name: Test password lock + when: ansible_facts.system in ['FreeBSD', 'OpenBSD', 'Linux'] + block: + - name: Remove ansibulluser + user: + name: ansibulluser + state: absent + remove: yes + + - name: Create ansibulluser with password and locked + user: + name: ansibulluser + password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." + password_lock: yes + register: create_with_lock_1 + + - name: Create ansibulluser with password and locked again + user: + name: ansibulluser + password: "$6$rounds=656000$TT4O7jz2M57npccl$33LF6FcUMSW11qrESXL1HX0BS.bsiT6aenFLLiVpsQh6hDtI9pJh5iY7x8J7ePkN4fP8hmElidHXaeD51pbGS." + password_lock: yes + register: create_with_lock_2 + + - name: Ensure task reported changes appropriately + assert: + msg: The password_lock tasks did not make changes appropriately + that: + - create_with_lock_1 is changed + - create_with_lock_2 is not changed + + - name: Verify account lock for BSD + when: ansible_facts.system in ['FreeBSD', 'OpenBSD'] + block: + - name: BSD | Get account status + shell: "{{ status_command[ansible_facts['system']] }}" + register: account_status_locked + + - name: FreeBSD | Ensure account is locked + assert: + that: + - "'LOCKED' in account_status_locked.stdout" + when: ansible_facts.system == 'FreeBSD' + + + - name: Verify account lock for Linux + when: ansible_facts.system == 'Linux' + block: + - name: LINUX | Get account status + getent: + database: shadow + key: ansibulluser + + - name: LINUX | Ensure account is locked + assert: + that: + - getent_shadow['ansibulluser'][0].startswith('!') + + + always: + - name: Unlock account + user: + name: ansibulluser + password_lock: no diff --git a/test/integration/targets/user/tasks/test_remove_user.yml b/test/integration/targets/user/tasks/test_remove_user.yml new file mode 100644 index 00000000000..dea71cbf0cf --- /dev/null +++ b/test/integration/targets/user/tasks/test_remove_user.yml @@ -0,0 +1,19 @@ +- name: try to delete the user + user: + name: ansibulluser + state: absent + force: true + register: user_test2 + +- name: make a new list of users + script: userlist.sh {{ ansible_facts.distribution }} + register: user_names2 + +- debug: + var: user_names2 + verbosity: 2 + +- name: validate results for testcase 2 + assert: + that: + - '"ansibulluser" not in user_names2.stdout_lines' diff --git a/test/integration/targets/user/tasks/test_shadow_backup.yml b/test/integration/targets/user/tasks/test_shadow_backup.yml new file mode 100644 index 00000000000..2655fbf229a --- /dev/null +++ b/test/integration/targets/user/tasks/test_shadow_backup.yml @@ -0,0 +1,21 @@ +- name: Test shadow backup on Solaris + when: ansible_facts.os_family == 'Solaris' + 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 diff --git a/test/integration/targets/user/tasks/test_ssh_key_passphrase.yml b/test/integration/targets/user/tasks/test_ssh_key_passphrase.yml new file mode 100644 index 00000000000..bb0486da7a7 --- /dev/null +++ b/test/integration/targets/user/tasks/test_ssh_key_passphrase.yml @@ -0,0 +1,29 @@ +# Test creating ssh key with passphrase +- name: Remove ansibulluser + user: + name: ansibulluser + state: absent + +- name: Create user with ssh key + user: + name: ansibulluser + state: present + generate_ssh_key: yes + force: yes + ssh_key_file: "{{ output_dir }}/test_id_rsa" + ssh_key_passphrase: secret_passphrase + +- name: Unlock ssh key + command: "ssh-keygen -y -f {{ output_dir }}/test_id_rsa -P secret_passphrase" + register: result + +- name: Check that ssh key was unlocked successfully + assert: + that: + - result.rc == 0 + +- name: Clean ssh key + file: + path: "{{ output_dir }}/test_id_rsa" + state: absent + when: ansible_os_family == 'FreeBSD'