#!/usr/bin/python3
#
# Copyright 2005-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/>.

"""
Migration tool for `univentionObjectType`
"""

from __future__ import print_function

import argparse
import sys

import univention.uldap


def buildModuleIdentifyMapping():
	import univention.admin.modules
	univention.admin.modules.update()
	return dict(
		(name, module.identify)
		for (name, module) in univention.admin.modules.modules.items()
		if hasattr(module, 'identify')
	)


def parseOptions():
	parser = argparse.ArgumentParser(description='Set the attribute univentionObjectType for each directory object')
	parser.add_argument('--verbose', action='store_true', default=False, help='do not hide warnings for unmatched component objects')
	parser.add_argument('-v', '--verify', action='store_true', default=False, help='check objects with already set univentionObjectType')
	parser.add_argument('-b', '--base', default='', help='only modify objects at or below SEARCHBASE', metavar='SEARCHBASE')
	group = parser.add_mutually_exclusive_group(required=True)
	group.add_argument('-n', '--no-action', dest='action', action='store_false', help='do not modify the directory, show what would have been done')
	group.add_argument('-a', '--action', dest='action', action='store_true', help='do modify the directory')

	options = parser.parse_args()
	return options


def warningSupressed(dn, attributes):
	# cn=admin,$ldap_base cn=backup,$ldap_base
	ocs = set(attributes['objectClass'])
	if set(attributes.keys()) == {'objectClass', 'userPassword', 'cn', 'sn'}:
		if ocs == {b'top', b'person'}:
			if attributes['sn'] == attributes['cn'] and len(attributes['cn']) == 1 and attributes['cn'][0] in (b'admin', b'backup', ):
				return True
	# Kerberos principal objects
	if ocs == {b'top', b'account', b'krb5Principal', b'krb5KDCEntry'}:
		return True
	if ocs == {b'top', b'person', b'krb5Principal', b'krb5KDCEntry'}:
		return True
	# samba idmap objects
	if ocs == {b'sambaIdmapEntry', b'sambaSidEntry'}:
		return True
	# old (2.4) Kolab objects
	if b'kolabSharedFolder' in ocs:
		return True
	if b'univentionKolabGroup' in ocs:
		return True
	# old (2.4) UMC ACLs
	if b'univentionConsoleACL' in ocs:
		return True
	if b'univentionConsoleOperations' in ocs:
		return True
	if b'univentionPolicyConsoleAccess' in ocs:
		return True
	# old (2.4) UDM visibility settings
	if b'univentionPolicyAdminSettings' in ocs:
		return True
	if b'univentionAdminUserSettings' in ocs:
		return True
	return warningHidden(dn, attributes)


def warningHidden(dn, attributes):
	ocs = set(attributes['objectClass'])
	if warningHidden.verbose:
		return False
	if b'univentionCitrixUserSessionsClass' in ocs:
		return True
	if b'univentionPolicyThinClientUser' in ocs:
		return True
	if b'univentionThinClientSession' in ocs:
		return True
	if b'univentionThinClientAutostart' in ocs:
		return True
	return False


warningHidden.verbose = False  # type: ignore


def main(options):
	errorsOccurred = False
	searchFilter = '(objectClass=*)' if options.verify else '(!(objectClass=univentionObject))'
	moduleIdentify = buildModuleIdentifyMapping()
	uldap = univention.uldap.getAdminConnection()
	if options.action is not True:
		uldap.modify = lambda dn, changes: sys.stdout.write('Would modify %r\n' % (dn, ))
	warningHidden.verbose = options.verbose

	for (dn, attributes, ) in uldap.search(filter=searchFilter, base=options.base):
		matches = [
			module
			for (module, identify) in moduleIdentify.items()
			if identify(dn, attributes)
		]
		if 'container/dc' in matches and ('container/ou' in matches or 'container/cn' in matches):
			# container/dc has priority (ldapbase ou=/cn= has multiple matches)
			matches = ['container/dc']

		if len(matches) == 1:
			if b'univentionObject' not in attributes['objectClass']:
				try:
					changes = [
						('objectClass', attributes['objectClass'], attributes['objectClass'] + [b'univentionObject'], ),
						('univentionObjectType', [], [matches[0].encode('UTF-8'), ], ),
					]
					uldap.modify(dn, changes)
				except univention.uldap.ldap.INVALID_SYNTAX as e:
					# this error should not happen, in case it does, it is an indicator that
					# LDAP schema extensions are missing (Bug #26304)
					print('ERROR: Could not set univentionObjectType! (%s)\nIt seems that the corresponding LDAP schema extensions are not installed correctly.' % e, file=sys.stderr)
					return False
			elif attributes['univentionObjectType'][0].decode('utf-8') != matches[0]:
				errorsOccurred = True
				print('Mismatch for %r: univentionObjectType is %r but should be %r!' % (dn, attributes['univentionObjectType'][0].decode('utf-8'), matches, ), file=sys.stderr)
		elif len(matches) > 1:
			raise ValueError('Multiple matches for %r: %r!' % (dn, matches, ))
		else:
			if not warningSupressed(dn, attributes):
				print('Warning: No match for %r' % (dn, ), file=sys.stderr)
				if options.verbose:
					for attr in ('objectClass', 'univentionObjectType'):
						for value in attributes.get(attr, []):
							print('\t%s: %s' % (attr, value))

	return not errorsOccurred


if __name__ == "__main__":
	options = parseOptions()
	if not main(options):
		sys.exit(1)
