#!/usr/bin/env python
# @package      hubzero-openldap
# @file         hzldapinit
# @author       David R. Benham <dbenham@purdue.edu>
# @copyright    Copyright (c) 2012 HUBzero Foundation, LLC.
# @license      http://www.gnu.org/licenses/lgpl-3.0.html LGPLv3
#
# Copyright (c) 2012 HUBzero Foundation, LLC.
#
# This file is part of: The HUBzero(R) Platform for Scientific Collaboration
#
# The HUBzero(R) Platform for Scientific Collaboration (HUBzero) is free
# software: you can redistribute it and/or modify it under the terms of
# the GNU Lesser General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# HUBzero 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# HUBzero is a registered trademark of HUBzero Foundation, LLC.
#
import argparse
import base64
import hashlib
import hubzero.config.passwords
import hubzero.config.webconfig
import ldap
import ldap.modlist
import os
import re
import subprocess
import sys
import traceback


def write_nslcd_conf():
	pass

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 exShellCommand(argArray, processInput=''):
	""" Wrapper to run and return output of a shell command """
	
	try:
		proc = subprocess.Popen(argArray,
			                    shell=False,
			                    stdin=subprocess.PIPE,
			                    stdout=subprocess.PIPE,
			                    stderr=subprocess.PIPE)
	
		procStdOut, procStdErr = proc.communicate(processInput)
		rc = proc.returncode

	except Exception, ex:	
		raise Exception('exShellCommand error ' + str(argArray) + ' ' + str(ex) + "\n" + traceback.format_exc())

	return rc, procStdOut, procStdErr

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

	ldifData = ldifData % dbnum

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


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 = 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 = exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
	if rc: 
		print procOutput 
		print procError
	else: 
		print procOutput


def getDBNumber():
	
	rc, procOutput, procErr = 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))

	# 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:
		return minbdbDBNumber, 'bdb'
	else:
		return minhdbDBNumber, 'hdb'


def addolcAccessEntries(dbnum, dbtype, suffix):

	print "adding olcAccess entries"

	# 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=update,%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=update,%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\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="cn=update,%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=update,%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,dc=%2"\n'
		            '  filter=(objectClass=posixGroup)\n'
		            '  attrs=entry,objectClass,cn,gidNumber,memberUid,userPassword\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="dc=dev30cn=update,%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=update,%2" read\n'
		            '  by * none\n'
		            'olcAccess: {7}to dn.subtree="%2"\n'
		            '  by dn="cn=admin,%2" write\n'
		            '  by dn="cn=update,%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 = exShellCommand(['ldapmodify', '-Y', 'EXTERNAL', '-H', 'ldapi:///'], ldifData)
	if rc: 
		print procError
		print procOutput 


def defaultSetup(adminUserpw, searchUserpw, syncUserpw):
	
	print "getting current info via slapcat"
	rc, procOutput, procErr = exShellCommand(['slapcat', '-b', 'cn=config', '-a', '(objectClass=olcDatabaseConfig)'])
	
	# Well 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 = 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 + ')'
		exit(1)
	
	# See if this file exists for this database
	configFileName = configFileDir + '/DB_CONFIG'
	print "checking for " + configFileName

	if not os.path.exists(configFileName):
		print configFileDir + "/DB_CONFIG file not present"
	
		# 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 = 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 = exShellCommand(['service', 'slapd', 'restart'])
	else:
		print configFileDir + '/DB_CONFIG' " exists, no need to create"

	
	# See if our ldap db is empty
	rc, procOutput, procErr = 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
	print "Removing olcAccess entires. They might not exist, don't be alarmed by errors complaining about it"
	removeOlcAccessEntry(maxDBNumber, dbtype)
	addolcAccessEntries(maxDBNumber, dbtype, suffix)


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

		print "olcRootPW already exists, 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 = 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 bind to localhost"

	l = ldap.open("localhost")
	l.simple_bind_s(ldapManagerUserDN, adminUserpw)


	# create top level entry if it's not already there
	dn = suffix
	resultid = l.search(suffix, ldap.SCOPE_SUBTREE, '(&(objectclass=top)(objectclass=dcObject))', None)
	result_type, result_data = l.result(resultid, 0)
	if not result_data == []:
		print "top level entry already exists"
	else:
		attrs = {}
		attrs['objectclass'] = ['top', 'dcObject', 'organization']
		attrs['o'] = suffix
		ldif = ldap.modlist.addModlist(attrs)
		print "adding root container"
		print "top level entry ldif attributes: " 
		print ldif
		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 syncuser
	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"
	l.unbind();


	# update the jos_components table record for the com_system component to add the ldap config data 
	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)


	print "\nldap default setup complete"

	return(0)
	


# #######################################################################
# Main


# arg parsing logic
parser = argparse.ArgumentParser(prog='hzldapinit')
parser.add_argument("--promptforadminpw", help="Prompt for password", action="store_true", default=False)
parser.add_argument("--promptforsearchpw", help="Prompt for password", action="store_true", default=False)
parser.add_argument("--promptforsyncuserpw", help="Prompt for password", action="store_true", default=False)

args =  parser.parse_args()

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

rc = defaultSetup(adminUserpw, searchUserpw, syncUserpw);

print "ldap admin user pw: " + adminUserpw
print "ldap serach user pw: " + searchUserpw
print "ldap sync user pw: " + syncUserpw

print "ldap config written to jos_components com_system component params field on database: " + hubzero.config.webconfig.getDefaultSite()

exit(rc)

