Fix create home dir fallback (#49262)
When a user home dir is not created with `useradd`, the home dir will now be created with umask from /etc/login.defs. Also fixed a bug in which after a local user is deleted, and the same user exists in the central user management system, the module would create that user's home.
This commit is contained in:
parent
37960ccc87
commit
eb8294e6d9
3 changed files with 157 additions and 76 deletions
6
changelogs/fragments/49262-user.yml
Normal file
6
changelogs/fragments/49262-user.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
bugfixes:
|
||||
- "user - fixed the fallback mechanism for creating a user home directory when
|
||||
the directory isn't created with `useradd` command. Home directory will now
|
||||
have a correct mode and it won't be created in a rare situation when a local
|
||||
user is being deleted but it exists on a central user system
|
||||
(https://github.com/ansible/ansible/pull/49262)."
|
|
@ -421,6 +421,7 @@ class User(object):
|
|||
distribution = None
|
||||
SHADOWFILE = '/etc/shadow'
|
||||
SHADOWFILE_EXPIRE_INDEX = 7
|
||||
LOGIN_DEFS = '/etc/login.defs'
|
||||
DATE_FORMAT = '%Y-%m-%d'
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
|
@ -854,10 +855,11 @@ class User(object):
|
|||
elif self.SHADOWFILE:
|
||||
# Read shadow file for user's encrypted password string
|
||||
if os.path.exists(self.SHADOWFILE) and os.access(self.SHADOWFILE, os.R_OK):
|
||||
for line in open(self.SHADOWFILE).readlines():
|
||||
if line.startswith('%s:' % self.name):
|
||||
passwd = line.split(':')[1]
|
||||
expires = line.split(':')[self.SHADOWFILE_EXPIRE_INDEX] or -1
|
||||
with open(self.SHADOWFILE, 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith('%s:' % self.name):
|
||||
passwd = line.split(':')[1]
|
||||
expires = line.split(':')[self.SHADOWFILE_EXPIRE_INDEX] or -1
|
||||
return passwd, expires
|
||||
|
||||
def get_ssh_key_path(self):
|
||||
|
@ -970,9 +972,8 @@ class User(object):
|
|||
def get_ssh_public_key(self):
|
||||
ssh_public_key_file = '%s.pub' % self.get_ssh_key_path()
|
||||
try:
|
||||
f = open(ssh_public_key_file)
|
||||
ssh_public_key = f.read().strip()
|
||||
f.close()
|
||||
with open(ssh_public_key_file, 'r') as f:
|
||||
ssh_public_key = f.read().strip()
|
||||
except IOError:
|
||||
return None
|
||||
return ssh_public_key
|
||||
|
@ -1001,11 +1002,23 @@ class User(object):
|
|||
shutil.copytree(skeleton, path, symlinks=True)
|
||||
except OSError as e:
|
||||
self.module.exit_json(failed=True, msg="%s" % to_native(e))
|
||||
else:
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError as e:
|
||||
self.module.exit_json(failed=True, msg="%s" % to_native(e))
|
||||
else:
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError as e:
|
||||
self.module.exit_json(failed=True, msg="%s" % to_native(e))
|
||||
# get umask from /etc/login.defs and set correct home mode
|
||||
if os.path.exists(self.LOGIN_DEFS):
|
||||
with open(self.LOGIN_DEFS, 'r') as f:
|
||||
for line in f:
|
||||
m = re.match(r'^UMASK\s+(\d+)$', line)
|
||||
if m:
|
||||
umask = int(m.group(1), 8)
|
||||
mode = 0o777 & ~umask
|
||||
try:
|
||||
os.chmod(path, mode)
|
||||
except OSError as e:
|
||||
self.module.exit_json(failed=True, msg="%s" % to_native(e))
|
||||
|
||||
def chown_homedir(self, uid, gid, path):
|
||||
try:
|
||||
|
@ -1173,9 +1186,10 @@ class FreeBsdUser(User):
|
|||
# find current login class
|
||||
user_login_class = None
|
||||
if os.path.exists(self.SHADOWFILE) and os.access(self.SHADOWFILE, os.R_OK):
|
||||
for line in open(self.SHADOWFILE).readlines():
|
||||
if line.startswith('%s:' % self.name):
|
||||
user_login_class = line.split(':')[4]
|
||||
with open(self.SHADOWFILE, 'r') as f:
|
||||
for line in f:
|
||||
if line.startswith('%s:' % self.name):
|
||||
user_login_class = line.split(':')[4]
|
||||
|
||||
# act only if login_class change
|
||||
if self.login_class != user_login_class:
|
||||
|
@ -1632,20 +1646,21 @@ class SunOS(User):
|
|||
minweeks = ''
|
||||
maxweeks = ''
|
||||
warnweeks = ''
|
||||
for line in open("/etc/default/passwd", 'r'):
|
||||
line = line.strip()
|
||||
if (line.startswith('#') or line == ''):
|
||||
continue
|
||||
m = re.match(r'^([^#]*)#(.*)$', line)
|
||||
if m: # The line contains a hash / comment
|
||||
line = m.group(1)
|
||||
key, value = line.split('=')
|
||||
if key == "MINWEEKS":
|
||||
minweeks = value.rstrip('\n')
|
||||
elif key == "MAXWEEKS":
|
||||
maxweeks = value.rstrip('\n')
|
||||
elif key == "WARNWEEKS":
|
||||
warnweeks = value.rstrip('\n')
|
||||
with open("/etc/default/passwd", 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if (line.startswith('#') or line == ''):
|
||||
continue
|
||||
m = re.match(r'^([^#]*)#(.*)$', line)
|
||||
if m: # The line contains a hash / comment
|
||||
line = m.group(1)
|
||||
key, value = line.split('=')
|
||||
if key == "MINWEEKS":
|
||||
minweeks = value.rstrip('\n')
|
||||
elif key == "MAXWEEKS":
|
||||
maxweeks = value.rstrip('\n')
|
||||
elif key == "WARNWEEKS":
|
||||
warnweeks = value.rstrip('\n')
|
||||
except Exception as err:
|
||||
self.module.fail_json(msg="failed to read /etc/default/passwd: %s" % to_native(err))
|
||||
|
||||
|
@ -1724,35 +1739,37 @@ class SunOS(User):
|
|||
minweeks, maxweeks, warnweeks = self.get_password_defaults()
|
||||
try:
|
||||
lines = []
|
||||
for line in open(self.SHADOWFILE, 'rb').readlines():
|
||||
line = to_native(line, errors='surrogate_or_strict')
|
||||
fields = line.strip().split(':')
|
||||
if not fields[0] == self.name:
|
||||
lines.append(line)
|
||||
continue
|
||||
fields[1] = self.password
|
||||
fields[2] = str(int(time.time() // 86400))
|
||||
if minweeks:
|
||||
try:
|
||||
fields[3] = str(int(minweeks) * 7)
|
||||
except ValueError:
|
||||
# mirror solaris, which allows for any value in this field, and ignores anything that is not an int.
|
||||
pass
|
||||
if maxweeks:
|
||||
try:
|
||||
fields[4] = str(int(maxweeks) * 7)
|
||||
except ValueError:
|
||||
# mirror solaris, which allows for any value in this field, and ignores anything that is not an int.
|
||||
pass
|
||||
if warnweeks:
|
||||
try:
|
||||
fields[5] = str(int(warnweeks) * 7)
|
||||
except ValueError:
|
||||
# mirror solaris, which allows for any value in this field, and ignores anything that is not an int.
|
||||
pass
|
||||
line = ':'.join(fields)
|
||||
lines.append('%s\n' % line)
|
||||
open(self.SHADOWFILE, 'w+').writelines(lines)
|
||||
with open(self.SHADOWFILE, 'rb') as f:
|
||||
for line in f:
|
||||
line = to_native(line, errors='surrogate_or_strict')
|
||||
fields = line.strip().split(':')
|
||||
if not fields[0] == self.name:
|
||||
lines.append(line)
|
||||
continue
|
||||
fields[1] = self.password
|
||||
fields[2] = str(int(time.time() // 86400))
|
||||
if minweeks:
|
||||
try:
|
||||
fields[3] = str(int(minweeks) * 7)
|
||||
except ValueError:
|
||||
# mirror solaris, which allows for any value in this field, and ignores anything that is not an int.
|
||||
pass
|
||||
if maxweeks:
|
||||
try:
|
||||
fields[4] = str(int(maxweeks) * 7)
|
||||
except ValueError:
|
||||
# mirror solaris, which allows for any value in this field, and ignores anything that is not an int.
|
||||
pass
|
||||
if warnweeks:
|
||||
try:
|
||||
fields[5] = str(int(warnweeks) * 7)
|
||||
except ValueError:
|
||||
# mirror solaris, which allows for any value in this field, and ignores anything that is not an int.
|
||||
pass
|
||||
line = ':'.join(fields)
|
||||
lines.append('%s\n' % line)
|
||||
with open(self.SHADOWFILE, 'w+') as f:
|
||||
f.writelines(lines)
|
||||
except Exception as err:
|
||||
self.module.fail_json(msg="failed to update users password: %s" % to_native(err))
|
||||
|
||||
|
@ -1843,23 +1860,25 @@ class SunOS(User):
|
|||
minweeks, maxweeks, warnweeks = self.get_password_defaults()
|
||||
try:
|
||||
lines = []
|
||||
for line in open(self.SHADOWFILE, 'rb').readlines():
|
||||
line = to_native(line, errors='surrogate_or_strict')
|
||||
fields = line.strip().split(':')
|
||||
if not fields[0] == self.name:
|
||||
lines.append(line)
|
||||
continue
|
||||
fields[1] = self.password
|
||||
fields[2] = str(int(time.time() // 86400))
|
||||
if minweeks:
|
||||
fields[3] = str(int(minweeks) * 7)
|
||||
if maxweeks:
|
||||
fields[4] = str(int(maxweeks) * 7)
|
||||
if warnweeks:
|
||||
fields[5] = str(int(warnweeks) * 7)
|
||||
line = ':'.join(fields)
|
||||
lines.append('%s\n' % line)
|
||||
open(self.SHADOWFILE, 'w+').writelines(lines)
|
||||
with open(self.SHADOWFILE, 'rb') as f:
|
||||
for line in f:
|
||||
line = to_native(line, errors='surrogate_or_strict')
|
||||
fields = line.strip().split(':')
|
||||
if not fields[0] == self.name:
|
||||
lines.append(line)
|
||||
continue
|
||||
fields[1] = self.password
|
||||
fields[2] = str(int(time.time() // 86400))
|
||||
if minweeks:
|
||||
fields[3] = str(int(minweeks) * 7)
|
||||
if maxweeks:
|
||||
fields[4] = str(int(maxweeks) * 7)
|
||||
if warnweeks:
|
||||
fields[5] = str(int(warnweeks) * 7)
|
||||
line = ':'.join(fields)
|
||||
lines.append('%s\n' % line)
|
||||
with open(self.SHADOWFILE, 'w+'):
|
||||
f.writelines(lines)
|
||||
rc = 0
|
||||
except Exception as err:
|
||||
self.module.fail_json(msg="failed to update users password: %s" % to_native(err))
|
||||
|
@ -2638,7 +2657,7 @@ def main():
|
|||
if err:
|
||||
result['stderr'] = err
|
||||
|
||||
if user.user_exists():
|
||||
if user.user_exists() and user.state == 'present':
|
||||
info = user.user_info()
|
||||
if info is False:
|
||||
result['msg'] = "failed to look up user name: %s" % user.name
|
||||
|
|
|
@ -229,6 +229,62 @@
|
|||
- '"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: python
|
||||
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:
|
||||
|
|
Loading…
Reference in a new issue