import string, Acquisition

from Globals import HTMLFile, MessageDialog, INSTANCE_HOME

from OFS.Folder import Folder

from Products.exUserFolder.exUserFolder import exUserFolder
from Products.exUserFolder.Plugins import PluginRegister

import ldap
import ldapurl
import sys
import time
import threading
import zLOG

bind_none = 1
bind_system = 2
bind_user = 3

LOG_STRING="XUF.LDAPAuthSource"

def manage_addLDAPAuthSource(self, REQUEST):
	""" Add a LDAP Auth Source """

	# Required
	url = REQUEST['LDAPUrl']
	compareDNOnServer = REQUEST['LDAPCompareDNOnServer']
	drefAliases = REQUEST['LDAPDereferenceAliases']
	startTLS = REQUEST['LDAPStartTLS']

	# Optional
	binddn = REQUEST.get('LDAPBindDN', '')
	bindPassword = REQUEST.get('LDAPBindDN', '')

	certDBPath = REQUEST.get('LDAPCertDBPath', '')

	groupAttribute = REQUEST.get('LDAPGroupAttribute','')
	groupAttributeIsDn = REQUEST.get('LDAPGroupAttributeIsDN', 0)

	requireGroup = REQUEST.get('LDAPRequireGroup', [])
	requireUser = REQUEST.get('LDAPRequireUser', [])
	requireDN = REQUEST.get('LDAPRequireDN', '')
	
	defaultRole = REQUEST.get('LDAPDefaultRole', '')
	defaultManager = REQUEST.get('LDAPDefaultManager', '')
	searchCacheSize = REQUEST.get('LDAPSearchCacheSize', 0)
	compareCacheSize = REQUEST.get('LDAPCompareCacheSize', 0)
	searchCacheTTL = REQUEST.get('LDAPSearchCacheTTL', 0)
	compareCacheTTL = REQUEST.get('LDAPCompareCacheTTL', 0)	
	
	ob=LDAPAuthSource(url, compareDNOnServer, drefAliases, startTLS,
					  binddn, bindPassword, certDBPath, groupAttribute,
					  groupAttributeIsDn, defaultRole, searchCacheSize,
					  compareCacheSize, searchCacheTTL, compareCacheTTL,
					  defaultManager, requireUser, requireGroup, requireDN)

	self._setObject('LDAPAuthSource', ob, None, None, 0)
	self.currentAuthSource=ob       


manage_addLDAPAuthSourceForm=HTMLFile('manage_addLDAPAuthSourceForm', globals())
manage_editLDAPAuthSourceForm=HTMLFile('manage_editLDAPAuthSourceForm', globals())

from LDAPCache import *
from LDAPConnection import *

LDAP_CACHE = LDAPCreateCache(50, nodeCompare=urlNodeCompare)
CONN_CACHE = []

class LDAPAuthSource(Folder):
	meta_type='Authorisation Source'
	id       ='LDAPAuthSource'
	title    ='LDAP Authorisation'
	icon     ='misc_/exUserFolder/exUserFolderPlugin.gif'
	

	manage_editForm=manage_editLDAPAuthSourceForm
	manage_tabs = Acquisition.Acquired

	def __init__(self, url, compareDnOnServer, drefAliases, startTLS,
				 binddn='', bindPassword='', certDBPath='',
				 groupAttribute='', groupAttributeIsDN = 0,
				 defaultRole = '', searchCacheSize=0, compareCacheSize=0,
				 searchCacheTTL=0, compareCacheTTL=0, defaultManager='',
				 requireUser=[], requireGroup=[], requireDN=''):

		self.setParams(url, compareDnOnServer, drefAliases, startTLS,
					  binddn, bindPassword, certDBPath, groupAttribute,
					  groupAttributeIsDN, defaultRole, searchCacheSize,
					  compareCacheSize, searchCacheTTL, compareCacheTTL,
					  defaultManager, requireUser, requireGroup, requireDN
					   )
		

	def setParams(self,url, compareDnOnServer, drefAliases, startTLS,
				 binddn='', bindPassword='', certDBPath='',
				 groupAttribute='', groupAttributeIsDN = 0,
				 defaultRole = '', searchCacheSize=0, compareCacheSize=0,
				 searchCacheTTL=0, compareCacheTTL=0, defaultManager='',
				 requireUser=[], requireGroup=[], requireDN=''):
		self.url = url
		self.compareDNOnServer = compareDnOnServer
		self.dereferenceAliases = drefAliases
		self.startTLS = startTLS
		self.bindDN = binddn
		self.bindPassword = bindPassword
		self.certDBPath = certDBPath
		self.groupAttribute = groupAttribute
		self.groupAttributeIsDN = groupAttributeIsDN
		self.defaultRole = defaultRole
		self.searchCacheSize=searchCacheSize
		self.compareCacheSize=compareCacheSize
		self.searchCacheTTL=searchCacheTTL
		self.compareCacheTTL=compareCacheTTL
		self.defaultManager=defaultManager
		self.requireGroup=requireGroup
		self.requireUser=requireUser
		self.requireDN=requireDN

		result = ldapurl.LDAPUrl(self.url)
		self._basedn=result.dn
		self._filter=result.filterstr
		if not self._filter:
			self._filter='(objectClass=*)'
			
		self._scope=result.scope
		if not self._scope:
			self._scope = ldap.SCOPE_SUB
		self._attribute=result.attrs
		self._hostport=result.hostport
		self._urlscheme=result.urlscheme
		hostport=string.split(self._hostport, ':')
		self._host=hostport[0]
		if len(hostport) == 2:
			self._port = int(hostport[1])
		else:
			if self._urlscheme=='ldap':
				self._port=389
			elif self._urlscheme=='ldaps':
				self._port=636
			else:
				# Ack..
				self._port=0
		
				  
	def manage_editAuthSource(self, REQUEST):
		""" Handle Editing LDAP Auth Params """
		# Required
		url = REQUEST['LDAPUrl']
		compareDNOnServer = REQUEST['LDAPCompareDNOnServer']
		drefAliases = REQUEST['LDAPDereferenceAliases']
		startTLS = REQUEST['LDAPStartTLS']

		# Optional
		binddn = REQUEST.get('LDAPBindDN', '')
		bindPassword = REQUEST.get('LDAPBindDN', '')

		certDBPath = REQUEST.get('LDAPCertDBPath', '')

		groupAttribute = REQUEST.get('LDAPGroupAttribute','')
		groupAttributeIsDN = REQUEST.get('LDAPGroupAttributeIsDN', 0)

		requireGroup = REQUEST.get('LDAPRequireGroup', [])
		requireUser = REQUEST.get('LDAPRequireUser', [])
		requireDN = REQUEST.get('LDAPRequireDN', '')
		
		defaultRole = REQUEST.get('LDAPDefaultRole', '')
		defaultManager = REQUEST.get('LDAPDefaultManager', '')
		searchCacheSize = REQUEST.get('LDAPSearchCacheSize', 0)
		compareCacheSize = REQUEST.get('LDAPCompareCacheSize', 0)
		searchCacheTTL = REQUEST.get('LDAPSearchCacheTTL', 0)
		compareCacheTTL = REQUEST.get('LDAPCompareCacheTTL', 0)	

		self.setParams(url, compareDNOnServer, drefAliases, startTLS,
					  binddn, bindPassword, certDBPath, groupAttribute,
					  groupAttributeIsDN, defaultRole, searchCacheSize,
					  compareCacheSize, searchCacheTTL, compareCacheTTL,
					  defaultManager, requireUser, requireGroup, requireDN)
		
		if REQUEST is not None:
			return self.MessageDialog(
				title = 'Edited',
				message = "Properties for %s changed." % self.id,
				action = 'manage_editLDAPAuthSourceForm')
		return ''

	def deleteUsers(self, userids):
		pass

	def createUser(self, username, password, roles):
		pass

	def updateUser(self, username, password, roles):
		self.currentPropSource.setUserProperty(username=username,
											   key='roles', value=roles)

	def listUserNames(self):
		return []

	def listUsers(self):
		return []

	def getUsers(self):
		return []

	def _ldap_createCaches(self):
		sCache = LDAPCreateCache(self.searchCacheSize, nodeCompare=searchNodeCompare)
		cCache = LDAPCreateCache(self.compareCacheSize, nodeCompare=compareNodeCompare)
		dnCache = LDAPCreateCache(self.compareCacheSize, nodeCompare=dnCompareNodeCompare)
		curl = URLNode(self.url, sCache, cCache, dnCache)

		LDAPCacheInsert(LDAP_CACHE, curl)
		return curl
		

	def _ldap_buildFilter(self, username):
		filter = "(&%s(%s=%s))"%(self._filter, self._attribute[0], username)
		return filter

	def listOneUser(self, username):

		l = self._ldap_Open()
		if not l:
			return []
		results = self._ldap_userExists(l, username)

		if not results:
			return []

		roles=[]
		if self.currentPropSource:
			roles = self.currentPropSource.getUserProperty(username=username, key='_roles', default=[])

		if not roles:
			roles=[]
		if self.defaultRole and self.defaultRole not in roles:
			roles.append(self.defaultRole)

		if self.defaultManager and self.defaultManager==username:
			roles.append('Manager')

		dn = results[0]
		data = results[1]
		self.currentPropSource.setUserProperty(username=username, key='dn', value=dn)
		for k,v in data.items():
			self.currentPropSource.setUserProperty(username=username, key=k, value=v)

		return [{'username':username, 'password':'', 'roles':roles},]

	def getConnections(self):
		return CONN_CACHE

	def addConnection(self, l):
		CONN_CACHE.append(l)

	def getLDAPCache(self, key):
		return LDAPCacheFetch(LDAP_CACHE, key)

	def _ldap_FindConnection(self):
		zLOG.LOG(LOG_STRING,
				 zLOG.DEBUG,
				 "Entering _ldapFindConnection")
		connections = self.getConnections()
		l = None
		for l in connections:
			if ( l.port == self._port and
				 l.host == self._host ):
				break
		else:
			l = None

		if l:
			if ( (self.bindDN and not l.bounddn) or
				 (not self.bindDN and l.bounddn) or
				 ((self.bindDN and l.bounddn) and self.bindDN != l.bounddn)):
				l.boundas = bind_none
			else:
				l.boundas = bind_system
		else:
			lock=threading.Lock()
			l = LDAPConnection(None, lock, None, self._host, self._port, bind_none)
			self.addConnection(l)
		return l

	def _ldap_Open(self):
		l = self._ldap_FindConnection()
		connected = self._ldap_connectToServer(l)
		if not connected:
			return None
		return l
			
	def _ldap_userExists(self, l, username):
		
		result = []
		filter = self._ldap_buildFilter(username)
		try:
			result = l.ldapConn.search_s(self._basedn, self._scope, filter)
		except:
			import traceback
			traceback.print_exc()
			return None

		resultData = result

		# Should only get one result
		if len(resultData) != 1:
			return None

		thisResult=resultData[0]

		curl = self.getLDAPCache(self.url)
		if not curl:
			curl = self._ldap_createCaches()

		dn = thisResult[0]
		if self.requireDN:
			if not dn:
				zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
						 "The User's DN has not been defined: failing auth")
				return None
			result = self._ldap_comparedn(l, dn, self.requireDN, curl)
			if result:
				return thisResult
			
		if self.requireUser:
			if not dn:
				zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
						 "The User's DN has not been defined: failing auth")
				return None
			for t in self.requireUser:
				if not t: # lines gives us one empty one..
					continue
				result = self._ldap_compare(l, dn, self._attribute, t, curl.compare_cache)
				if result:
					return thisResult

				for tt in string.split(t):
					result = self._ldap_compare(l, dn, self._attribute, tt, curl.compare_cache)
					if result:
						return thisResult
		if self.requireGroup:
			if self.groupAttributeIsDN:
				if not dn:
					zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
							 "The User's DN has not been defined: failing auth")
					return 0
			w = 'group'
			for t in self.requireGroup:
				if not t:
					continue
				zLOG.LOG(LOG_STRING, zLOG.DEBUG,
						 "testing for group membership in '%s'"%(t))
				for ent in self.groupAttribute:
					if not ent:
						continue
					zLOG.LOG(LOG_STRING, zLOG.DEBUG,
							 "testing for group membership in %s=%s"%(ent,
																	  [username,dn][self.groupAttributeIsDN]))
					result = self._ldap_compare(l, t, ent, 
												[username,dn][self.groupAttributeIsDN],
												curl.dn_compare_cache)
					if result:
						return thisResult
		
		return thisResult

	def _ldap_comparedn(self, ldc, dn, reqdn, curl):
		if not self.compareDNOnServer:
			return cmp(dn, reqdn) == 0

		newnode = DNCompareNode(reqdn)
		node = curl.dnCompareCache.fetch(newnode)
		if node:
			return 1
		
		try:
			ldc.lock.acquire()
			connected = self._ldap_connectToServer(ldc)
			if not connected:
				return 0
			try:
				result = ldc.ldapConn.search_ext_s(reqdn, ldap.SCOPE_BASE,
												   '(objectclass=*)',
												   None, 1)
			except:
				return 0
			if result != ldap.SUCCESS:
				return 0

			entry = result[0]
			searchdn=entry[0]
			if dn != searchdn:
				return 0
			newnode.dn = dn
			curl.dnCompareCache.insert(newnode)
			return 1
		finally:
			ldc.lock.release()
			

	def _ldap_compare(self, ldc, dn, attrib, value, cache):
		curtime = time.time()
		theCompareNode=CompareNode(dn, attrib, value, 0.0)
		compare_node = cache.fetch(theCompareNode)
		if compare_node:
			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Found It...")
			if (curtime - compare_node.lastcompare) > self.compareCacheTTL:
				zLOG.LOG(LOG_STRING, zLOG.DEBUG,
						 "...but it's too old.")
				cache.remove(compare_node)
			else:
				zLOG.LOG(LOG_STRING, zLOG.DEBUG,
						 "...and it's good.")
				return 1
		try:
			ldc.lock.acquire()
			connected = self._ldap_connectToServer(ldc)
			if not connected:
				return 0

			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Doing LDAP compare of %s=%s in entry %s"%(attrib, value, dn))
			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "LDAP OP: compare")

			try:
				result = ldc.ldapConn.compare_s(dn, attrib, value)
			except ldap.SERVER_DOWN:
				self.freeConnection(ldc, 1)
				return 0

			if result == ldap.COMPARE_TRUE:
				zLOG.LOG(LOG_STRING, zLOG.DEBUG,
						 "Compare succeeded; caching result")
				compare_node.lastcompare=curtime
				cache.insert(compare_node)
				return 1
			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Compare failed")
			return 0
		finally:
			ldc.lock.release()
			
	def _ldap_connectToServer(self, l):

		zLOG.LOG(LOG_STRING, zLOG.DEBUG,
				 "Entering _ldap_connectToServer")
		
		if not l.ldapConn:
			l.boundas=bind_none
			if l.bounddn:
				l.bounddn = None

			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Opening Connection to ldap server(s) '%s'"%(self._host))
			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "LDAP OP: init")

			l.init()
			if not l.ldapConn:
				zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
						 "Could not connect to LDAP server")
				return 0

			try:
				l.ldapConn.set_option(ldap.OPT_DEREF, self.dereferenceAliases)
			except:
				zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
						 "Setting LDAP dereference option failed")
				
			if self.startTLS and 0:
				version = ldap.VERSION3
				try:
					result = l.ldapConn.set_option(ldap.OPT_PROTOCOL_VERSION, version)
				except:
					zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
							 "Setting LDAP version option failed")
					return 0

				zLOG.LOG(LOG_STRING, zLOG.DEBUG,
						 "Starting TLS for this connection")
				l.withtls = 1
				try:
					l.ldapConn.start_tls_s()
				except:
					zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
							 "Start TLS Failed.")
					return 0
			else:
				l.withtls=0

		if l.boundas == bind_system:
			return 1

		zLOG.LOG(LOG_STRING, zLOG.DEBUG,
				 "Binding to server '%s' as %s/%s"%(self._host,
													self.bindDN,
													self.bindPassword))

		zLOG.LOG(LOG_STRING, zLOG.DEBUG,
				 "LDAP OP: simple bind")

		result = l.ldapConn.simple_bind(self.bindDN, self.bindPassword)

		if result == ldap.SERVER_DOWN:
			self.freeConnection(l, 1)
			return 0

		if result != 1:
			self.freeConnection(l)
			zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
					 "Could not bind to LDAP server '%s' as %s: %s"%(self._host,
																	 self.bindDN,
																	 result))
			return 0

		l.bounddn = self.bindDN
		l.boundas = bind_system
		return 1

	def freeConnection(self, ldc, log=0):

		if log:
			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Server is down")
		if ldc.ldapConn:
			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Freeing connection to ldap server(s) '%s'"%(self._host))
			ldc.ldapConn.unbind_s()
			ldc.ldapConn=None
			ldc.boundas = bind_none
			if ldc.bounddn:
				ldc.bounddn=None

	def remoteAuthMethod(self, username, password):

		zLOG.LOG(LOG_STRING, zLOG.DEBUG,
				 "Entering remoteAuthMethod")

		l = self._ldap_FindConnection()
		curl = self.getLDAPCache(self.url)
		if not curl:
			curl = self._ldap_createCaches()
		
		if not l:
			zLOG.LOG(LOG_STRING,
					 zLOG.PROBLEM, "Could not find/create LDAPConnection")
			return 0

			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Using URL: %s"%(self.url))

			searchNode = curl.searchCache.fetch(username)
			if searchNode and searchNode.bindpw:
				zLOG.LOG(LOG_STRING, zLOG.DEBUG,
						 "Found entry in cache for '%s'..."%(username))
				curtime=time.time()
				if curtime == searchNode.lastBind > self.searchCacheTTL:
					zLOG.LOG(LOG_STRING, zLOG.DEBUG,
							 "... but entry is too old (%d seconds)"%(curtime-searchnode.lastbind))
					curl.searchCache.remove(username)
				elif searchNode.bindpw != password:
					zLOG.LOG(LOG_STRING, zLOG.DEBUG,
							 "... but entry password doesn't match")
					curl.searchCache.remove(username)
				else:
					zLOG.LOG(LOG_STRING, zLOG.DEBUG,
							 "... and entry is valid")
					return 1
			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Entry for %s is not in the cache"%(username))

		filtbuf = self._ldap_buildFilter(username)
		try:
			l.lock.acquire()

			if not self._ldap_connectToServer(l):
				return 0

			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Performing a search (scope = %d) with filter %s"%(
				self._scope, filtbuf))

			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "LDAP OP: search")

			result = None
			try:
				result = l.ldapConn.search_s(self._basedn, self._scope,
											 filtbuf)
			except ldap.SERVER_DOWN:
				self.freeConnection(l, 1)
				return 0
			except:
				zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
						 "LDAP search for %s failed: LDAP error: %s; URI %s"%(
					filtbuf, result, self.url))
				return 0

			resultData=result
			count = len(resultData)
			if count != 1:
				zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
						 "Search must return exactly 1 entry; found %s entries for search %s: URI %s"%(count, filtbuf, self.url))
				return 0

			
			entry=resultData[0]
			dn=entry[0]
			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "DN returned from search is %s"%(dn))

			if len(password) <= 0:
				zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
						 "Empty Password")

				return 0

			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "Validating user '%s' via bind"%(username))

			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "LDAP OP: simple bind")

			l.boundas = bind_user

			try:
				result = l.ldapConn.simple_bind_s(dn, password)
			except ldap.SERVER_DOWN:
				self.freeConnection(l, 1)
				return 0
			except e:
				zLOG.LOG(LOG_STRING, zLOG.PROBLEM,
						 "User bind as %s failed: LDAP error: %s; URI %s"%(
					dn, e, self.url))
				return 0

			zLOG.LOG(LOG_STRING, zLOG.DEBUG,
					 "authenticate: accepting")

		finally:
			l.lock.release()

		zLOG.LOG(LOG_STRING, zLOG.DEBUG,
				 "Adding user '%s' to the cache"%(dn))

		sNode = SearchNode(username, dn, password, time.time())
		curl.search_cache.insert(sNode)
		return 1

LDAPAuthReg=PluginRegister('LDAPAuthSource', 'LDAP Authentication Source', LDAPAuthSource, manage_addLDAPAuthSourceForm, manage_addLDAPAuthSource, manage_editLDAPAuthSourceForm)
exUserFolder.authSources['LDAPAuthSource']=LDAPAuthReg
