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:
Strahinja Kustudic 2019-01-14 22:01:26 +01:00 committed by ansibot
parent 37960ccc87
commit eb8294e6d9
3 changed files with 157 additions and 76 deletions

View 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)."

View file

@ -421,6 +421,7 @@ class User(object):
distribution = None distribution = None
SHADOWFILE = '/etc/shadow' SHADOWFILE = '/etc/shadow'
SHADOWFILE_EXPIRE_INDEX = 7 SHADOWFILE_EXPIRE_INDEX = 7
LOGIN_DEFS = '/etc/login.defs'
DATE_FORMAT = '%Y-%m-%d' DATE_FORMAT = '%Y-%m-%d'
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
@ -854,10 +855,11 @@ class User(object):
elif self.SHADOWFILE: elif self.SHADOWFILE:
# Read shadow file for user's encrypted password string # Read shadow file for user's encrypted password string
if os.path.exists(self.SHADOWFILE) and os.access(self.SHADOWFILE, os.R_OK): if os.path.exists(self.SHADOWFILE) and os.access(self.SHADOWFILE, os.R_OK):
for line in open(self.SHADOWFILE).readlines(): with open(self.SHADOWFILE, 'r') as f:
if line.startswith('%s:' % self.name): for line in f:
passwd = line.split(':')[1] if line.startswith('%s:' % self.name):
expires = line.split(':')[self.SHADOWFILE_EXPIRE_INDEX] or -1 passwd = line.split(':')[1]
expires = line.split(':')[self.SHADOWFILE_EXPIRE_INDEX] or -1
return passwd, expires return passwd, expires
def get_ssh_key_path(self): def get_ssh_key_path(self):
@ -970,9 +972,8 @@ class User(object):
def get_ssh_public_key(self): def get_ssh_public_key(self):
ssh_public_key_file = '%s.pub' % self.get_ssh_key_path() ssh_public_key_file = '%s.pub' % self.get_ssh_key_path()
try: try:
f = open(ssh_public_key_file) with open(ssh_public_key_file, 'r') as f:
ssh_public_key = f.read().strip() ssh_public_key = f.read().strip()
f.close()
except IOError: except IOError:
return None return None
return ssh_public_key return ssh_public_key
@ -1001,11 +1002,23 @@ class User(object):
shutil.copytree(skeleton, path, symlinks=True) shutil.copytree(skeleton, path, symlinks=True)
except OSError as e: except OSError as e:
self.module.exit_json(failed=True, msg="%s" % to_native(e)) self.module.exit_json(failed=True, msg="%s" % to_native(e))
else: else:
try: try:
os.makedirs(path) os.makedirs(path)
except OSError as e: except OSError as e:
self.module.exit_json(failed=True, msg="%s" % to_native(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): def chown_homedir(self, uid, gid, path):
try: try:
@ -1173,9 +1186,10 @@ class FreeBsdUser(User):
# find current login class # find current login class
user_login_class = None user_login_class = None
if os.path.exists(self.SHADOWFILE) and os.access(self.SHADOWFILE, os.R_OK): if os.path.exists(self.SHADOWFILE) and os.access(self.SHADOWFILE, os.R_OK):
for line in open(self.SHADOWFILE).readlines(): with open(self.SHADOWFILE, 'r') as f:
if line.startswith('%s:' % self.name): for line in f:
user_login_class = line.split(':')[4] if line.startswith('%s:' % self.name):
user_login_class = line.split(':')[4]
# act only if login_class change # act only if login_class change
if self.login_class != user_login_class: if self.login_class != user_login_class:
@ -1632,20 +1646,21 @@ class SunOS(User):
minweeks = '' minweeks = ''
maxweeks = '' maxweeks = ''
warnweeks = '' warnweeks = ''
for line in open("/etc/default/passwd", 'r'): with open("/etc/default/passwd", 'r') as f:
line = line.strip() for line in f:
if (line.startswith('#') or line == ''): line = line.strip()
continue if (line.startswith('#') or line == ''):
m = re.match(r'^([^#]*)#(.*)$', line) continue
if m: # The line contains a hash / comment m = re.match(r'^([^#]*)#(.*)$', line)
line = m.group(1) if m: # The line contains a hash / comment
key, value = line.split('=') line = m.group(1)
if key == "MINWEEKS": key, value = line.split('=')
minweeks = value.rstrip('\n') if key == "MINWEEKS":
elif key == "MAXWEEKS": minweeks = value.rstrip('\n')
maxweeks = value.rstrip('\n') elif key == "MAXWEEKS":
elif key == "WARNWEEKS": maxweeks = value.rstrip('\n')
warnweeks = value.rstrip('\n') elif key == "WARNWEEKS":
warnweeks = value.rstrip('\n')
except Exception as err: except Exception as err:
self.module.fail_json(msg="failed to read /etc/default/passwd: %s" % to_native(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() minweeks, maxweeks, warnweeks = self.get_password_defaults()
try: try:
lines = [] lines = []
for line in open(self.SHADOWFILE, 'rb').readlines(): with open(self.SHADOWFILE, 'rb') as f:
line = to_native(line, errors='surrogate_or_strict') for line in f:
fields = line.strip().split(':') line = to_native(line, errors='surrogate_or_strict')
if not fields[0] == self.name: fields = line.strip().split(':')
lines.append(line) if not fields[0] == self.name:
continue lines.append(line)
fields[1] = self.password continue
fields[2] = str(int(time.time() // 86400)) fields[1] = self.password
if minweeks: fields[2] = str(int(time.time() // 86400))
try: if minweeks:
fields[3] = str(int(minweeks) * 7) try:
except ValueError: fields[3] = str(int(minweeks) * 7)
# mirror solaris, which allows for any value in this field, and ignores anything that is not an int. except ValueError:
pass # mirror solaris, which allows for any value in this field, and ignores anything that is not an int.
if maxweeks: pass
try: if maxweeks:
fields[4] = str(int(maxweeks) * 7) try:
except ValueError: fields[4] = str(int(maxweeks) * 7)
# mirror solaris, which allows for any value in this field, and ignores anything that is not an int. except ValueError:
pass # mirror solaris, which allows for any value in this field, and ignores anything that is not an int.
if warnweeks: pass
try: if warnweeks:
fields[5] = str(int(warnweeks) * 7) try:
except ValueError: fields[5] = str(int(warnweeks) * 7)
# mirror solaris, which allows for any value in this field, and ignores anything that is not an int. except ValueError:
pass # mirror solaris, which allows for any value in this field, and ignores anything that is not an int.
line = ':'.join(fields) pass
lines.append('%s\n' % line) line = ':'.join(fields)
open(self.SHADOWFILE, 'w+').writelines(lines) lines.append('%s\n' % line)
with open(self.SHADOWFILE, 'w+') as f:
f.writelines(lines)
except Exception as err: except Exception as err:
self.module.fail_json(msg="failed to update users password: %s" % to_native(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() minweeks, maxweeks, warnweeks = self.get_password_defaults()
try: try:
lines = [] lines = []
for line in open(self.SHADOWFILE, 'rb').readlines(): with open(self.SHADOWFILE, 'rb') as f:
line = to_native(line, errors='surrogate_or_strict') for line in f:
fields = line.strip().split(':') line = to_native(line, errors='surrogate_or_strict')
if not fields[0] == self.name: fields = line.strip().split(':')
lines.append(line) if not fields[0] == self.name:
continue lines.append(line)
fields[1] = self.password continue
fields[2] = str(int(time.time() // 86400)) fields[1] = self.password
if minweeks: fields[2] = str(int(time.time() // 86400))
fields[3] = str(int(minweeks) * 7) if minweeks:
if maxweeks: fields[3] = str(int(minweeks) * 7)
fields[4] = str(int(maxweeks) * 7) if maxweeks:
if warnweeks: fields[4] = str(int(maxweeks) * 7)
fields[5] = str(int(warnweeks) * 7) if warnweeks:
line = ':'.join(fields) fields[5] = str(int(warnweeks) * 7)
lines.append('%s\n' % line) line = ':'.join(fields)
open(self.SHADOWFILE, 'w+').writelines(lines) lines.append('%s\n' % line)
with open(self.SHADOWFILE, 'w+'):
f.writelines(lines)
rc = 0 rc = 0
except Exception as err: except Exception as err:
self.module.fail_json(msg="failed to update users password: %s" % to_native(err)) self.module.fail_json(msg="failed to update users password: %s" % to_native(err))
@ -2638,7 +2657,7 @@ def main():
if err: if err:
result['stderr'] = err result['stderr'] = err
if user.user_exists(): if user.user_exists() and user.state == 'present':
info = user.user_info() info = user.user_info()
if info is False: if info is False:
result['msg'] = "failed to look up user name: %s" % user.name result['msg'] = "failed to look up user name: %s" % user.name

View file

@ -229,6 +229,62 @@
- '"ansibulluser" not in user_names2.stdout_lines' - '"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: - block:
- name: create non-system user on macOS to test the shell is set to /bin/bash - name: create non-system user on macOS to test the shell is set to /bin/bash
user: user: