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'