#!/usr/share/ucs-test/runner python
# coding=utf-8
## desc: Dump access to all available objects and compare with rulse of 4.1R2v9
## roles: [domaincontroller_master]
## tags: [ucsschool]
## timeout: 3600
## exposure: dangerous
## packages: [ucs-school-ldap-acls-master]

import re
import os
import sys
import ldif
import shutil
import time
import subprocess
import traceback
from multiprocessing import Pool

import univention.admin.uldap
import univention.testing.ucr as ucr_test
import univention.testing.udm
import univention.testing.strings as uts
import univention.testing.ucsschool as utu

# OUTPUT of "slapacl -d0 -D cn=admin,dc=nstx,dc=local -b uid=Administrator,cn=users,dc=nstx,dc=local 2>&1"
# ==> PLEASE NOTE THAT BINARY VALUES (LIKE "KRB5KEY") MAY CONTAIN LINEBREAKS THAT MAKE PARSING HARDER!
#
# authcDN: "cn=admin,dc=nstx,dc=local"
# entry: manage(=mwrscxd)
# children: manage(=mwrscxd)
# uid=Administrator: manage(=mwrscxd)
# krb5PrincipalName=Administrator@NSTX.LOCAL: manage(=mwrscxd)
# uidNumber=2002: manage(=mwrscxd)
# sambaAcctFlags=[U          ]: manage(=mwrscxd)
# krb5MaxLife=86400: manage(=mwrscxd)
# cn=Administrator: manage(=mwrscxd)
# krb5MaxRenew=604800: manage(=mwrscxd)
# loginShell=/bin/bash: manage(=mwrscxd)
# univentionObjectType=users/user: manage(=mwrscxd)
# displayName=Administrator: manage(=mwrscxd)
# sambaSID=S-1-5-21-3846281231-3184689532-2158317326-500: manage(=mwrscxd)
# gecos=Administrator: manage(=mwrscxd)
# sn=Administrator: manage(=mwrscxd)
# homeDirectory=/home/Administrator: manage(=mwrscxd)
# structuralObjectClass=inetOrgPerson: manage(=mwrscxd)
# entryUUID=c711836e-92a3-1036-98ae-19810040644d: manage(=mwrscxd)
# creatorsName=cn=admin,dc=nstx,dc=local: manage(=mwrscxd)
# createTimestamp=20170301082113Z: manage(=mwrscxd)
# univentionPolicyReference=cn=default-admins,cn=admin-settings,cn=users,cn=policies,dc=nstx,dc=local: manage(=mwrscxd)
# objectClass=krb5KDCEntry: manage(=mwrscxd)
# objectClass=univentionPerson: manage(=mwrscxd)
# objectClass=person: manage(=mwrscxd)
# objectClass=automount: manage(=mwrscxd)
# objectClass=top: manage(=mwrscxd)
# objectClass=inetOrgPerson: manage(=mwrscxd)
# objectClass=sambaSamAccount: manage(=mwrscxd)
# objectClass=organizationalPerson: manage(=mwrscxd)
# objectClass=univentionPWHistory: manage(=mwrscxd)
# objectClass=univentionMail: manage(=mwrscxd)
# objectClass=univentionObject: manage(=mwrscxd)
# objectClass=shadowAccount: manage(=mwrscxd)
# objectClass=krb5Principal: manage(=mwrscxd)
# objectClass=univentionPolicyReference: manage(=mwrscxd)
# objectClass=posixAccount: manage(=mwrscxd)
# univentionUMCProperty=appcenterSeen=2: manage(=mwrscxd)
# univentionUMCProperty=favorites=appcenter:appcenter,updater,udm:users/user,udm:groups/group,udm:computers/computer,apps:ucsschool: manage(=mwrscxd)
# description=Built-in account for administering the computer/domain: manage(=mwrscxd)
# shadowLastChange=17229: manage(=mwrscxd)
# gidNumber=5001: manage(=mwrscxd)
# sambaPrimaryGroupSID=S-1-5-21-3846281231-3184689532-2158317326-513: manage(=mwrscxd)
# pwhistory=$6$vRMTALPNn0OeUf/5$9Ql3H5jhwHMIfM8q816e/usMSViXY3S0R5l1YejNk6718aGPInlzKu0ZpSbiHGwtAN2Lz2IoHVxCBBfX7Td8B0 $6$oVtqfZgD.GRL1Lm2$F5b.NQQQjmNji56fOdAQoa04yH5SjBE6zjqGgIKiF43ubKSLDWAqQlorTMJYnGqH9ROTQ0zeki9t52jmjQPuK1 $6$mg9.eCcNgoszfFT3$PffIE27/wXxgmIgg2droezrLizh0xIMCmcHbSnNi.H/F8PAYB.0aQVI4hwTqe95uBTalyBXgsOIKcQ6pXczox1: manage(=mwrscxd)
# userPassword=****: manage(=mwrscxd)
# sambaNTPassword=CAA1239D44DA7EDF926BCE39F5C65D0F: manage(=mwrscxd)
# sambaPasswordHistory=390CFF5B17A555A5DB5BF14533A4B6E91AB9F6F3B25B4301BAF338FFAFC3442CDF15C89035597E162593E89108CD5775F1B3FE39C0B1711E05FBC38753CE22FE1863FE4A700E8A97B4BA601D207E57B0C67F70737659810F6BA6C3E231E8D0067B8149DCF354038D43EB671B6A55AB03D9A9E98FCFD02A424608FB0747DE1FF6: manage(=mwrscxd)
# krb5Key=0Q<A1>+0)<A0>^C^B^A^R<A1>"^D ((<9F>VQ;}yo<AF>^F<A2><D6>^Z^O^g<A4><E4>x<BD>=^B<98><EF><CC>`<DB>R<97><ED>!<A2>"0 <A0>^C^B^A^C<A1>^Y^D^WNSTX.LOCALAdministrator: manage(=mwrscxd)
# krb5Key=0A<A1>ESC0^Y<A0>^C^B^A^Q<A1>^R^D^P<DE><C9>^ZGp̏{<AA>^F<96><9E>N/<A2>"0 <A0>^C^B^A^C<A1>^Y^D^WNSTX.LOCALAdministrator: manage(=mwrscxd)
# krb5Key=0I<A1>#0!<A0>^C^B^A^P<A1>^Z^D^X<C1><E9>><E9>v<8A>^B#^?<D0>h^P)4F#^D굑)^S^K<EC><A2>"0 <A0>^C^B^A^C<A1>^Y^D^WNSTX.LOCALAdministrator: manage(=mwrscxd)
# krb5Key=0A<A1>ESC0^Y<A0>^C^B^A^W<A1>^R^D^Pʡ#<9D>D<DA>~ߒk<CE>9<F5><C6>]^O<A2>"0 <A0>^C^B^A^C<A1>^Y^D^WNSTX.LOCALAdministrator: manage(=mwrscxd)
# krb5Key=09<A1>^S0^Q<A0>^C^B^A^C<A1>
# ^D^HpWu;zb<A4>^Y<A2>"0 <A0>^C^B^A^C<A1>^Y^D^WNSTX.LOCALAdministrator: manage(=mwrscxd)
# krb5Key=09<A1>^S0^Q<A0>^C^B^A^B<A1>
# ^D^HpWu;zb<A4>^Y<A2>"0 <A0>^C^B^A^C<A1>^Y^D^WNSTX.LOCALAdministrator: manage(=mwrscxd)
# krb5Key=09<A1>^S0^Q<A0>^C^B^A^A<A1>
# ^D^HpWu;zb<A4>^Y<A2>"0 <A0>^C^B^A^C<A1>^Y^D^WNSTX.LOCALAdministrator: manage(=mwrscxd)
# krb5KDCFlags=126: manage(=mwrscxd)
# krb5KeyVersionNumber=4: manage(=mwrscxd)
# sambaPwdLastSet=1489162588: manage(=mwrscxd)
# entryCSN=20170310161628.837230Z#000000#000#000000: manage(=mwrscxd)
# modifiersName=uid=Administrator,cn=users,dc=nstx,dc=local: manage(=mwrscxd)
# modifyTimestamp=20170310161628Z: manage(=mwrscxd)


def normalize_permission(perms):
	level_to_priv = {
		'none': '0',
		'disclose': 'd',
		'auth': 'xd',
		'compare': 'cxd',
		'search': 'scxd',
		'read': 'rscxd',
		'write': 'wrscxd',
		'add': 'arscxd',
		'delete': 'zrscxd',
		'manage': 'mwrscxd',
	}
	if not perms.startswith('='):
		perms = '=%s' % level_to_priv[perms.split('(', 1)[0]]
	return perms


def run_one_test(args):
	result_dir, thread_id, binddn, dn_list = args
	try:
		output = open(os.path.join(result_dir, 'dn%02d.ldif' % (thread_id,)), 'wb')
		time_start = time.time()
		writer = ldif.LDIFWriter(output)

		len_dn_list = len(dn_list)
		for j, dn in enumerate(dn_list):
			if j % 50 == 0:
				print >>sys.stdout, 'Thread %02d (pid %d): %05d/%05d' % (thread_id, os.getpid(), j, len_dn_list)
				sys.stdout.flush()

			entry = {}
			cmd = ['slapacl', '-d0', '-D', binddn, '-b', dn]
			process = subprocess.Popen(cmd, stderr=subprocess.PIPE)
			_, stderr = process.communicate()
			for line in re.findall('^(?:[a-zA-Z0-9]+=.*?: .*?=[a-z0]+[)]?|entry: .*?|children: .*?)$', stderr, re.DOTALL | re.MULTILINE):
				attr, value = line.rsplit(': ', 1)
				attr = attr.split('=', 1)[0]
				if attr not in ('authcDN',):  # ignore some attributes
					entry.setdefault(attr, set()).add(normalize_permission(value.strip()))
			writer.unparse(dn, entry)
		msg = '*** Runtime for parse_acls(Thread %02d - pid %d): %fs' % (thread_id, os.getpid(), time.time() - time_start,)
	except Exception:
		print >>sys.stderr, 'TRACEBACK IN THREAD %d (%s):' % (thread_id, binddn)
		print >>sys.stderr, traceback.format_exc()
		raise
	return msg


class Bunch(object):
	def __init__(self, **kwds):
		self.__dict__.update(kwds)


class LDAPDiffCheck(object):
	def __init__(self, ucr, udm, schoolenv):
		self.ucr = ucr
		self.udm = udm
		self.schoolenv = schoolenv
		self.lo = univention.admin.uldap.getAdminConnection()[0]
		self.generic = Bunch()
		self.schoolA = Bunch()
		self.schoolB = Bunch()
		self.setup()

	def setup(self):
		self.generic.master_dn = self.lo.searchDn(filter='univentionObjectType=computers/domaincontroller_master')[0]
		self.generic.backup_dn = self.udm.create_object(
			"computers/domaincontroller_backup",
			name='schoolTestBackup',
			position="cn=dc,cn=computers,%(ldap/base)s" % self.ucr,
			domain=self.ucr.get('domainname'),
			mac=uts.random_mac(),
			ip=uts.random_ip(),
		)
		self.generic.slave_dn = self.udm.create_object(
			"computers/domaincontroller_slave",
			name='schoolTestSlave',
			position="cn=dc,cn=computers,%(ldap/base)s" % self.ucr,
			domain=self.ucr.get('domainname'),
			mac=uts.random_mac(),
			ip=uts.random_ip(),
		)
		self.generic.member_dn = self.udm.create_object(
			"computers/memberserver",
			name='schoolTestMember',
			position="cn=computers,%(ldap/base)s" % self.ucr,
			domain=self.ucr.get('domainname'),
			mac=uts.random_mac(),
			ip=uts.random_ip(),
		)
		self.generic.winclient_dn = self.udm.create_object(
			"computers/windows",
			name='schoolTestWinDom',
			position="cn=computers,%(ldap/base)s" % self.ucr,
			mac=uts.random_mac(),
			ip=uts.random_ip(),
		)
		self.generic.domain_user_dn = self.udm.create_user(username='domainUser')[0]

		for suffix, school in (('A', self.schoolA), ('B', self.schoolB), ):
			print '---%s-----------------------------------------------------' % (suffix,)
			school.name, school.dn = self.schoolenv.create_ou(ou_name='school%s' % (suffix,), name_edudc='schooldc%s' % (suffix,))

			if suffix == 'A':
				schools = [self.schoolA.name]
			else:
				schools = [self.schoolA.name, self.schoolB.name]
			school.teacher_name, school.teacher_dn = self.schoolenv.create_user(school.name, username='teacher%s' % (suffix,), schools=schools, is_teacher=True,
																				classes='%s-class1' % (school.name,))
			school.teacher_staff_name, school.teacher_staff_dn = self.schoolenv.create_user(school.name, username='teachstaff%s' % (suffix,), schools=schools, is_teacher=True, is_staff=True,
																				classes='%s-class1' % (school.name,))
			school.staff_name, school.staff_dn = self.schoolenv.create_user(school.name, username='staff%s' % (suffix,), schools=schools, is_staff=True)
			school.student_name, school.student_dn = self.schoolenv.create_user(school.name, username='student%s' % (suffix,), schools=schools,
																				classes='%s-class1' % (school.name,))
			school.admin_name, school.admin_dn = self.schoolenv.create_school_admin(school.name, username='schooladmin%s' % (suffix,), schools=schools)

			school.schoolserver_dn = self.lo.searchDn(base=school.dn, filter='univentionObjectType=computers/domaincontroller_slave')[0]

			school.winclient_dn = self.udm.create_object(
				"computers/windows",
				name='schoolwin%s' % (suffix,),
				position="cn=computers,%s" % (school.dn,),
				mac=uts.random_mac(),
				ip=uts.random_ip(),
			)

			# create additional class, first workgroup and first computer room
			school.class2_name, school.class2_dn = self.schoolenv.create_school_class(
				school.name, class_name=None, description='Test class 2',
				users=[school.student_dn, school.teacher_dn]
			)
			school.workgroup1_name, school.workgroup1_dn = self.schoolenv.create_workgroup(
				school.name, workgroup_name=None, description='Test workgroup',
				users=[school.student_dn, school.teacher_dn]
			)
			school.room1_name = self.schoolenv.create_computerroom(school.name, description='Test room1')[0]

	def collect_dns(self, valid_ou_names=None):
		valid_ous = valid_ou_names or [',ou=%s' % (x,) for x in [self.schoolA.name, self.schoolB.name, 'Domain Controllers']]
		self.dn_list = [dn for dn in self.lo.searchDn() if (
			(not dn.startswith('ou=') and (',ou=' not in dn)) or    # accept all NON-OU objects
			(any([x in dn for x in valid_ous])))]                   # and all objects of "valid" OUs to get comparable results

	def run_all_tests(self, result_dir):
		os.makedirs(result_dir)

		pool = Pool()  # uses NUMBER_OF_CPUS worker processes by default

		work_items = [(result_dir, i, binddn, self.dn_list) for i, binddn in enumerate([
			'cn=admin,%(ldap/base)s' % self.ucr,
			self.generic.master_dn,
			self.generic.backup_dn,
			self.generic.slave_dn,
			self.generic.member_dn,
			self.generic.winclient_dn,
			'uid=Administrator,cn=users,%(ldap/base)s' % self.ucr,
			self.generic.domain_user_dn,
			self.schoolA.schoolserver_dn,
			self.schoolB.schoolserver_dn,
			self.schoolA.winclient_dn,
			self.schoolB.winclient_dn,
			self.schoolA.teacher_dn,
			self.schoolB.teacher_dn,
			self.schoolA.student_dn,
			self.schoolB.student_dn,
			self.schoolA.teacher_staff_dn,
			self.schoolB.teacher_staff_dn,
			self.schoolA.staff_dn,
			self.schoolB.staff_dn,
			self.schoolA.admin_dn,
			self.schoolB.admin_dn,
		])]
		for result_dir, i, binddn, dn_list in work_items:
			with open(os.path.join(result_dir, 'dn.txt'), 'a+') as fd:
				fd.write('%02d ==> %s\n' % (i, binddn))

		results = pool.imap_unordered(run_one_test, work_items)
		print 'DONE'
		print repr(results)
		for result in results:
			print result


def main():
	with utu.UCSTestSchool() as schoolenv:
		with univention.testing.udm.UCSTestUDM() as udm:
			with ucr_test.UCSTestConfigRegistry() as ucr:

				test_suite = LDAPDiffCheck(ucr, udm, schoolenv)
				test_suite.collect_dns()

				BASE_DIR = '/etc/univention/templates/files/etc/ldap/slapd.conf.d'
				FN_TEMPL1_ORIG = os.path.join(BASE_DIR, '61ucsschool_presettings')
				FN_TEMPL2_ORIG = os.path.join(BASE_DIR, '65ucsschool')
				FN_TEMPL1_BACKUP = FN_TEMPL1_ORIG + '.BACKUP'
				FN_TEMPL2_BACKUP = FN_TEMPL2_ORIG + '.BACKUP'
				FN_TEMPL1_TEST = '78_ldap_acls_dump.oldconfig.61ucsschool_presettings'
				FN_TEMPL2_TEST = '78_ldap_acls_dump.oldconfig.65ucsschool'
				now = int(time.time())
				try:
					print '\n'
					shutil.move(FN_TEMPL1_ORIG, FN_TEMPL1_BACKUP)
					shutil.move(FN_TEMPL2_ORIG, FN_TEMPL2_BACKUP)
					with open(FN_TEMPL1_ORIG, 'w') as fd_dst, open(FN_TEMPL1_TEST, 'r') as fd_src:
						fd_dst.write(fd_src.read())
					with open(FN_TEMPL2_ORIG, 'w') as fd_dst, open(FN_TEMPL2_TEST, 'r') as fd_src:
						fd_dst.write(fd_src.read())
					subprocess.check_call(['ucr', 'commit', '/etc/ldap/slapd.conf'])
					if subprocess.check_call(['invoke-rc.d', 'slapd', 'restart']):
						utu.fail('Restarting slapd with test config 1 failed!')
					time.sleep(3)
					testdir = '/var/log/univention/78_ldap_acls_dump.%d.oldconfig' % (now,)
					test_suite.run_all_tests(testdir)
				except Exception:
					print >>sys.stderr, 'SOMETHING FAILED WITH OLD CONFIG'
					print >>sys.stderr, traceback.format_exc()
					raise
				finally:
					shutil.move(FN_TEMPL1_BACKUP, FN_TEMPL1_ORIG)
					shutil.move(FN_TEMPL2_BACKUP, FN_TEMPL2_ORIG)
					subprocess.call(['ucr', 'commit', '/etc/ldap/slapd.conf'])
					subprocess.call(['invoke-rc.d', 'slapd', 'restart'])
					time.sleep(3)

				print '\n'
				testdir = '/var/log/univention/78_ldap_acls_dump.%d.newconfig' % (now,)
				test_suite.run_all_tests(testdir)

				print 'Use following command for diff:'
				print './78_ldap_acls_dump.diff'

				# for debugging purposes
				if os.path.exists('/tmp/78_ldap_acls_dump.debug'):
					fn = '/tmp/78_ldap_acls_dump.continue'
					print '=== DEBUGGING MODE ==='
					print 'Waiting for cleanup until {!r} exists...'.format(fn)
					while not os.path.exists(fn):
						time.sleep(1)

if __name__ == '__main__':
	main()
