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

"""
synchronise attributes uniqueMember to memberUID of group objects.

Update the UIDs in memberUid of all groups to match the uid of the objects
referenced by uniqueMember.
"""

from __future__ import print_function

import ldap
import sys
from argparse import ArgumentParser

import univention.config_registry

import univention.debug as ud


class ConsistencyError(Exception):

	"""Inconsistence detected."""


def main():
	"""synchronise attributes uniqueMember to memberUID of group objects."""
	configRegistry = univention.config_registry.ConfigRegistry()
	configRegistry.load()

	parser = ArgumentParser()
	parser.add_argument(
		'-t', '--test', action='store_true',
		dest='test', default=False, help='just test the modification')
	parser.add_argument(
		'-d', action='store', default=2, type=int,
		dest='debug', help='set debug level')
	parser.add_argument(
		'-c', '--continue', action='store_true',
		dest='cont', default=False, help='continue on error')

	options = parser.parse_args()

	ud.init('/var/log/univention/sync-memberuid.log', ud.FLUSH, ud.NO_FUNCTION)
	ud.set_level(ud.ADMIN, options.debug)

	base_dn = configRegistry['ldap/base']

	lo = ldap.initialize('ldap://localhost:7389')
	bindpw = open('/etc/ldap.secret').read()
	if bindpw[-1] == '\n':
		bindpw = bindpw[:-1]
	lo.simple_bind_s("cn=admin," + base_dn, bindpw)

	try:
		process_groups(lo, base_dn, options.test, options.cont)
	except ConsistencyError:
		sys.exit(1)


def process_groups(lo, base_dn, test=False, cont=False):
	groups = lo.search_s(base_dn, ldap.SCOPE_SUBTREE, '(&(objectClass=posixGroup)(objectClass=univentionGroup))', ('uniqueMember', 'memberUid'))

	if test:
		print('Test Mode: The following groups have to be modified:')
	for grp_dn, grp_attrs in groups:
		old = set(grp_attrs.get('memberUid', ()))

		ud.debug(ud.ADMIN, ud.PROCESS, 'Group: %s' % grp_dn)
		new = set()
		member_dns = grp_attrs.get('uniqueMember', ())
		for uniqueMember in member_dns:
			uniqueMember = uniqueMember.decode('utf-8')
			try:
				result = lo.search_s(uniqueMember, ldap.SCOPE_BASE, '(objectclass=*)')
			except ldap.NO_SUCH_OBJECT as ex:
				ud.debug(ud.ADMIN, ud.WARN, 'searching %s failed: %s' % (uniqueMember, ex))
				print('WARNING: DN %s not found' % uniqueMember, file=sys.stderr)
				continue
			if not result:
				ud.debug(ud.ADMIN, ud.WARN, 'empty result for uniqueMember %s' % uniqueMember)
				print('WARNING: empty result for uniqueMember %s' % uniqueMember, file=sys.stderr)
				continue
			_, uniqueMemberAttrs = result[0]
			uniqueMemberUid = uniqueMemberAttrs.get('uid')
			if uniqueMemberUid:
				new.add(uniqueMemberUid[0])

		if old != new:
			ud.debug(ud.ADMIN, ud.INFO, '  members: %s' % member_dns)
			ud.debug(ud.ADMIN, ud.INFO, '  old memberUid: %s' % old)
			ud.debug(ud.ADMIN, ud.INFO, '  new memberUid: %s' % new)
			if test:
				print('Group:', grp_dn)
				continue
			add = list(new - old)
			if add:
				try:
					lo.modify_s(grp_dn, [(ldap.MOD_ADD, 'memberUid', add)])
				except ldap.LDAPError as ex:
					ud.debug(ud.ADMIN, ud.ERROR, 'adding memberUid entries failed: %s' % ex)
					if not cont:
						raise ConsistencyError()
			remove = list(old - new)
			if remove:
				try:
					lo.modify_s(grp_dn, [(ldap.MOD_DELETE, 'memberUid', remove)])
				except ldap.LDAPError as ex:
					ud.debug(ud.ADMIN, ud.ERROR, 'removing memberUid entries failed: %s' % ex)
					if not cont:
						raise ConsistencyError()


if __name__ == '__main__':
	main()
