#!/usr/bin/env python3
# Copyright (C) 2010-2016 Petter Reinholdtsen <pere@hungry.com>
#               2010 Morten Werner Forsbring <werner@debian.org>
#
# 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 <pere@hungry.com>"

#
# 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 math
import time
import syslog

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'

  # File mode of new home directory
  dirmode = 0o700

  # Last password change, use today
  pwlastchange = math.floor(time.time() / (60 * 60 * 24 ))

  pwminage = 0
  pwmaxage = 99999
  pwwarn = 7

  # 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 uid < 1000
  if userinfo[2] < minimum_uid:
    return pamh.PAM_SUCCESS

  # 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]"

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

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

      # 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")

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

      # 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

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

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)
