#!/usr/bin/python3
#
# Like what you see? Join us!
# https://www.univention.com/about-us/careers/vacancies/
#
# Copyright 2007-2024 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 sys
from argparse import ArgumentParser
from logging import getLogger

import ldap

import univention.config_registry


log = getLogger('ADMIN')


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()

    univention.logging.basicConfig(filename='/var/log/univention/sync-memberuid.log', univention_debug_level=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', ()))

        log.info('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:
                log.warning('searching %s failed: %s', uniqueMember, ex)
                print('WARNING: DN %s not found' % uniqueMember, file=sys.stderr)
                continue
            if not result:
                log.warning('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:
            log.debug('  members: %s', member_dns)
            log.debug('  old memberUid: %s', old)
            log.debug('  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:
                    log.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:
                    log.error('removing memberUid entries failed: %s', ex)
                    if not cont:
                        raise ConsistencyError()


if __name__ == '__main__':
    main()
