# Copyright 2020 VMware, Inc.
# All rights reserved. -- VMware Confidential

import logging
import subprocess

logger = logging.getLogger(__name__)

def do_patching(ctx):
    DC_NAME = run_cmd("/usr/lib/vmware-vmafd/bin/vmafd-cli get-dc-name-ex --server-name localhost".split())

    DOMAIN_NAME = run_cmd("/usr/lib/vmware-vmafd/bin/vmafd-cli get-domain-name --server-name localhost".split())

    DOMAIN_DN = ""
    for dc in DOMAIN_NAME.split('.'):
        DOMAIN_DN += "dc=" + dc + ','
    DOMAIN_DN = DOMAIN_DN[:-1]

    DC_ACCOUNT_DN = ""
    DC_ACCOUNT_PW = ""
    lwreg_values = run_cmd("/opt/likewise/bin/lwregshell list_values [HKEY_THIS_MACHINE\\Services\\vmdir]".split())
    for line in lwreg_values.splitlines():
        if "dcAccountDN" in line:
            DC_ACCOUNT_DN = line.split('REG_SZ          "')[1][:-1]
        elif "dcAccountPassword" in line:
            DC_ACCOUNT_PW = line.split('REG_SZ          "')[1][:-1]

    # Remove any prepended backslashes in the password
    # The likewise shell util displays values in a format where it prepends
    # a backslash in front of any special characters (such as double quotes
    # and the backslash character itself).
    actual_pw = ""
    keep_next_char = False
    for char in DC_ACCOUNT_PW:
        if keep_next_char:
            actual_pw += char
            keep_next_char = False
        else:
            if char == '\\':
                # Skip the backslash and keep the next character
                keep_next_char = True
            else:
                actual_pw += char

    DC_ACCOUNT_PW = actual_pw

    logger.info("DC Name: " + DC_NAME)
    logger.info("Domain Name: " + DOMAIN_NAME)
    logger.info("Domain DN: " + DOMAIN_DN)
    logger.info("DC Account DN: " + DC_ACCOUNT_DN)

    # Check if any UPN formatted FSPs exist
    logger.info("Searching for any UPN formatted FSPs...")
    upn_fsps = run_cmd(["ldapsearch", "-LLL", "-o", "ldif-wrap=no", "-h", DC_NAME, "-b", "cn=ForeignSecurityPrincipals," + DOMAIN_DN, "(&(objectclass=foreignSecurityPrincipal)(externalObjectId=*@*))", "-D", DC_ACCOUNT_DN, "-w", DC_ACCOUNT_PW])

    if len(upn_fsps) == 0:
        logger.info("No UPN formatted FSPs found. No further action needed.")
        return

    # Decode non-ASCII entries, if any
    decoded_fsps = ""
    for line in upn_fsps.splitlines():
        if line.startswith("dn:: "):
            decoded_line = decode_non_ascii(line)
            decoded_fsps += decoded_line
        else:
            decoded_fsps += line + '\n'

    logger.info("UPN formatted FSPs found:\n\n" + decoded_fsps + "\n\n")

    # Get all groups that have FSP members
    logger.info("Finding all groups with FSP members...")
    grps_with_fsps = run_cmd(["ldapsearch", "-LLL", "-o", "ldif-wrap=no", "-h", DC_NAME, "-b", DOMAIN_DN, "(&(objectclass=group)(member=*cn=ForeignSecurityPrincipals," + DOMAIN_DN + "))", "-D", DC_ACCOUNT_DN, "-w", DC_ACCOUNT_PW])

    if len(grps_with_fsps) == 0:
        logger.info("No groups with FSP members found. No further action needed.")
        return

    # Decode non-ASCII entries, if any
    decoded_grps = ""
    for line in grps_with_fsps.splitlines():
        if line.startswith("dn:: ") or line.startswith("member:: "):
            decoded_line = decode_non_ascii(line)
            decoded_grps += decoded_line
        else:
            decoded_grps += line + '\n'

    logger.info("Groups with FSP members found:\n\n" + decoded_grps + "\n\n")

    # Normalize all UPN formatted FSP group memberships
    # We only want to normalize the member attribute value of the group object
    # if the member is an externalObjectId that is in UPN format and not already
    # in all lowercase (normalized format). This will cause vmdir to create a
    # new FSP object with a normalized ID (all lowercase UPN). We DO NOT delete
    # the old FSP object as it is not necessary to do so. The old FSP object
    # will simply no longer be referenced by our code.
    logger.info("Normalizing all UPN FSP group memberships...\n")

    dn_prefix = "dn: "
    non_ascii_dn_prefix = "dn:: "
    fsp_prefix = "member: externalObjectId="
    non_ascii_fsp_prefix = "member:: externalObjectId="

    grps = decoded_grps.split("\n\n")
    for grp in grps:
        attributes = grp.splitlines()
        # Get group DN
        if attributes[0].startswith(dn_prefix):
            grp_dn = attributes[0][len(dn_prefix):]
        elif attributes[0].startswith(non_ascii_dn_prefix):
            grp_dn = attributes[0][len(non_ascii_dn_prefix):]
        else:
            raise RuntimeError("Error while parsing LDIF. Group DN was expected at line '" + attributes[0] + "'")
        for attribute in attributes:
            # Normalize UPN FSP members
            if (attribute.startswith(fsp_prefix) or attribute.startswith(non_ascii_fsp_prefix)) and '@' in attribute:
                # Get FSP ID
                if attribute.startswith(fsp_prefix):
                    fsp = attribute[len(fsp_prefix):]
                else:
                    fsp = attribute[len(non_ascii_fsp_prefix):]
                logger.info("UPN formatted FSP member '" + fsp + "' found for group '" + grp_dn + "'")
                if fsp.islower():
                    logger.info("FSP is already in normalized format. Skipping...\n")
                else:
                    normalized_fsp = fsp.lower()
                    logger.info("Normalizing FSP to '" + normalized_fsp + "'")
                    ldif_mod = dn_prefix + grp_dn + "\nchangetype: modify\nadd: member\n" + fsp_prefix + normalized_fsp + "\n-\ndelete: member\n" + fsp_prefix + fsp
                    run_cmd(["ldapmodify", "-h", DC_NAME, "-D", DC_ACCOUNT_DN, "-w", DC_ACCOUNT_PW], ldif_mod)
                    logger.info("ldapmodify complete. Continuing...\n")

    logger.info("FSP normalization done.")

def decode_non_ascii(entry):
    p1 = subprocess.Popen(["echo", entry], stdout=subprocess.PIPE)
    decoded_entry = subprocess.check_output(["perl", "-MMIME::Base64", "-n", "-00", "-e", '''s/\n +//g;s/(?<=:: )(\S+)/decode_base64($1)/eg;print'''], stdin=p1.stdout, universal_newlines=True)
    p1.stdout.close()
    return decoded_entry

def run_cmd(cmd, cmd_input=None):
    process = subprocess.run(cmd, input=cmd_input, check=True, capture_output=True, universal_newlines=True)
    return process.stdout.strip()
