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

View file

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