#!/usr/bin/env python3 # Copyright (C) 2010-2016 Petter Reinholdtsen # 2023 Guido Berhoerster # 2010 Morten Werner Forsbring # # Licensed under the GNU General Public License Version 2 # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA # 02110-1301, USA. __author__ = "Petter Reinholdtsen " # # Create local user and redirected home directory. # If the local user logging in have uid >= 1000, create primary group # and user in /etc/passwd and /etc/group, and create a home directory # under /home/ if none exist already. import os import sys import pwd import grp import subprocess import shutil import tempfile import syslog from pathlib import Path HOOK_PATH = Path("/etc/mklocaluser.d") def get_minimum_uid(): min_uid = 1000 with open("/etc/login.defs") as f: for line in f: parts = line.strip().split(maxsplit=1) if len(parts) == 2 and parts[0] == "UID_MIN": try: min_uid = int(parts[1]) except ValueError: pass break return min_uid 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 # Ignore users belwo minimum UID if userinfo.pw_uid < get_minimum_uid(): return pamh.PAM_SUCCESS # 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 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 //. Luckily # the pam_python.so implementation provides an easy-to-use interface to # pam_getenv/pam_putenv: pamh.env['HOME'] = str(new_home) return pamh.PAM_SUCCESS def pam_sm_setcred(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 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 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 def pam_sm_close_session(pamh, flags, argv): return pamh.PAM_SUCCESS def pam_sm_chauthtok(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) 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)