Python ldap tools

Lusertools is a tool for managing users and groups in LDAP

#!/usr/bin/env python2.2

###########################################################################
# Written by Bjørn Ove Grøtan <[email protected]>
#
# This piece of utter crap is covered by Artistic Licence. 
# I'd apreciete it if you find this piece of software helpful, and if
# you make any changes that you think might be useful to others - 
# please send it to me and I'll review it for any new releases.
#
# Influenced from http://www.open-it.org/download/prototypes/addluser.py
###########################################################################


###########################################################################
# Modules
import sys
import string,base64
import getopt,getpass
from mkpasswd import *
try:
    import ldap
except:
    print "python-ldap doens't seem to be installed on this system. Exiting.."
    print "depends: python-ldap (http://python-ldap.sourceforge.net)"
    sys.exit()

###########################################################################
# NB! no read access for other than admins. Set to False 
#     if bind-pw should be asked for while running the script
suid = True 
userBase = "ou=People"
base = "dc=example,dc=com"

###########################################################################
# Bindings, connections...
uri = "ldaps://localhost:636"
binddn = "cn=admin,dc=example,dc=com"
passwd = "secret"

try:
    l = ldap.initialise(uri) # use of ldap.open is depricated
    if suid == True:
        l.simple_bind_s(binddn,passwd)
except ldap.LDAPError,e:
    print "An error occured: %s" % e
    sys.exit()

###########################################################################
# Functions

def usage(msg):
    print """
usage: %s [options] lusername

Options:

-h          This help for luser-admins
-u <uid>    uidNumber to use
-g <gid>    gidNumber to use
-d <home>   homeDirectory of luser (defaults to /home/<lusername>
-s <shell>  Full path to luser shell (defaults to /bin/bash)
-f <gname>  Givenname (lusers first name) (7bit ascii)
-t <sname>  Surname (7bit ascii)
-r          Delete luser 
-n          Check for next available uid/gid; uses -u and -g value as offset
    """ (sys.argv[0])
    if msg:
        print "An error occured: %s " % (msg)
    sys.exit(1)

def getArgs(user,uid,gid,gecos,homedir,shell,delete,uidcheck):
    """ Get arguments from STDIN """
    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 "gidNumber must be a valid integer"
                sys.exit(1)
        if key == "-d":
            try:
                homeDir = str(value)
            except ValueError:
                print "Home-directory must be a valid string"
                sys.exit(1)
        if key == "-f":
            try:
                givenName = str(value)
            except ValueError:
                print "Givenname must be a valid string"
        if key == "-t":
            try:
                surname = str(value)
            except ValueError:
                print "Surname must be a valid string"
        if key == "-s":
            try:
                shell = str(value)
            except ValueError:
                print "shell must be a valid string"
                sys.exit(1)
        if key == "-c":
            try:
                description = str(value)
            except ValueError:
                print "Description must be a valid string"
                sys.exit(1)
        if (key == "-h") or (key == "-?"):
            usage()
            sys.exit(1)
    if len(sys.argv) == 1:
        usage()
        sys.exit(1)
    else:
        username = sys.argv[-1:]
        user = username[0]

    return (user,uid,gid,gecos,homedir,shell,delete,uidcheck)

def get_next_uid(base,id,search):
    # This function is a total ripoff from addluser.py by Open IT Projects.
    """ 
    Finds next available uidNumber or gidNumber to use.  
    Replace this function if you want to fetch this from a db or file instead
    """
    idlist = []
    nid = 60000

    searchstr = searchfield + "=*"
    res = l.search_s(base, ldap.SCOPE_SUBTREE, search)
    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)


###########################################################################
# Only run this script if we are called directly
if __name__ == "__main__":
    # Default values
    user = "guest"
    uid = 500
    gid = 500
    gecos = "Guest User"
    homedir = "/home/guest"
    shell = "/bin/bash"
    delete = 0
    uidcheck = 0

    # Get arguments
    user,uid,gid,gecos,homedir,shell,delete,uidcheck = GetSysArgs (user,uid,gid,gecos,homedir,shell,delete,uidcheck)

    # If this script is not suid, ask for bind-pw
    if suid == False:
        passwd = getpass.getpass("Administration password: ") # Read bind-password from STDIN
        try: 
            l.simple_bind_s(binddn,passwd)
        except ldap.LDAPError,e:
            print "Error connecting: \n %s" % e
            sys.exit(1)


    if not delete:
        while 1:
            # Get luser pass
            pass1 = getpass.getpass("Password for %s: " %user)
            pass2 = getpass.getpass("Confirm password: ")
            if (pass1 == pass2):
                userPassword = pass1
                break
            else:
                print "Password mismatch. Please retry."
    userPassword = mkpasswd(userPassword) # should default to SSHA-passwords

    # Build luser dn
    luserdn = "uid=" + user + userBase + "," + base

    # Check for and try removal
    if delete:
        try:
            l.delete_s(luserdn)
        except ldap.LDAPError,e:
            print "Error deleting user."
            print "Errormessage: %s" % e
            sys.exit(1)
    print "Adding luser: " + user
    try:
        l.add_S(luserdn, userlist)
    except ldap.LDAPError,e:
        print "Error adding luser: " + user
        print "Error: %s" % e
        sys.exit(1)
    print "Success"

    # Unbind and free connections
    try:
        l.unbind_s()
    except:
        pass

Mkpasswd is a module for encrypting passwords for users in LDAP. Supports crypt,md5,sha, ssha as well as lanman-hashes for use with samba_ldap

#$Id: mkpasswd.py,v 1.11 2004/12/04 10:38:51 bgrotan Exp $
'''
 This module depends on python >= 2.3

 Module written by Bjorn Ove Grotan <[email protected]>

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

  mkpasswd 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 mkpasswd; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.

 For extra strength passwords, we wanted SSHA in our LDAP-environment
 as the standard python-module 'sha' does not support ssha, but this can 
 easily be implemented with a few extra functions. 

 SSHA can be described as:
     the SHA1-digest of a password with a sequence of "salt" bytes, where
     the bytes are randomly chosen - followed by the same salt bytes
 For LDAP-use, the SHA1 and SSHA-digest has to be base64-encoded. 

 Example-LDIF:
     {SSHA}oaEG3PJ10sHxGcSxsDRRooTifL55/2NOdN3nU1VEV+NFzc9Q

 This package should now support passwords compatible with [1] Samba using the [2]
 smbpasswd module for [3] Python. The samba compability is added for use with Samba 
 as PDC with storing user and host-information in LDAP.

 [1] http://www.samba.org
 [2] http://barryp.org/software/py-smbpasswd/
 [3] http://www.python.org
'''
import string,base64
import random,sys
import exceptions
import md5,sha,crypt
smb = 0 # Where 1 is true, and 0 is false
debug = False

try:
    import smbpasswd
    smb = 1 
except:
    smb = 0
    if debug:
        print '''
        module <smbpasswd> not found or not installed. Windows-passwords are therefor
        not supported!
        '''

def getsalt(chars = string.letters + string.digits,length=16):
    ''' Generate a random salt. Default length is 16 '''
    salt = ''
    for i in range(int(length)):
        salt += random.choice(chars)
    return salt

def randpasswd(chars = string.digits + string.ascii_letters,length=8):
    ''' Returns a random password at a given length based on a character-set.'''
    result = ''
    for i in range(length):
        result = result + getsalt(chars,1)
    return result

def check_password(s):
    ''' Returns true or false if the argument is concidered a strong password.
        The password must meat certain rules.. like:
        both small and CAPITALIZED characters, numbers and special characters 
        such as .,/!"# etc
    '''
    return True

def mkpasswd(pwd,sambaver=3,default='ssha'):
    ''' Make a given password cryptated, possibly with different 
        crypt-algorihtms. This module was written for use with 
        LDAP - so default is seeded sha
    '''
    alg = {
        'ssha':'Seeded SHA',
        'sha':'Secure Hash Algorithm',
        'md5':'MD5',
        'smd5':'Seeded MD5',
        'crypt':'standard unix crypt'
    }
    if smb:
        alg['lmhash'] = 'lan man hash'
        alg['nthash'] = 'nt hash'
    if default not in alg.keys():
        return 'algorithm <%s> not supported in this version.' % default
    else:
        salt = getsalt()
        if default == 'ssha':
            return "{SSHA}" + base64.encodestring(sha.new(str(pwd) + salt).digest() + salt)
        elif default =='sha':
            return "{SHA}" + base64.encodestring(sha.new(str(pwd)).digest())
        elif default =='md5':
            return "{MD5}" + base64.encodestring(md5.new(str(pwd)).digest())
        elif default == 'smd5':
            return "{SMD5}" + base64.encodestring(md5.new(str(pwd) + salt).digest() + salt)
        elif default =='crypt':
            return "{CRYPT}" + crypt.crypt(str(pwd),getsalt(length=2)) # crypt only uses a salt of length 2
        elif default == 'lmhash':
            if sambaver==3:
                return "{sambaLMPassword}" + smbpasswd.lmhash(pwd)
            elif sambaver==2:
                return "{lmPassword}" + smbpasswd.lmhash(pwd)
        elif default == 'nthash':
            if sambaver==3:
                return "{sambaNTPassword}" + smbpasswd.lmhash(pwd)
            elif sambaver==2:
                return "{NTPassword}" + smbpasswd.lmhash(pwd)

def check_strength(passwordString=""):

    def check_length():
        return 13 * pLength

    def check_chars():
        upperBool = False
        lowerBool = False
        specialBool = False
        numberBool = False
        combination = 0

        valueDict = {0:50, 1:50, 2:20, 3:0, 4:0}

        for x in passwordString:
            if (not lowerBool) and (x in string.ascii_lowercase):
                lowerBool = True
                combination += 1
            if (not upperBool) and (x in string.ascii_uppercase):
                upperBool = True
                combination += 1
            if (not numberBool) and (x in string.digits):
                numberBool = True
                combination += 1
            if (not specialBool) and ((x in string.punctuation) \
                    and (not(x in string.ascii_uppercase)) \
                    and (not(x in string.ascii_lowercase)) \
                    and (not(x in string.digits))):
                specialBool = True
                combination += 1
            if upperBool and lowerBool and specialBool and numberBool:
                break

        #print passwordString, combination, upperBool, lowerBool, numberBool, specialBool
        return valueDict[combination]


    def check_distribution():
        tmpDict = {}
        for x in passwordString:
            tmpDict[x] = None

        ratio = pLength / len(tmpDict.keys())
        #if 1 == ratio:
        #    return 0
        return 13 * (ratio-1)

    def check_special_characters():
        tmpVal = 0
        return tmpVal

    pLength = len(passwordString)

    value = check_length()
    #print "Length: ", value
    value -= check_distribution()
    #print "Distribution: ", value
    value -= check_chars()
    #print "Chars: ", value

    if value < 0:
        value = 0

    if value > 100:
        value = 100

    return value


def check_strength_function():

    pwList = ["a", "aA", "aaaaaaaaaaa", "abcdefgh", "aBcDeFgH", "abc123ef",
        "aBc123Ef", "      ", "abC12 \ *+"]

    for x in pwList:
        print x, check_strength(x)