[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