[debian-edu-commits] [Git][debian-edu/upstream/libpam-mklocaluser][master] Rewrite the user creation to use standard tools
Mike Gabriel (@sunweaver)
gitlab at salsa.debian.org
Fri Sep 22 17:28:39 BST 2023
Mike Gabriel pushed to branch master at Debian Edu / upstream / libpam-mklocaluser
Commits:
08f13850 by Guido Berhoerster at 2023-09-22T11:41:25+02:00
Rewrite the user creation to use standard tools
This rewrites the user creation using modern python3 and fixing shell injection
bugs by not using a shell to execute external commands. Furthermore, it avoids
direct manipulation of files and uses standard tools such as getent and
userad/usermod instead which obey system-wide preferences.
- - - - -
1 changed file:
- debian/pam-python.py
Changes:
=====================================
debian/pam-python.py
=====================================
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
# Copyright (C) 2010-2016 Petter Reinholdtsen <pere at hungry.com>
+# 2023 Guido Berhoerster <guido+freiesoftware at berhoerster.name>
# 2010 Morten Werner Forsbring <werner at debian.org>
#
# Licensed under the GNU General Public License Version 2
@@ -33,189 +34,192 @@ import pwd
import grp
import subprocess
import shutil
-import math
-import time
+import tempfile
import syslog
+from pathlib import Path
-def append_line(filename, line):
- f = open(filename, 'a')
- f.write(line)
- f.close()
-
-def chown_recursive(path, uid, gid):
- os.chown(path, uid, gid)
- for root, dirs, files in os.walk(path):
- for dirname in dirs:
- os.chown(os.path.join(root, dirname), uid, gid)
- for filename in files:
- os.chown(os.path.join(root, filename), uid, gid)
-
-def runcmd(pamh, cmd):
- proc = subprocess.Popen(cmd, shell=True, \
- stdout=subprocess.PIPE, \
- stderr=subprocess.PIPE,)
- while proc.poll() == None:
- pass
- (resultstdout, resultstderr) = proc.communicate(input=None)
- if proc.returncode != 0:
- msg = "Command '%s' failed with %s" % ( cmd, resultstderr.strip())
- syslog.syslog(msg)
-# print "output: %s" % msg
-def check_and_create_localuser(pamh, user):
- # Location of local users
- topdir = "/home"
-
- # Ignore users with uid below this one
- minimum_uid = 1000
-
- # Create user entries with this shell
- shell = '/bin/bash'
+HOOK_PATH = Path("/etc/mklocaluser.d")
+MINIMUM_UID = 1000 # FIXME read UID_MIN from login.defs?
- # File mode of new home directory
- dirmode = 0o700
- # Last password change, use today
- pwlastchange = math.floor(time.time() / (60 * 60 * 24 ))
+def check_and_create_localuser(pamh, user):
+ # Fetch current user and group info, possibly from LDAP or NIS.
+ try:
+ userinfo = pwd.getpwnam(user)
+ except KeyError as err:
+ syslog.syslog(f"Unknown username, should never happen: {err}")
+ return pamh.PAM_USER_UNKNOWN
- pwminage = 0
- pwmaxage = 99999
- pwwarn = 7
+ # Ignore users belwo minimum UID
+ if userinfo.pw_uid < MINIMUM_UID:
+ return pamh.PAM_SUCCESS
- # Fetch current user and group info, possibly from LDAP or NIS.
- userinfo = pwd.getpwnam(user)
- uid = userinfo[2]
- gid = userinfo[3]
- gecos = userinfo[4]
- homedir = userinfo[5]
+ # Ignore users with existing entry in /etc/passwd
+ try:
+ subprocess.run(
+ ["getent", "passwd", "-s", "compat", user],
+ capture_output=True, text=True, check=True
+ )
+ except subprocess.CalledProcessError as err:
+ if err.returncode != 2:
+ syslog.syslog(f"{err} {err.stderr.strip()}")
+ return pamh.PAM_SYSTEM_ERR
+ else:
+ return pamh.PAM_SUCCESS
+
+ # Check whether home directory is set
+ if userinfo.pw_dir is None:
+ syslog.syslog(f"Home directory is not set for user {user}")
+ return pamh.PAM_USER_UNKNOWN
+ home = Path(userinfo.pw_dir)
+
+ # Determine location of local home directory
+ try:
+ result = subprocess.run(
+ ["useradd", "-D"], capture_output=True, text=True, check=True
+ )
+ except subprocess.CalledProcessError as err:
+ syslog.syslog(f"{err} {err.stderr.strip()}")
+ return pamh.PAM_SYSTEM_ERR
+ useradd_defaults = dict(
+ line.split("=", maxsplit=1) for line in result.stdout.split()
+ )
+ new_home = Path(useradd_defaults.get("HOME", "/home")) / user
+
+ # Ensure neither old nor new home already exist
+ if home.is_dir() or new_home.is_dir():
+ return pamh.PAM_SUCCESS
- # Ignore users with uid < 1000
- if userinfo[2] < minimum_uid:
- return pamh.PAM_SUCCESS
+ try:
+ groupname = grp.getgrgid(userinfo.pw_gid).gr_name
+ except KeyError:
+ syslog.syslog(f"Unknown primary group with gid {userinfo.pw_gid}")
+ groupname = "[unknown]"
+
+ # Create local user
+ syslog.syslog(
+ f"Creating local passwd/shadow entry uid={userinfo.pw_uid}({user}) "
+ f"gid={userinfo.pw_gid}({groupname}) gecos='{userinfo.pw_gecos}' "
+ f"home={new_home}"
+ )
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
+ # Use alternative path to the root directory to trick useradd into
+ # using files
+ root = Path(tmpdir) / "root"
+ root.symlink_to("/")
+ try:
+ # Use "--prefix" option in order to create a local user, do not set
+ # a group since it will not be found
+ subprocess.run(
+ [
+ "useradd", "--prefix", root, "--uid", str(userinfo.pw_uid),
+ "--no-user-group", "--create-home", "--home-dir", new_home,
+ "--comment", userinfo.pw_gecos, user
+ ],
+ capture_output=True, text=True, check=True
+ )
+ # Set the correct group
+ subprocess.run(
+ ["usermod", "-g", str(userinfo.pw_gid), user],
+ capture_output=True, text=True, check=True
+ )
+ except subprocess.CalledProcessError as err:
+ syslog.syslog(f"{err} {err.stderr.strip()}")
+ return pamh.PAM_SYSTEM_ERR
+
+ # Flush nscd cache to get rid of original user entry
+ nscd = shutil.which("nscd")
+ if nscd:
+ subprocess.run(
+ [nscd, "-i", "passwd"],
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
+ )
+
+ # Hook for adjusting the freshly created home directory
+ if HOOK_PATH.is_dir:
+ try:
+ subprocess.run(
+ ["run-parts", HOOK_PATH],
+ env=os.environ | {"ORIGHOMEDIR": home, "USER": user},
+ check=True
+ )
+ except subprocess.CalledProcessError as err:
+ syslog.syslog(f"{err} {err.stderr.strip()}")
+
+ # At this point, the HOME environment variable is still set to the
+ # value (i.e. path) as provided by the LDAP database. With pam_mklocaluser,
+ # we want a HOME path with the pattern /<topdir>/<user>. Luckily
+ # the pam_python.so implementation provides an easy-to-use interface to
+ # pam_getenv/pam_putenv:
+ pamh.env['HOME'] = str(new_home)
- # Ignore users with existing entry in /etc/passwd
- cmd = "/bin/grep \"^%s:\" /etc/passwd >/dev/null" % user
- proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, )
- while proc.poll() == None:
- pass
- result = proc.communicate(input=None)[0]
- if proc.returncode == 0:
return pamh.PAM_SUCCESS
- if None == homedir:
- syslog.syslog("Home directory is not set for user %s" % user)
- return pamh.PAM_USER_UNKNOWN
- newhomedir = os.path.join(topdir, user)
- if not os.path.isdir(homedir) and not os.path.isdir(newhomedir):
- try:
- groupinfo = grp.getgrgid(gid)
- groupname = groupinfo[0]
- except KeyError as e:
- syslog.syslog("Unknown primary group with gid %d" % gid)
- groupname = "[unknown]"
+def pam_sm_setcred(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
- syslog.syslog("Creating local passwd/shadow entry uid=%d(%s) gid=%d(%s) gecos='%s' home=%s" % (uid, user, gid, groupname, gecos, newhomedir))
- try:
- # Add user entry with overridden home directory in /etc/passwd.
- # Can not use adduser, as it refuses to add a user if it already
- # is visible via NSS.
- append_line('/etc/passwd', \
- "%s:x:%d:%d:%s:%s:%s\n" % \
- (user, uid, gid, gecos, newhomedir, shell))
+def pam_sm_authenticate(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
- # Add shadow entry too.
- # FIXME Should only add it if it is missing.
- append_line('/etc/shadow', \
- "%s:x:%d:%d:%d:%d:::\n" \
- % (user, pwlastchange, pwminage, pwmaxage, pwwarn))
- syslog.syslog("Creating local home directory for user '%s'" % user)
- # Copy content of /etc/skel
- shutil.copytree("/etc/skel/.", newhomedir, True)
+def pam_sm_acct_mgmt(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
- # Change perm of new home dir
- os.chmod(newhomedir, dirmode)
- chown_recursive(newhomedir, uid, gid)
- # Flush nscd cache to get rid of original user entry
- if os.access("/usr/sbin/nscd", os.X_OK):
- runcmd(pamh, "/usr/sbin/nscd -i passwd")
+def pam_sm_open_session(pamh, flags, argv):
+ syslog.openlog("pam_mklocaluser", syslog.LOG_PID, syslog.LOG_AUTH)
+ try:
+ user = pamh.get_user(None)
+ except pamh.exception as exc:
+ return exc.pam_result
+ if user is None:
+ syslog.syslog("No user, ignoring pam-python for mklocaluser")
+ return pamh.PAM_USER_UNKNOWN
+
+ # Only create local users for console logins
+ try:
+ if pamh.rhost is not None and len(pamh.rhost) != 0:
+ syslog.syslog("Remote login, ignoring pam-python for mklocaluser")
+ return pamh.PAM_SUCCESS
+ except pamh.exception as exc:
+ return exc.pam_result
- # Hook for adjusting the freshly created home directory
- # FIXME Should be rewritten in python, I guess
- runcmd(pamh, "if [ -d /etc/mklocaluser.d ]; then ORIGHOMEDIR='%s' USER='%s' /bin/run-parts /etc/mklocaluser.d ; fi" % (homedir, user))
+ try:
+ return check_and_create_localuser(pamh, user)
+ except Exception as exc:
+ syslog.syslog(f"Unexpected exception, should never happen: {exc}")
+ return pamh.PAM_SYSTEM_ERR
- # At this point, the HOME environment variable is still set to the
- # value (i.e. path) as provided by the LDAP database. With pam_mklocaluser,
- # we want a HOME path with the pattern /<topdir>/<user>. Luckily
- # the pam_python.so implementation provides an easy-to-use interface to
- # pam_getenv/pam_putenv:
- pamh.env['HOME'] = newhomedir
- except Exception as e:
- syslog.syslog("Failure while creating local user: %s " % (e))
- pass
+def pam_sm_close_session(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
- return pamh.PAM_SUCCESS
-def pam_sm_setcred(pamh, flags, argv):
- return pamh.PAM_SUCCESS
+def pam_sm_chauthtok(pamh, flags, argv):
+ return pamh.PAM_SUCCESS
-def pam_sm_authenticate(pamh, flags, argv):
- return pamh.PAM_SUCCESS
-def pam_sm_acct_mgmt(pamh, flags, argv):
- return pamh.PAM_SUCCESS
+# Test if the code work. Argument is username to simulate login for.
+if __name__ == '__main__':
+ syslog.openlog("pam_mklocaluser", syslog.LOG_PID, syslog.LOG_AUTH)
-def pam_sm_open_session(pamh, flags, argv):
- syslog.openlog("pam_mklocaluser", syslog.LOG_PID, syslog.LOG_AUTH)
- try:
- user = pamh.get_user(None)
- except pamh.exception as e:
- return e.pam_result
- if user == None:
- syslog.syslog("No user, ignoring pam-python for mklocaluser")
- return pamh.PAM_USER_UNKNOWN
-
- # Only create local users for console logins
- try:
- if pamh.rhost != None and 0 != len(pamh.rhost):
- syslog.syslog("Remote login, ignoring pam-python for mklocaluser")
- return pamh.PAM_SUCCESS
- except pamh.exception as e:
- return e.pam_result
-
- try:
- return check_and_create_localuser(pamh, user)
- except KeyError as e:
- syslog.syslog("Unknown username, should never happen: %s" % e)
- return pamh.PAM_USER_UNKNOWN
- except Exception as e:
- syslog.syslog("Unexpected exception, should never happen: %s" % e)
- return pamh.PAM_SYSTEM_ERR
+ class pam_handler:
+ PAM_SUCCESS = 1
+ PAM_USER_UNKNOWN = 2
+ PAM_SYSTEM_ERR = 3
+ PAM_TRY_AGAIN = 4
+ PAM_TEXT_INFO = 5
-def pam_sm_close_session(pamh, flags, argv):
- return pamh.PAM_SUCCESS
+ def Message(self, tag, str):
+ return str
-def pam_sm_chauthtok(pamh, flags, argv):
- return pamh.PAM_SUCCESS
+ def conversation(self, msg):
+ print("PAM conversation: " + msg)
+ return
-# Test if the code work. Argument is username to simulate login for.
-if __name__ == '__main__':
- syslog.openlog("pam_mklocaluser", syslog.LOG_PID, syslog.LOG_AUTH)
- class pam_handler:
- PAM_SUCCESS = 1
- PAM_USER_UNKNOWN = 2
- PAM_SYSTEM_ERR = 3
- PAM_TRY_AGAIN = 4
- PAM_TEXT_INFO = 5
- def Message(self, tag, str):
- return str
- def conversation(self, msg):
- print("PAM conversation: " + msg)
- return
- pamh = pam_handler()
- user = sys.argv[1]
- check_and_create_localuser(pamh, user)
+ pamh = pam_handler()
+ user = sys.argv[1]
+ check_and_create_localuser(pamh, user)
View it on GitLab: https://salsa.debian.org/debian-edu/upstream/libpam-mklocaluser/-/commit/08f13850169afe11dc6d28827ecfaa1dcac2ee41
--
View it on GitLab: https://salsa.debian.org/debian-edu/upstream/libpam-mklocaluser/-/commit/08f13850169afe11dc6d28827ecfaa1dcac2ee41
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/debian-edu-commits/attachments/20230922/8f56caed/attachment-0001.htm>
More information about the debian-edu-commits
mailing list