pull/86143/merge
Sam Doran 23 hours ago committed by GitHub
commit d1f6fb2b4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,2 @@
bugfixes:
- user - fix modifying users on BusyBox (https://github.com/ansible/ansible/issues/66679)

@ -0,0 +1,2 @@
bugfixes:
- user - create accounts in an unlocked state by default on BusyBox (https://github.com/ansible/ansible/issues/68676)

@ -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 = f'{self.name}:{self._build_password_string()}'
data = '{name}:{password}'.format(name=self.name, password=self.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)
# 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,14 +3284,56 @@ 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])
cmd = [self.module.get_bin_path('chpasswd', True)] new_password = self._build_password_string(current_password)
cmd.append('--encrypted') if self.update_password == 'always':
data = '{name}:{password}'.format(name=self.name, password=self.password) lock_status_mismatch = self.password_lock and not current_password.startswith('!')
rc, out, err = self.execute_command(cmd, data=data) password_changed = new_password != current_password
if lock_status_mismatch or password_changed:
cmd = [self.module.get_bin_path('chpasswd', True)]
cmd.append('--encrypted')
data = f'{self.name}:{new_password}'
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

@ -19,9 +19,7 @@
- import_tasks: test_expires_warn.yml - import_tasks: test_expires_warn.yml
- import_tasks: test_ssh_key_passphrase.yml - import_tasks: test_ssh_key_passphrase.yml
- include_tasks: test_password_lock.yml - include_tasks: test_password_lock.yml
when: ansible_distribution != 'Alpine'
- include_tasks: test_password_lock_new_user.yml - include_tasks: test_password_lock_new_user.yml
when: ansible_distribution != 'Alpine'
- include_tasks: test_local.yml - include_tasks: test_local.yml
when: ansible_distribution != 'Alpine' when: ansible_distribution != 'Alpine'
- include_tasks: test_umask.yml - include_tasks: test_umask.yml

Loading…
Cancel
Save