#!/usr/bin/python
# @package      hubzero-openldap
# @file         hzldap
# @author       David R. Benham <dbenham@purdue.edu>
# @copyright    Copyright (c) 2012-2015 HUBzero Foundation, LLC.
# @license      http://opensource.org/licenses/MIT MIT
#
# Copyright (c) 2012-2015 HUBzero Foundation, LLC.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# HUBzero is a registered trademark of HUBzero Foundation, LLC.
#
import argparse
import base64
import ConfigParser
import hashlib
import hubzero.data.db
import hubzero.utilities.misc
import hubzero.utilities.user
import hubzero.config.passwords
import hubzero.config.webconfig
import ldap
import ldap.modlist
import os
import re
import subprocess
import sys
import traceback
from distutils.version import LooseVersion, StrictVersion

hubname = hubzero.config.webconfig.getDefaultSite()
docroot = hubzero.config.hubzerositeconfig.getHubzeroConfigOption(hubname, "DocumentRoot")

version = hubzero.config.webconfig.getCMSversion(docroot)

if StrictVersion(version) >= StrictVersion("1.2.0"):
        hubzero.utilities.misc.JOOMLA_25 = True
else:
    hubzero.utilities.misc.JOOMLA_25 = False


def serviceInit(service, action = 'start'):

    if service == 'apache2':
        if not os.path.exists('/etc/init.d/apache2') and os.path.exists('/etc/init.d/httpd'):
            service = 'httpd'
    if service == 'httpd':
        if not os.path.exists('/etc/init.d/httpd') and os.path.exists('/etc/init.d/apache2'):
            service = 'apache2'
    if service == 'mysql':
        if not os.path.exists('/etc/init.d/mysql') and os.path.exists('/etc/init.d/mysqld'):
            service = 'mysqld'
    if service == 'mysqld':
        if not os.path.exists('/etc/init.d/mysqld') and os.path.exists('/etc/init.d/mysql'):
            service = 'mysql'
    if service == 'crond':
        if not os.path.exists('/etc/init.d/crond') and os.path.exists('/etc/init.d/cron'):
            service = 'cron'
    if service == 'cron':
        if not os.path.exists('/etc/init.d/cron') and os.path.exists('/etc/init.d/crond'):
            service = 'crond'

    serviceScript = '/etc/init.d/' + service

    print "checking " + serviceScript

    if not os.path.exists(serviceScript):
        print "missing " + serviceScript

    print serviceScript + " " + action

    try:
        rc, procStdOut, procStdErr = hubzero.utilities.misc.exShellCommand([serviceScript, action])
    except:
        print serviceScript + " " + action + " failed"
        return -1

    if rc : print procStdErr

    return rc


def replace(path, regexp, replace = None):

    if replace == None or replace == False:
        replace = ''

    if os.path.exists(path):
        with open(path, 'r+') as f:
            ftxt = f.read()
            txt = ftxt.split("\n")

            for i in range(0,len(txt)):
                txt[i] = re.sub(regexp, replace, txt[i])

            txt = '\n'.join(txt)

            if txt != ftxt:
                f.seek(0)
                f.write(txt)
                f.truncate()
                return 1

            return 0


def lineinfile(path, line, regexp = None, insertbefore = None, insertafter = None, create = False, ifexists = False, state = True):
    """search a file for a line, and ensure that it is present or absent.

    Keyword arguments:
    path            -- the file to modify
    line            -- the line to insert/replace into the file
    regexp          -- the regular expression to look for in the file
    insertbefore    -- the line will be inserted before the last match of specified regular expression
    insertafter     -- the line will be inserted after the last match of specified regular expression
    create          -- if true the file will be created if it does not already exist
    state           -- whether the line should be there (true) or not (false)
    ifexists        -- whether to run only if file exists

    Returns:
    0 - No change required
    1 - Changes made
    """

    if insertbefore != None and insertafter != None:
        raise ValueError('invalid argument combination: insertbefore and insertafter')

    if (insertbefore != None or insertafter != None) and not state:
        raise ValueError('invalid argument combination: insertbefore or insertafter with state == false')

    if (not os.path.exists(path)) and create:
        with open(path, 'w+') as f:
            if line != None and state:
                f.write(line)
        return 1

    if (not os.path.exists(path)) and ifexists:
        return 0

    with open(path, 'r+') as f:
        ftxt = f.read()
        txt = ftxt.split("\n")
        regexp_match = -1
        insertafter_match = -1
        insertbefore_match = -1
        line_match = -1

        for i in range(0,len(txt)):
            if regexp == None and txt[i] == line and state:
                return 0

            if txt[i] == line:
                line_match = i
            if (regexp != None and re.match(regexp, txt[i])):
                regexp_match = i
            if (insertafter != None and insertafter  != 'EOF' and re.match(insertafter,  txt[i])):
                insertafter_match = i
            if (insertbefore != None and insertbefore != 'BOF' and re.match(insertbefore, txt[i])):
                insertbefore_match= i

        if state:
            if (regexp_match != -1 and insertafter == None and insertbefore == None):
                txt[regexp_match] = line
            elif (regexp_match != -1):
                return 0
            elif (insertbefore == 'BOF'):
                txt.insert(0, line)
            elif (insertafter == 'EOF'):
                txt.append(line)
            elif (insertafter_match != -1):
                txt.insert(insertafter_match + 1, line)
            elif (insertbefore_match != -1):
                txt.insert(insertbefore_match + 1, line)
            elif (insertafter != None):
                txt.append(line)
            elif (insertbefore != None):
                txt.append(line)
            else:
                txt.append(line)
        else:
            if regexp_match != -1:
                txt.pop(regexp_match)
            elif line_match != -1:
                txt.pop(line_match)

        txt = '\n'.join(txt)

        if txt != ftxt:
            f.seek(0)
            f.write(txt)
            f.truncate()
            return 1

        return 0


def _write_nslcd_conf(suffix, searchAcctDN, searchAcctPW, adminAcctDN):
    
    fileText = """

# /etc/nslcd.conf
# nslcd configuration file. See nslcd.conf(5)
# for details.

# The user and group nslcd should run as.
uid nslcd
gid nslcd

# The location at which the LDAP server(s) should be reachable.
uri ldap://localhost/

# The search base that will be used for all queries.
base %suffix%

# The LDAP protocol version to use.
ldap_version 3

# The DN to bind with for normal lookups.
binddn %searchAcctDN%
bindpw %searchAcctPW%

# The DN used for password modifications by root.
#rootpwmoddn %adminAcctDN% 

# SSL options
#ssl off
#tls_reqcert demand
#tls_cacertfile /etc/ssl/certs/ca-bundle.crt

# The search scope.
#scope sub

# initial server connection time limit (default 30 s is too long)
bind_timelimit 1

# Search timelimit.
timelimit 5

# Reconnect policy.
reconnect_sleeptime 0

# The search scope.
#scope sub

base passwd ou=users,%suffix%
base shadow ou=users,%suffix%
base group ou=groups,%suffix%
scope passwd one
scope group one
scope shadow one
filter shadow (&(objectClass=posixAccount)(host=web))
pam_authz_search (&(objectClass=posixAccount)(uid=$username)(host=web))

"""

    filename = "/etc/nslcd.conf"

    fileText = fileText.replace("%suffix%", suffix)
    fileText = fileText.replace("%searchAcctDN%", searchAcctDN)
    fileText = fileText.replace("%searchAcctPW%", searchAcctPW)
    fileText = fileText.replace("%adminAcctDN%", adminAcctDN)
    
    f1 = open(filename, 'w')
    f1.write(fileText)
    f1.close();
    
    hubzero.utilities.misc.exShellCommand(['chmod', '0640', filename])
    hubzero.utilities.misc.exShellCommand(['chown', 'root.nslcd', filename])



def _write_nsswitch_conf():
    
    fileText = """# /etc/nsswitch.conf
#
# Example configuration of GNU Name Service Switch functionality.
# If you have the `glibc-doc-reference' and `info' packages installed, try:
# `info libc "Name Service Switch"' for information about this file.

passwd:         %r1%
group:          %r1%
shadow:         %r1%

hosts:          files dns
networks:       files

protocols:      db files
services:       db files
ethers:         db files
rpc:            db files

netgroup:       nis    
"""

    if hubzero.utilities.misc.isRHEL():
        fileText = fileText.replace("%r1%", 'files sss')

    if hubzero.utilities.misc.isDebian():
        fileText = fileText.replace("%r1%", 'compat ldap')

    filename = "/etc/nsswitch.conf"

    f1 = open(filename, 'w')
    f1.write(fileText)
    f1.close();
    
    hubzero.utilities.misc.exShellCommand(['chmod', '0644', filename])
    hubzero.utilities.misc.exShellCommand(['chown', 'root.root', filename])


def getAlphaNumericPW():
    validPW = False
    while not validPW:
        print "Enter user password and press return"
        pw = raw_input("Enter an password")
    
        p = re.compile("[a-zA-Z0-9]*")
        m = p.match(pw)
    
        if not m:
            print "Password invalid - must be alphanumeric"
        else:
            validPW = True

    return pw


def removeConfigEntry(dbnum, dbtype, name):
    """Remove configuration entries"""
    
    print "removing '%s' from dn: olcDatabase={%d}%s,cn=config" % (name,dbnum,dbtype)

    ldifData = ("dn: olcDatabase={%s}" + dbtype + ",cn=config\n"
        "changetype: modify\n"
        "delete: %s\n")

    ldifData = ldifData % (dbnum, name)

    rc, procOutput, procError = hubzero.utilities.misc.exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)

    if rc and "No such attribute (16)" not in procError:
            print procOutput.rstrip() 
            print procError.rstrip()


def removeOlcAccessEntry(dbnum, dbtype):
    """Remove any previous olcAccess entires for specified database"""
    
    ldifData = ("dn: olcDatabase={%s}" + dbtype + ",cn=config\n"
        "changetype: modify\n"
        "delete: olcAccess\n"
        "-\n"
        "delete: olcTLSCertificateFile\n"
        "-\n"
        "delete: olcTLSCertificateKeyFile")

    ldifData = ldifData % dbnum

    rc, procOutput, procError = hubzero.utilities.misc.exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)

    if rc: 
        print procOutput.rstrip() 
        print procError.rstrip()


def change_olcSuffix(dbnum, dbtype, newSuffix):
    """Modify the olcSuffix for the specified DB number"""
    
    ldifData = ("dn: olcDatabase={%s}" + dbtype + ",cn=config\n"
                "changetype: modify\n"
                "replace: olcSuffix\n"
                "olcSuffix: %s\n"
                "-\n"
                "replace: olcRootDN\n"
                "olcRootDN: cn=Manager,%s")

    ldifData = ldifData % (dbnum, newSuffix, newSuffix)

    rc, procOutput, procError = hubzero.utilities.misc.exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
    if rc: 
        print procOutput 
        print procError
    else: 
        print procOutput


def remove_olcRootPW(dbnum, dbtype):
    """Remove the olcRootPW entry for the specified DB number"""
    
    ldifData = ("dn: olcDatabase={%s}" + dbtype + ",cn=config\n"
                "changetype: modify\n"
                "delete: olcRootPW")

    ldifData = ldifData % (dbnum)
    
    rc, procOutput, procError = hubzero.utilities.misc.exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
    if rc: 
        print procOutput 
        print procError
    else: 
        print procOutput


def getDBNumber():
    
    rc, procOutput, procErr = hubzero.utilities.misc.exShellCommand(['slapcat', '-b', 'cn=config', '-a', '(objectClass=olcDatabaseConfig)'])
        
    # find the number of the min olcDatabase entries that has the format bdb 
    minbdbDBNumber = 999
    for m in re.finditer("olcDatabase=\{(\d+)\}bdb", procOutput):
        if int(m.group(1)) < minbdbDBNumber:
            minbdbDBNumber = int(m.group(1))

    # find the number of the min olcDatabase entries that has the format hdb 
    minhdbDBNumber = 999
    for m in re.finditer("olcDatabase=\{(-?\d+)\}hdb", procOutput):
        if int(m.group(1)) < minhdbDBNumber:
            minhdbDBNumber = int(m.group(1))

    # find the number of the min olcDatabase entries that has the format mdb
    minmdbDBNumber = 999
    for m in re.finditer("olcDatabase=\{(-?\d+)\}mdb", procOutput):
        if int(m.group(1)) < minmdbDBNumber:
            minmdbDBNumber = int(m.group(1))

    # This way, if all databses use hdb or all use bdb, or even if there is a mix
    # we take the lowest (first) one
    if (minbdbDBNumber < minhdbDBNumber) and (minbdbDBNumber < minmdbDBNumber):
        return minbdbDBNumber, 'bdb'
    if (minhdbDBNumber < minbdbDBNumber) and (minhdbDBNumber < minmdbDBNumber):
        return minhdbDBNumber, 'hdb'
    if (minmdbDBNumber < minhdbDBNumber) and (minmdbDBNumber < minbdbDBNumber):
        return minmdbDBNumber, 'mdb'


def addOlcAccessEntries(dbnum, dbtype, suffix):

    # our access rules    
    ldifData = ('dn: olcDatabase={%1}' + dbtype + ',cn=config\n'
                    'changetype: modify\n'
                    'add: olcAccess\n'
                    'olcAccess: {0}to dn.one="%2"\n'
                    '  filter=(objectClass=simpleSecurityObject) attrs=userPassword\n'
                    '  by dn="cn=admin,%2" write\n'
                    '  by dn="cn=syncuser,%2" read\n'
                    '  by anonymous auth\n'
                    '  by * none\n'
                    'olcAccess: {1}to dn.one="ou=users,%2"\n'
                    '  filter=(objectClass=posixAccount) attrs=userPassword\n'
                    '  by dn="cn=admin,%2" write\n'
                    '  by dn="cn=syncuser,%2" read\n'
                    '  by anonymous auth\n'
                    '  by * none\n'
                    'olcAccess: {2}to dn.one="ou=users,%2"\n'
                    '  filter=(objectClass=posixAccount)\n'
                    '  attrs=entry,objectClass,cn,gecos,gidNumber,homeDirectory,loginShell,uid,uidNumber,host\n'
                    '  by dn="cn=admin,%2" write\n'
                    '  by dn="cn=syncuser,%2" read\n'
                    '  by dn="cn=search,%2" read\n'
                    '  by self read\n'
                    '  by * none\n'
                    'olcAccess: {3}to dn.one="ou=users,%2"\n'
                    '  filter=(&(objectClass=posixAccount)(objectClass=shadowAccount))\n'
                    '  attrs=shadowExpire,shadowFlag,shadowInactive,shadowLastChange,shadowMax,shadowMin,shadowWarning\n'
                    '  by dn="cn=admin,%2" write\n'
                    '  by dn="cn=syncuser,%2" read\n'
                    '  by dn="cn=search,%2" read\n'
                    '  by * none\n'
                    'olcAccess: {4}to dn.one="ou=users,%2"\n'
                    '  filter=(objectClass=posixAccount)\n'
                    '  by dn="cn=admin,%2" write\n'
                    '  by dn="cn=update,%2" read\n'
                    '  by * none\n'
                    'olcAccess: {5}to dn.one="ou=groups,%2"\n'
                    '  filter=(objectClass=posixGroup)\n'
                    '  attrs=entry,objectClass,cn,gidNumber,memberUid,userPassword\n'
                    '  by dn="cn=admin,%2" write\n'
                    '  by dn="cn=syncuser,%2" read\n'
                    '  by dn="cn=search,%2" read\n'
                    '  by * none\n'
                    'olcAccess: {6}to dn.one="ou=groups,%2"\n'
                    '  filter=(objectClass=posixGroup)\n'
                    '  by dn="cn=admin,%2" write\n'
                    '  by dn="cn=syncuser,%2" read\n'
                    '  by * none\n'
                    'olcAccess: {7}to dn.subtree="%2"\n'
                    '  by dn="cn=admin,%2" write\n'
                    '  by dn="cn=syncuser,%2" read\n'
                    '  by dn="cn=search,%2" search\n'
                    '  by * none')
    
    ldifData =  ldifData.replace('%1', str(dbnum))
    ldifData =  ldifData.replace('%2', suffix)
    
    #print "olcAccess" + ldifData
    
    rc, procOutput, procError = hubzero.utilities.misc.exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
    if rc: 
        print procOutput.rstrip()
        print procError.rstrip()




def _init_ldap(args):

    # See if account info is already stored, i.e. ldap has already been setup for us
    adminUserDN = hubzero.config.hubzerositeconfig.getHubzeroConfigOption('ldap','adminuserDN')
    searchUserDN = hubzero.config.hubzerositeconfig.getHubzeroConfigOption('ldap','adminuserDN')
    syncUserDN = hubzero.config.hubzerositeconfig.getHubzeroConfigOption('ldap','adminuserDN')

    adminUserPW = hubzero.config.hubzerositeconfig.getHubzeroConfigOption('ldap','adminuserPW')
    searchUserPW = hubzero.config.hubzerositeconfig.getHubzeroConfigOption('ldap','adminuserPW')
    syncUserPW = hubzero.config.hubzerositeconfig.getHubzeroConfigOption('ldap','adminuserDPW')

    # if ldap has not been previously setup
    if not adminUserDN:    

        # prompt for passwords or create from scratch
        if not args.promptforadminpw:
            adminUserPW = hubzero.config.passwords.generateAlphaNumPassword(10)
        else:
            adminUserPW = getAlphaNumericPW()

        if not args.promptforsearchpw:
            searchUserPW = hubzero.config.passwords.generateAlphaNumPassword(10)
        else:
            searchUserPW = getAlphaNumericPW()
    
        if not args.promptforsyncuserpw:
            syncUserPW = hubzero.config.passwords.generateAlphaNumPassword(10)
        else:
            syncUserPW = getAlphaNumericPW()
    

        print "getting current info via slapcat"
        rc, procOutput, procErr = hubzero.utilities.misc.exShellCommand(['slapcat', '-b', 'cn=config', '-a', '(objectClass=olcDatabaseConfig)'])
    
        # We'll use this later
        fullSlapcatOutput = procOutput
    
        # find the number of the max olcDatabase entries
        maxDBNumber, dbtype = getDBNumber()
    
        # find the oldDbDirectory for the max db 
        rc, procOutput, procErr = hubzero.utilities.misc.exShellCommand(['slapcat', '-b', 'cn=config', '-a', '(&(objectClass=olcDatabaseConfig)(olcDatabase={' + str(maxDBNumber) + '}' + dbtype + '))'])
    
        matchObj = re.search(r'olcDbDirectory: (.*)$' , procOutput, re.M)
        if matchObj:
            print 'found olcDbDirectory for for olcDatabase={' + str(maxDBNumber) + '}' + dbtype + ': ' + matchObj.group(1)
            configFileDir = matchObj.group(1)
        else:
            print 'Error, cannot find an entry for olcDbDirectory for (olcDatabase={' + str(maxDBNumber) + '}' + dbtype + ')'
            return(1)
    
        if dbtype != 'mdb':
            # See if this file exists for this database
            configFileName = configFileDir + '/DB_CONFIG'
            print "checking " + configFileName

            if not os.path.exists(configFileName):
                print configFileDir + "/DB_CONFIG file not present, using defaults"
    
                # add the data to ldap
                ldifData = ("dn: olcDatabase={"+ str(maxDBNumber) +"}" + dbtype + ",cn=config\n"
                "changetype: modify\n"
                "add: olcDbConfig\n"
                "olcDbConfig: set_cachesize 0 268435456 1\n"
                "olcDbConfig: set_lg_regionmax 262144\n"
                "olcDbConfig: set_lg_bsize 2097152")
    
                rc, procOutput, procError = hubzero.utilities.misc.exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
                if rc: 
                    print procOutput 
                    print procError
                else: 
                    print procOutput

                # restarting the server should generate the file we were looking for from the attribute mods we did above
                rc, procOutput, procError = hubzero.utilities.misc.exShellCommand(['service', 'slapd', 'restart'])
    
        # See if our ldap db is empty
        rc, procOutput, procErr = hubzero.utilities.misc.exShellCommand(['slapcat', '-n', str(maxDBNumber)])
        
        #if procOutput:
        #    print "Current DB does not appear to be empty"
        #    print procOutput
        #    exit(1)

        # get the olcSuffix
        matchObj = re.search(r'olcSuffix: (.*)$' , fullSlapcatOutput, re.M)
        if matchObj:
            print "found olcSuffix: " + matchObj.group(1)
            suffix = matchObj.group(1)
        else:
            print "Error, cannot find olcSuffix"
            exit(1)    

        ldapManagerUserDN = "cn=Manager," + suffix

        # Since the database was empty, we're assuming it's ok to clear out any existing rules
        removeConfigEntry(maxDBNumber, dbtype, "olcAccess")
        removeConfigEntry(maxDBNumber, dbtype, "olcTLSCertificateFile")
        removeConfigEntry(maxDBNumber, dbtype, "olcTLSCertificateKeyFile")
        print "adding olcAccess entires"
        addOlcAccessEntries(maxDBNumber, dbtype, suffix)

        # if olcRootPW doesn't exist, make it
        if fullSlapcatOutput.find('olcRootPW:') == -1:

            print "olcRootPW doesn't exist, adding it"
            
            # create a root user for the new database to allow remote connections
            ldifData = ("dn: olcDatabase={%s}" + dbtype + ",cn=config\n"
                        "changetype: modify\n"
                        "replace: olcRootDN\n"
                        "olcRootDN: %s\n"
                        "-\n"
                        "add: olcRootPW\n"
                        "olcRootPW: %s")

        else:

            print "olcRootPW already exists, changing it"

            # create a root user for the new database to allow remote connections
            ldifData = ("dn: olcDatabase={%s}" + dbtype + ",cn=config\n"
                        "changetype: modify\n"
                        "replace: olcRootDN\n"
                        "olcRootDN: %s\n"
                        "-\n"
                        "replace: olcRootPW\n"
                        "olcRootPW: %s")
        
        ldapPWHash = "{MD5}" + base64.encodestring(hashlib.md5(str(adminUserPW)).digest())
        ldifData = ldifData % (maxDBNumber, ldapManagerUserDN, ldapPWHash)

        rc, procOutput, procError = hubzero.utilities.misc.exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
        if rc: 
            print procError
            print procOutput 


        # Switch over to native ldap calls for the remainder of the setup using the above user and pw we just setup
        print "attempting ldap bind to localhost"

        l = ldap.initialize('ldap://localhost')
        print "ldapManagerUserDN=" + ldapManagerUserDN,
        print "adminUserPW=" + adminUserPW
        l.simple_bind_s(ldapManagerUserDN, adminUserPW)

        # create top level entry if it's not already there. For some reason this fails with an 
        # ldap.NO_SUCH_OBJECT error if it's not there, unlike other ldap searches that return 
        # an empty result set. This is why we have odd exception handling here and noplace else
        dn = suffix
        resultid = l.search(suffix, ldap.SCOPE_SUBTREE, '(&(objectclass=top)(objectclass=dcObject))', None)
        try:
            result_type, result_data = l.result(resultid, 0)
            if not result_data == []:
                print "top level entry already exists"

        except ldap.NO_SUCH_OBJECT:
            attrs = {}
            attrs['objectclass'] = ['top', 'dcObject', 'organization']
            attrs['o'] = suffix
            ldif = ldap.modlist.addModlist(attrs)
            print "adding root container"
            l.add_s(dn, ldif)
    

        # create admin
        dn = "cn=admin," + suffix
        resultid = l.search(suffix, ldap.SCOPE_SUBTREE, '(&(&(objectclass=organizationalRole)(objectclass=simpleSecurityObject))(cn=admin))', None)
        result_type, result_data = l.result(resultid, 0)
        if not result_data == []:
            print "admin user already exists, deleting and readding"
            l.delete_s(dn)
        
        attrs = {}
        attrs['objectclass'] = ['organizationalRole', 'simpleSecurityObject']
        attrs['cn'] = 'admin'
        attrs['userPassword'] = ldapPWHash = "{MD5}" + base64.encodestring(hashlib.md5(str(adminUserPW)).digest())
        ldif = ldap.modlist.addModlist(attrs)
        print "adding admin user"
        l.add_s(dn, ldif)


        # create sync user
        dn = "cn=syncuser," + suffix
        resultid = l.search(suffix, ldap.SCOPE_SUBTREE, '(&(&(objectclass=organizationalRole)(objectclass=simpleSecurityObject))(cn=syncuser))', None)
        result_type, result_data = l.result(resultid, 0)
    
        if not result_data == []:
            print "syncuser user already exists, deleting and readding"
            l.delete_s(dn)
    
        attrs = {}
        attrs['objectclass'] = ['organizationalRole', 'simpleSecurityObject']
        attrs['cn'] = 'syncuser'
        attrs['userPassword'] = ldapPWHash = "{MD5}" + base64.encodestring(hashlib.md5(str(syncUserPW)).digest())
        ldif = ldap.modlist.addModlist(attrs)
        print "adding sync user"
        l.add_s(dn, ldif)


        # create search user
        dn = "cn=search," + suffix
        resultid = l.search(suffix, ldap.SCOPE_SUBTREE, '(&(&(objectclass=organizationalRole)(objectclass=simpleSecurityObject))(cn=search))', None)
        result_type, result_data = l.result(resultid, 0)
        if not result_data == []:
            print "search user already exists, deleting and readding"
            l.delete_s(dn)
    
        attrs = {}
        attrs['objectclass'] = ['organizationalRole', 'simpleSecurityObject']
        attrs['cn'] = 'search'
        attrs['userPassword'] = ldapPWHash = "{MD5}" + base64.encodestring(hashlib.md5(str(searchUserPW)).digest())
        ldif = ldap.modlist.addModlist(attrs)
        print "adding search user"
        l.add_s(dn, ldif)


        # add users ou
        dn = "ou=users," + suffix
        resultid = l.search(suffix, ldap.SCOPE_SUBTREE, '(&(objectclass=organizationalUnit)(ou=users))', None)
        result_type, result_data = l.result(resultid, 0)
        if not result_data == []:
            print "ou=users already exists"
        else:
            attrs = {}
            attrs['objectclass'] = ['organizationalUnit']
            attrs['ou'] = 'users'
            attrs['description'] = 'Users container'
            ldif = ldap.modlist.addModlist(attrs)
            print "adding users root ou container"
            l.add_s(dn, ldif)


        # add groups ou
        dn = "ou=groups," + suffix
        resultid = l.search(suffix, ldap.SCOPE_SUBTREE, '(&(objectclass=organizationalUnit)(ou=groups))', None)
        result_type, result_data = l.result(resultid, 0)
        if not result_data == []:
            print "ou=groups already exists"
        else:
            attrs = {}
            attrs['objectclass'] = ['organizationalUnit']
            attrs['ou'] = 'groups'
            attrs['description'] = 'Groups container'
            ldif = ldap.modlist.addModlist(attrs)
            print "adding groups root ou container"
            l.add_s(dn, ldif)
    

        print "unbinding from ldap " + suffix
        l.unbind();


        ## Some debain/redhat differences from here on out

        ## RHEL
        if hubzero.utilities.misc.isRHEL():
            sssdConfFilename = "/etc/sssd/sssd.conf"
            sssdConfConfig = ConfigParser.ConfigParser()
            sssdConfConfig.optionxform = str

            if os.path.exists(sssdConfFilename):
                f1 = open(sssdConfFilename, "r")
                sssdConfConfig.readfp(f1)
                f1.close()

            if not sssdConfConfig.has_section("domain/default"):
                sssdConfConfig.add_section("domain/default")
            sssdConfConfig.set("domain/default", "autofs_provider", 'ldap')
            sssdConfConfig.set("domain/default", "cache_credentials", 'True')
            sssdConfConfig.set("domain/default", "id_provider", 'ldap')
            sssdConfConfig.set("domain/default", "auth_provider", 'ldap')
            sssdConfConfig.set("domain/default", "chpass_provider", 'ldap')
            sssdConfConfig.set("domain/default", "ldap_tls_cacertdir", '/etc/openldap/cacerts')
            if not sssdConfConfig.has_section("sssd"):
                sssdConfConfig.add_section("sssd")
            sssdConfConfig.set("sssd", "services", 'nss, pam, autofs')
            sssdConfConfig.set("sssd", "domains", 'default, ldap')
            sssdConfConfig.set("sssd", "config_file_version", '2')
            if not sssdConfConfig.has_section("nss"):
                sssdConfConfig.add_section("nss")
            sssdConfConfig.set("nss", "homedir_substring", '/home')
            sssdConfConfig.set("nss", "filter_groups", 'root')
            sssdConfConfig.set("nss", "filter_users", 'root')

            if not sssdConfConfig.has_section("domain/LDAP"):
                sssdConfConfig.add_section("domain/LDAP")
            sssdConfConfig.set("domain/LDAP", "enumerate", 'true')
            sssdConfConfig.set("domain/LDAP", "tls_reqcert", 'never')
            sssdConfConfig.set("domain/LDAP", "access_provider", 'ldap')
            sssdConfConfig.set("domain/LDAP", "ldap_user_search_base", "ou=users," + suffix)
            sssdConfConfig.set("domain/LDAP", "ldap_group_search_base", "ou=groups," + suffix)
            sssdConfConfig.set("domain/LDAP", "ldap_default_bind_dn", "cn=admin," + suffix)
            sssdConfConfig.set("domain/LDAP", "ldap_default_authtok_type", 'password')
            sssdConfConfig.set("domain/LDAP", "ldap_default_authtok", adminUserPW)
            sssdConfConfig.set("domain/LDAP", "ldap_id_use_start_tls", 'False')
            sssdConfConfig.set("domain/LDAP", "ldap_auth_disable_tls_never_use_in_production", 'true')
            sssdConfConfig.set("domain/LDAP", "id_provider", 'ldap')
            sssdConfConfig.set("domain/LDAP", "ldap_uri", 'ldap://localhost')
            sssdConfConfig.set("domain/LDAP", "ldap_access_filter", 'host=web')

            f2 = os.open(sssdConfFilename, os.O_RDWR | os.O_CREAT, 0600)
            os.fchmod(f2, 0600)
            f3 = os.fdopen(f2,"w")
            sssdConfConfig.write(f3)
            f3.close()

            # These aren't all strictly necessary, but are included for completeness of being equivalent to
            # authconfig call being replaced. They are also very fragile, a much more complex
            # and "intelligent" pam.d config file modification function would be needed to
            # improve this.
            #
            # @TODO: is broken_shadow needed?

            replace(path = '/etc/pam.d/password-auth-ac',         regexp = r'^\s*auth\s+(required|requisite)\s+.*pam_unix\.so.*$',           replace='auth        sufficient    pam_unix.so nullok try_first_pass');
            lineinfile(path = '/etc/pam.d/password-auth-ac',    insertafter = r'^\s*auth\s+.*pam_unix\.so.*$',           line='auth        sufficient    pam_sss.so use_first_pass');
            replace(path = '/etc/pam.d/password-auth-ac',   regexp =r'(^\s*auth\s*.*)(success\s*=\s*1)(.*pam_unix\.so\s+.*$)', replace='\\1success=2\\3')
            
            replace(path = '/etc/pam.d/system-auth-ac',           regexp = r'^\s*auth\s+(required|requisite)\s+.*pam_unix\.so.*$',           replace='auth        sufficient    pam_unix.so nullok try_first_pass');
            lineinfile(path = '/etc/pam.d/system-auth-ac',      insertafter = r'^\s*auth\s+.*pam_unix\.so.*$',           line='auth        sufficient    pam_sss.so use_first_pass');
            replace(path = '/etc/pam.d/system-auth-ac',   regexp =r'(^\s*auth\s*.*)(success\s*=\s*1)(.*pam_unix\.so\s+.*$)', replace='\\1success=2\\3')

            lineinfile(path = '/etc/pam.d/fingerprint-auth-ac',      regexp = r'^\s*account\s+.*pam_unix\.so.*$',        line='account     required      pam_unix.so broken_shadow');
            lineinfile(path = '/etc/pam.d/password-auth-ac',         regexp = r'^\s*account\s+.*pam_unix\.so.*$',        line='account     required      pam_unix.so broken_shadow');
            lineinfile(path = '/etc/pam.d/smartcard-auth-ac',        regexp = r'^\s*account\s+.*pam_unix\.so.*$',        line='account     required      pam_unix.so broken_shadow');
            lineinfile(path = '/etc/pam.d/system-auth-ac',           regexp = r'^\s*account\s+.*pam_unix\.so.*$',        line='account     required      pam_unix.so broken_shadow');

            lineinfile(path = '/etc/pam.d/smartcard-auth-ac',   insertafter = r'^\s*account\s+sufficient\s+pam_succeed', line='account     [default=bad success=ok user_unknown=ignore] pam_sss.so');
            lineinfile(path = '/etc/pam.d/password-auth-ac',    insertafter = r'^\s*account\s+sufficient\s+pam_succeed', line='account     [default=bad success=ok user_unknown=ignore] pam_sss.so');
            lineinfile(path = '/etc/pam.d/system-auth-ac',      insertafter = r'^\s*account\s+sufficient\s+pam_succeed', line='account     [default=bad success=ok user_unknown=ignore] pam_sss.so');
            lineinfile(path = '/etc/pam.d/fingerprint-auth-ac', insertafter = r'^\s*account\s+sufficient\s+pam_succeed', line='account     [default=bad success=ok user_unknown=ignore] pam_sss.so');
            lineinfile(path = '/etc/pam.d/password-auth-ac',    insertafter = r'^\s*password\s+.*pam_unix\.so.*$',       line='password    sufficient    pam_sss.so use_authtok');
            lineinfile(path = '/etc/pam.d/system-auth-ac',      insertafter = r'^\s*password\s+.*pam_unix\.so.*$',       line='password    sufficient    pam_sss.so use_authtok');
            lineinfile(path = '/etc/pam.d/fingerprint-auth-ac', insertafter = r'^\s*session\s+.*pam_unix\.so.*$',        line='session     optional      pam_sss.so');
            lineinfile(path = '/etc/pam.d/password-auth-ac',    insertafter = r'^\s*session\s+.*pam_unix\.so.*$',        line='session     optional      pam_sss.so');
            lineinfile(path = '/etc/pam.d/smartcard-auth-ac',   insertafter = r'^\s*session\s+.*pam_unix\.so.*$',        line='session     optional      pam_sss.so');
            lineinfile(path = '/etc/pam.d/system-auth-ac',      insertafter = r'^\s*session\s+.*pam_unix\.so.*$',        line='session     optional      pam_sss.so');

            lineinfile(path = '/etc/nsswitch.conf', regexp = r'\s*passwd:\s*.*$', line='passwd: files sss')
            lineinfile(path = '/etc/nsswitch.conf', regexp = r'\s*shadow:\s*.*$', line='shadow: files sss')
            lineinfile(path = '/etc/nsswitch.conf', regexp = r'\s*group:\s*.*$', line='group: files sss')
            lineinfile(path = '/etc/nsswitch.conf', regexp = r'\s*services:\s*.*$', line='services: files sss')
            lineinfile(path = '/etc/nsswitch.conf', regexp = r'\s*netgroup:\s*.*$', line='netgroup: files sss')
            lineinfile(path = '/etc/nsswitch.conf', regexp = r'\s*automount:\s*.*$', line='automount: files sss')

            serviceInit('sssd', 'restart')
            serviceInit('slapd', 'restart')

            print "Setting slapd for autostart"
            if os.path.exists('/etc/init.d/slapd'):
                rc, procOutput, procError = hubzero.utilities.misc.exShellCommand(['chkconfig', 'slapd', 'on'])
                #print procOutput.strip()
                if rc: 
                    print procError


        ## Debian
        if hubzero.utilities.misc.isDebian():

            print "writing out /etc/nslcd.conf"
            _write_nslcd_conf(suffix, "cn=search," + suffix, searchUserPW, "cn=admin," + suffix)

            serviceInit('nslcd','stop')
            serviceInit('nslcd','start')
            serviceInit('nscd','stop')
            serviceInit('nscd','start')

            print "writing out /etc/nsswitch.conf"
            _write_nsswitch_conf()


        # Write ldap info to the /etc/hubzero.conf file 
        hubzero.config.hubzerositeconfig.setHubzeroConfigOption('ldap', 'adminuserDN', adminUserDN) 
        hubzero.config.hubzerositeconfig.setHubzeroConfigOption('ldap', 'searchuserDN', searchUserDN) 
        hubzero.config.hubzerositeconfig.setHubzeroConfigOption('ldap', 'syncuserDN', syncUserDN) 
        hubzero.config.hubzerositeconfig.setHubzeroConfigOption('ldap', 'adminuserPW', adminUserPW) 
        hubzero.config.hubzerositeconfig.setHubzeroConfigOption('ldap', 'searchuserPW', searchUserPW) 
        hubzero.config.hubzerositeconfig.setHubzeroConfigOption('ldap', 'syncuserPW', syncUserPW) 

    # end if for ldap already being setup for us


    # update the jos_components table record for the com_system component
    hubzero.config.webconfig.addComponentParam("com_system", "ldap_managerdn" , "cn=admin," + suffix)
    hubzero.config.webconfig.addComponentParam("com_system", "ldap_managerpw" , adminUserPW)
    hubzero.config.webconfig.addComponentParam("com_system", "ldap_basedn" , suffix)
    hubzero.config.webconfig.addComponentParam("com_system", "ldap_searchdn" , "cn=search," + suffix)
    hubzero.config.webconfig.addComponentParam("com_system", "ldap_searchpw" , searchUserPW)

    # add hubrepo password to hubzero.secrets file, if file is not there, create it
    hubzeroSecretsFilename = "/etc/hubzero.secrets"
    secretsConfig = ConfigParser.ConfigParser()
    secretsConfig.optionxform = str

    # read in existing data from hubzero.secrets
    if os.path.exists(hubzeroSecretsFilename):
        f1 = open(hubzeroSecretsFilename, "r")
        secretsConfig.readfp(f1)
        f1.close()

    # write our new password data
    secretsConfig.set("DEFAULT", "LDAP-ADMINPW", adminUserPW)
    secretsConfig.set("DEFAULT", "LDAP-SEARCHPW", searchUserPW)
    secretsConfig.set("DEFAULT", "LDAP-SYNCPW", syncUserPW)

    f2 = open(hubzeroSecretsFilename, "w")
    secretsConfig.write(f2)
    f2.close()

    print ""
    print "==============================================="
    print "LDAP default setup complete"
    print "==============================================="
    print "LDAP admin user pw: " + adminUserPW
    print "LDAP search user pw: " + searchUserPW
    print "LDAP sync user pw: " + syncUserPW
    print ""
    print "LDAP config written to com_system component params field in database: " + hubzero.config.webconfig.getDefaultSite()
    print ""

    return(0)
    

def _delete_ldap_user(args):
    hubzero.utilities.user._delete_ldap_user(args.username)


def _add_ldap_user(args):
    
    username = args.username
    suffix = getLDAPSiteSuffix()
    
    if not args.promptforpw:
        pw = hubzero.config.passwords.generateAlphaNumPassword(10)
    else:
        pw = getAlphaNumericPW()    

    hubzero.utilities.user._add_ldap_user(username, pw, args.uidnumber, 'none')
    
    print "New user pw: " + pw


def _sync_ldap_users(args):
    hubzero.utilities.user.syncusers("", args.purgeorphans, args.syncattributes)


def _sync_ldap_groups(args):
    hubzero.utilities.group.syncgroups("", args.purgeorphans, args.addgroupmemberships)


def _delete_ldap_groups(args):
    
    # by only deleting groups in db, any ldap only groups will be untouched
    if not args.deleteorphans:
        print "Orphaned groups in ldap without a corresponding group in the database will be preserved"

        dbPW = hubzero.config.webconfig.getWebConfigDBPassword()
        dbUserName = hubzero.config.webconfig.getWebConfigDBUsername()
        dbName = hubzero.config.webconfig.getWebConfigDBName()
        db =  hubzero.data.db.MySQLConnection("localhost", dbName, dbUserName, dbPW)
    
        # grab all users groups db 
        sql = "SELECT `cn`, `gidNumber` from jos_xgroups"

        for group in db.query(sql, None):
            print "Deleting " + group.cn

            try:
                hubzero.utilities.group.del_ldap_group(group.cn)
            except ldap.NO_SUCH_OBJECT:
                print " Group not found"

    else:
        print "Orphaned groups in ldap without a corresponding group in the database will be DELETED"

        # grab all groups from ldap
        hubLDAPAcctMgrDN = hubzero.config.webconfig.getComponentParam("com_system", "ldap_managerdn")
        hubLDAPAcctMgrPW = hubzero.config.webconfig.getComponentParam("com_system", "ldap_managerpw")
        hubLDAPBaseDN = hubzero.config.webconfig.getComponentParam("com_system", "ldap_basedn")
        siteSuffix = getLDAPSiteSuffix()
        
        l = ldap.initialize('ldap://localhost')
        l.simple_bind_s(hubLDAPAcctMgrDN, hubLDAPAcctMgrPW)
        
        try:
            r = l.search_s("ou=groups," + siteSuffix, ldap.SCOPE_SUBTREE, '(objectClass=posixGroup)',   ['cn'])

        except ldap.NO_SUCH_OBJECT:
            print "No groups"
    
        for dn,entry in r:
            cn = str(entry['cn'][0])
            hubzero.utilities.group.delete_ldap_group(cn)


# #######################################################################
# Main
# main parser
parser = argparse.ArgumentParser(prog="hzldap")
subparsers = parser.add_subparsers()

parser_init = subparsers.add_parser('init', help='initialize hub ldap')
parser_init.add_argument("--promptforadminpw", help="Prompt for password", action="store_true", default=False)
parser_init.add_argument("--promptforsearchpw", help="Prompt for password", action="store_true", default=False)
parser_init.add_argument("--promptforsyncuserpw", help="Prompt for password", action="store_true", default=False)
parser_init.set_defaults(func=_init_ldap)

parser_adduser = subparsers.add_parser('adduser', help='add ldap user')
parser_adduser.add_argument("username", help="username")
parser_adduser.add_argument("--promptforpw", help='prompt for a user password, if not provided, one will be created and output and output to screen', action="store_true", default=False)
parser_adduser.add_argument("--uidnumber", help="Hub UserIDNumber", default="99999999")
parser_adduser.set_defaults(func=_add_ldap_user)

parser_deleteuser = subparsers.add_parser('deleteuser', help='delete ldap user')
parser_deleteuser.add_argument("username", help="username")
parser_deleteuser.set_defaults(func=_delete_ldap_user)

parser_syncusers = subparsers.add_parser('syncusers', help='add all users from db into ldap')
parser_syncusers.add_argument("--purgeorphans", help='remove any ldap user not in the jos_users table', action="store_true", default=True)
parser_syncusers.add_argument("--syncattributes", help='sync non required ldap user attributes', action="store_true", default=True)
parser_syncusers.set_defaults(func=_sync_ldap_users)

parser_syncgroups = subparsers.add_parser('syncgroups', help='add all groups from db into ldap')
parser_syncgroups.add_argument("--addgroupmemberships", help='add newly created users group memberships', action="store_true", default=True)
parser_syncgroups.add_argument("--purgeorphans", help='remove any ldap group not in the jos_xgroups table', action="store_true", default=True)
parser_syncgroups.set_defaults(func=_sync_ldap_groups)

args =  parser.parse_args()
rc = args.func(args)

exit(rc)

