#!/usr/bin/env python3
# coding:utf-8

import sys
import re
import sre_constants
from io import StringIO
from contextlib import redirect_stdout
from contextlib import redirect_stderr
from time import time
from math import ceil


msg0100 = 'fetching configuration for: \'{0}\''
msg0101 = 'Samba4 AD returned: \'{0}\''
msg0102 = 'samba-tool failed with ({0}):\n{1}\n{2}'
msg0103 = 'AD user \'{0}\' does not exists'
msg0104 = 'skipping user \'{0}({1})\' due to black/white listing'
msg0105 = 'skipping group \'{0}({1})\' due to black/white listing'
msg0106 = 'skipping ou \'{0}({1})\' due to black/white listing'
msg0107 = 'unknown attribute: \'{0}\''
msg0108 = 'unknown object type: \'{0}\''
msg0109 = 'retrieved original dn form call: \'{0}\''
msg0110 = 'not configured to handle AD OUs'
msg0111 = 'no sambaSamAccount for user \'{0}\''
msg0130 = 'get a list of AD users'
msg0131 = 'unable to get AD user list'
msg0132 = 'AD user list: {0}'
msg0133 = 'get a list of AD groups'
msg0134 = 'unable to get AD group list'
msg0135 = 'AD group list: {0}'
msg0136 = 'get members of AD group:\'{0}\''
msg0137 = 'unable to get AD group members'
msg0138 = 'AD group members: {0}'
msg0139 = 'get a list of AD organizational units'
msg0171 = 'AD organizational units list: {0}'
msg0147 = 'renaming AD group \'{0}\' to \'{1}\''
msg0148 = 'not creating AD group \'{0}\' in GosaGroup.modify call'
msg0149 = 'failed to rename AD group \'{0}\' to \'{1}\''
msg0150 = 'creating/modifying AD group: \'{0}({1})\''
msg0151 = 'no sambaGroupMapping for group \'{0}\''
msg0152 = 'AD group \'{0}\' already exists'
msg0153 = 'failed to create AD group: \'{0}({1})\''
msg0154 = 'creating new AD group: \'{0}\''
msg0155 = 'no AD group DN change for: \'{0}\''
msg0156 = 'moving AD group \'{0}\' to \'{1}\''
msg0157 = 'failed to move AD group \'{0}\' to \'{1}\''
msg0158 = 'as no original DN was provided in call, no DN changes are in order for: \'{0}\''
msg0159 = 'placing new AD group in DN \'{0}\''
msg0160 = 'sync users to AD group: \'{0}\' -> \'{1}\''
msg0161 = 'group members: {0}'
msg0162 = 'adding members to AD group \'{0}\' : {1}'
msg0163 = 'failed to add members to AD group: \'{0}\''
msg0164 = 'removing members from AD group \'{0}\' : {1}'
msg0165 = 'failed to remove members from AD group: \'{0}\''
#msg0166 = 'adding member \'{0}\' with primary group SID to \'{1}\''
msg0167 = 'unable to get group SID for \'{0}\''
msg0170 = 'unable to get AD organizational units list'
msg0171 = 'AD organizational units list: {0}'
msg0180 = 'removing AD group: \'{0}({1})\''
msg0181 = 'AD group \'{0}\' does not exists'
msg0182 = 'failed to delete AD group: \'{0}\''
msg0190 = 'sync back AD group SID for: \'{0}\' -> \'{1}\''
msg0191 = 'failed to retrieve AD SID for group: \'{0}\''
msg0192 = 'failed to sync back AD SID for group: \'{0}\' -> \'{1}\''
msg0193 = 'successfull synced back AD SID for group: \'{0}\' -> \'{1}\''
msg0194 = 'SID in sync for group: \'{0}\' -> \'{1}\''
msg0209 = 'placing new AD user in DN \'{0}\''
msg0210 = 'creating/modifying AD user: \'{0}({1})\''
msg0212 = 'AD user \'{0}\' already exists'
msg0213 = 'failed to create AD user: \'{0}({1})\''
msg0214 = 'creating new AD user: \'{0}\''
msg0215 = 'skipping Gosa template: \'{0}\''
msg0216 = 'no AD user DN change for: \'{0}\''
msg0217 = 'moving AD user \'{0}\' to \'{1}\''
msg0218 = 'failed to move AD user \'{0}\' to \'{1}\''
msg0219 = 'as no original DN was provided in call, no DN changes are in order for: \'{0}\''
msg0220 = 'sync back AD user SID for: \'{0}\' -> \'{1}\''
msg0221 = 'failed to retrieve AD SID for user: \'{0}\''
msg0222 = 'failed to sync back AD SID for user: \'{0}\' -> \'{1}\''
msg0223 = 'successfull synced back AD SID for user: \'{0}\' -> \'{1}\''
msg0224 = 'SID in sync for user: \'{0}\' -> \'{1}\''
msg0230 = 'deleting AD user: \'{0}({1})\''
msg0231 = 'failed to delete AD user: \'{0}\''
msg0240 = 'password change for AD user: \'{0}({1})\''
msg0241 = 'no password for password change provided for user: \'{0}\''
msg0242 = 'password change failed for AD user: \'{0}\''
msg0250 = 'locking AD user: \'{0}({1})\''
msg0251 = 'failed to lock AD user: \'{0}\''
msg0260 = 'unlocking AD user: \'{0}({1})\''
msg0261 = 'failed to lock AD user: \'{0}\''
msg0270 = 'set expiry date for AD user \'{0}({1})\' to {2}'
msg0271 = 'failed to et expiry date for AD user: \'{0}\''
msg0290 = 'AD OU already extist: \'{0}({1})\''
msg0291 = 'skipping OU as of wrong path depth({0}): {1}'
msg0292 = 'creating new AD OU: \'{0}\''
msg0293 = 'intermediate AD OU exists: \'{0}\''
msg0294 = 'skipping OU: {0}'
msg0295 = 'failed to create AD OU {0}'
msg0296 = 'ldap search should have provided data, but did not'
msg0300 = 'removing AD OU: \'{0}({1})\''
msg0301 = 'failed to delete AD OU: \'{0}({1})\''
msg0310 = 'skipping OU as of wrong path depth({0}): {1}'
msg0311 = 'adding {0} to OU remove list'
msg0312 = 'skipping OU: {0}'
msg0313 = 'full OU remove list: {0}'
msg0314 = 'removing AD OU: {0}'
msg0315 = 'failed to remove AD OU {0}'
msg0316 = 'AD OU does not extist: \'{0}\''
msg0330 = 'creating a new OU as the original OU did not exist in AD: \'{0}({1})\''
msg0331 = 'the new OU already exists in AD: \'{0}({1})\''
msg0332 = 'failed to rename/move AD OU \'{0}\' to \'{1}\''
msg0333 = 'the base of the new OU does not exist in AS: \'{0}\''
msg0334 = 'missing original OU DN in call for: \'{0}\''
msg0335 = 'renaming/moving AD OU \'{0}({1})\' to \'{2}({3})\''
msg0336 = 'original and new OU are identical, nothing to do for: \'{0}({1})\''


class LoopBreakWL(Exception): pass
class LoopBreakBL(Exception): pass


class Samba4AD():

	def __init__(self, postcmd):

		self.postcmd = postcmd
		self.s4ad_config = self.__getS4ADConfig()
		self.s4ad = self.s4ad_config['handlesamba4ad']
		if not self.s4ad: return

		if self.s4ad_config['url'] and self.s4ad_config['user'] and self.s4ad_config['pwd']:

			self.adconfigured = True
		else:
			self.adconfigured = False

		# TODO: do something with self.adconfigured in members
		# TODO: config for local AD access without creds

		self.openldap_users = {}

		# stdout and stderr for samba.netcmd
		self.sn_out = StringIO()
		self.sn_err = StringIO()

		# should never be called in post remove
		if self.postcmd.action == 'remove': return

		if b'inetOrgPerson' in self.ldap_data['objectClass']:
			mapping = 'usermapping'
		elif b'posixGroup' in self.ldap_data['objectClass']:
			mapping = 'groupmapping'
		elif b'organizationalUnit' in self.ldap_data['objectClass']:
			mapping = 'oumapping'
		else:
			self.logger.warn(msg0108.format(self.dn))
			mapping = None

		self.ad_cn = self.__getAdName(self.cn, mapping)

		self.dn_list = self.__getDnList()
		self.ad_dn_list = self.__getAdDnList()
		self.ad_dn = self.__getAdDn()

		self.base = self.__getDnStr(self.dn_list, -1)
		self.ad_base = self.__getDnStr(self.ad_dn_list, -1)
		self.ad_base_s = self.__getDnStr(self.ad_dn_list[len(self.__getDnList(self.searchbase)):-1])

		if self.postcmd.module == 'GosaPassword': return
		try:
			self.odn = self.opt[0]
			self.odn_list = self.__getDnList(self.odn)
			self.ocn = self.odn_list[-1][1]
			self.ad_odn_list = self.__getAdDnList(self.odn_list)
			self.ad_ocn = self.ad_odn_list[-1][1]
			self.ad_odn = self.__getAdDn(self.ad_odn_list)
			self.obase = self.__getDnStr(self.odn_list, -1)
			self.ad_obase = self.__getDnStr(self.ad_odn_list, -1)
			self.ad_obase_s = self.__getDnStr(self.ad_odn_list[len(self.__getDnList(self.searchbase)):-1])
			self.logger.debug(msg0109.format(self.odn))

		except IndexError:
			self.odn = None
			self.odn_list = None
			self.ocn = None
			self.ad_odn_list = None
			self.ad_ocn = None
			self.ad_odn = None
			self.obase = None
			self.ad_obase = None
			self.ad_obase_s = None


	def __getS4ADConfig(self):

		cfg_def = {
			'handlesamba4ad' : False,
			'url' : '',
			'user' : '',
			'pwd' : '',
			'usermapping' : {'nobody' : 'Guest', },
			'userwhitelist' : [],
			'userblacklist' : [],
			'groupmapping' : {
				'domadmins' : 'Domain Admins',
				'domcomputers' : 'Domain Computers',
				'domguests' : 'Domain Guests',
				'domusers' : 'Domain Users',
				},
			'groupwhitelist' : [],
			'groupblacklist' : [],
			'oumapping' : {},
			'returnsid' : True,
			'syncusers' : True,
			'useadou' : False,
			'stripou' : ['users', 'groups'],
			}
		cfg_ver = {
			'handlesamba4ad' : ('bool', ),
			'url' : ('str', ),
			'user' : ('str', ),
			'pwd' : ('str', ),
			'usermapping' : ('dict', 'str', 'str'),
			'userwhitelist' : ('list', 'reobj'),
			'userblacklist' : ('list', 'reobj'),
			'groupmapping' : ('dict', 'str', 'str'),
			'groupwhitelist' : ('list', 'reobj'),
			'groupblacklist' : ('list', 'reobj'),
			'oumapping' : ('dict', 'str', 'str'),
			'returnsid' : ('bool', ),
			'syncusers' : ('bool', ),
			'useadou' : ('bool', ),
			'stripou' : ('list', 'str'),
			}

		# get Samba 4 AD connection configuration
		self.logger.debug(msg0100.format('Samba4AD'))
		self.config.updateDefaults('Samba4AD', cfg_def)
		self.config.updateVerify(cfg_ver)

		return self.config.getSection('Samba4AD')


	def __splitLdif(self, ldif_str):

		ldif_dict = {}

		for line in ldif_str.split('\n'):

			if not line: continue

			attrib, value = line.split(': ', 1)

			if attrib in ldif_dict:
				ldif_dict[attrib].append(value)
			else:
				ldif_dict[attrib] = [value, ]

		return ldif_dict


	def __splitList(self, list_str):

		ret_list = []

		for line in list_str.split('\n'):

			if not line: continue
			ret_list.append(line.strip())

		return ret_list


	def __bwlist(self, in_dct, wl_re, bl_re):

		out_dct = {}
		for key in in_dct.keys():

			value = in_dct[key]
			try:
				for wl in wl_re:
					if wl.match(key) or wl.match(value):
						raise LoopBreakWL

				if wl_re: continue

				for bl in bl_re:
					if bl.match(key) or bl.match(value):
						raise LoopBreakBL

			except LoopBreakWL:
				pass

			except LoopBreakBL:
				continue

			out_dct[key] = value

		return out_dct


	def __getName(self, adname, mapping='usermapping'):

		usermapping_rev = {v: k for k, v in self.s4ad_config[mapping].items()}

		try:
			name = usermapping_rev[adname]

		except KeyError:
			name = adname.lower()

		return name


	def __getAdName(self, name, mapping='usermapping'):

		try:
			adname = self.s4ad_config[mapping][name]

		except KeyError:
			adname = name

		return adname


	def __getDnStr(self, dn=None, d=None):

		if not dn:
			dn = self.__getDnList()
		else:
			if isinstance(dn, str):
				if d == None: return dn
				dn = self.__getDnList(dn)

		if d == None: d = len(dn)
		return ','.join( [ '{0}={1}'.format(n, m) for n, m in reversed(dn[0:d]) ] )


	def __getDnList(self, dn=None):

		if not dn:
			try:
				return self.dn_list
			except AttributeError:
				dn = self.dn
		else:
			if isinstance(dn, list): return dn

		return [n.split('=') for n in reversed(dn.split(','))]


	def __getAdDnList(self, dn=None):

		def getFn(ft, fn):
			try:
				return self.__getAdName(fn,
						{'uid' : 'usermapping',
						'cn' : 'groupmapping',
						'ou' : 'oumapping',}[ft]
					)
			except KeyError:
				return fn

		if not dn:
			try:
				return self.ad_dn_list
			except AttributeError:
				pass

			dn = self.__getDnList()
		else:
			if isinstance(dn, str):
				dn = self.__getDnList(dn)

		return [
			[ 'CN' if n == 'uid' else n.upper(), getFn(n, m) ]
			for n, m in dn
			if not (n == 'ou' and m in self.s4ad_config['stripou'])
		]


	def __getAdDn(self, dn=None):

		if not dn:
			try:
				return self.ad_dn
			except AttributeError:
				pass

			dn = self.__getAdDnList()
		else:
			if isinstance(dn, str):
				dn = self.__getAdDnList(dn)

		return ','.join( [ '{0}={1}'.format(n, m) for n, m in reversed(dn) ] )


	def __bwListUser(self):

		u_wl = self.s4ad_config['userwhitelist']
		u_bl = self.s4ad_config['userblacklist']

		if self.__bwlist({self.cn : self.ad_cn}, u_wl, u_bl):
			return True

		self.logger.info(msg0104.format(self.cn, self.ad_cn))
		return False


	def __createADuser(self):

		# check for samba account in openldap
		if b'sambaSamAccount' not in self.ldap_data['objectClass']:
			self.logger.info(msg0111.format(self.cn))
			return False

		# check for template
		if b'gosaUserTemplate' in self.ldap_data['objectClass']:
			self.logger.info(msg0215.format(self.cn))
			return False

		# verify base DN existence
		if self.s4ad_config['useadou']:
			self.__createADou(self.base)

		# get AD users list
		adusers = self.__snc_listADuser()

		# create AD user
		if adusers and self.ad_cn.lower() in map(str.lower, adusers):
			self.logger.info(msg0212.format(self.ad_cn))

			# sync some attibutes for existing users
			# surname, givenname, displayname
			self.__syncUserName(self.ad_cn, 'sn')
			self.__syncUserName(self.ad_cn, 'givenName')
			self.__syncUserName(self.ad_cn, 'displayName')

			# possible OU move of AD user
			if self.s4ad_config['useadou']:

				if self.odn:
					if self.dn == self.odn:
						self.logger.info(msg0216.format(self.ad_dn))
					else:
						# move user to new base DN
						self.logger.info(msg0217.format(self.ad_cn, self.ad_base))
						ret = self.__snc_moveADuser(self.ad_cn, self.ad_base)
						if ret == False:
							self.exit(1, msg0218.format(self.ad_cn, self.ad_base))
						self.logger.info(msg0101.format(ret.strip()))
				else:
					self.logger.info(msg0219.format(self.ad_dn))
			else:
				self.logger.info(msg0110)
		else:
			self.logger.info(msg0214.format(self.ad_cn))

			dn = None
			if self.s4ad_config['useadou']:
				dn = self.ad_base_s
				# from scratch contructed dn as a dev reference
				#dn = self.__getDnStr(self.__getAdDnList(self.__getDnList(
				#	self.dn.removesuffix(',' + self.searchbase))), -1)
				self.logger.info(msg0209.format(dn))

			ret = self.__snc_createADuser(self.ad_cn, self.ldap_data, dn)
			if ret == False:
				self.exit(1, msg0213.format(self.cn, self.ad_cn))
			self.logger.info(msg0101.format(ret.strip()))

		# sync user SID back to openldap
		self.__syncUserSID()

		# sync openldap users expiration to AD
		self.__syncUserExpiry()

		return True


	def __syncUserSID(self):

		if not self.s4ad_config['returnsid']: return

		ret = self.__snc_getObjectItem('user', self.ad_cn, 'objectSid')
		if ret == False:
			self.logger.warn(msg0221.format(self.ad_cn))
		else:
			if ret == self.ldap_data['sambaSID'][0].decode():
				self.logger.info(msg0224.format(self.ad_cn, self.cn))
				return

			self.logger.info(msg0220.format(self.ad_cn, self.cn))

			mod_list = (
				((self.dn, 'base', '(objectClass=*)', ('sambaSID', ) ), (
					('attrib', 'replace',
						(ret, ),
						('sambaSID', ), ),
				)),
				)

			if not self.ldapmod.modify(mod_list):
				self.logger.warn(msg0222.format(self.ad_cn, self.cn))
			else:
				self.logger.info(msg0223.format(self.ad_cn, self.cn))


	def __syncUserExpiry(self):

		if 'sambaKickoffTime' in self.ldap_data.keys():
			#print('sambaKickoffTime: {0}'.format(self.ldap_data['sambaKickoffTime']))
			exp_secs = int(self.ldap_data['sambaKickoffTime'][0].decode())
			now_secs = int(time())
			exp_delta_secs = exp_secs - now_secs
			exp_delta_days = str(ceil(exp_delta_secs / (60*60*24)))
		else:
			exp_delta_days = None

		self.logger.info(msg0270.format(self.cn, self.ad_cn, exp_delta_days))

		ret = self.__snc_syncUserExpiry(self.ad_cn, exp_delta_days)
		if ret == False:
			self.logger.warn(msg0271.format(self.ad_cn))
		else:
			self.logger.info(msg0101.format(ret.strip()))


	def __syncUserName(self, aduser, attr):

		ad_val = self.__snc_getObjectItem('user', aduser, attr)

		if ad_val == False:
			self.logger.warn('unable to get \'{0}\' for AD user \'{1}\''.format(attr, aduser))
			return False

		try:
			if attr == 'displayName':
				o_attr = 'cn'
			else:
				o_attr = attr
			val = self.ldap_data[o_attr][0].decode()

		except (KeyError, IndexError):
			self.logger.warn('unable to get \'{0}\' user \'{1}\''.format(o_attr, self.cn))
			return False

		if ad_val == val:
			self.logger.debug('no \'{0}\' change for AD user \'{1}\''.format(attr, aduser))
			return True
		else:
			self.logger.info('change \'{0}\' for AD user \'{1}\''.format(attr, aduser))
			return self.__snc_renameUser(aduser, attr, val)


	def __getOpenldapUsers(self):

		if self.openldap_users:
			return self.openldap_users

		ol_users = self.gosa_ldap.searchLdap(
			self.searchbase,
			'sub',
			'(&(objectClass=posixAccount)(objectClass=sambaSamAccount))',
			('uid', 'sambaPrimaryGroupSID', )
			)

		for u_dn, u_data in ol_users:

			user = u_data['uid'][0].decode()
			# skip old nt domain machine accounts
			if user[-1] == '$': continue

			gsid = u_data['sambaPrimaryGroupSID'][0].decode()
			aduser = self.__getAdName('usermapping', user)

			self.openldap_users[user] = {
				'dn' : u_dn,
				'aduser' : aduser,
				'sambaPrimaryGroupSID': gsid,
				}

		return self.openldap_users


	def __bwListGroup(self):

		g_wl = self.s4ad_config['groupwhitelist']
		g_bl = self.s4ad_config['groupblacklist']

		if self.__bwlist({self.cn : self.ad_cn}, g_wl, g_bl):
			return True

		self.logger.info(msg0105.format(self.cn, self.ad_cn))
		return False


	def __createADgroup(self, mod=False):

		# check for samba group mapping in openldap
		if b'sambaGroupMapping' not in self.ldap_data['objectClass']:
			self.logger.info(msg0151.format(self.cn))
			return False

		# verify base DN existence
		if self.s4ad_config['useadou']:
			self.__createADou(self.base)

		# do we need to rename the AD group?
		if self.ocn and self.cn != self.ocn:
			self.logger.info(msg0147.format(self.ad_ocn, self.ad_cn))
			ret = self.__snc_renameADgroup(self.ad_ocn, self.ad_cn)
			if ret == False:
				self.exit(1, msg0149.format(self.ad_ocn, self.ad_cn))
			self.logger.info(msg0101.format(ret.strip()))

		# get AD groups list
		adgroups = self.__snc_listADgroup()

		# create AD group
		if adgroups and self.ad_cn.lower() in map(str.lower, adgroups):

			self.logger.info(msg0152.format(self.ad_cn))

			# possible OU move of AD group
			if self.s4ad_config['useadou']:

				if self.odn:
					if self.base == self.obase:
						self.logger.info(msg0155.format(self.ad_dn))
					else:
						# move group to new base DN
						self.logger.info(msg0156.format(self.ad_cn, self.ad_base))
						ret = self.__snc_moveADgroup(self.ad_cn, self.ad_base)
						if ret == False:
							self.exit(1, msg0157.format(self.ad_cn, self.ad_base))
						self.logger.info(msg0101.format(ret.strip()))
				else:
					self.logger.info(msg0158.format(self.ad_dn))
			else:
				self.logger.info(msg0110)
		else:

			# FIXME: for some reason GosaGroup.modify gets calles twice sometimes
			# with a first call where odn and dn are the same (new) names although
			# the dn was catually changed, so odn should reflect the old dn
			# to prevent a new OU is being created instead of the old one renamed
			# we catch that here as we can not rely on a correct call

			if mod:
				self.logger.debug(msg0148.format(self.ad_cn))
				return True

			self.logger.info(msg0154.format(self.ad_cn))

			# extract nis domain name from object dn
			# assuming to use the 'dc' parts as nis name
			nisname = '.'.join(
				m for n, m in (
				o.split('=') for o in self.dn.split(',')
				) if n == 'dc')

			dn = None
			if self.s4ad_config['useadou']:
				dn = self.ad_base_s
				self.logger.info(msg0159.format(dn))

			ret = self.__snc_createADgroup(self.ad_cn, nisname, self.ldap_data, dn)
			if ret == False:
				self.exit(1, msg0153.format(self.cn, self.ad_cn))
			self.logger.info(msg0101.format(ret.strip()))

		# sync group SID to back to openldap
		self.__syncGroupSID()

		# sync openldap users to AD
		self.__syncADgroupMembers()

		return True


	def __createADou(self, dn=None):

		if dn:
			ad_dn = self.__getAdDnList(dn)
		else:
			dn = self.dn_list
			ad_dn = self.ad_dn_list

		# get AD OU list
		ou_dn_lists = self.__snc_listADou()

		# does OU exist in AD?
		if ad_dn in ou_dn_lists:

			self.logger.info(
				msg0290.format(
					self.__getDnStr(dn),
					self.__getDnStr(ad_dn)
				)
			)
			return

		for depth in range(len(self.searchbase.split(',')) + 1, len(ad_dn) + 1):

			for ou_dn in ou_dn_lists:

				# are we in the correct ou depth?
				if len(ou_dn) != depth:
					self.logger.debug(msg0291.format(depth, ou_dn))
					continue

				# intermediate ou exists
				if ad_dn[0:depth] == ou_dn:
					break

				self.logger.debug(msg0294.format(ou_dn))
			else:
				# creating new (intermediate) ou
				new_ou_dn = self.__getDnStr(ad_dn, depth)
				self.logger.info(msg0292.format(new_ou_dn))

				if self.dn_list == dn:
					ldap_data = self.ldap_data
				else:
					ldap_data = self.gosa_ldap.getAttributeList(self.__getDnStr(dn))
					if ldap_data == None:
						self.logger.warn(msg0296)
						ldap_data = {}
				ret = self.__snc_createADou(new_ou_dn, ldap_data)
				if ret == False:
					self.logger.error(msg0295.format(new_ou_dn))
					break
				self.logger.info(msg0101.format(ret.strip()))
				continue

			# intermediate ou exists if we get here
			self.logger.debug(msg0293.format(self.__getDnStr(ad_dn, depth)))


	def __removeADou(self, dn=None):

		if dn:
			ad_dn = self.__getAdDnList(dn)
		else:
			dn = self.dn_list
			ad_dn = self.ad_dn_list

		# get AD OU list
		ou_dn_lists = self.__snc_listADou()

		# does OU exist in AD?
		if ad_dn not in ou_dn_lists:

			self.logger.info(msg0316.format(self.__getDnStr(ad_dn)))
			return

		# generate OU list to remove
		depth = len(ad_dn)
		r_list = []
		for ou_dn in ou_dn_lists:

			if len(ou_dn) < depth:
				self.logger.debug(msg0310.format(depth, ou_dn))
				continue

			if ad_dn == ou_dn[0:depth]:
				self.logger.debug(msg0311.format(ou_dn))
				r_list.append(ou_dn)
				continue

			self.logger.debug(msg0312.format(ou_dn))

		# sorting list by reversed length for removal
		r_list.sort(key=len, reverse=True)
		self.logger.debug(msg0313.format(r_list))

		# remove OUs
		for ou_dn in r_list:
			self.logger.info(msg0314.format(self.__getDnStr(ou_dn)))
			ret = self.__snc_removeADou(self.__getDnStr(ou_dn))
			if ret == False:
				self.logger.error(msg0315.format(self.__getDnStr(ou_dn)))
				break
			self.logger.info(msg0101.format(ret.strip()))


	def __modifyADou(self, dn=None, odn=None):

		if dn:
			dn = self.__getDnList(dn)
			dn_str = self.__getDnStr(dn)
			ad_dn = self.__getAdDnList(dn)
			ad_dn_str = self.__getDnStr(ad_dn)
		else:
			dn = self.dn_list
			dn_str = self.dn
			ad_dn = self.ad_dn_list
			ad_dn_str = self.ad_dn

		if odn:
			odn = self.__getDnList(odn)
			odn_str = self.__getDnStr(odn)
			ad_odn = self.__getAdDnList(odn)
			ad_odn_str = self.__getDnStr(ad_odn)
		else:
			if not self.odn:
				self.exit(253, msg0334.format(dn_str))

			odn = self.odn_list
			odn_str = self.odn
			ad_odn = self.ad_odn_list
			ad_odn_str = self.ad_odn

		# abbort if dn didn't change
		if dn_str == odn_str:
			self.logger.info(msg0336.format(dn_str, ad_dn_str))
			return

		self.logger.info(msg0335.format(odn_str, ad_odn_str, dn_str, ad_dn_str))

		# get AD OU list
		ou_dn_lists = self.__snc_listADou()

		# does the new OU exist in AD?
		if ad_dn in ou_dn_lists:

			self.logger.error(msg0331.format(dn_str, ad_dn_str))
			return

		# does the original OU exist in AD?
		if ad_odn not in ou_dn_lists:

			self.logger.warn(msg0330.format(odn_str, ad_odn_str))

			# create the current dn, as there is no original dn existent
			self.__createADou(dn)
			return

		# does the base of the new OU exist in AD?
		ad_dn_base = ad_dn[:-1]
		if ad_dn_base not in ou_dn_lists:
			self.logger.warn(msg0333.format(self.__getDnStr(ad_dn_base)))
			self.__createADou(dn[:-1])

		# we can perform rename as ẃell as move with a single call to
		# "samba-tool ou rename", we only need to make sure the base
		# does exist
		ret = self.__snc_renameADou(ad_odn_str, ad_dn_str)
		if ret == False:
			self.logger.error(msg0332.format(ad_odn_str, ad_dn_str))
			return
		self.logger.info(msg0101.format(ret.strip()))


	def __syncGroupSID(self):

		if not self.s4ad_config['returnsid']: return

		ret = self.__snc_getObjectItem('group', self.ad_cn, 'objectSid')
		if ret == False:
			self.logger.warn(msg0191.format(self.ad_cn))
		else:
			if ret == self.ldap_data['sambaSID'][0].decode():
				self.logger.info(msg0194.format(self.ad_cn, self.cn))
				return

			self.logger.info(msg0190.format(self.ad_cn, self.cn))

			mod_list = (
				((self.dn, 'base', '(objectClass=*)', ('sambaSID', ) ), (
					('attrib', 'replace',
						(ret, ),
						('sambaSID', ), ),
				)),
				)

			if not self.ldapmod.modify(mod_list):
				self.logger.warn(msg0192.format(self.ad_cn, self.cn))
			else:
				self.logger.info(msg0193.format(self.ad_cn, self.cn))


	def __syncADgroupMembers(self):

		if not self.s4ad_config['syncusers']: return
		self.logger.info(msg0160.format(self.cn, self.ad_cn))

		# get group SID
		try:
			group_sid = self.ldap_data['sambaSID'][0].decode()
			self.logger.debug('GroupSID: {0}'.format(group_sid))

		except KeyError:
			self.logger.error(msg0167.format(self.cn))
			return

		# get AD user list
		adusers = self.__snc_listADuser()

		# get AD group members
		adgroup_members = self.__snc_listADgroupMembers(self.ad_cn)

		# get openldap user list
		ol_users = self.__getOpenldapUsers()

		# get openldap group members
		if 'memberUid' in self.ldap_data.keys():
			group_members = [m.decode() for m in self.ldap_data['memberUid']]
		else:
			# empty group in openldap
			group_members = []

		# add members with the primary group SID to group members
		group_members.extend(
				[u for u in ol_users.keys()
				if ol_users[u]['sambaPrimaryGroupSID'] == group_sid
				and u not in group_members]
			)

		self.logger.debug(msg0161.format(group_members))

		# create member delta lists for the group
		# only add users existant in AD, that way we don't add for example templates
		members_add = [
			u for u in group_members
			if self.__getAdName('usermapping', u) not in map(str.lower, adgroup_members)
			and self.__getAdName('usermapping', u) in map(str.lower, adusers)
			]

		# don't remove users exclusivly existing in AD, like 'krbtgt'
		members_remove = [
			au for au in adgroup_members
			if self.__getName(au) not in group_members
			and self.__getName(au) in ol_users
			]

		#self.logger.info('members to add: {0}'.format(members_add))
		#self.logger.info('members to remove: {0}'.format(members_remove))

		# add members to AD group
		if members_add:

			self.logger.info(msg0162.format(self.ad_cn, members_add))
			ret = self.__snc_addADgroupMembers(self.ad_cn, members_add)

			if ret == False:
				self.logger.warn(msg0163.format(self.ad_cn))
			else:
				self.logger.info(msg0101.format(ret.strip()))

		# remove members from AD group
		if members_remove:

			self.logger.info(msg0164.format(self.ad_cn, members_remove))
			ret = self.__snc_removeADgroupMembers(self.ad_cn, members_remove)

			if ret == False:
				self.logger.warn(msg0165.format(self.ad_cn))
			else:
				self.logger.info(msg0101.format(ret.strip()))


	def __getOUNames(self, ou=None):

		if not ou:
			ou = self.dn
		ou = ou.removesuffix(',' + self.searchbase)
		ou_list = [i.split('=')[1] for i in reversed(ou.split(','))]
		ad_ou_list = [self.__getAdName(i, 'oumapping') for i in ou_list]
		ad_ou = ','.join(['OU={0}'.format(i) for i in reversed(ad_ou_list)])

		return ou, ad_ou, ou_list, ad_ou_list


	def __wrapSambaTool(self, command):

		creds = (
			'--URL=' + self.s4ad_config['url'],
			'--username=' + self.s4ad_config['user'],
			'--password=' + self.s4ad_config['pwd'],
			)

		outpos = self.sn_out.truncate()
		errpos = self.sn_err.truncate()

		with redirect_stdout(self.sn_out) as out, redirect_stderr(self.sn_err) as err:

			#if 'samba.netcmd' not in sys.modules.keys():
			from samba.netcmd.main import cmd_sambatool
			cmd = cmd_sambatool()

			try:
				retval = cmd._run('samba-tool', *command, *creds)
			except SystemExit as e:
				retval = e.code
			except Exception as e:
				cmd.show_command_error(e)
				retval = 1

		self.sn_out.seek(outpos)
		self.sn_err.seek(errpos)
		retout = self.sn_out.getvalue()
		reterr = self.sn_err.getvalue()

		"""
		# alternative testcode
		outpos = self.sn_out.truncate()
		errpos = self.sn_err.truncate()

		old_stdout = sys.stdout
		old_stderr = sys.stderr
		sys.stdout = self.sn_out
		sys.stderr = self.sn_err

		from samba.netcmd.main import cmd_sambatool
		reload(sys.modules['samba.netcmd'])
		cmd = cmd_sambatool()

		try:
			retval = cmd._run('samba-tool', *command, *creds)
		except SystemExit as e:
			retval = e.code
		except Exception as e:
			cmd.show_command_error(e)
			retval = 1

		sys.stdout = old_stdout
		sys.stderr = old_stderr
		self.sn_out.seek(outpos)
		self.sn_err.seek(errpos)
		retout = self.sn_out.getvalue()
		reterr = self.sn_err.getvalue()
		"""

		if retval != None:
			self.logger.error(msg0102.format(retval, reterr, retout))
			return False

		return retout


	def __snc_listADuser(self):

		self.logger.debug(msg0130)

		cmd_list = ['user', 'list',]
		ret = self.__wrapSambaTool(cmd_list)
		if ret == False:
			self.logger.warn(msg0131)
			return []

		ret_list = self.__splitList(ret)
		self.logger.debug(msg0132.format(ret_list))

		return ret_list


	def __snc_listADgroup(self):

		self.logger.debug(msg0133)

		cmd_list = ['group', 'list',]
		ret = self.__wrapSambaTool(cmd_list)
		if ret == False:
			self.logger.warn(msg0134)
			return []

		ret_list = self.__splitList(ret)
		self.logger.debug(msg0135.format(ret_list))

		return ret_list


	def __snc_listADgroupMembers(self, adgroup):

		self.logger.debug(msg0136.format(adgroup))

		cmd_list = ['group', 'listmembers', adgroup]
		ret = self.__wrapSambaTool(cmd_list)
		if ret == False:
			self.logger.warn(msg0137)
			return []

		ret_list = self.__splitList(ret)
		self.logger.debug(msg0138.format(ret_list))

		return ret_list


	def __snc_listADou(self):

		self.logger.debug(msg0139)

		cmd_list = ['ou', 'list', '--full-dn', ]
		ret = self.__wrapSambaTool(cmd_list)
		if ret == False:
			self.logger.warn(msg0170)
			return []

		ret_list = [[m.split('=') for m in reversed(n.split(','))] for n in self.__splitList(ret)]
		self.logger.debug(msg0171.format(ret_list))

		return ret_list


	def __snc_getObjectItem(self, obj, obj_name, item):

		cmd_list = [obj, 'show',]
		cmd_list.append(obj_name)
		cmd_list.append('--attributes=' + item)

		ret = self.__wrapSambaTool(cmd_list)
		if ret == False: return False
		ret_dict = self.__splitLdif(ret)
		return ret_dict[item][0]


	def __snc_renameUser(self, username, attr, value):

		cmd_mapping ={
			'sn' : '--surname',
			'givenName' : '--given-name',
			'displayName' : '--display-name',
			}

		cmd_list = ['user', 'rename',]
		cmd_list.append(username)
		try:
			cmd_list.append(cmd_mapping[attr] + "=" + value)
		except KeyError:
			self.logger.error(msg0107.format(attr))
			return False

		ret = self.__wrapSambaTool(cmd_list)
		if ret == False: return False
		return True


	def __snc_createADuser(self, aduser, ldap_data, dn=None):

		attr_mapping = {
			'sn' : '--surname=',
			'givenName' : '--given-name=',
			'sambaProfilePath' : '--profile-path=',
			'sambaLogonScript' : '--script-path=',
			'sambaHomeDrive' : '--home-drive=',
			'sambaHomePath' : '--home-directory=',
			'homeDirectory' : '--unix-home=',
			'uid' : '--uid=',
			'uidNumber' : '--uid-number=',
			'gidNumber' : '--gid-number=',
			'gecos' : '--gecos=',
			'loginShell' : '--login-shell=',
			}

		cmd_list = ['user', 'create',]
		cmd_list.append(aduser)
		cmd_list.append('--random-password')
		cmd_list.append('--use-username-as-cn')
		if dn:
			cmd_list.append('--userou=' + dn)

		for attr in attr_mapping.keys():
			cmd_list.append(attr_mapping[attr] + ldap_data[attr][0].decode())

		return self.__wrapSambaTool(cmd_list)


	def __snc_removeADuser(self, aduser):

		cmd_list = ['user', 'delete',]
		cmd_list.append(aduser)

		return self.__wrapSambaTool(cmd_list)


	def __snc_moveADuser(self, aduser, dn):

		cmd_list = ['user', 'move',]
		cmd_list.append(aduser)
		cmd_list.append(dn)

		return self.__wrapSambaTool(cmd_list)


	def __snc_modifyADpassword(self, aduser, pwd):

		cmd_list = ['user', 'setpassword',]
		cmd_list.append(aduser)
		cmd_list.append('--newpassword=' + pwd)

		return self.__wrapSambaTool(cmd_list)


	def __snc_lockADpassword(self, user):

		cmd_list = ['user', 'disable',]
		cmd_list.append(user)

		return self.__wrapSambaTool(cmd_list)


	def __snc_unlockADpassword(self, user):

		cmd_list = ['user', 'enable',]
		cmd_list.append(user)

		return self.__wrapSambaTool(cmd_list)


	def __snc_syncUserExpiry(self, user, days):

		cmd_list = ['user', 'setexpiry',]
		cmd_list.append(user)
		if days == None:
			cmd_list.append('--noexpiry')
		else:
			cmd_list.append('--days=' + days)

		return self.__wrapSambaTool(cmd_list)


	def __snc_setSurname(self, user, name):

		cmd_list = ['user', 'rename',]
		cmd_list.append(user)
		cmd_list.append('--surname=' + name)

		return self.__wrapSambaTool(cmd_list)


	def __snc_createADgroup(self, adgroup, nisname, ldap_data, dn=None):

		attr_mapping = {
			'description' : '--description=',
			'gidNumber' : '--gid-number=',
			}

		cmd_list = ['group', 'add',]
		cmd_list.append(adgroup)
		if dn:
			cmd_list.append('--groupou=' + dn)

		for attr in attr_mapping.keys():
			cmd_list.append(attr_mapping[attr] + ldap_data[attr][0].decode())
		cmd_list.append('--nis-domain=' + nisname)

		return self.__wrapSambaTool(cmd_list)


	def __snc_addADgroupMembers(self, adgroup, members_add):

		cmd_list = ['group', 'addmembers', adgroup]
		cmd_list.append(','.join(members_add))

		return self.__wrapSambaTool(cmd_list)


	def __snc_removeADgroupMembers(self, adgroup, members_remove):

		cmd_list = ['group', 'removemembers', adgroup]
		cmd_list.append(','.join(members_remove))

		#print(' '.join(cmd_list))
		return self.__wrapSambaTool(cmd_list)


	def __snc_removeADgroup(self, adgroup):

		cmd_list = ['group', 'delete',]
		cmd_list.append(adgroup)

		return self.__wrapSambaTool(cmd_list)


	def __snc_moveADgroup(self, adgroup, dn):

		cmd_list = ['group', 'move',]
		cmd_list.append(adgroup)
		cmd_list.append(dn)

		return self.__wrapSambaTool(cmd_list)


	def __snc_renameADgroup(self, adgroup, newname):

		cmd_list = ['group', 'rename',]
		cmd_list.append(adgroup)
		cmd_list.append('--samaccountname=' + newname)

		return self.__wrapSambaTool(cmd_list)


	def __snc_createADou(self, adou, ldap_data):

		attr_mapping = {
			'description' : '--description=',
			}

		cmd_list = ['ou', 'create',]
		cmd_list.append(adou)

		for attr in attr_mapping.keys():
			try:
				cmd_list.append(attr_mapping[attr] + ldap_data[attr][0].decode())
			except (KeyError, IndexError):
				pass

		return self.__wrapSambaTool(cmd_list)


	def __snc_removeADou(self, adou):

		cmd_list = ['ou', 'delete',]
		cmd_list.append(adou)

		return self.__wrapSambaTool(cmd_list)


	def __snc_renameADou(self, adou, new_adou):

		cmd_list = ['ou', 'rename',]
		cmd_list.extend( (adou, new_adou, ) )

		return self.__wrapSambaTool(cmd_list)


	# TODO: for modifications on existing AD users we need to directly
	# edit sambas ldb for now we have to edit the user by hand with:
	# > samba-tool user edit <username>
	# TODO: maybe sync uuid ? --> 'objectGUID'
	# TODO: set primaryGroupID with:
	# samba-tool user setprimarygroup <username> <groupname>
	# i.e. samba-tool user setprimarygroup administrator "Domain Admins"
	# user must be member of that group
	# 
	def createADuser(self, mod=False):

		if not self.s4ad: return
		self.logger.info(msg0210.format(self.cn, self.ad_cn))

		# black/white list users
		if not self.__bwListUser(): return False

		# create user
		ret = self.__createADuser()

		#if not mod: return ret
		# for future use: do user modifications here

		return ret


	def removeADuser(self):

		if not self.s4ad: return
		self.logger.info(msg0230.format(self.cn, self.ad_cn))

		# black/white list groups
		if not self.__bwListUser(): return

		# get AD users list
		adusers = self.__snc_listADuser()

		# remove user from AD
		if adusers and self.ad_cn.lower() not in map(str.lower, adusers):
			self.logger.info(msg0103.format(self.ad_cn))
		else:
			ret = self.__snc_removeADuser(self.ad_cn)
			if ret == False:
				self.exit(1, msg0231.format(self.ad_cn))
			self.logger.info(msg0101.format(ret.strip()))


	def modifyADpassword(self, pwd=None):

		if not self.s4ad: return
		self.logger.info(msg0240.format(self.cn, self.ad_cn))

		# black/white list groups
		if not self.__bwListUser(): return

		# check for samba account in openldap
		if b'sambaSamAccount' not in self.ldap_data['objectClass']:
			self.logger.info(msg0111.format(self.cn))
			return

		# get AD users list
		adusers = self.__snc_listADuser()

		# change user password in AD
		#if adusers and aduser not in adusers:
		if adusers and self.ad_cn.lower() not in map(str.lower, adusers):
			self.logger.warn(msg0103.format(self.ad_cn))
		else:
			try:
				ret = self.__snc_modifyADpassword(self.ad_cn, pwd or self.opt[0])

			except IndexError:
				# no passwort as optional data
				self.exit(1, msg0241.format(self.ad_cn))

			if ret == False:
				self.exit(1, msg0242.format(self.ad_cn))
			self.logger.info(msg0101.format(ret.strip()))


	def lockADpassword(self):

		if not self.s4ad: return
		self.logger.info(msg0250.format(self.cn, self.ad_cn))

		# black/white list groups
		if not self.__bwListUser(): return

		# check for samba account in openldap
		if b'sambaSamAccount' not in self.ldap_data['objectClass']:
			self.logger.info(msg0111.format(self.cn))
			return

		# get AD users list
		adusers = self.__snc_listADuser()

		# lock user in AD
		#if adusers and aduser not in adusers:
		if adusers and self.ad_cn.lower() not in map(str.lower, adusers):
			self.logger.warn(msg0103.format(self.ad_cn))
		else:
			ret = self.__snc_lockADpassword(self.ad_cn)
			if ret == False:
				self.exit(1, msg0251.format(self.ad_cn))
			self.logger.info(msg0101.format(ret.strip()))


	def unlockADpassword(self):

		if not self.s4ad: return
		self.logger.info(msg0260.format(self.cn, self.ad_cn))

		# black/white list groups
		if not self.__bwListUser(): return

		# check for samba account in openldap
		if b'sambaSamAccount' not in self.ldap_data['objectClass']:
			self.logger.info(msg0111.format(self.cn))
			return

		# get AD users list
		adusers = self.__snc_listADuser()

		# lock user in AD
		#if adusers and aduser not in adusers:
		if adusers and self.ad_cn.lower() not in map(str.lower, adusers):
			self.logger.warn(msg0103.format(self.ad_cn))
		else:
			ret = self.__snc_unlockADpassword(self.ad_cn)
			if ret == False:
				self.exit(1, msg0261.format(self.ad_cn))
			self.logger.info(msg0101.format(ret.strip()))


	# TODO: for modifications on existing AD groups we need to directly
	# edit sambas ldb for now we have to edit the group by hand with:
	# > samba-tool group edit <groupname>
	def createADgroup(self, mod=False):

		if not self.s4ad: return
		self.logger.info(msg0150.format(self.cn, self.ad_cn))

		# black/white list groups
		if not self.__bwListGroup(): return

		# create group
		self.__createADgroup(mod)

		#if not mod: return
		# for future use: do group modifications here


	def removeADgroup(self):

		if not self.s4ad: return
		self.logger.info(msg0180.format(self.cn, self.ad_cn))

		# black/white list groups
		if not self.__bwListGroup(): return

		# get AD groups list
		adgroups = self.__snc_listADgroup()

		# remove group from AD
		if adgroups and self.ad_cn.lower() not in map(str.lower, adgroups):
			self.logger.info(msg0181.format(self.ad_cn))
		else:
			ret = self.__snc_removeADgroup(self.ad_cn)
			if ret == False:
				self.exit(1, msg0182.format(self.ad_cn))
			self.logger.info(msg0101.format(ret.strip()))


	def createADou(self):

		if not self.s4ad: return
		if not self.s4ad_config['useadou']:
			self.logger.info(msg0110)
			return

		self.__createADou()


	def modifyADou(self):

		if not self.s4ad: return
		if not self.s4ad_config['useadou']:
			self.logger.info(msg0110)
			return

		self.__modifyADou()


	def removeADou(self):

		if not self.s4ad: return
		if not self.s4ad_config['useadou']:
			self.logger.info(msg0110)
			return

		self.__removeADou()


