#!/usr/bin/python

# $Id: addluser.py,v 1.4 2003/01/21 15:54:33 bortzmeyer Exp $
# created by Joe Little <jlittle@open-it.org>
# some patches from Stephane Bortzmeyer <bortzmeyer@eureg.org>

# provides useradd style additions to an LDAP directory service
# This prototype is PublicDomain at this point (heh its a prototype!)
# It represents one piece from an upcoming suite of tools, which combined
# should be GPLed as a package. 

# It depends upon python-ldap (http://python-ldap.sourceforge.net)
#
# CHANGELOG
# v.01 -- Initial release
# v.02 -- added -r (remove) functionality, fixed default cn behavior
# v.03 -- uid checking added. Use with -n flag. It will find the next
#	  UID/GID after the uid and gid number provided by the user
#         via the command line, or after the default uid/gid number,
#	  if no uid/gid is provided as an argument
#         Separately, it uses uidcheck to make sure there is no duplicate
#         UID in system. Skips GID check since having same GID is allowed
# v.04 -- added SALT routine as provided by Luke Howard. Much better..
# v.05 -- changed importation to ldap instead of _ldap
#         Added password confirmation block to ensure correct user password
# v.06 -- Hooks, to be executed before or after creation/destruction
#         Configuration in a separate file


import sys
import os
import getpass
import crypt
import ldap
import whrandom
import ConfigParser

myname = "ldapsimpletools" # It is the name of the package, not of the script

# def usage statment
def print_usage(errormsg=''):
  print """
usage: %s [options] username

Options:

-h or -?	Print out this message

-u uid		uid number to use

-g gid		gid number to use

-d home		home directory of user 

-s shell	path of shell to use

-c comment	GECOS field entry

-r 		remove user from LDAP (other options ignored)

-n		Check for next available uid/gid; uses -u and -g value as offset

""" % (sys.argv[0])
  if errormsg:
    print ' Error: %s' % (errormsg)
  sys.exit(1)

def getsalt():
  salt = ""
  for j in range(2):
    i = whrandom.randint(0,9) % 3
    if i == 0 :
      i = (whrandom.randint(0,9) % 11)
    elif i == 1 :
      i = (whrandom.randint(0,9) % 25)
    elif i == 2 :
      i = (whrandom.randint(0,9) % 25)
    salt = salt + str(i)                                                        
  return (salt)

# RFC2307 ldif creation def
def BuildUser(schema,user,uid,gid,gecos,homedir,shell,cn,userpass,mail):

  class_list = ["posixAccount", "shadowAccount"]
  if schema:
    class_list.append(schema)
  user_ldif = [
      ("uid", [user]),
      ("sn", [user]),
      ("cn", [cn]),
      ("mail", [mail]),
      ("objectClass", class_list),
      ("shadowLastChange", ["12000"]),
      ("shadowMax", ["99999"]),
      ("shadowWarning", ["7"]),
      ("shadowInactive", ["-1"]),
      ("shadowExpire", ["-1"]),
      ("shadowFlag", ["123456789"]),
      ("userPassword", [userpass]),
      ("loginshell", [shell]),
      ("uidNumber", [str(uid)]),
      ("gidNumber", [str(gid)]),
      ("homeDirectory", [homedir]),
      ("gecos", [gecos])
    ]
  return (user_ldif)

# command line parsing
def GetSysArgs(user,uid,gid,gecos,homedir,shell,delete,uidcheck):

  import getopt

  homedir_set = 0
  gecos_set = 0
  
  try:
    optlist, args=getopt.getopt(sys.argv[1:], "?hu:g:d:s:c:f:l:rn")
  except getopt.error,e:
    print_usage(str(e))
    sys.exit(1)

  for key,value in optlist:

    if key == "-r":
      delete = 1

    if key == "-n":
      uidcheck = 1

    if key == "-u":
      try:
        uid = int(value)
      except ValueError:
	print 'uid must be a number'
	sys.exit(1)

    if key == "-g":
      try:
	gid = int(value)
      except ValueError:
	print 'gid must be a number'
	sys.exit(1)

    if key == "-d":
      try:
	homedir = str(value)
        homedir_set = 1
      except ValueError:
	print 'home directory must be a valid string'
        sys.exit(1)

    if key == "-s":
      try:
	shell = str(value)
      except ValueError:
	print 'shell must be a valid string'
	sys.exit(1)

    if key == "-c":
      try:
	gecos = str(value)
        gecos_set = 1
      except ValueError:
	print 'comment field must be a valid string'
	sys.exit(1)

    if (key == "-h") or (key == "-?"):
      print_usage()
      sys.exit(1)

  if len(sys.argv) == 1:
    print_usage() 
    sys.exit(1)
  else:
    username = sys.argv[-1:] 
    user = username[0]
    
  if not homedir_set:
    config.set('account','user', user)
    homedir = config.get('account','homedir')

  if not delete and not gecos_set and config.getboolean('account', 'ask_gecos'):
    print "Full name of " + user + ":",
    gecos = sys.stdin.readline()
    gecos = gecos[0:-1]
    
  return (
    user,uid,gid,gecos,homedir,shell,delete,uidcheck
  )


# checkuid function. connects with server and finds next available uidnumber
# or gidnumber to use. Uses current uid/gid value (or default) as offset
def check_id (base_dn, id, searchfield):
  idlist = []
  nid = 60000

  searchstr = searchfield + "=*"
  res = l.search_s(base_dn, ldap.SCOPE_SUBTREE, searchstr)
  for i in res:
    j = i[1]
    idlist.append (int(j[searchfield][0]))
  idlist.sort()
  testid = id
  while 1:
    if idlist.count(testid) == 0:
      nid = testid
      break
    testid = testid + 1 

  return (nid)

# actual main

# Get the defaults
# html/lib/module-ConfigParser.html in the Python documentation
config = ConfigParser.ConfigParser()
config.read(['/etc/ldap/' + myname, os.path.expanduser('~/.' + myname + '.cfg')])
# Defaults
config.set('DEFAULT', 'base_dn', "NOT_SET")
config.set('DEFAULT', 'mail_domain', "")
config.set('DEFAULT', 'schema', "")
config.set('DEFAULT', 'login_dn', 'cn=admin,' + '%(base_dn)s')
config.set('DEFAULT', 'name', 'localhost')
config.set('DEFAULT', 'user', 'guest')
config.set('DEFAULT', 'uid', 1000)
config.set('DEFAULT', 'gid', 100)
config.set('DEFAULT', 'gecos', 'Guest User')
config.set('DEFAULT', 'homedir', '/home/%(user)s')
config.set('DEFAULT', 'shell', '/bin/sh')
config.set('DEFAULT', 'delete', '0') # I hate ConfigParser
config.set('DEFAULT', 'ask_gecos', '0') 
config.set('DEFAULT', 'uidcheck', '1')
#
base_dn = config.get('server', 'base_dn')
ldap_host = config.get('server', 'name')
local_schema = config.get('server', 'schema')
mail_domain = config.get('server', 'mail_domain')
login_dn = config.get('server', 'login_dn')
user = config.get('account','user')
homedir = config.get('account','homedir')
gecos = config.get('account','gecos')
shell = config.get('account','shell')
uid = config.getint('account', 'uid')
gid = config.getint('account', 'gid')
delete_user = config.getboolean('account', 'delete')
uidcheck = config.getboolean('account', 'uidcheck')

# Get command line arguments which may override the defaults
user,uid,gid,gecos,homedir,shell,delete_user,uidcheck = GetSysArgs (user,uid,gid,gecos,homedir,shell,delete_user,uidcheck)

l = ldap.open(ldap_host)

if not delete_user:
  while 1:
    # get user pass
    pass1 = getpass.getpass ("Password for %s: " %user)
    pass2 = getpass.getpass ("Confirm password: ")
    if (pass1 == pass2) :
      userpass_entry = pass1
      break
    else:
      print "Passwords do not match. Please retry password."
  
  userpass = "{crypt}" + crypt.crypt(userpass_entry, getsalt())

# get password for root dn
login_pw = getpass.getpass ("Password for %s on %s: " %
                            (login_dn, ldap_host))

# bind as root dn
l.simple_bind_s (login_dn, login_pw)

# if checking of uid select, establish new uid now
if uidcheck:
  cuid = check_id (base_dn, uid, "uidNumber")
  uid = cuid
  cuid = check_id (base_dn, gid, "gidNumber")
  gid = cuid
  print "UID=%d GID=%d" % (uid,gid)

# build ldif to add, if thats our operation
if not delete_user:
  cn = user
  if mail_domain:
    mail = "%s@%s" % (user, mail_domain)
  else:
    mail = None
  user_info = BuildUser (local_schema,user,uid,gid,gecos,homedir,shell,cn,userpass,mail)
  # check if UID is already in use on server
  if not uidcheck:
    cuid = check_id (base_dn, uid, "uidNumber")
    if (cuid != uid):
      print "UID already in use, consider using -n flag"
      sys.exit(1)

# build user dn
dn = "uid=" + user + ",ou=People," + base_dn

# check for removal
if delete_user:
  if config.has_option('account','pre_delete_hook'):
    os.system (config.get('account', 'pre_delete_hook'))
  try:
    l.delete_s(dn)
    print "Deleted " + user
    if config.has_option('account','post_delete_hook'):
      os.system (config.get('account', 'post_delete_hook'))
    sys.exit(0)
  except ldap.LDAPError:
    print "Error in deleting entry"
    print ' Error: %s' % (sys.exc_type)
    sys.exit(1)

# add user otherwise
print "Adding " + user
if config.has_option('account','pre_create_hook'):
  os.system (config.get('account', 'pre_create_hook'))

try:
  l.add_s (dn, user_info)
except ldap.LDAPError:
  print "error in creating entry"
  print ' Error: %s' % (sys.exc_type)
  sys.exit(1)

print "Success"
if config.has_option('account','post_create_hook'):
  os.system (config.get('account', 'post_create_hook'))

# end connection

l.unbind()
