|
|
|
@ -38,6 +38,7 @@ options:
|
|
|
|
non_unique:
|
|
|
|
non_unique:
|
|
|
|
description:
|
|
|
|
description:
|
|
|
|
- Optionally when used with the C(-u) option, this option allows to change the user ID to a non-unique value.
|
|
|
|
- Optionally when used with the C(-u) option, this option allows to change the user ID to a non-unique value.
|
|
|
|
|
|
|
|
- Not supported on distributions using BusyBox.
|
|
|
|
type: bool
|
|
|
|
type: bool
|
|
|
|
default: no
|
|
|
|
default: no
|
|
|
|
version_added: "1.1"
|
|
|
|
version_added: "1.1"
|
|
|
|
@ -148,6 +149,7 @@ options:
|
|
|
|
description:
|
|
|
|
description:
|
|
|
|
- Whether to generate a SSH key for the user in question.
|
|
|
|
- Whether to generate a SSH key for the user in question.
|
|
|
|
- This will B(not) overwrite an existing SSH key unless used with O(force=yes).
|
|
|
|
- This will B(not) overwrite an existing SSH key unless used with O(force=yes).
|
|
|
|
|
|
|
|
- Requires C(ssh-keygen) from OpenSSH.
|
|
|
|
type: bool
|
|
|
|
type: bool
|
|
|
|
default: no
|
|
|
|
default: no
|
|
|
|
version_added: "0.9"
|
|
|
|
version_added: "0.9"
|
|
|
|
@ -217,6 +219,7 @@ options:
|
|
|
|
- This will check C(/etc/passwd) for an existing account before invoking commands. If the local account database
|
|
|
|
- This will check C(/etc/passwd) for an existing account before invoking commands. If the local account database
|
|
|
|
exists somewhere other than C(/etc/passwd), this setting will not work properly.
|
|
|
|
exists somewhere other than C(/etc/passwd), this setting will not work properly.
|
|
|
|
- This requires that the above commands as well as C(/etc/passwd) must exist on the target host, otherwise it will be a fatal error.
|
|
|
|
- This requires that the above commands as well as C(/etc/passwd) must exist on the target host, otherwise it will be a fatal error.
|
|
|
|
|
|
|
|
- Not supported on distributions using BusyBox.
|
|
|
|
type: bool
|
|
|
|
type: bool
|
|
|
|
default: no
|
|
|
|
default: no
|
|
|
|
version_added: "2.4"
|
|
|
|
version_added: "2.4"
|
|
|
|
@ -311,6 +314,8 @@ notes:
|
|
|
|
C(/Library/Preferences/com.apple.loginwindow.plist).
|
|
|
|
C(/Library/Preferences/com.apple.loginwindow.plist).
|
|
|
|
- On FreeBSD, this module uses C(pw useradd) and C(chpass) to create, C(pw usermod) and C(chpass) to modify,
|
|
|
|
- On FreeBSD, this module uses C(pw useradd) and C(chpass) to create, C(pw usermod) and C(chpass) to modify,
|
|
|
|
C(pw userdel) remove, C(pw lock) to lock, and C(pw unlock) to unlock accounts.
|
|
|
|
C(pw userdel) remove, C(pw lock) to lock, and C(pw unlock) to unlock accounts.
|
|
|
|
|
|
|
|
- On distributions using BusyBox, this module uses C(adduser), C(chpasswd), C(deluser), and C(delgroup).
|
|
|
|
|
|
|
|
The C(/etc/passwd) file is modified directly by this module and is backed up before modification.
|
|
|
|
- On all other platforms, this module uses C(useradd) to create, C(usermod) to modify, and
|
|
|
|
- On all other platforms, this module uses C(useradd) to create, C(usermod) to modify, and
|
|
|
|
C(userdel) to remove accounts.
|
|
|
|
C(userdel) to remove accounts.
|
|
|
|
seealso:
|
|
|
|
seealso:
|
|
|
|
@ -501,6 +506,7 @@ import select
|
|
|
|
import shutil
|
|
|
|
import shutil
|
|
|
|
import socket
|
|
|
|
import socket
|
|
|
|
import subprocess
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
import tempfile
|
|
|
|
import time
|
|
|
|
import time
|
|
|
|
import math
|
|
|
|
import math
|
|
|
|
import typing as t
|
|
|
|
import typing as t
|
|
|
|
@ -541,6 +547,7 @@ except AttributeError:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_HASH_RE = re.compile(r'[^a-zA-Z0-9./=]')
|
|
|
|
_HASH_RE = re.compile(r'[^a-zA-Z0-9./=]')
|
|
|
|
|
|
|
|
LOCK_INDICATOR = '!'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def getspnam(b_name):
|
|
|
|
def getspnam(b_name):
|
|
|
|
@ -3114,6 +3121,35 @@ class BusyBox(User):
|
|
|
|
- remove_user()
|
|
|
|
- remove_user()
|
|
|
|
- modify_user()
|
|
|
|
- modify_user()
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
def _build_password_string(self, current_password=None):
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Build the appropriate password string based on the current password and
|
|
|
|
|
|
|
|
module parameters.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This method will return '*' at a minimum to avoid creating an enabled
|
|
|
|
|
|
|
|
account with no password.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
lock = LOCK_INDICATOR if self.password_lock else ''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Order of precedence when choosing the password:
|
|
|
|
|
|
|
|
# 1. password from module parameters
|
|
|
|
|
|
|
|
# 2. current password
|
|
|
|
|
|
|
|
# 3. string to enable the account but without a password
|
|
|
|
|
|
|
|
password = '*'
|
|
|
|
|
|
|
|
if self.password is not None:
|
|
|
|
|
|
|
|
password = self.password
|
|
|
|
|
|
|
|
elif current_password:
|
|
|
|
|
|
|
|
if current_password == LOCK_INDICATOR:
|
|
|
|
|
|
|
|
# Special handling when the password is only a '!' to avoid
|
|
|
|
|
|
|
|
# unnecessary changes to the password to values like '!!' or '!*'.
|
|
|
|
|
|
|
|
lock = ''
|
|
|
|
|
|
|
|
password = current_password
|
|
|
|
|
|
|
|
elif current_password.startswith(LOCK_INDICATOR):
|
|
|
|
|
|
|
|
# Preserve the existing password but unlock the account even if
|
|
|
|
|
|
|
|
# no password hash was provided in the module parameters.
|
|
|
|
|
|
|
|
password = current_password.lstrip(LOCK_INDICATOR)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return f'{lock}{password}'
|
|
|
|
|
|
|
|
|
|
|
|
def create_user(self):
|
|
|
|
def create_user(self):
|
|
|
|
cmd = [self.module.get_bin_path('adduser', True)]
|
|
|
|
cmd = [self.module.get_bin_path('adduser', True)]
|
|
|
|
@ -3126,7 +3162,8 @@ class BusyBox(User):
|
|
|
|
|
|
|
|
|
|
|
|
if self.group is not None:
|
|
|
|
if self.group is not None:
|
|
|
|
if not self.group_exists(self.group):
|
|
|
|
if not self.group_exists(self.group):
|
|
|
|
self.module.fail_json(msg='Group {0} does not exist'.format(self.group))
|
|
|
|
self.module.fail_json(msg=f'Group {self.group} does not exist')
|
|
|
|
|
|
|
|
|
|
|
|
cmd.append('-G')
|
|
|
|
cmd.append('-G')
|
|
|
|
cmd.append(self.group)
|
|
|
|
cmd.append(self.group)
|
|
|
|
|
|
|
|
|
|
|
|
@ -3171,18 +3208,17 @@ class BusyBox(User):
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
if self.password is not None:
|
|
|
|
|
|
|
|
cmd = [self.module.get_bin_path('chpasswd', True)]
|
|
|
|
cmd = [self.module.get_bin_path('chpasswd', True)]
|
|
|
|
cmd.append('--encrypted')
|
|
|
|
cmd.append('--encrypted')
|
|
|
|
data = '{name}:{password}'.format(name=self.name, password=self.password)
|
|
|
|
data = f'{self.name}:{self._build_password_string()}'
|
|
|
|
rc, out, err = self.execute_command(cmd, data=data)
|
|
|
|
rc, out, err = self.execute_command(cmd, data=data)
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
# Add to additional groups
|
|
|
|
# Add to additional groups
|
|
|
|
if self.groups is not None and len(self.groups):
|
|
|
|
if self.groups:
|
|
|
|
groups = self.get_groups_set()
|
|
|
|
groups = self.get_groups_set() or set()
|
|
|
|
add_cmd_bin = self.module.get_bin_path('adduser', True)
|
|
|
|
add_cmd_bin = self.module.get_bin_path('adduser', True)
|
|
|
|
for group in groups:
|
|
|
|
for group in groups:
|
|
|
|
cmd = [add_cmd_bin, self.name, group]
|
|
|
|
cmd = [add_cmd_bin, self.name, group]
|
|
|
|
@ -3210,13 +3246,26 @@ class BusyBox(User):
|
|
|
|
rc = None
|
|
|
|
rc = None
|
|
|
|
out = ''
|
|
|
|
out = ''
|
|
|
|
err = ''
|
|
|
|
err = ''
|
|
|
|
info = self.user_info()
|
|
|
|
user_info = self.user_info()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if not user_info:
|
|
|
|
|
|
|
|
return rc, out, err
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gid = user_info[3]
|
|
|
|
|
|
|
|
if self.group is not None:
|
|
|
|
|
|
|
|
if not self.group_exists(self.group):
|
|
|
|
|
|
|
|
self.module.fail_json(msg=f'Group {self.group} does not exist')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
group_info = self.group_info(self.group)
|
|
|
|
|
|
|
|
if group_info:
|
|
|
|
|
|
|
|
gid = group_info[2]
|
|
|
|
|
|
|
|
|
|
|
|
add_cmd_bin = self.module.get_bin_path('adduser', True)
|
|
|
|
add_cmd_bin = self.module.get_bin_path('adduser', True)
|
|
|
|
remove_cmd_bin = self.module.get_bin_path('delgroup', True)
|
|
|
|
remove_cmd_bin = self.module.get_bin_path('delgroup', True)
|
|
|
|
|
|
|
|
|
|
|
|
# Manage group membership
|
|
|
|
# Manage group membership
|
|
|
|
if self.groups is not None and len(self.groups):
|
|
|
|
if self.groups:
|
|
|
|
groups = self.get_groups_set()
|
|
|
|
groups = self.get_groups_set() or set()
|
|
|
|
group_diff = set(current_groups).symmetric_difference(groups)
|
|
|
|
group_diff = set(current_groups).symmetric_difference(groups)
|
|
|
|
|
|
|
|
|
|
|
|
if group_diff:
|
|
|
|
if group_diff:
|
|
|
|
@ -3235,15 +3284,57 @@ class BusyBox(User):
|
|
|
|
self.module.fail_json(name=self.name, msg=err, rc=rc)
|
|
|
|
self.module.fail_json(name=self.name, msg=err, rc=rc)
|
|
|
|
|
|
|
|
|
|
|
|
# Manage password
|
|
|
|
# Manage password
|
|
|
|
if self.update_password == 'always' and self.password is not None and info[1] != self.password:
|
|
|
|
current_password = to_native(user_info[1])
|
|
|
|
|
|
|
|
new_password = self._build_password_string(current_password)
|
|
|
|
|
|
|
|
if self.update_password == 'always':
|
|
|
|
|
|
|
|
lock_status_mismatch = self.password_lock and not current_password.startswith('!')
|
|
|
|
|
|
|
|
password_changed = new_password != current_password
|
|
|
|
|
|
|
|
if lock_status_mismatch or password_changed:
|
|
|
|
cmd = [self.module.get_bin_path('chpasswd', True)]
|
|
|
|
cmd = [self.module.get_bin_path('chpasswd', True)]
|
|
|
|
cmd.append('--encrypted')
|
|
|
|
cmd.append('--encrypted')
|
|
|
|
data = '{name}:{password}'.format(name=self.name, password=self.password)
|
|
|
|
data = f'{self.name}:{new_password}'
|
|
|
|
rc, out, err = self.execute_command(cmd, data=data)
|
|
|
|
rc, out, err = self.execute_command(cmd, data=data)
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Manage user settings
|
|
|
|
|
|
|
|
uid = user_info[2]
|
|
|
|
|
|
|
|
if self.uid is not None:
|
|
|
|
|
|
|
|
uid = self.uid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
passwd_entry = [
|
|
|
|
|
|
|
|
self.name,
|
|
|
|
|
|
|
|
'x',
|
|
|
|
|
|
|
|
to_native(uid),
|
|
|
|
|
|
|
|
to_native(gid),
|
|
|
|
|
|
|
|
self.comment or user_info[4],
|
|
|
|
|
|
|
|
self.home or user_info[5],
|
|
|
|
|
|
|
|
self.shell or user_info[6],
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
contents = []
|
|
|
|
|
|
|
|
change = False
|
|
|
|
|
|
|
|
with open(self.PASSWORDFILE, 'r') as password_file:
|
|
|
|
|
|
|
|
for line in password_file:
|
|
|
|
|
|
|
|
if line.startswith(self.name):
|
|
|
|
|
|
|
|
fields = line.strip().split(':')
|
|
|
|
|
|
|
|
if fields != passwd_entry:
|
|
|
|
|
|
|
|
change = True
|
|
|
|
|
|
|
|
line = ':'.join(passwd_entry) + '\n'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
contents.append(line)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if change:
|
|
|
|
|
|
|
|
rc = 0
|
|
|
|
|
|
|
|
if not self.module.check_mode:
|
|
|
|
|
|
|
|
tmpfd, tmpfile = tempfile.mkstemp(dir=self.module.tmpdir)
|
|
|
|
|
|
|
|
with open(tmpfile, 'w') as f:
|
|
|
|
|
|
|
|
f.writelines(contents)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.module.backup_local(self.PASSWORDFILE)
|
|
|
|
|
|
|
|
self.module.atomic_move(tmpfile, self.PASSWORDFILE)
|
|
|
|
|
|
|
|
|
|
|
|
return rc, out, err
|
|
|
|
return rc, out, err
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|