user - properly handle password and password lock when used together (#73016)

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.
pull/73183/head
Sam Doran 4 years ago committed by GitHub
parent cec5267cee
commit 264e08f21a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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)

@ -193,9 +193,10 @@ options:
version_added: "1.9" version_added: "1.9"
password_lock: password_lock:
description: description:
- Lock the password (usermod -L, pw lock, usermod -C). - Lock the password (C(usermod -L), C(usermod -U), C(pw lock)).
- BUT implementation differs on different platforms, this option does not always mean the user cannot login using other methods. - 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. Do not change the password in the same task. - 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. - Currently supported on Linux, FreeBSD, DragonFlyBSD, NetBSD, OpenBSD.
type: bool type: bool
version_added: "2.6" version_added: "2.6"
@ -660,7 +661,10 @@ class User(object):
if self.password is not None: if self.password is not None:
cmd.append('-p') 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 self.create_home:
if not self.local: if not self.local:
@ -846,9 +850,15 @@ class User(object):
# usermod will refuse to unlock a user with no password, module shows 'changed' regardless # usermod will refuse to unlock a user with no password, module shows 'changed' regardless
cmd.append('-U') 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('-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, '', '') (rc, out, err) = (None, '', '')
@ -1210,6 +1220,31 @@ class FreeBsdUser(User):
SHADOWFILE_EXPIRE_INDEX = 6 SHADOWFILE_EXPIRE_INDEX = 6
DATE_FORMAT = '%d-%b-%Y' 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): def remove_user(self):
cmd = [ cmd = [
self.module.get_bin_path('pw', True), self.module.get_bin_path('pw', True),
@ -1281,6 +1316,7 @@ class FreeBsdUser(User):
# system cannot be handled currently - should we error if its requested? # system cannot be handled currently - should we error if its requested?
# create the user # create the user
(rc, out, err) = self.execute_command(cmd) (rc, out, err) = self.execute_command(cmd)
if rc is not None and rc != 0: if rc is not None and rc != 0:
self.module.fail_json(name=self.name, msg=err, rc=rc) self.module.fail_json(name=self.name, msg=err, rc=rc)
@ -1292,7 +1328,18 @@ class FreeBsdUser(User):
self.password, self.password,
self.name 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) return (rc, out, err)
@ -1396,45 +1443,38 @@ class FreeBsdUser(User):
cmd.append('-e') cmd.append('-e')
cmd.append(str(calendar.timegm(self.expires))) cmd.append(str(calendar.timegm(self.expires)))
(rc, out, err) = (None, '', '')
# modify the user if cmd will do anything # modify the user if cmd will do anything
if cmd_len != len(cmd): 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: if rc is not None and rc != 0:
self.module.fail_json(name=self.name, msg=err, rc=rc) 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 # 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 = [ cmd = [
self.module.get_bin_path('chpass', True), self.module.get_bin_path('chpass', True),
'-p', '-p',
self.password, self.password,
self.name 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 # we have to lock/unlock the password in a distinct command
if self.password_lock and not info[1].startswith('*LOCKED*'): _rc, _out, _err = self._handle_lock()
cmd = [ if rc is None:
self.module.get_bin_path('pw', True), rc = _rc
'lock', out += _out
self.name err += _err
]
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 (rc, out, err) return (rc, out, err)

File diff suppressed because it is too large Load Diff

@ -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

@ -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'"

@ -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

@ -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"

@ -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

@ -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'

@ -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'

@ -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'

@ -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

@ -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

@ -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

@ -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

@ -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'

@ -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

@ -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'
Loading…
Cancel
Save