[debian-edu-commits] [Git][debian-edu/upstream/libpam-mklocaluser][personal/gber/user-creation] 2 commits: Ensure this is ordered before other session type modules

Guido Berhörster (@gber) gitlab at salsa.debian.org
Fri Sep 22 10:41:44 BST 2023



Guido Berhörster pushed to branch personal/gber/user-creation at Debian Edu / upstream / libpam-mklocaluser


Commits:
70120820 by Guido Berhoerster at 2023-09-21T14:29:17+02:00
Ensure this is ordered before other session type modules

Since this potentially changes the home directory, the module should be ordered
before others which require the correct location of the home directory and/or
start executables, particularly pam_systemd.

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

- - - - -


2 changed files:

- debian/pam-auth-update/mklocaluser
- debian/pam-python.py


Changes:

=====================================
debian/pam-auth-update/mklocaluser
=====================================
@@ -1,6 +1,6 @@
 Name: Create local accounts and home directory on first time login
 Default: yes
-Priority: 0
+Priority: 1024
 Session-Interactive-Only: yes
 Session-Type: Additional
 Session-Final:


=====================================
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/-/compare/e9b5f13b890eaa5cbd4589bbc4738ffd6da137ea...08f13850169afe11dc6d28827ecfaa1dcac2ee41

-- 
View it on GitLab: https://salsa.debian.org/debian-edu/upstream/libpam-mklocaluser/-/compare/e9b5f13b890eaa5cbd4589bbc4738ffd6da137ea...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/32af2e18/attachment-0001.htm>


More information about the debian-edu-commits mailing list