#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention Common Python Library
#
# Copyright 2014-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
from __future__ import print_function
from __future__ import absolute_import
import ldap
import ldap.sasl
from ldap.filter import filter_format
import os
import sys
import subprocess
import locale
import socket
import tempfile
import ipaddress
import time
from datetime import datetime, timedelta
import pipes
from typing import Optional  # noqa: F401
import six
from univention.config_registry import ConfigRegistry
import univention.uldap
import univention.lib.package_manager
from univention.lib.misc import custom_groupname
import univention.debug as ud
from univention.config_registry.interfaces import Interfaces
import dns.resolver
if not six.PY2:
	import ldb
	from samba.dcerpc import nbt, security
	from samba.dcerpc.security import DOMAIN_RID_ADMINS, DOMAIN_RID_ADMINISTRATOR
	from samba.ndr import ndr_unpack
	from samba.net import Net
	from samba.param import LoadParm
else:
	from collections import namedtuple
	DOMAIN_RID_ADMINS = 512
	DOMAIN_RID_ADMINISTRATOR = 500
# Ensure univention debug is initialized
[docs]def initialize_debug():
	# type: () -> None
	# Use a little hack to determine if univention.debug has been initialized
	# get_level(..) returns always ud.ERROR if univention.debug is not initialized
	oldLevel = ud.get_level(ud.MODULE)
	if oldLevel == ud.PROCESS:
		ud.set_level(ud.MODULE, ud.DEBUG)
		is_ready = (ud.get_level(ud.MODULE) == ud.DEBUG)
	else:
		ud.set_level(ud.MODULE, ud.PROCESS)
		is_ready = (ud.get_level(ud.MODULE) == ud.PROCESS)
	if not is_ready:
		ud.init('/var/log/univention/join.log', ud.FLUSH, ud.FUNCTION)
		ud.set_level(ud.MODULE, ud.PROCESS)
	else:
		ud.set_level(ud.MODULE, oldLevel) 
[docs]class failedToSetService(Exception):
	'''ucs_addServiceToLocalhost failed''' 
[docs]class invalidUCSServerRole(Exception):
	'''Invalid UCS Server Role''' 
[docs]class failedADConnect(Exception):
	'''Connection to AD Server failed''' 
[docs]class failedToSetAdministratorPassword(Exception):
	'''Failed to set the password of the UCS Administrator to the AD password''' 
[docs]class failedToCreateAdministratorAccount(Exception):
	'''Failed to create the administrator account in UCS''' 
[docs]class sambaSidNotSetForAdministratorAccount(Exception):
	'''sambaSID is not set for Administrator account in UCS''' 
[docs]class failedToSearchForWellKnownSid(Exception):
	'''failed to search for well known SID''' 
[docs]class failedToAddAdministratorAccountToDomainAdmins(Exception):
	'''failed to add Administrator account to Domain Admins''' 
[docs]class domainnameMismatch(Exception):
	'''Domain Names don't match''' 
[docs]class connectionFailed(Exception):
	'''Connection to AD failed''' 
[docs]class notDomainAdminInAD(Exception):
	'''User is not member of Domain Admins group in AD''' 
[docs]class univentionSambaWrongVersion(Exception):
	'''univention-samba candidate has wrong version''' 
[docs]class timeSyncronizationFailed(Exception):
	'''Time synchronization failed.''' 
[docs]class manualTimeSyncronizationRequired(timeSyncronizationFailed):
	'''Time difference critical for Kerberos but synchronization aborted.''' 
[docs]class sambaJoinScriptFailed(Exception):
	'''26univention-samba.inst failed''' 
[docs]class failedToAddServiceRecordToAD(Exception):
	'''failed to add SRV record in AD''' 
[docs]class failedToGetUcrVariable(Exception):
	'''failed to get ucr variable''' 
[docs]def is_localhost_in_admember_mode(ucr=None):
	# type: (Optional[ConfigRegistry]) -> bool
	if not ucr:
		ucr = ConfigRegistry()
		ucr.load()
	return ucr.is_true('ad/member', False) 
[docs]def is_localhost_in_adconnector_mode(ucr=None):
	# type: (Optional[ConfigRegistry]) -> bool
	if not ucr:
		ucr = ConfigRegistry()
		ucr.load()
	if ucr.is_false('ad/member', True) and ucr.get('connector/ad/ldap/host'):
		return True
	return False 
[docs]def is_domain_in_admember_mode(ucr=None):
	# type: (Optional[ConfigRegistry]) -> bool
	if not ucr:
		ucr = ConfigRegistry()
		ucr.load()
	lo = univention.uldap.getMachineConnection()
	res = lo.search(base=ucr.get('ldap/base'), filter='(&(univentionServerRole=master)(univentionService=AD Member))')
	if res:
		return True
	return False 
def _get_kerberos_ticket(principal, password, ucr=None):
	# type: (str, str, Optional[ConfigRegistry]) -> None
	ud.debug(ud.MODULE, ud.INFO, "running _get_kerberos_ticket")
	if not ucr:
		ucr = ConfigRegistry()
		ucr.load()
	# We need to remove the target credential cache first,
	# otherwise kinit may use an old ticket and run into "krb5_get_init_creds: Clock skew too great".
	cmd1 = ("/usr/bin/kdestroy",)
	p1 = subprocess.Popen(cmd1, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
	stdout, stderr = p1.communicate()
	if p1.returncode != 0:
		ud.debug(ud.MODULE, ud.ERROR, "kdestroy failed:\n%s" % stdout.decode('UTF-8', 'replace'))
	with tempfile.NamedTemporaryFile('w+') as f:
		os.fchmod(f.fileno(), 0o600)
		f.write(password)
		f.flush()
		cmd2 = ("/usr/bin/kinit", "--no-addresses", "--password-file=%s" % (f.name,), principal)
		p1 = subprocess.Popen(cmd2, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
		stdout, stderr = p1.communicate()
		if p1.returncode != 0:
			msg = "kinit failed:\n%s" % (stdout.decode('UTF-8', 'replace'),)
			ud.debug(ud.MODULE, ud.ERROR, msg)
			raise connectionFailed(msg)
		if stdout:
			ud.debug(ud.MODULE, ud.WARN, "kinit output:\n%s" % stdout.decode('UTF-8', 'replace'))
[docs]def check_connection(ad_domain_info, username, password):
	ud.debug(ud.MODULE, ud.INFO, "running check_connection")
	test_share = '//%s/sysvol' % ad_domain_info["DC IP"]
	cmd = ('smbclient', '-U', '%s%%%s' % (username, password), '-c', 'quit', test_share)
	p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
	stdout, stderr = p1.communicate()
	if p1.returncode != 0:
		raise connectionFailed(stdout.decode('UTF-8', 'replace')) 
[docs]def flush_nscd_hosts_cache():
	# type: () -> None
	if os.path.exists("/usr/sbin/nscd"):
		cmd = ("/usr/sbin/nscd", "--invalidate=hosts")
		subprocess.call(cmd) 
[docs]def decode_sid(value):
	if six.PY3:
		return ndr_unpack(security.dom_sid, value)
	# SID in AD
	#
	#   | Byte 1         | Byte 2-7           | Byte 9-12                | Byte 13-16 |
	#   ----------------------------------------------------------------------------------------------------------------
	#   | Der erste Wert | Gibt die Laenge    | Hier sind jetzt          | siehe 9-12 |
	#   | der SID, also  | des restlichen     | die eiegntlichen         |            |
	#   | der Teil nach  | Strings an, da die | SID Daten.               |            |
	#   | S-             | SID immer relativ  | In einem int Wert        |            |
	#   |                | kurz ist, meistens | sind die Werte           |            |
	#   |                | nur das 2. Byte    | Hexadezimal gespeichert. |            |
	#
	sid = 'S-'
	sid += "%d" % ord(value[0])
	sid_len = ord(value[1])
	sid += "-%d" % ord(value[7])
	for i in range(0, sid_len):
		res = ord(value[8 + (i * 4)]) + (ord(value[9 + (i * 4)]) << 8) + (ord(value[10 + (i * 4)]) << 16) + (ord(value[11 + (i * 4)]) << 24)
		sid += "-%u" % res
	return sid 
[docs]def check_ad_account(ad_domain_info, username, password, ucr=None):
	'''
	returns True if account is Administrator in AD
	returns False if account is just a member of Domain Admins
	raises exception notDomainAdminInAD if neither criterion is met.
	'''
	ud.debug(ud.MODULE, ud.INFO, "running check_account")
	ad_server_ip = ad_domain_info["DC IP"]
	ad_server_name = ad_domain_info["DC DNS Name"]
	ad_ldap_base = ad_domain_info["LDAP Base"]
	ad_domain = ad_domain_info["Domain"]
	ad_realm = ad_domain.upper()
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	try:
		time_sync(ad_server_ip)
	except timeSyncronizationFailed as ex:
		ud.debug(ud.MODULE, ud.WARN, "Time sync failed, trying to authenticate anyway. Original exception: %s" % (ex,))
	(previous_dns_ucr_set, previous_dns_ucr_unset) = set_nameserver([ad_server_ip], ucr)
	(previous_krb_ucr_set, previous_krb_ucr_unset) = prepare_kerberos_ucr_settings(realm=ad_realm, ucr=ucr)
	(previous_host_static_ucr_set, previous_host_static_ucr_unset) = prepare_dns_reverse_settings(ad_domain_info, ucr=ucr)
	try:
		principal = "%s@%s" % (username, ad_realm)
		_get_kerberos_ticket(principal, password, ucr)
		auth = ldap.sasl.gssapi("")
	except Exception:
		set_ucr(previous_dns_ucr_set, previous_dns_ucr_unset)
		set_ucr(previous_krb_ucr_set, previous_krb_ucr_unset)
		set_ucr(previous_host_static_ucr_set, previous_host_static_ucr_unset)
		flush_nscd_hosts_cache()
		raise
	# Ok, ready and set for kerberized LDAP lookup
	try:
		subprocess.call(['systemctl', 'stop', 'nscd'])
		lo_ad = univention.uldap.access(host=ad_server_name, port=389, base=ad_ldap_base, binddn=None, bindpw=None, start_tls=0, use_ldaps=False, decode_ignorelist=["objectSid"])
		lo_ad.lo.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
		lo_ad.lo.set_option(ldap.OPT_REFERRALS, 0)
		lo_ad.lo.sasl_interactive_bind_s("", auth)
	except (ldap.INVALID_CREDENTIALS, ldap.UNWILLING_TO_PERFORM) as exc:
		ud.debug(ud.MODULE, ud.ERROR, str(exc))
		raise connectionFailed(exc)
	finally:
		subprocess.call(['systemctl', 'start', 'nscd'])
		set_ucr(previous_dns_ucr_set, previous_dns_ucr_unset)
		set_ucr(previous_krb_ucr_set, previous_krb_ucr_unset)
		set_ucr(previous_host_static_ucr_set, previous_host_static_ucr_unset)
		flush_nscd_hosts_cache()
	try:
		res = lo_ad.search(scope="base", attr=["objectSid"])
	except ldap.OPERATIONS_ERROR:
		# Try again
		try:
			subprocess.call(['systemctl', 'stop', 'nscd'])
			lo_ad = univention.uldap.access(host=ad_server_name, port=389, base=ad_ldap_base, binddn=None, bindpw=None, start_tls=0, use_ldaps=False, decode_ignorelist=["objectSid"])
			lo_ad.lo.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
			lo_ad.lo.set_option(ldap.OPT_REFERRALS, 0)
			lo_ad.lo.sasl_interactive_bind_s("", auth)
		except (ldap.INVALID_CREDENTIALS, ldap.UNWILLING_TO_PERFORM) as exc:
			msg = "second attempt: " + str(exc)
			ud.debug(ud.MODULE, ud.ERROR, msg)
			raise connectionFailed(exc)
		finally:
			subprocess.call(['systemctl', 'start', 'nscd'])
			set_ucr(previous_dns_ucr_set, previous_dns_ucr_unset)
			set_ucr(previous_krb_ucr_set, previous_krb_ucr_unset)
			set_ucr(previous_host_static_ucr_set, previous_host_static_ucr_unset)
			flush_nscd_hosts_cache()
		res = lo_ad.search(scope="base", attr=["objectSid"])
	if not res or "objectSid" not in res[0][1]:
		msg = "Determination of AD domain SID failed"
		ud.debug(ud.MODULE, ud.ERROR, msg)
		raise connectionFailed(msg)
	domain_sid = decode_sid(res[0][1]["objectSid"][0])
	res = lo_ad.search(filter=filter_format("(sAMAccountName=%s)", [username]), attr=["objectSid", "primaryGroupID"])
	if not res or "objectSid" not in res[0][1]:
		msg = "Determination user SID failed"
		ud.debug(ud.MODULE, ud.ERROR, msg)
		raise connectionFailed(msg)
	user_sid = decode_sid(res[0][1]["objectSid"][0])
	admin_sid = u"%s-%d" % (domain_sid, DOMAIN_RID_ADMINISTRATOR)
	admins_sid = "%s-%d" % (domain_sid, DOMAIN_RID_ADMINS)
	if six.PY3:
		admin_sid = security.dom_sid(admin_sid)
		admins_sid = security.dom_sid(admins_sid)
	if user_sid == admin_sid:
		ud.debug(ud.MODULE, ud.PROCESS, "User is default AD Administrator")
		return True
	if int(res[0][1]["primaryGroupID"][0]) == DOMAIN_RID_ADMINS:
		ud.debug(ud.MODULE, ud.PROCESS, "User is primary member of Domain Admins")
		return False
	user_dn = res[0][0]
	res = lo_ad.search(filter=filter_format("(sAMAccountName=%s)", [username]), base=user_dn, scope="base", attr=["tokenGroups"])
	if not res or "tokenGroups" not in res[0][1]:
		msg = "Lookup of AD group memberships for user failed"
		ud.debug(ud.MODULE, ud.ERROR, msg)
		raise connectionFailed(msg)
	if "tokenGroups" not in res[0][1]:
		raise notDomainAdminInAD()
	for group_sid_ndr in res[0][1]["tokenGroups"]:
		group_sid = decode_sid(group_sid_ndr)
		if group_sid == admins_sid:
			return False
	else:
		ud.debug(ud.MODULE, ud.ERROR, "User is not member of Domain Admins")
		raise notDomainAdminInAD() 
def _sid_of_ucs_sambadomain(lo=None, ucr=None):
	if not lo:
		lo = univention.uldap.getMachineConnection()
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	res = lo.search(filter=filter_format("(&(objectclass=sambadomain)(sambaDomainName=%s))", [ucr.get("windows/domain")]), attr=["sambaSID"], unique=True)
	if not res:
		ud.debug(ud.MODULE, ud.ERROR, "No UCS LDAP search result for sambaDomainName=%s" % ucr.get("windows/domain"))
		raise ldap.NO_SUCH_OBJECT({'desc': 'no object'})
	ucs_domain_sid = res[0][1].get("sambaSID", [None])[0]
	if not ucs_domain_sid:
		ud.debug(ud.MODULE, ud.ERROR, "No sambaSID found for sambaDomainName=%s" % ucr.get("windows/domain"))
		raise ldap.NO_SUCH_OBJECT({'desc': 'no object'})
	return ucs_domain_sid.decode('ASCII')
def _dn_of_udm_domain_admins(lo=None, ucr=None):
	if not lo:
		lo = univention.uldap.getMachineConnection()
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	ucs_domain_sid = _sid_of_ucs_sambadomain(lo, ucr)
	domain_admins_sid = "%s-%d" % (ucs_domain_sid, DOMAIN_RID_ADMINS)
	res = lo.searchDn(filter=filter_format("(sambaSID=%s)", [domain_admins_sid]), unique=True)
	if not res:
		ud.debug(ud.MODULE, ud.ERROR, "Failed to determine DN of UCS Domain Admins group")
		raise ldap.NO_SUCH_OBJECT({'desc': 'no object'})
	return res[0]
def _create_domain_admin_account_in_udm(username, password, lo=None, ucr=None):
	if not lo:
		lo = univention.uldap.getMachineConnection()
	ud.debug(ud.MODULE, ud.INFO, "running _create_domain_admin_account_in_udm")
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	domain_admins_dn = _dn_of_udm_domain_admins(lo, ucr)
	cmd = ("univention-directory-manager", "users/user", "create", "--position", "cn=users,%s" % ucr.get("ldap/base"), "--set", "username=%s" % username, "--set", "lastname=tmp", "--set", "password=%s" % password, "--set", "primaryGroup=%s" % domain_admins_dn)
	p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
	stdout, stderr = p1.communicate()
	if p1.returncode != 0:
		ud.debug(ud.MODULE, ud.ERROR, "Account creation for %s failed" % username)
		if stdout:
			ud.debug(ud.MODULE, ud.ERROR, "udm users/user create output:\n%s" % stdout.decode('UTF-8', 'replace'))
		return False
	return True
def _ucs_sid_is_well_known_administrator(user_sid, lo=None, ucr=None):
	if not lo:
		lo = univention.uldap.getMachineConnection()
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	ucs_domain_sid = _sid_of_ucs_sambadomain(lo, ucr)
	administrator_sid = "%s-%d" % (ucs_domain_sid, DOMAIN_RID_ADMINISTRATOR)
	if user_sid == administrator_sid:
		return True
	return False
def _add_udm_account_to_domain_admins(user_dn, lo=None, ucr=None):
	if not lo:
		lo = univention.uldap.getMachineConnection()
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	domain_admins_dn = _dn_of_udm_domain_admins(lo, ucr)
	cmd = ("univention-directory-manager", "users/user", "modify", "--dn", user_dn, "--append", "groups=%s" % domain_admins_dn)
	p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
	stdout, stderr = p1.communicate()
	if p1.returncode != 0:
		ud.debug(ud.MODULE, ud.ERROR, "Adding %s to Domain Admins failed" % user_dn)
		if stdout:
			ud.debug(ud.MODULE, ud.ERROR, "udm users/user modify groups output:\n%s" % stdout.decode('UTF-8', 'replace'))
		return False
	return True
def _set_udm_account_password(user_dn, password):
	cmd = ('univention-directory-manager', 'users/user', 'modify', '--dn', user_dn, '--set', 'password=%s' % password, '--set', 'overridePWHistory=1', '--set', 'overridePWLength=1')
	p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
	stdout, stderr = p1.communicate()
	if p1.returncode != 0:
		ud.debug(ud.MODULE, ud.ERROR, "Failed to set AD password in UDM for %s" % user_dn)
		if stdout:
			ud.debug(ud.MODULE, ud.ERROR, "udm users/user modify password output:\n%s" % stdout.decode('UTF-8', 'replace'))
		return False
	return True
[docs]def prepare_administrator(username, password, ucr=None):
	ud.debug(ud.MODULE, ud.PROCESS, "Prepare administrator account")
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	# First check if account exists in LDAP, otherwise create it:
	lo = univention.uldap.getMachineConnection()
	res = lo.search(filter=filter_format("(&(uid=%s)(objectClass=shadowAccount))", (username,)), attr=["userPassword", "sambaSID"])
	if not res:
		ud.debug(ud.MODULE, ud.INFO, "No UCS LDAP search result for uid=%s" % username)
		try:
			success = _create_domain_admin_account_in_udm(username, password, lo, ucr)
		except ldap.NO_SUCH_OBJECT:
			success = False
		if not success:
			raise failedToCreateAdministratorAccount()
		return
	# Second, if the account existed already, check if it has the well known Administrator SID
	user_dn = res[0][0]
	user_sid = res[0][1].get("sambaSID", [None])[0]
	old_hash = res[0][1].get("userPassword", [None])[0]
	if not user_sid:
		ud.debug(ud.MODULE, ud.ERROR, "UCS LDAP search for sambaSID of uid=%s failed" % username)
		raise sambaSidNotSetForAdministratorAccount()
	is_well_known_admin = False
	try:
		is_well_known_admin = _ucs_sid_is_well_known_administrator(user_sid.decode('ASCII'), lo, ucr)
	except ldap.NO_SUCH_OBJECT:
		raise failedToSearchForWellKnownSid()
	# Third, if the account doesn't have the well known Administrator SID, add it to Domain Admins
	if not is_well_known_admin:
		try:
			success = _add_udm_account_to_domain_admins(user_dn, lo, ucr)
		except ldap.NO_SUCH_OBJECT:
			success = False
		if not success:
			raise failedToAddAdministratorAccountToDomainAdmins()
		return
	# Finally, if the account does have the Administrator SID, set it's UDM password to the AD one.
	if old_hash == b'{KINIT}':
		return
	success = _set_udm_account_password(user_dn, password)
	if not success:
		raise failedToSetAdministratorPassword() 
def _mapped_ad_dn(ad_dn, ad_ldap_base, ucr=None):
	"""
	>>> _mapped_ad_dn('uid=Administrator + CN=admin,OU=users,CN=univention,Foo=univention,bar=base', 'foo=univention,bar = base', {'ldap/base': 'dc=base'})
	'uid=Administrator+cn=admin,ou=users,cn=univention,dc=base'
	"""
	parent = ad_dn
	while parent:
		if univention.uldap.access.compare_dn(parent, ad_ldap_base):
			break
		parent = univention.uldap.parentDn(parent)
	else:
		ud.debug(ud.MODULE, ud.ERROR, "Mapping of AD DN %r failed, base is not %r" % (ad_dn, ad_ldap_base))
		return
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	base = ldap.dn.str2dn(ad_ldap_base)
	dn = [[(attr[0].lower() if attr[0] in ('CN', 'OU') else attr[0], attr[1], attr[2]) for attr in x] for x in ldap.dn.str2dn(ad_dn)[:-len(base)]]
	return ldap.dn.dn2str(dn + ldap.dn.str2dn(ucr.get("ldap/base")))
[docs]def synchronize_account_position(ad_domain_info, username, password, ucr=None):
	ud.debug(ud.MODULE, ud.PROCESS, "running synchronize_account_position")
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	# First determine target position from AD:
	ad_server_ip = ad_domain_info["DC IP"]
	ad_server_name = ad_domain_info["DC DNS Name"]
	ad_ldap_base = ad_domain_info["LDAP Base"]
	ad_domain = ad_domain_info["Domain"]
	ad_realm = ad_domain.upper()
	try:
		time_sync(ad_server_ip)
	except timeSyncronizationFailed as ex:
		ud.debug(ud.MODULE, ud.WARN, "Time sync failed, trying to authenticate anyway. Original exception: %s" % (ex,))
	principal = "%s@%s" % (username, ad_realm)
	_get_kerberos_ticket(principal, password, ucr)
	try:
		lo_ad = univention.uldap.access(host=ad_server_name, port=389, base=ad_ldap_base, binddn=None, bindpw=None, start_tls=0, use_ldaps=False, decode_ignorelist=["objectSid"])
		lo_ad.lo.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3)
		lo_ad.lo.set_option(ldap.OPT_REFERRALS, 0)
		auth = ldap.sasl.gssapi("")
		lo_ad.lo.sasl_interactive_bind_s("", auth)
	except (ldap.INVALID_CREDENTIALS, ldap.UNWILLING_TO_PERFORM):
		return False  # Massive failure, but no issue to be raised here.
	res = lo_ad.searchDn(filter=filter_format("(sAMAccountName=%s)", [username]))
	if not res:
		ud.debug(ud.MODULE, ud.ERROR, "Lookup of AD DN for user %s failed" % username)
		return False  # Massive failure, but no issue to be raised here.
	ad_user_dn = res[0]
	# Second determine position in UCS LDAP:
	lo = univention.uldap.getMachineConnection()
	res = lo.searchDn(filter=filter_format("(&(uid=%s)(objectClass=shadowAccount))", (username,)), unique=True)
	if not res:
		ud.debug(ud.MODULE, ud.ERROR, "No UCS LDAP search result for uid=%s" % username)
		return False  # Massive failure, but no issue to be raised here.
	ucs_user_dn = res[0]
	if ucs_user_dn.lower() == ad_user_dn.lower():
		return True
	mapped_ad_user_dn = _mapped_ad_dn(ad_user_dn, ad_ldap_base, ucr)
	target_position = lo.parentDn(mapped_ad_user_dn)
	cmd = ("univention-directory-manager", "users/user", "move", "--dn", ucs_user_dn, "--position", target_position)
	p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
	stdout, stderr = p1.communicate()
	if p1.returncode != 0:
		ud.debug(ud.MODULE, ud.ERROR, "Moving UDM object %s to %s failed" % (ucs_user_dn, target_position))
		if stdout:
			ud.debug(ud.MODULE, ud.ERROR, "udm users/user modify groups output:\n%s" % stdout.decode('UTF-8', 'replace'))
		return False
	return True 
def _server_supports_ssl(server):
	ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
	ldapuri = "ldap://%s:389" % (server)
	lo = ldap.initialize(ldapuri)
	try:
		lo.start_tls_s()
	except ldap.UNAVAILABLE:
		return False
	except ldap.SERVER_DOWN:
		return False
	return True
[docs]def server_supports_ssl(server):
	ud.debug(ud.MODULE, ud.PROCESS, "Check if server supports SSL")
	# we have to create a new process because there is only one sec context allowed in python-ldap
	p1 = subprocess.Popen([sys.executable, "-c", 'import univention.lib.admember; print(univention.lib.admember._server_supports_ssl(%r))' % (server,)], close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	stdout, stderr = p1.communicate()
	if p1.returncode == 0 and stdout.strip() == b'True':
		ud.debug(ud.MODULE, ud.PROCESS, "SSL True")
		return True
	else:
		ud.debug(ud.MODULE, ud.PROCESS, "SSL False")
		return False 
[docs]def enable_ssl():
	ud.debug(ud.MODULE, ud.PROCESS, "Enable connector SSL")
	univention.config_registry.handler_set([
		u'connector/ad/ldap/ssl=yes',
		u'ldap/sasl/secprops/maxssf=128',
	]) 
[docs]def disable_ssl():
	ud.debug(ud.MODULE, ud.PROCESS, "Disable connector SSL")
	univention.config_registry.handler_set([u'connector/ad/ldap/ssl=no'])
	univention.config_registry.handler_unset([u'ldap/sasl/secprops/maxssf']) 
def _add_service_to_localhost(service):
	ud.debug(ud.MODULE, ud.PROCESS, "Adding service %s to localhost" % service)
	res = subprocess.call('. /usr/share/univention-lib/ldap.sh; ucs_addServiceToLocalhost %s' % (pipes.quote(service),), shell=True)
	if res != 0:
		raise failedToSetService()
def _remove_service_from_localhost(service):
	ud.debug(ud.MODULE, ud.PROCESS, "Remove service %s from localhost" % service)
	res = subprocess.call('. /usr/share/univention-lib/ldap.sh; ucs_removeServiceFromLocalhost %s' % (pipes.quote(service),), shell=True)
	if res != 0:
		raise failedToSetService()
[docs]def add_admember_service_to_localhost():
	_add_service_to_localhost('AD Member') 
[docs]def add_adconnector_service_to_localhost():
	_add_service_to_localhost('AD Connector') 
[docs]def remove_admember_service_from_localhost():
	_remove_service_from_localhost('AD Member') 
[docs]def info_handler(msg):
	ud.debug(ud.MODULE, ud.PROCESS, msg) 
[docs]def error_handler(msg):
	ud.debug(ud.MODULE, ud.ERROR, msg) 
[docs]def remove_install_univention_samba(info_handler=info_handler, step_handler=None, error_handler=error_handler, install=True, uninstall=True):  # TODO: replace with univention-remove?
	pm = univention.lib.package_manager.PackageManager(
		info_handler=info_handler,
		step_handler=step_handler,
		error_handler=error_handler,
		always_noninteractive=True,
	)
	if not pm.update():
		return False
	pm.noninteractive()
	# uninstall first to get rid of the configured samba/* ucr vars
	if uninstall and pm.is_installed('univention-samba'):
		ud.debug(ud.MODULE, ud.PROCESS, "Uninstall univention-samba")
		if not pm.uninstall('univention-samba'):
			return False
	# install
	if install:
		ud.debug(ud.MODULE, ud.PROCESS, "Install univention-samba")
		if not pm.install('univention-samba'):
			ud.debug(ud.MODULE, ud.PROCESS, "Installation of univention-samba failed. Try to re-create sources.list and try again.")
			univention.config_registry.handler_commit(['/etc/apt/sources.list.d/15_ucs-online-version.list', '/etc/apt/sources.list.d/20_ucs-online-component.list'])
			if not pm.update():
				return False
			if not pm.install('univention-samba'):
				ud.debug(ud.MODULE, ud.ERROR, "Installation of univention-samba failed. Abort.")
				return False
	return True 
SAMBA_TOOL_FIELDNAMES_TO_CLDAP_RES = {
	'Forest': 'forest',
	'Domain': 'dns_domain',
	'Netbios domain': 'domain_name',
	'DC name': 'pdc_dns_name',
	'DC netbios name': 'pdc_name',
	'Server site': 'server_site',
	'Client site': 'client_site'
}
[docs]def cldap_finddc(ip, use_samba_lib=six.PY3):
	if use_samba_lib:
		lp = LoadParm()
		lp.load('/dev/null')
		net = Net(creds=None, lp=lp)
		cldap_res = net.finddc(address=ip, flags=nbt.NBT_SERVER_LDAP | nbt.NBT_SERVER_DS | nbt.NBT_SERVER_WRITABLE)
	else:
		cmd = ['samba-tool', 'domain', 'info', ip]
		p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
		output, _ = p1.communicate()
		if not output:
			raise RuntimeError("No output from command: %s" % " ".join(cmd))
		res = {}
		for line in output.rstrip().decode('UTF-8').split('\n'):
			try:
				fieldname, value = line.split(':', 1)
			except ValueError as exc:
				raise RuntimeError("Parsing samba-tool output failed: %s (%s)\nfull output:\n%s" % (line, exc, output))
			fieldname = fieldname.rstrip()
			try:
				res[SAMBA_TOOL_FIELDNAMES_TO_CLDAP_RES[fieldname]] = value.lstrip()
			except KeyError:
				pass  # Unknown field, output may have changed
		for fieldname, key in list(SAMBA_TOOL_FIELDNAMES_TO_CLDAP_RES.items()):
			if key not in res:
				raise RuntimeError("Missing field in samba-tool output: %s\nfull output:\n%s" % (fieldname, output))
		CLDAP_RES = namedtuple('CLDAP_RES', SAMBA_TOOL_FIELDNAMES_TO_CLDAP_RES.values())
		cldap_res = CLDAP_RES(**res)
	return cldap_res 
[docs]def get_defaultNamingContext(ad_server_ip, use_samba_lib=six.PY3):
	if use_samba_lib:
		try:
			remote_ldb = ldb.Ldb()
			remote_ldb.connect(url="ldap://%s" % ad_server_ip)
			return str(remote_ldb.get_default_basedn())
		except ldb.LdbError as exc:
			raise RuntimeError(exc)
	else:
		cmd = ['ldapsearch', '-xLLL', '-h', ad_server_ip, '-s', 'base', '-b', '', 'defaultNamingContext']
		try:
			p1 = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
			output, _ = p1.communicate()
		except OSError as exc:
			raise RuntimeError("Command failed: %s (%s)" % (" ".join(cmd), exc))
		lines = output.rstrip().decode('UTF-8').split('\n')
		if len(lines) < 2:
			raise RuntimeError("No output from command: %s" % " ".join(cmd))
		if not lines[1].startswith('defaultNamingContext: '):
			raise RuntimeError("defaultNamingContext not found on %s" % (ad_server_ip,))
		return lines[1][22:] 
[docs]def lookup_adds_dc(ad_server=None, ucr=None, check_dns=True):
	'''CLDAP lookup'''
	ud.debug(ud.MODULE, ud.PROCESS, "Lookup ADDS DC")
	ad_domain_info = {}
	ips = []
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	if not ad_server:
		ad_server = ucr.get('domainname')
	# get ip addresses
	try:
		ipaddress.ip_address(u'%s' % (ad_server,))
		ips.append(ad_server)
	except ValueError:
		dig_sources = []
		dig_sources_ucr = []
		for source in ['dns/forwarder1', 'dns/forwarder2', 'dns/forwarder3', 'nameserver1', 'nameserver2', 'nameserver3']:
			if source in ucr:
				dig_sources.append("@%s" % ucr[source])
				dig_sources_ucr.append(source)
		for dig_source in dig_sources:
			try:
				cmd = ['dig', dig_source, ad_server, '+short', '+nocookie']
				ud.debug(ud.MODULE, ud.PROCESS, "running %s" % cmd)
				p1 = subprocess.Popen(cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
				stdout, stderr = p1.communicate()
				stdout, stderr = stdout.decode('UTF-8', 'replace'), stderr.decode('UTF-8', 'replace')
				ud.debug(ud.MODULE, ud.PROCESS, "stdout: %s" % stdout)
				ud.debug(ud.MODULE, ud.PROCESS, "stderr: %s" % stderr)
				if p1.returncode == 0:
					for i in stdout.split(u'\n'):
						if i:
							ips.append(i)
				if ips:
					break
			except OSError as ex:
				ud.debug(ud.MODULE, ud.ERROR, "%s failed: %s" % (cmd, ex.args[1]))
	# no ip addresses
	if not ips:
		raise failedADConnect(["DNS lookup of AD Server %s failed. Sources: %s" % (ad_server, ", ".join(dig_sources_ucr))])
	ad_server_ip = None
	check_results = []
	for ip in ips:
		try:  # check cldap
			cldap_res = cldap_finddc(ip)
		except RuntimeError as ex:
			ud.debug(ud.MODULE, ud.ERROR, "Connection to AD Server %s failed: %s" % (ip, ex.args[0]))
			check_results.append("CLDAP: %s" % ex.args[0])
		else:
			if not check_dns:
				ad_server_ip = ip
				break
			try:  # check dns
				cmd = ['dig', '@%s' % ip, '+nocookie']
				ud.debug(ud.MODULE, ud.PROCESS, "running %s" % cmd)
				p1 = subprocess.Popen(cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
				stdout, stderr = p1.communicate()
				stdout, stderr = stdout.decode('UTF-8', 'replace'), stderr.decode('UTF-8', 'replace')
				ud.debug(ud.MODULE, ud.PROCESS, "stdout: %s" % stdout)
				ud.debug(ud.MODULE, ud.PROCESS, "stderr: %s" % stderr)
				if p1.returncode == 0:  # yes, this is also a DNS server, we are good
					ad_server_ip = ip
					break
			except OSError as ex:
				ud.debug(ud.MODULE, ud.ERROR, "%s failed: %s" % (cmd, ex.args[1]))
				check_results.append("DNS: %s" % ex.args[1])
	if ad_server_ip is None:
		raise failedADConnect(["Connection to AD Server %s failed (%s)" % (ad_server, ",".join(check_results))])
	ad_ldap_base = None
	try:
		ad_ldap_base = get_defaultNamingContext(ad_server_ip)
	except RuntimeError as ex:
		raise failedADConnect(["Could not detect LDAP base on %s: %s" % (ad_server_ip, ex.args[1])])
	ad_domain_info = {
		"Forest": cldap_res.forest,
		"Domain": cldap_res.dns_domain,
		"Netbios Domain": cldap_res.domain_name,
		"DC DNS Name": cldap_res.pdc_dns_name,
		"DC Netbios Name": cldap_res.pdc_name,
		"Server Site": cldap_res.server_site,
		"Client Site": cldap_res.client_site,
		"LDAP Base": ad_ldap_base,
		"DC IP": ad_server_ip,
	}
	ud.debug(ud.MODULE, ud.PROCESS, "AD Info: %s" % ad_domain_info)
	return ad_domain_info 
[docs]def set_timeserver(timeserver, ucr=None):
	ud.debug(ud.MODULE, ud.PROCESS, "Setting timeserver to %s" % timeserver)
	univention.config_registry.handler_set([u'timeserver=%s' % (timeserver,)])
	restart_service("ntp") 
[docs]def stop_service(service):
	return invoke_service(service, "stop") 
[docs]def start_service(service):
	return invoke_service(service, "start") 
[docs]def restart_service(service):
	return invoke_service(service, "restart") 
[docs]def invoke_service(service, cmd):
	init_script = '/etc/init.d/%s' % service
	if not os.path.exists(init_script):
		return
	try:
		p1 = subprocess.Popen([init_script, cmd], close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		stdout, stderr = p1.communicate()
	except OSError as ex:
		ud.debug(ud.MODULE, ud.ERROR, "%s %s failed: %s" % (init_script, cmd, ex.args[1],))
		return
	if p1.returncode:
		ud.debug(ud.MODULE, ud.ERROR, "%s %s failed (%d)" % (init_script, cmd, p1.returncode,))
		return
	ud.debug(ud.MODULE, ud.PROCESS, "%s %s: %s" % (init_script, cmd, stdout.decode('UTF-8', 'replace'))) 
[docs]def do_time_sync(ad_ip):
	ud.debug(ud.MODULE, ud.PROCESS, "Synchronizing time to %s" % ad_ip)
	p1 = subprocess.Popen(["rdate", "-s", "-n", ad_ip], close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	stdout, stderr = p1.communicate()
	if p1.returncode:
		ud.debug(ud.MODULE, ud.ERROR, "rdate -s -p failed (%d)" % (p1.returncode,))
		return False
	return True 
[docs]def time_sync(ad_ip, tolerance=180, critical_difference=360):
	'''Try to sync the local time with an AD server'''
	stdout = b""
	env = os.environ.copy()
	env["LC_ALL"] = "C"
	try:
		p1 = subprocess.Popen(["rdate", "-p", "-n", ad_ip], close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
		stdout, stderr = p1.communicate()
	except OSError as ex:
		ud.debug(ud.MODULE, ud.ERROR, "rdate -p -n %s: %s" % (ad_ip, ex.args[1]))
		return False
	if p1.returncode:
		ud.debug(ud.MODULE, ud.ERROR, "rdate failed (%d)" % (p1.returncode,))
		return False
	TIME_FORMAT = "%a %b %d %H:%M:%S %Z %Y"
	time_string = stdout.strip().decode('ASCII')
	old_locale = locale.getlocale(locale.LC_TIME)
	try:
		locale.setlocale(locale.LC_TIME, (None, None))  # 'C' as env['LC_ALL'] some lines earlier
		remote_datetime = datetime.strptime(time_string, TIME_FORMAT)
	except ValueError:
		raise timeSyncronizationFailed("AD Server did not return proper time string: %s" % time_string)
	finally:
		locale.setlocale(locale.LC_TIME, old_locale)
	local_datetime = datetime.today()
	delta_t = local_datetime - remote_datetime
	if abs(delta_t) < timedelta(0, tolerance):
		ud.debug(ud.MODULE, ud.PROCESS, "Time difference is less than %d seconds, skipping reset of local time" % (tolerance,))
	elif local_datetime > remote_datetime:
		if abs(delta_t) >= timedelta(0, critical_difference):
			raise manualTimeSyncronizationRequired("Remote clock is behind local clock by more than %s seconds, refusing to turn back time." % critical_difference)
		else:
			ud.debug(ud.MODULE, ud.WARN, "Remote clock is behind local clock by more than %s seconds, refusing to turn back time, should be accurate enough." % (tolerance,))
			return False
	else:
		if not do_time_sync(ad_ip):
			raise timeSyncronizationFailed("Time synchronization failed")
	return True 
[docs]def check_server_role(ucr=None):
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	if ucr.get("server/role") != "domaincontroller_master":
		raise invalidUCSServerRole("The function become_ad_member can only be run on an UCS Primary Directory Node") 
[docs]def check_domain(ad_domain_info, ucr=None):
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	if ad_domain_info["Domain"].lower() != ucr["domainname"].lower():
		raise domainnameMismatch("The domain of the AD Server does not match the local domain: %s" % (ad_domain_info["Domain"],)) 
[docs]def set_nameserver(server_ips, ucr=None):
	previous_ucr_set = []
	previous_ucr_unset = []
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	count = 1
	for server_ip in server_ips:
		var = u'nameserver%d' % count
		val = ucr.get(var)
		if val is not None:
			previous_ucr_set.append(u'%s=%s' % (var, val))
		else:
			previous_ucr_unset.append(u'%s' % (var,))
		univention.config_registry.handler_set([u'%s=%s' % (var, server_ip)])
		count += 1
	for i in range(count, 4):
		var = u'nameserver%s' % i
		val = ucr.get(var)
		if val is not None:
			previous_ucr_set.append(u'%s=%s' % (var, val))
			univention.config_registry.handler_unset([var])
	return (previous_ucr_set, previous_ucr_unset) 
[docs]def rename_well_known_sid_objects(username, password, ucr=None):
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	ud.debug(ud.MODULE, ud.PROCESS, "Matching well known object names")
	# First determine current name Domain Admins (trivial)
	lo = univention.uldap.getMachineConnection()
	ucs_domain_sid = _sid_of_ucs_sambadomain(lo, ucr)
	domain_admins_sid = "%s-%d" % (ucs_domain_sid, DOMAIN_RID_ADMINS)
	res = lo.search(filter=filter_format("(&(sambaSID=%s)(objectClass=sambaGroupMapping))", [domain_admins_sid]), attr=["cn"], unique=True)
	if not res or "cn" not in res[0][1]:
		ud.debug(ud.MODULE, ud.ERROR, "Lookup of group name for Domain Admins sid failed")
		domain_admins_name = u"Domain Admins"  # sensible guess
	else:
		domain_admins_name = res[0][1]["cn"][0].decode('UTF-8')
	# Next run the renaming script
	binddn = '%s@%s' % (username, ucr.get('kerberos/realm'))
	with tempfile.NamedTemporaryFile('w+') as fd:
		fd.write(password)
		fd.flush()
		p1 = subprocess.Popen(
			['/usr/share/univention-ad-connector/scripts/well-known-sid-object-rename', '--binddn', binddn, '--bindpwdfile', fd.name],
			stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
		stdout, stderr = p1.communicate()
		ud.debug(ud.MODULE, ud.PROCESS, "%s" % stdout.decode('UTF-8', 'replace'))
	if p1.returncode != 0:
		msg = "well-known-sid-object-rename failed with %d (%s)" % (p1.returncode, stderr.decode('UTF-8', 'replace'))
		ud.debug(ud.MODULE, ud.ERROR, msg)
		raise connectionFailed(msg)
	# Finally wait for replication and slapd restart to ensure that new LDAP ACLs are active:
	res = lo.search(filter=filter_format("(&(sambaSID=%s)(objectClass=sambaGroupMapping))", [domain_admins_sid]), attr=["cn"], unique=True)
	if not res or "cn" not in res[0][1]:
		ud.debug(ud.MODULE, ud.ERROR, "Lookup of new group name for Domain Admins sid failed")
		new_domain_admins_name = u"Domain Admins"
	else:
		new_domain_admins_name = res[0][1]["cn"][0].decode('UTF-8')
	wait_for_postrun = False
	if new_domain_admins_name != domain_admins_name:
		t0 = time.time()
		ud.debug(ud.MODULE, ud.INFO, "Waiting for well-known-sid-name-mapping listener to map Domain Admins")
		while custom_groupname(domain_admins_name) != new_domain_admins_name:
			if (time.time() - t0) > 15:
				break
			time.sleep(1)
		else:
			wait_for_postrun = True
	if wait_for_postrun:
		ud.debug(ud.MODULE, ud.ERROR, "Waiting for postrun of well-known-sid-name-mapping")
		time.sleep(15) 
[docs]def make_deleted_objects_readable_for_this_machine(username, password, ucr=None):
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	ud.debug(ud.MODULE, ud.PROCESS, "Make Deleted Objects readable for this machine")
	with tempfile.NamedTemporaryFile('w+') as fd:
		fd.write(password)
		fd.flush()
		binddn = '%s@%s' % (username, ucr.get('kerberos/realm'))
		p1 = subprocess.Popen(
			['/usr/share/univention-ad-connector/scripts/make-deleted-objects-readable-for-this-machine', '--binddn', binddn, '--bindpwdfile', fd.name],
			stdout=subprocess.PIPE, stderr=subprocess.PIPE,
			close_fds=True)
		stdout, stderr = p1.communicate()
		ud.debug(ud.MODULE, ud.PROCESS, "%s" % stdout.decode('UTF-8', 'replace'))
	if p1.returncode != 0:
		msg = "make-deleted-objects-readable-for-this-machine failed with %d (%s)" % (p1.returncode, stderr.decode('UTF-8', 'replace'))
		ud.debug(ud.MODULE, ud.ERROR, msg)
		raise connectionFailed(msg) 
[docs]def prepare_dns_reverse_settings(ad_domain_info, ucr=None):
	# For python-ldap / GSSAPI / AD we need working reverse DNS lookups
	# Otherwise one ends up with:
	#
	# SASL(-1): generic failure: GSSAPI Error: Miscellaneous failure (see text)
	#           (Matching credential (ldap/10.20.30.123@10.20.30.123) not found)
	#
	# Or even worse, in case there had been a (nscd cached?) PTR record
	# in the ucs.domain:
	#
	# SASL(-1): generic failure: GSSAPI Error: Miscellaneous failure (see text)
	#           (Matching credential (ldap/adhost.ucs.domain@UCS.DOMAIN) not found)
	#
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	# Flush the cache, just in case
	flush_nscd_hosts_cache()
	# Test DNS resolution (just for fun)
	try:
		hostname, aliaslist, ipaddrlist = socket.gethostbyaddr(ad_domain_info['DC IP'])
		ud.debug(ud.MODULE, ud.INFO, "%s resolves to %s" % (ad_domain_info['DC IP'], hostname))
	except (socket.herror, socket.gaierror) as exc:
		ud.debug(ud.MODULE, ud.INFO, "Resolving %s failed: %s" % (ad_domain_info['DC IP'], exc.args[1]))
	# Set a hosts/static anyway, to be safe from DNS issues (Bug #38285)
	previous_ucr_set = []
	previous_ucr_unset = []
	ad_server_name = ad_domain_info['DC DNS Name']
	ip = socket.gethostbyname(ad_server_name)
	ucr_key = u'hosts/static/%s' % (ip,)
	ucr_set = [u'%s=%s' % (ucr_key, ad_server_name), ]
	for setting in ucr_set:
		var = setting.split("=", 1)[0]
		old_val = ucr.get(var)
		if old_val is not None:
			previous_ucr_set.append(u'%s=%s' % (var, old_val))
		else:
			previous_ucr_unset.append(u'%s' % (var,))
	ud.debug(ud.MODULE, ud.PROCESS, "Setting UCR variables: %s" % ucr_set)
	univention.config_registry.handler_set(ucr_set)
	return (previous_ucr_set, previous_ucr_unset) 
[docs]def prepare_kerberos_ucr_settings(realm=None, ucr=None):
	ud.debug(ud.MODULE, ud.PROCESS, "Prepare Kerberos UCR settings")
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	previous_ucr_set = []
	previous_ucr_unset = []
	ucr_set = [
		u'kerberos/defaults/dns_lookup_kdc=true',
	]
	if realm and realm != ucr.get('kerberos/realm'):
		ucr_set.append(u'kerberos/realm=%s' % realm)
	for setting in ucr_set:
		var = setting.split("=", 1)[0]
		old_val = ucr.get(var)
		if old_val is not None:
			previous_ucr_set.append(u'%s=%s' % (var, old_val))
		else:
			previous_ucr_unset.append(u'%s' % (var,))
	ud.debug(ud.MODULE, ud.PROCESS, "Setting UCR variables: %s" % ucr_set)
	univention.config_registry.handler_set(ucr_set)
	ucr_unset = [
		u'kerberos/kdc',
		u'kerberos/kpasswdserver',
		u'kerberos/adminserver',
	]
	for var in ucr_unset:
		val = ucr.get(var)
		if val is not None:
			previous_ucr_set.append(u'%s=%s' % (var, val))
	ud.debug(ud.MODULE, ud.PROCESS, "Unsetting UCR variables: %s" % ucr_unset)
	univention.config_registry.handler_unset(ucr_unset)
	return (previous_ucr_set, previous_ucr_unset) 
[docs]def set_ucr(ucr_set, ucr_unset):
	univention.config_registry.handler_set(ucr_set)
	univention.config_registry.handler_unset(ucr_unset) 
[docs]def prepare_ucr_settings():
	ud.debug(ud.MODULE, ud.PROCESS, "Prepare UCR settings")
	# Show warnings in UMC
	# Change displayed name of users from "username" to "displayName" (as in AD)
	ucr_set = [
		u'ad/member=true',
		u'connector/ad/mapping/user/password/kinit=true',
		u'directory/manager/web/modules/users/user/display=displayName',
		u'nameserver/external=true',
		u'connector/ad/mapping/group/primarymail=true',
		u'connector/ad/mapping/user/primarymail=true',
	]
	modules = ('computers/computer', 'groups/group', 'users/user', 'dns/dns')
	ucr_set += [u'directory/manager/web/modules/%s/show/adnotification=true' % (module,) for module in modules]
	ud.debug(ud.MODULE, ud.PROCESS, "Setting UCR variables: %s" % ucr_set)
	univention.config_registry.handler_set(ucr_set)
	prepare_kerberos_ucr_settings() 
[docs]def revert_ucr_settings():
	ud.debug(ud.MODULE, ud.PROCESS, "Revert UCR settings")
	# TODO something else?
	ucr_unset = [
		u'ad/member',
		u'directory/manager/web/modules/users/user/display',
		u'kerberos/defaults/dns_lookup_kdc',
	]
	modules = ('computers/computer', 'groups/group', 'users/user', 'dns/dns')
	ucr_unset += [u'directory/manager/web/modules/%s/show/adnotification' % (module,) for module in modules]
	ud.debug(ud.MODULE, ud.PROCESS, "Unsetting UCR variables: %s" % ucr_unset)
	univention.config_registry.handler_unset(ucr_unset)
	ucr_set = [
		u'nameserver/external=false',
	]
	ud.debug(ud.MODULE, ud.PROCESS, "Setting UCR variables: %s" % ucr_set)
	univention.config_registry.handler_set(ucr_set) 
[docs]def prepare_connector_settings(username, password, ad_domain_info, ucr=None):
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	ud.debug(ud.MODULE, ud.PROCESS, "Prepare connector settings")
	binddn = '%s$' % (ucr.get('hostname'))
	ucr_set = [
		u'connector/ad/ldap/host=%s' % ad_domain_info["DC DNS Name"],
		u'connector/ad/ldap/base=%s' % ad_domain_info["LDAP Base"],
		u'connector/ad/ldap/binddn=%s' % binddn,
		u'connector/ad/ldap/bindpw=/etc/machine.secret',
		u'connector/ad/ldap/kerberos=true',
		u'connector/ad/mapping/syncmode=read',
		u'connector/ad/mapping/user/ignorelist=krbtgt,root,pcpatch',
	]
	ud.debug(ud.MODULE, ud.PROCESS, "Setting UCR variables: %s" % ucr_set)
	univention.config_registry.handler_set(ucr_set) 
[docs]def revert_connector_settings(ucr=None):
	ud.debug(ud.MODULE, ud.PROCESS, "Revert connector settings")
	# TODO something else?
	ucr_unset = [
		u'connector/ad/ldap/host',
		u'connector/ad/ldap/base',
		u'connector/ad/ldap/binddn',
		u'connector/ad/ldap/bindpw',
		u'connector/ad/ldap/kerberos',
		u'connector/ad/mapping/syncmode',
		u'connector/ad/mapping/user/ignorelist',
	]
	ud.debug(ud.MODULE, ud.PROCESS, "Unsetting UCR variables: %s" % ucr_unset)
	univention.config_registry.handler_unset(ucr_unset) 
[docs]def disable_local_samba4():
	ud.debug(ud.MODULE, ud.PROCESS, "Disable local samba4")
	stop_service("samba")
	univention.config_registry.handler_set([u'samba4/autostart=false']) 
[docs]def disable_local_heimdal():
	ud.debug(ud.MODULE, ud.PROCESS, "Disable local heimdal")
	stop_service("heimdal-kdc")
	univention.config_registry.handler_set([u'kerberos/autostart=false']) 
[docs]def run_samba_join_script(username, password, ucr=None):
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	ud.debug(ud.MODULE, ud.PROCESS, "Running samba join script")
	lo = univention.uldap.getMachineConnection()
	res = lo.searchDn(filter=filter_format("(&(uid=%s)(objectClass=shadowAccount))", (username,)), unique=True)
	if not res:
		ud.debug(ud.MODULE, ud.ERROR, "No UCS LDAP search result for uid=%s" % username)
		raise sambaJoinScriptFailed()
	binddn = res[0]
	with tempfile.NamedTemporaryFile('w+') as fd:
		fd.write(password)
		fd.flush()
		my_env = os.environ
		my_env['SMB_CONF_PATH'] = '/etc/samba/smb.conf'
		cmd = ('/usr/lib/univention-install/26univention-samba.inst', '--binddn', binddn, '--bindpwdfile', fd.name)
		p1 = subprocess.Popen(cmd, close_fds=True, env=my_env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
		stdout, stderr = p1.communicate()
		ud.debug(ud.MODULE, ud.PROCESS, "%s" % stdout.decode('UTF-8', 'replace'))
	if p1.returncode != 0:
		if stderr:
			ud.debug(ud.MODULE, ud.ERROR, "stderr:\n%s" % (stderr.decode('UTF-8', 'replace'),))
		ud.debug(ud.MODULE, ud.ERROR, "26univention-samba.inst failed with %d" % (p1.returncode,))
		raise sambaJoinScriptFailed() 
[docs]def add_host_record_in_ad(uid=None, binddn=None, bindpw=None, bindpwdfile=None, fqdn=None, ip=None, sso=False):
	pwdfile = None
	create_pwdfile = False
	ucr = univention.config_registry.ConfigRegistry()
	ucr.load()
	domainname = ucr.get('domainname')
	if binddn:
		uids = [y[1] for x in ldap.dn.str2dn(binddn) for y in x if ('uid' in y)]
		if uids:
			uid = uids[0]
	if bindpwdfile:
		create_pwdfile = False
		pwdfile = bindpwdfile
	elif bindpw:
		create_pwdfile = True
		pwdfile = bindpw
	# take myself as default
	if not ip:
		ip = Interfaces().get_default_ip_address().ip
	if sso and not fqdn:
		fqdn = ucr.get('ucs/server/sso/fqdn', 'ucs-sso.' + domainname)
	if not uid or not pwdfile or not fqdn or not ip:
		print('Missing binddn/bindpw/bindpwdfile/fqdn or ip, do nothing!')
		return False
	ad_domain_info = lookup_adds_dc()
	ad_ip = ad_domain_info['DC IP']
	found = False
	print("Create %s (%s) A record on %s" % (fqdn, ip, ad_ip))
	# check if we are already defined as host record
	try:
		resolver = dns.resolver.Resolver()
		resolver.lifetime = 10
		resolver.nameservers = [ad_ip]
		response = resolver.query(fqdn, 'A')
		for data in response:
			if str(data) == str(ip):
				found = True
	except dns.resolver.NXDOMAIN:
		found = False
	except Exception as err:
		print('failed to query for A record (%s, %s)' % (err.__class__.__name__, err))
		found = False
	if found:
		print('%s A record for %s found' % (fqdn, ip))
		return True
	# create host record  # FIXME; missing quoting
	fd = tempfile.NamedTemporaryFile('w+', delete=False)
	fd.write('server %s\n' % ad_ip)
	fd.write('update add %s 86400 A %s\n' % (fqdn, ip))
	fd.write('send\n')
	fd.write('quit\n')
	fd.close()
	# create pwd file
	if create_pwdfile:
		tmp = tempfile.NamedTemporaryFile('w+', delete=False)
		tmp.write('%s' % pwdfile)
		tmp.close()
		pwdfile = tmp.name
	cmd = ['kinit', '--password-file=%s' % pwdfile, uid]
	cmd += ['nsupdate', '-v', '-g', fd.name]
	try:
		p1 = subprocess.Popen(cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		stdout, stderr = p1.communicate()
		ud.debug(ud.MODULE, ud.PROCESS, '%s' % stdout.decode('UTF-8', 'replace'))
		if p1.returncode:
			print('%s failed with %d (%s)' % (cmd, p1.returncode, stderr.decode('UTF-8', 'replace')))
			print('failed to add A record for ucs-sso to %s' % ad_ip)
			return False
	finally:
		os.unlink(fd.name)
		if create_pwdfile:
			os.unlink(pwdfile)
	return True 
[docs]def get_domaincontroller_srv_record(domain, nameserver=None):
	if not domain:
		return False
	resolver = dns.resolver.Resolver()
	resolver.lifetime = 10  # make sure that we get an early timeout
	if nameserver:
		resolver.nameservers = [nameserver]
	# perform a SRV lookup
	try:
		response = resolver.query('_domaincontroller_master._tcp.%s.' % domain, 'SRV')
		if len(response) != 1:
			ud.debug(ud.MODULE, ud.ERROR, 'Non-unique SRV record: %s!' % (response.rrset,))
			return None
		return str(response[0].target)
	except dns.resolver.NoAnswer:
		ud.debug(ud.MODULE, ud.WARN, 'Received no answer to query for _domaincontroller_master._tcp.%s. SRV record.' % (domain,))
	except dns.resolver.NXDOMAIN:
		ud.debug(ud.MODULE, ud.WARN, 'Domain (%s) not resolvable!' % (domain,))
	except dns.resolver.NoNameservers:
		ud.debug(ud.MODULE, ud.WARN, 'No name servers in domain (%s) available to answer the query.' % (domain,))
	except dns.exception.Timeout as exc:
		ud.debug(ud.MODULE, ud.WARN, 'Lookup for Primary Directory Node record timed out: %s' % (exc,))
	return None 
[docs]def add_domaincontroller_srv_record_in_ad(ad_ip, username, password, ucr=None):
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	ud.debug(ud.MODULE, ud.PROCESS, "Create _domaincontroller_master SRV record on %s" % ad_ip)
	hostname = ucr.get('hostname')
	domainname = ucr.get('domainname')
	fqdn_with_trailing_dot = "%s.%s." % (hostname, domainname)
	srv_record = "_domaincontroller_master._tcp.%s" % (domainname,)
	current_record = get_domaincontroller_srv_record(domainname)
	if current_record == fqdn_with_trailing_dot:
		ud.debug(ud.MODULE, ud.PROCESS, "Ok, SRV record %s already points to this server" % (srv_record,))
		return True
	if current_record:
		# remove the existing SRV record. Important when replacing an existing Primary Directory Node system!
		# we need Administrator permissions to do this.
		ud.debug(ud.MODULE, ud.PROCESS, "Removing previous SRV record %s" % (current_record,))
		with tempfile.NamedTemporaryFile('w+') as fd, tempfile.NamedTemporaryFile('w+') as fd2:
			fd2.write(password)
			fd2.flush()
			# FIXME: missing quoting
			fd.write('server %s\n' % ad_ip)
			fd.write('update delete %s. SRV\n' % (srv_record,))
			fd.write('send\n')
			fd.write('quit\n')
			fd.flush()
			cmd = ['kinit', '--password-file=%s' % (fd2.name,), username, 'nsupdate', '-v', '-g', fd.name]
			p1 = subprocess.Popen(cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
			stdout, stderr = p1.communicate()
			ud.debug(ud.MODULE, ud.PROCESS, "%s" % stdout.decode('UTF-8', 'replace'))
			if p1.returncode:
				ud.debug(ud.MODULE, ud.ERROR, "%s failed with %d (%s)" % (cmd, p1.returncode, stderr.decode('UTF-8', 'replace')))
				ud.debug(ud.MODULE, ud.ERROR, "failed to remove SRV record. Ignoring error.")
			subprocess.call(['kdestroy'])
	# FIXME: missing quoting
	fd = tempfile.NamedTemporaryFile('w+', delete=False)
	fd.write('server %s\n' % ad_ip)
	fd.write('update add %s. 10800 SRV 0 0 0 %s\n' % (srv_record, fqdn_with_trailing_dot))
	fd.write('send\n')
	fd.write('quit\n')
	fd.close()
	cmd = ['kinit', '--password-file=/etc/machine.secret']
	# use the machine account so that the server has permissions to modify this record
	cmd += [r'%s\$' % hostname]
	cmd += ['nsupdate', '-v', '-g', fd.name]
	try:
		p1 = subprocess.Popen(cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
		stdout, stderr = p1.communicate()
		ud.debug(ud.MODULE, ud.PROCESS, "%s" % stdout.decode('UTF-8', 'replace'))
		if p1.returncode:
			ud.debug(ud.MODULE, ud.ERROR, "%s failed with %d (%s)" % (cmd, p1.returncode, stderr.decode('UTF-8', 'replace')))
			ud.debug(ud.MODULE, ud.ERROR, "failed to add SRV record to %s" % ad_ip)
			# raise failedToAddServiceRecordToAD("failed to add SRV record to %s" % ad_ip)
			return False
	finally:
		os.unlink(fd.name)
	return True 
[docs]def get_ucr_variable_from_ucs(host, server, var):
	cmd = ['univention-ssh', '/etc/machine.secret']
	cmd += [r'%s\$@%s' % (host, server)]
	cmd += ['/usr/sbin/ucr get %s' % (pipes.quote(var),)]
	p1 = subprocess.Popen(cmd, close_fds=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	stdout, stderr = p1.communicate()
	if p1.returncode:
		ud.debug(ud.MODULE, ud.ERROR, "%s failed with %d (%s)" % (cmd, p1.returncode, stderr.decode('UTF-8', 'replace')))
		raise failedToGetUcrVariable("failed to get UCR variable %s from %s" % (var, server))
	return stdout.decode('UTF-8', 'replace').strip() 
[docs]def set_nameserver_from_ucs_master(ucr=None):
	if not ucr:
		ucr = univention.config_registry.ConfigRegistry()
		ucr.load()
	ud.debug(ud.MODULE, ud.PROCESS, "Set nameservers")
	for var in ['nameserver1', 'nameserver2', 'nameserver3']:
		value = get_ucr_variable_from_ucs(ucr.get('hostname'), ucr.get('ldap/master'), var)
		if value:
			ud.debug(ud.MODULE, ud.PROCESS, "Setting %s=%s" % (var, value))
			univention.config_registry.handler_set([u'%s=%s' % (var, value)]) 
[docs]def revert_backup_ad_member():
	# type: () -> None
	# TODO something else?
	remove_install_univention_samba(install=False)
	revert_ucr_settings() 
[docs]def revert_slave_ad_member():
	# type: () -> None
	# TODO something else?
	remove_install_univention_samba(install=False)
	revert_ucr_settings() 
[docs]def revert_member_ad_member():
	# type: () -> None
	# TODO something else?
	remove_install_univention_samba(install=False)
	revert_ucr_settings() 
[docs]def revert_container_ad_member():
	# type: () -> None
	revert_ucr_settings()