#!/usr/bin/python3
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

import os
import subprocess
import tempfile
import ldif
import ldap.dn
from copy import deepcopy
import argparse

from univention.admin.password import krb5_asn1

N = {}


def export_ldap_database():
    print("Exporting LDAP database...")
    _, fname = tempfile.mkstemp(suffix=".ldif")
    subprocess.run(["slapcat", "-l", fname])
    print(f"... saved online data at {fname}")
    return fname


def cleanup(ldap_database):
    os.unlink(ldap_database)
    print(f"... and removed online data again {ldap_database}")


def get_new_dn(old_dn, object_type):
    splitted_dn = ldap.dn.str2dn(old_dn)

    n = N.get(object_type, 0) + 1
    first_part = splitted_dn[0][0]
    first_part = first_part[0], f"{object_type.split('/')[1]}{n}", first_part[2]
    splitted_dn[0][0] = first_part
    N[object_type] = n
    return ldap.dn.dn2str(splitted_dn)  # .replace(ucr.get("ldap/base"), "dc=ucs,dc=test")


def anonymize(ldap_database, output_file=None):
    new_dns = {}
    new_uids = {}
    ldap_record_list = ldif.LDIFRecordList(open(ldap_database))
    ldap_record_list.parse()

    _new_records = []
    if not output_file:
        _, fname = tempfile.mkstemp(suffix=".ldif")
    else:
        fname = os.path.splitext(os.path.expanduser(output_file))[0] + ".ldif"
    writer = ldif.LDIFWriter(open(fname, "w"))
    print(f"... writing anonymized data to {fname}")

    # anonymize specific attributes
    for dn, entry in ldap_record_list.all_records:
        # exclude temporary objects
        if "cn=temporary,cn=univention" in dn:
            continue

        # special case: cn=admin
        if b"person" in entry.get("objectClass", []) and entry.get("cn", [None])[0] == b"admin":
            # set password to univention
            entry["userPassword"] = [b"{crypt}$6$a5nKfWRTu.v0n4Xs$uIlgIEGTegjgCbgeHo0kbYQbF2q6kXHQLphkYukf3lMWp5OfgAnsOiqKcHi7kaBjIDxCanrye45tB5Ejdgl14/"]

        object_type = entry.get("univentionObjectType", [None])[0]
        if object_type == b"users/user":
            new_dn = get_new_dn(dn, object_type.decode("utf-8"))
            new_cn = ldap.dn.explode_dn(new_dn, True)[0].encode()
            new_dns[dn.lower().encode()] = new_dn.encode()
            new_uids[dn.lower().encode()] = new_cn
            dn = new_dn
            entry["uid"] = [new_cn]
            entry["cn"] = entry["uid"]
            entry["displayName"] = entry["uid"]
            entry["gecos"] = entry["uid"]
            if "univentionBirthday" in entry:
                entry["univentionBirthday"] = [b"1970-01-01"]
            entry["givenName"] = [b"Kim"]
            entry["sn"] = [new_cn.title()]
            entry["krb5PrincipalName"] = [new_cn + b"@" + old.split(b"@")[1] for old in entry["krb5PrincipalName"]]
            entry["homeDirectory"] = [b"/home/" + new_cn]
            if "mailPrimaryAddress" in entry:
                entry["mailPrimaryAddress"] = [new_cn + b"@" + old.split(b"@")[1] for old in entry["mailPrimaryAddress"]]
            entry.pop("jpegPhoto", None)
            entry.pop("pwhistory", None)
            entry.pop("pager", None)
            entry.pop("st", None)
            entry.pop("l", None)
            entry.pop("c", None)
            entry.pop("description", None)
            entry.pop("postalCode", None)
            entry.pop("street", None)
            entry.pop("mail", None)
            entry.pop("mailAlternativeAddress", None)
            entry.pop("homePostalAddress", None)
            entry.pop("homePhone", None)
            entry.pop("mobile", None)

            # passwords. set to "univention"
            entry["userPassword"] = [b"{crypt}$6$a5nKfWRTu.v0n4Xs$uIlgIEGTegjgCbgeHo0kbYQbF2q6kXHQLphkYukf3lMWp5OfgAnsOiqKcHi7kaBjIDxCanrye45tB5Ejdgl14/"]
            entry["sambaNTPassword"] = [b"CAA1239D44DA7EDF926BCE39F5C65D0F"]
            entry.pop("sambaPasswordHistory", None)
            entry.pop("sambaLMPassword", None)
            entry["krb5Key"] = krb5_asn1("saltupn@UCS.TEST", "univention")
            entry.pop("univentionRadiusPassword", None)
        elif object_type == b"groups/group":
            new_dn = get_new_dn(dn, object_type.decode("utf-8"))
            new_cn = ldap.dn.explode_dn(new_dn, True)[0].encode()
            new_dns[dn.lower().encode()] = new_dn.encode()
            dn = new_dn
            entry["cn"] = [new_cn]
        elif object_type and object_type.startswith(b"computers/"):
            new_dn = get_new_dn(dn, object_type.decode("utf-8"))
            new_cn = ldap.dn.explode_dn(new_dn, True)[0].encode()
            new_dns[dn.lower().encode()] = new_dn.encode()
            new_uids[dn.lower().encode()] = new_cn + b"$"
            dn = new_dn
            entry["cn"] = [new_cn]
            entry["sn"] = [new_cn]
            entry["uid"] = [new_cn + b"$"]
            entry["displayName"] = entry["uid"]
            entry["aRecord"] = [b"10.10.10.10"]
            entry["krb5PrincipalName"] = [b"host/" + new_cn + b"." + entry.get("associatedDomain", [b"ucs.test"])[0] + b"@" + old.split(b"@")[1] for old in entry["krb5PrincipalName"]]

            # passwords. set to "univention"
            entry["userPassword"] = [b"{crypt}$6$a5nKfWRTu.v0n4Xs$uIlgIEGTegjgCbgeHo0kbYQbF2q6kXHQLphkYukf3lMWp5OfgAnsOiqKcHi7kaBjIDxCanrye45tB5Ejdgl14/"]
            entry["sambaNTPassword"] = [b"CAA1239D44DA7EDF926BCE39F5C65D0F"]
            entry.pop("sambaPasswordHistory", None)
            entry.pop("sambaLMPassword", None)
            entry["krb5Key"] = krb5_asn1("saltupn@UCS.TEST", "univention")
            entry.pop("univentionRadiusPassword", None)
        elif object_type == b"dns/forward_zone":
            entry["aRecord"] = [b"10.10.10.10"]
        elif object_type == b"dns/host_record":
            entry["aRecord"] = [b"10.10.10.10"]
        elif object_type == b"policies/dhcp_dns":
            entry["univentionDhcpDomainNameServers"] = [b"10.10.10.10"]
        elif object_type == b"networks/network":
            entry["univentionNetwork"] = [b"10.10.10.0"]
            entry["univentionNextIp"] = [b"10.10.10.11"]
        _new_records.append((dn, entry))

    # now we have all new dns, next we replace all references
    for dn, entry in _new_records:
        if "memberUid" in entry:
            entry["memberUid"] = [new_uids.get(member.lower()) for member in entry.get("uniqueMember", []) if member.lower() in new_uids]
        for attr, value in deepcopy(entry).items():
            new_value = []
            for val in value:
                if val.lower() in new_dns:
                    new_value.append(new_dns[val.lower()])
                else:
                    new_value.append(val)
            entry[attr] = new_value

        # finally time to write it
        writer.unparse(dn, entry)

    print(f"We anonymized or removed what we considered worthy. Usernames, birthdays, mail adresses, etc. Please note that there may be remnants of technical data, e.g. server names, DNS data, etc. Also, maybe some user data that needed protection was saved in attributes we did not consider or we did not know of (objects can be extended by Apps, ...). Please check the file before sending somewhere: {fname}")


def parser():
    """
    Parse command line arguments.
    :return:
    """
    parser = argparse.ArgumentParser(description="%(prog)s creates an offline copy of your LDAP server. It will then anonymize the data with regards to user data like names, mail addresses, passwords, ... Use case is a file that could be used in test environments to analyze e.g. performance.",
                                     usage="%(prog)s --i-understand")
    parser.add_argument("--i-understand", action="store_true", required=True, help="This will create an (anonymized) copy of the complete LDAP database. I understand that this may take a while and that some data may not be completely anonymized based on my own layout (maybe I stored the name of a user in an extended attribute). I will have the chance to examine the file afterwards, though.")
    parser.add_argument("-o", "--output", action="store", default=None, help="Output file name. If not set, a temporary file will be created.")
    return parser.parse_args()


def main():
    _args = parser()
    ldap_database = export_ldap_database()
    anonymize(ldap_database, output_file=_args.output)
    cleanup(ldap_database)


if __name__ == "__main__":
    main()
