#!/usr/bin/python3
'''cern-get-certificate: A tool for configuring CERN certificates on servers.'''

import argparse
import glob
import os
import sys
import configparser
import time
import re
import subprocess
import tempfile
import locale
import xml.etree.ElementTree as ET
from datetime import datetime
from dateutil.parser import parse
import requests
from requests_kerberos import HTTPKerberosAuth

CERT_CONFIG = "/etc/sysconfig/cern-get-certificate"     # conf file in /etc/sysconfig/ (.spec)
CERT_PRIVPATH = '/etc/pki/tls/private/'
CERT_PUBPATH = '/etc/pki/tls/certs/'
CERT_CA_TEMP = '/tmp/cgc-ca-temp.pem'
CERT_MINDAYS = 7
TMPSUFF = "._tmp_"
KRB5_DEF_KEYTAB = '/etc/krb5.keytab'
KRB5CFGTMP = '/tmp/cgk.krb5.conf'


def main():
    '''Main function of cern-get-certificate.'''

    ## Parsing user arguments
    parser = argparse.ArgumentParser()
    mutually_excl_grp1 = parser.add_mutually_exclusive_group(required=True)
    mutually_excl_grp1.add_argument(
        "--status",
        required=False,
        action="store_true",
        help="Check current status of autoenrollment, certificate expiry and its existence.")
    mutually_excl_grp1.add_argument(
        "--autoenroll",
        required=False,
        action="store_true",
        help="Enable the autoenrollment (automatic renewal of certificate) then obtain a host\
          certificate from CERN CA and store it in file.")
    mutually_excl_grp1.add_argument(
        "--noautoenroll",
        required=False,
        action="store_true",
        help="Disable the autoenrollment (automatic renewal of certificate). Certificate is\
          not removed but it remains valid until expiry date.")
    mutually_excl_grp1.add_argument(
        "--renew",
        required=False,
        action="store_true",
        help="Renew the certificate. Checks the validity of certificate and requests a new\
              one if it would expire in less than 7 (default) days from the time of check.")
    parser.add_argument(
        "--force",
        required=False,
        action="store_true",
        help="Override default checks and perform the operation.\
              Can be used with --autoenroll or --renew.")
    parser.add_argument(
        "--config",
        required=False,
        action="store",
        type=str,
        help="Use alternate configuration file. "
        "Note: /etc/cron.daily/cern-get-certificate should be altered if non-default "
        "configuration file is being used.")
    mutually_excl_grp2 = parser.add_mutually_exclusive_group()
    mutually_excl_grp2.add_argument(
        "--cern",
        required=False,
        action="store_true",
        help="Process CERN CA certificates (this is the default option, may be omitted).")
    mutually_excl_grp2.add_argument(
        "--grid",
        required=False,
        action="store_true",
        help="Process CERN Grid CA certificates.")
    parser.add_argument(
        "--hostname",
        required=False,
        type=str,
        action="store",
        help="Request certificate for HOSTNAME instead of default system hostname."
        "This option is to be used ONLY when system has multiple interfaces / ip addresses\
              allocated."
        "A keytab containing entries for non-default HOSTNAME is required."
        "(this can be obtained with cern-get-keytab)"
        "HOSTNAME should be specified as fully qualified hostname (XXX.cern.ch)"
        "This option will not funciton for DNS host aliases."
        "If this option is used, it should be set also in the configuration file"
        "for future certificate renewal.")
    parser.add_argument(
        "--usekrb5cfg",
        required=False,
        action="store_true",
        help="Use temporary kerberos configuration file: /tmp/cgk.krb5.conf created by: "
        " cern-get-keytab --leavekrb5cfg for client initialization."
        "This is to be used in order to guarantee that same Active Directory Domain "
        "Controller that cern-get-keytab used will be used, therefore eliminating the "
        "need to wait for (asynchronous) AD replication.")
    mutually_excl_grp1.add_argument(
        "--cron",
        required=False,
        action="store_true",
        help="Same as --renew. It is used by the cronjob for autoenrollment.")
    parser.add_argument(
        "--verbose",
        required=False,
        action="store_true",
        help="Display verbose information.")
    parser.add_argument(
        "--debug",
        required=False,
        action="store_true",
        help="Display debug information.")

    ## Main variables definition prioritization: config properties > user args > default values

    # For compatibility with Perl both in stdout and exit code.
    try:
        options = parser.parse_args()
    except SystemExit:
        errorout("One of --autoenroll, --noautoenroll, --renew or --status must be supplied.")

    # This script must be executed by root.
    if os.geteuid() != 0:
        errorout("You must be 'root' to run this program.")

    CA_CERT_TYPE = "CernHostCertificate"
    SUFF = ""
    if options.grid:
        CA_CERT_TYPE = "GridHostCertificate"
        SUFF = ".grid"

    # Use default config file if not otherwise defined in the user arguments through "--config".
    cfgfile = CERT_CONFIG
    if options.config:
        cfgfile = options.config # If defined in user args, use that config file.
    if not cfgfile or not (os.path.isfile(cfgfile) and os.access(cfgfile, os.R_OK)):
        errorout(f"Config file not readable {cfgfile}", 0)
    cfg = configparser.ConfigParser()
    cfg.read(cfgfile)
    try:
        privpath_prop = cfg.get("config", "keypath")
        pubpath_prop = cfg.get("config", "certpath")
        keytab_prop = cfg.get("config", "keytab")
        mindays_prop = cfg.getint("config", "days")
        autorenew_prop = cfg.getint("config", "autorenew")
        hostname_prop = cfg.get("config", "hostname")
        autorenewexec_prop = cfg.get("config", "autorenewexec")
        chown_uid_prop = cfg.get("config", "uid")
        chown_gid_prop = cfg.get("config", "gid")
    except configparser.NoOptionError:
        errorout(f"One of the configuration properties is missing! Check {cfgfile}")
    print_verbose(f"Using settings from config file: {cfgfile}", options.verbose)

    cert_privpath = check_prop("DIR", 'keypath', privpath_prop, cfgfile, CERT_PRIVPATH)
    cert_pubpath = check_prop("DIR", 'certpath', pubpath_prop, cfgfile, CERT_PUBPATH)
    keytab = check_prop("FILE", 'keytab', keytab_prop, cfgfile, KRB5_DEF_KEYTAB)
    mindays = fallback(mindays_prop, CERT_MINDAYS)
    autorenew = fallback(autorenew_prop, 1)
    hostname = os.uname()[1].strip()
    if hostname_prop:
        hostname = hostname_prop
        print_verbose(f"hostname value is {hostname} taken from {cfgfile}", options.verbose)
    elif options.hostname is not None:
        hostname = options.hostname
        print_verbose(f"hostname value is {hostname} taken from arguments", options.verbose)
    autorenewexec = None
    if autorenewexec_prop:
        autorenewexec = autorenewexec_prop.strip('"\'')
    chown_uid = 0
    chown_gid = 0
    if chown_uid_prop:
        chown_uid = int(chown_uid_prop)
    if chown_gid_prop:
        chown_gid = int(chown_gid_prop)

    keyfile = os.path.join(cert_privpath, f"{hostname}{SUFF}.key")
    csrfile = os.path.join(cert_privpath, f"{hostname}{SUFF}.csr")
    cert_parent_name = os.path.join(cert_pubpath, f"{hostname}{SUFF}")

    # We do not do "kerb init". Complying with Perl.
    print_verbose("Initializing Kerberos client", options.verbose)
    ## Need to run " cern-get-keytab --leavekrb5cfg " first (?) if you use "--usekrb5cfg" arg.
    # This is done as in Perl. Probably setting the ENV in the end has no effect.
    if options.usekrb5cfg:
        print_verbose(f"Using temporary kerberos configuration file: {KRB5CFGTMP}", options.verbose)
        if os.path.isfile(KRB5CFGTMP) and not os.access(KRB5CFGTMP, os.R_OK):
            errorout(f"Temporary kerberos configuration file: {KRB5CFGTMP} not readable ?")
        os.environ["KRB5_CONFIG"] = KRB5CFGTMP

    ## Python kerberos
    print_verbose("Authenticating using keytab file.", options.verbose)
    print_debug(f"using keytab file name: {keytab}", options.debug)
    principal = None
    print_debug(f"resolving keytab file {keytab}", options.debug)
    klist = do_execute(f"/usr/bin/klist -k {keytab}", options.verbose, return_output=True)
    if not klist:
        errorout("Cannot authenticate using keytab file.")
    print_debug(f"scanning keytab file for principal name ({hostname})", options.debug)
    for line in klist.splitlines():
        if hostname in line and "host/" in line:
            principal = line.split()[1].split("@")[0]
            print_debug(f"found principal name: {principal}", options.debug)
            break
    if principal:
        print_debug(f"authen. as: {principal} using keytab file: {keytab}", options.debug)
        if not kinit(principal, keytab, options.verbose):
            errorout("Cannot authenticate using keytab file.")
    else:
        print_verbose("principal name not found.", options.verbose)
        errorout("Cannot authenticate using keytab file.")

    ## Setting locale for openssl commands. Perl: every openssl execution (LC_ALL=C).
    locale.setlocale(locale.LC_ALL, 'C')

    ### 4 CASES: status (check current status), renew, enable autoenrollment, disable autoenrollment
    pem_cert = cert_parent_name + ".pem"
    crt_cert = cert_parent_name + ".crt"

    if options.status:                      ## CASE1: STATUS (check current status)
        print_verbose("checking status.", options.verbose)
        check_status(CA_CERT_TYPE, pem_cert, crt_cert, keyfile, autorenew, autorenewexec,
                     mindays, options.grid, options.verbose, options.debug)
        sys.exit(0)

    elif options.noautoenroll:              ## CASE2: DISABLE AUTOENROLLMENT
        print_verbose("Disabling certificate autoenrollment.", options.verbose)
        disable_enrol(CA_CERT_TYPE, options.verbose, options.debug)
        sys.exit(0)

    elif options.renew or options.cron:     ## CASE3: RENEW
        # "--cron" activates the same flow as "--renew". Only used from the cron job
        renew_cert(CA_CERT_TYPE, mindays, keyfile, csrfile, pem_cert, crt_cert, autorenew,
                   options.cron, chown_uid, chown_gid, options.force, options.verbose,
                   options.debug)

    elif options.autoenroll:                ## CASE4: AUTOENROLL
        if (os.access(keyfile, os.R_OK)
                or os.access(pem_cert, os.R_OK)
                or os.access(crt_cert, os.R_OK)) and not options.force:
            errorout("key / cert file(s) already exist, use --force to overwrite.\n" +
                     f"private key file: {keyfile}\n" +
                     f"cert PEM file:    {pem_cert}\n" +
                     f"cert DER file:    {crt_cert}")
    else:
        errorout("Command line arguments error.")

    ### CASE GENERAL: This part runs for --renew/--cron and --autoenroll (CASES 3 and 4).
    is_first_enrol = False
    if not manage_enrollment(CA_CERT_TYPE, "GET", options.verbose, options.debug):
        manage_enrollment(CA_CERT_TYPE, "ENABLE", options.verbose, options.debug)
        is_first_enrol = True
    else:
        print_verbose("Already enrolled. Continue...", options.verbose)

    ## Create Certificate Request in order to (eventually) request the certificate.
    print_verbose("Creating CSR.", options.verbose)
    cert_req_content = create_cert_req(hostname, keyfile, csrfile, options.verbose, options.debug)

    ## is_first_enrol = True =>kerberos creds and cert request.
    ## is_first_enrol = False => only cert request
    print_verbose("Sending certificate request to CA", options.verbose)
    cert_content = ""
    if is_first_enrol:
        i = 1
        sleep = 10
        tries = 12
        # Waiting 20s before the very first attempt increases drastically the chances of getting
        # a certificate with no complications!
        print("Waiting for Active Directory data replication - 30s")
        time.sleep(30)

        while i <= tries:
            print_verbose("Reauthenticating using keytab file.", options.verbose)
            # Reauthenticating krb5. If not, we always get "Permission denied: not authorized"
            if kinit(principal, keytab, options.verbose):
                klist = do_execute("/usr/bin/klist", options.verbose, return_output=True)
            else:
                errorout("Cannot authenticate using keytab file.")
            cert_content = request_cert(CA_CERT_TYPE, options.verbose,
                                        options.debug, cert_req_content)

            if "BEGIN CERTIFICATE" in cert_content:
                break

            if "last request from host received less than 1 hour ago" in cert_content\
                                                                            or i == tries:
                errorout(f"cannot receive/parse server data:{cert_content}")

            # For other kinds of cert_content we retry requesting a certificate, as in Perl.
            print_debug(f"CA server returned:{cert_content}", options.debug)
            print(f"Waiting for Active Directory data replication\
                   - {i * sleep}s (max: {sleep * tries}s).")
            time.sleep(sleep)
            i += 1

    else:   # Else clause for the successful case when cert is acquired and while loop breaks
        cert_content = request_cert(CA_CERT_TYPE, options.verbose, options.debug, cert_req_content)
        if "BEGIN CERTIFICATE" not in cert_content:
            print_debug(f"Not first enrol request result:\n{cert_content}", options.debug)
            errorout("No certificate data received from CA.")

    ## Integrating the certificate to this machine.
    integrate_cert(cert_content, cert_parent_name, options.verbose, options.debug)

    ## As all files are now created as ._tmp_, the manage_files makes all tmp files permanent.
    manage_files(csrfile, keyfile, pem_cert, crt_cert, chown_uid, chown_gid, options.verbose)

    print("Certificate obtained from CA, stored as:\n" +
          f"private key file: {keyfile}\n" +
          f"cert PEM file:    {cert_parent_name}" + ".pem\n" +
          f"cert DER file:    {cert_parent_name}" + ".crt")

    if autorenewexec:
        print_verbose("autorenewexec set: executing", options.verbose)
        cmd_list = autorenewexec.split()
        try:
            subprocess.run(cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                           universal_newlines=True, check=True)
        except subprocess.CalledProcessError as e:
            print_verbose(f"autorenewexec command failed with exit code {e.returncode}",
                          options.verbose)
            print_verbose(f"stdout: {e.stdout}", options.verbose)
            print_verbose(f"stderr: {e.stderr}", options.verbose)
            print(f"Warning: autorenewexec command failed: {' '.join(cmd_list)}")
            print("This is non-fatal - certificate renewal completed successfully")
        except FileNotFoundError as e:
            print_verbose(f"autorenewexec command not found: {e}", options.verbose)
            print(f"Warning: autorenewexec command not found: {' '.join(cmd_list)}")
            print("This is non-fatal - certificate renewal completed successfully")
    sys.exit(0)


def check_status(CA_CERT_TYPE, pem_cert, crt_cert, keyfile, autorenew, autorenewexec,
                 mindays, grid, verbose, debug):
    """Checking current status of machine's certificate"""
    grid_title = ""
    if grid:
        grid_title = "Grid "

    key_presence = pem_presence = crt_presence = "absent"
    if os.access(keyfile, os.R_OK):
        key_presence = "present"
    if os.access(crt_cert, os.R_OK):
        crt_presence = "present"

    ced = cert_validity = "unknown  (no certificate file)"
    if os.access(pem_cert, os.R_OK):
        pem_presence = "present"
        ced = checkcertdays(pem_cert, verbose, debug)
        if ced < 0:
            ced = "expired"
        if verify_cert(pem_cert, verbose, debug):
            cert_validity = "valid"
        else:
            cert_validity = "revoked"

    autorenewal_status = "enabled"
    autorenewexec_status = "disabled"
    if autorenew == 0:
        autorenewal_status = "disabled"
    elif autorenewexec:
        autorenewexec_status = autorenewexec

    autorenewal_days = mindays
    if mindays <= 0:
        autorenewal_days = "disabled"

    autoenrollment_status = "disabled"
    if manage_enrollment(CA_CERT_TYPE, "GET", verbose, debug):
        autoenrollment_status = "enabled"

    print(f"\
Host certificate status: CERN {grid_title}Certification Authority\n\
--------------------------------------------------------------------------------\n\
cert private key file  : {key_presence}  {keyfile}\n\
cert PEM file          : {pem_presence}  {pem_cert}\n\
cert DER file          : {crt_presence}  {crt_cert}\n\
cert days until expiry : {ced}\n\
cron autorenewal status: {autorenewal_status}\n\
cron autorenewal days  : {autorenewal_days}\n\
cron exec on autorenew : {autorenewexec_status}\n\
cert validity (OCSP)   : {cert_validity}\n\
autoenrollment status  : {autoenrollment_status}\n\
--------------------------------------------------------------------------------\n")


def disable_enrol(CA_CERT_TYPE, verbose, debug):
    """disable_enrol() disables CA autoenrolment of the current machine"""
    if manage_enrollment(CA_CERT_TYPE, "GET", verbose, debug):
        if manage_enrollment(CA_CERT_TYPE, "DISABLE", verbose, debug):
            print("Certificate autoenrollment disabled")
    else:
        print("This system is not set up for certificate autoenrollment.")


def renew_cert(CA_CERT_TYPE, mindays, keyfile, csrfile, pem_cert, crt_cert,
               autorenew, cron, chown_uid, chown_gid, force, verbose, debug):
    """renew_cert() renews the certificate."""
    # Trigger renewal with --cron, only if "autorenew" allows it.
    if cron and not autorenew:
        print("Certificate autorenewal disabled in configuration file, not renewing.")
        sys.exit(0)

    if not os.access(pem_cert, os.R_OK):
        print(f"No certificate file: {pem_cert},\
                not renewing (Did you run --autoenroll?)")
        sys.exit(0)

    manage_files(csrfile, keyfile, pem_cert, crt_cert, chown_uid, chown_gid, verbose)

    print_verbose("Checking certificate validity end date.", verbose)

    days = checkcertdays(pem_cert, verbose, debug)
    if days > mindays and not force:
        print(f"Not renewing: {pem_cert} still valid for {days} days\
                (min. is {mindays} days).")
        sys.exit(0)

    if not manage_enrollment(CA_CERT_TYPE, "GET", verbose, debug):
        errorout("This system is not set for certificate autoenrollment.\
                    Did you run --autoenroll?")


def soap_request(CA_CERT_TYPE, op_type, verbose, debug, cert_req_content=""):
    '''
        soap_request() makes SOAP requests to Certification Authority of CERN.
        certificate template: cert_tpl = "CernHostCertificate" OR "GridHostCertificate"
        4 operations/actions: op_type = "GET" or "NEW" or "ENABLE" or "DISABLE"
        ARG: certificate template, the type of request and certificate request if op_type = "NEW"
        RET: In succesfull response it returns a dictionary holding response info.
    '''
    base_url = "https://ca.cern.ch/ca-services"
    autoenrol_tpl = "autoenrollmentHostCertificateTemplate"

    url = f"{base_url}/autoenrollment/Autoenrollment.asmx"
    headers = {}
    headers["Content-Type"] = "text/xml; charset=UTF-8"

    attr = {}
    attr["xmlns:xsi"] = "http://www.w3.org/2001/XMLSchema-instance"
    attr["xmlns:xsd"] = "http://www.w3.org/2001/XMLSchema"
    attr["xmlns:soap"] = "http://schemas.xmlsoap.org/soap/envelope/"
    xml_root = ET.Element("soap:Envelope", attrib=attr)
    xml_body = ET.SubElement(xml_root, "soap:Body")

    if op_type == "GET":
        op_set = "GetHostCertificatesAutoenrollmentSettings"
        headers["SOAPAction"] = f"{base_url}/{op_set}"
        xml_op = ET.SubElement(xml_body, f"{op_set}", xmlns=f"{base_url}")
        ET.SubElement(xml_op, f"{autoenrol_tpl}").text = CA_CERT_TYPE

    elif op_type == "ENABLE":
        op_set = "EnableHostCertificatesAutoenrollment"
        headers["SOAPAction"] = f"{base_url}/{op_set}"
        xml_op = ET.SubElement(xml_body, f"{op_set}", xmlns=f"{base_url}")
        ET.SubElement(xml_op, f"{autoenrol_tpl}").text = CA_CERT_TYPE
        ET.SubElement(xml_op, "requestor").text = "root"

    elif op_type == "DISABLE":
        op_set = "DisableHostCertificatesAutoenrollment"
        headers["SOAPAction"] = f"{base_url}/{op_set}"
        xml_op = ET.SubElement(xml_body, f"{op_set}", xmlns=f"{base_url}")
        ET.SubElement(xml_op, f"{autoenrol_tpl}").text = CA_CERT_TYPE
        ET.SubElement(xml_op, "requestor").text = "root"

    else:       # for op_type == "NEW"
        op_set = "NewHostCertificate"
        headers["SOAPAction"] = f"{base_url}/{op_set}"
        xml_op = ET.SubElement(xml_body, f"{op_set}", xmlns=f"{base_url}")
        ET.SubElement(xml_op, f"{autoenrol_tpl}").text = CA_CERT_TYPE
        ET.SubElement(xml_op, "base64CertificateRequest").text = cert_req_content

    xml = ET.tostring(xml_root, encoding='utf8', method='xml')

    print_verbose(f"SOAPAction url : {headers['SOAPAction']}", verbose)
    print_debug("curl POST header:", debug)
    print_debug(f"Content-Type:\t{headers['Content-Type']}", debug)
    print_debug(f"SOAPAction:\t\t{headers['SOAPAction']}", debug)
    print_debug(f"curl POST body:\n{xml}", debug)

    server_response = ""
    result_dict = {}
    try:
        req_result = requests.post(url, headers=headers, data=xml, auth=HTTPKerberosAuth())
        print_debug(f"SOAP result status code:\t{req_result.status_code}", debug)
        req_result.raise_for_status()
    except requests.exceptions.RequestException as err:
        errorout(err)
    if req_result.content:
        server_response = req_result.content.decode("utf-8")
        # We strip() the stdout result, or else ElementTree is complaining while parsing it!
        xml_tree = ET.fromstring(server_response.strip())
        print_debug(f"SOAP response :\n{server_response.strip()}", debug)

        xml_tag_prefix = "{https://ca.cern.ch/ca-services}"

        for succ in xml_tree.iter(f"{xml_tag_prefix}Success"):
            result_dict["Success"] = succ.text
            if succ.text == "true":
                if op_type == "NEW":
                    for title in ["ActivityID", "RequestID", "Certificate"]:
                        for child in xml_tree.iter(f"{xml_tag_prefix}{title}"):
                            result_dict[title] = child.text
                else:       # op_type in ["ENABLE", "DISABLE", "GET"]
                    for title in ["ActivityID", "Enabled", "DomainController"]:
                        for child in xml_tree.iter(f"{xml_tag_prefix}{title}"):
                            result_dict[title] = child.text
                return result_dict
            if succ.text == "false" and op_type == "NEW":
                for title in ["ActivityID", "Message"]:
                    for child in xml_tree.iter(f"{xml_tag_prefix}{title}"):
                        result_dict[title] = child.text
                return result_dict
            # If succ.text neither "true" nor "false", something's very wrong.
            errorout(f"Something changed in the CA API for {op_type} request.\
                        Please check:\
                        https://ca.cern.ch/ca-services/autoenrollment/Autoenrollment.asmx")
    else:
        errorout("Python request's returncode is 200,\
                  but there is no content returned from the server.\
                  Something's very wrong here.")





def manage_enrollment(CA_CERT_TYPE, op_type, verbose, debug):
    '''
        manage_enrollment() is checking whether enrollment
        is enabled (returns True) or disabled (returns False)
        certificate template: cert_tpl = "CernHostCertificate" OR "GridHostCertificate"
        3 operations/actions: op_type = "GET" or "ENABLE" or "DISABLE"
        ARG:	certificate template and the type of request.
        RET:	Different return for different op_type
    '''
    is_req_success = False
    is_enrol_enabled = False
    req_res_dict = soap_request(CA_CERT_TYPE, op_type, verbose, debug)

    # GET: If unsuccesful req -> exit. Returns True or False based on Enabled value
    if req_res_dict["Success"] == "true":
        is_req_success = True
        print_verbose("curl RCV data:", verbose)
        print_verbose(f"Success\t: {req_res_dict['Success']}", verbose)
        print_verbose(f"Enabled\t: {req_res_dict['Enabled']}", verbose)
        print_verbose(f"Domain Ctrl\t: {req_res_dict['DomainController']}", verbose)
        print_verbose(f"Activity ID\t: {req_res_dict['ActivityID']}", verbose)

        if op_type == "GET":
            if req_res_dict["Enabled"] == "true":
                is_enrol_enabled = True
            return is_enrol_enabled

        if op_type == "DISABLE":
            if req_res_dict["Enabled"] == "false":
                is_enrol_enabled = False
                return is_req_success
            errorout(f"DISABLE soap request is wrong. 'Enabled' field returned\
                    {req_res_dict['Enabled']}. Contact the developers to debug.")

        if op_type == "ENABLE":
            if req_res_dict["Enabled"] == "true":
                ## If ENABLE is succesful, it returns the 'DomainController' value
                is_enrol_enabled = True
                return req_res_dict['DomainController']

    errorout(f"{op_type} soap request is wrong. 'Success' field returned\
            {req_res_dict['Success']}. Contact the developers to debug.")


def request_cert(CA_CERT_TYPE, verbose, debug, cert_req_content):
    '''
        request_cert() uses the temp csr (content: "BEGIN/END CERTIFICATE REQUEST")
        It makes the request to CA and if succesfull receives the certificate
        (content: "BEGIN/END CERTIFICATE") from CA.
        ARG: certificate template, content of certificate request
        RET: If succesful => certificate. Else => the request's response message.
    '''
    req_res_dict = soap_request(CA_CERT_TYPE, op_type="NEW", verbose=verbose,
                                debug=debug, cert_req_content=cert_req_content)

    if req_res_dict["Success"] == "true":
        if verbose:
            print_verbose("curl RCV data:", verbose)
            print_verbose(f"Success\t: {req_res_dict['Success']}", verbose)
            print_verbose(f"Activity ID\t: {req_res_dict['ActivityID']}", verbose)
            print_verbose(f"Request ID\t: {req_res_dict['RequestID']}", verbose)
            print_verbose(f"Certificate:\n{req_res_dict['Certificate']}", verbose)
        cert_list = req_res_dict['Certificate'].split("\n")
        if "BEGIN CERTIFICATE" in cert_list[0] and "END CERTIFICATE" in cert_list[-2]:
            print_verbose("Certificate is valid! Continue...", verbose)
            return req_res_dict["Certificate"]
    else:
        print_verbose(f"Success\t:{req_res_dict['Success']}", verbose)
        print_verbose("because", verbose)
        print_verbose(f"Message\t:{req_res_dict['Message']}", verbose)
        # Message can be: "last request from host received less than 1 hour ago"
        # OR "^Permission denied: not authorized." OR "^Denied by Policy Module"
        return f"{req_res_dict['Message']} \
            [RequestId: ????,ActivityID: {req_res_dict['ActivityID']}]"


def create_cert_req(hostname, keyfile, csrfile, verbose, debug):
    '''
        create_cert_req() returns the content of CERTIFICATE REQUEST (csr) as ._tmp_ file
        It creates a private key (.key) and a certificate request (.csr) marked as temporary.
        e.g. /etc/pki/tls/private/ga-cs9-test1.cern.ch.key._tmp_   -> Private key
        e.g. /etc/pki/tls/private/ga-cs9-test1.cern.ch.csr._tmp_   -> Certificate Request
        ARG: hostname, filepaths of private key (.key) and certificate request file (.csr)
        RET: It returns the content of the and certificate request file (.csr)
    '''
    config_content = f"\
[req]\ndistinguished_name=req_distinguished_name\n\
[req_distinguished_name]\n\
[san]\nsubjectAltName=DNS:{hostname}\n"

    config_file_tmp = generate_temp_file("cgc")
    with open(config_file_tmp, "w", encoding="utf-8") as tmp_cfg:
        tmp_cfg.write(config_content)

    keyfile_tmp = keyfile + TMPSUFF
    csrfile_tmp = csrfile + TMPSUFF
    if debug:
        cmd_verb = "-verbose"
        cmd = ["/usr/bin/openssl", "req", "-new", "-subj", f"/CN={hostname}", "-nodes", "-sha512",
               "-reqexts", "san", "-config", config_file_tmp, "-newkey", "rsa:2048", "-keyout",
               keyfile_tmp, "-out", csrfile_tmp, cmd_verb]
        print_debug(f"executing: {' '.join(cmd)}", debug)
    else:
        cmd = ["/usr/bin/openssl", "req", "-new", "-subj", f"/CN={hostname}", "-nodes", "-sha512",
               "-reqexts", "san", "-config", config_file_tmp, "-newkey", "rsa:2048", "-keyout",
               keyfile_tmp, "-out", csrfile_tmp]
        print_verbose(f"executing: {' '.join(cmd)}", verbose)
    result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                            encoding="utf-8")
    print_debug(f"stdout for creating a new CSR:\n{result.stdout}", debug)
    print_debug(f"stderr for creating a new CSR:\n{result.stderr}", debug)
    if result.returncode != 0:
        errorout(f"openssl for creating a new CSR failed. stderr returned:\n{result.stderr}")
    print_verbose("openssl for creating a new CSR ran succesfully.", verbose)
    if os.path.isfile(keyfile_tmp):
        os.chmod(keyfile_tmp, 0o600)
    if os.path.isfile(csrfile_tmp):
        os.chmod(csrfile_tmp, 0o600)

    ## Reading the CSR and checking if it is valid to return it to caller.
    print_verbose(f"Reading certificate data from: {csrfile_tmp}", verbose)
    tmp_csr_content = ""
    tmp_csr_content_list = []
    with open(csrfile_tmp, "r", encoding="utf-8") as content:
        for line in content.readlines():
            tmp_csr_content_list.append(line)
            tmp_csr_content += line
    if "BEGIN CERTIFICATE REQUEST" in tmp_csr_content_list[0] and\
    "END CERTIFICATE REQUEST" in tmp_csr_content_list[-1]:
        print_verbose("CSR is valid. Continue...", verbose)
        return tmp_csr_content
    errorout(f"{csrfile_tmp} does not contain CSR. It contains: {tmp_csr_content}")


def verify_cert(pem_cert, verbose, debug):
    '''
        verify_cert(): a wrapper function that executes openssl to check certificate validity.
        ARG: filepath for the existing .pem file.
        RET: True, if the existing .pem file is verified by the CA. False otherwise.
    '''
    is_verified = False
    # The call to openssl requires a single 'issuer' file
    # To avoid hard coding files, we concatinate all possible candidates
    # into a file, removing after we've called openssl
    file_pattern = os.path.join(CERT_PUBPATH, 'CERN*pem')
    matching_files = glob.glob(file_pattern)
    with open(CERT_CA_TEMP, 'w', encoding='utf-8') as cgcstatus:
        for file in matching_files:
            with open(file, 'r', encoding='utf-8') as infile:
                content = infile.read()
                cgcstatus.write(content)
    # Returncode is usually 1, but the stdout is legit
    # If stdout is like: "/etc/pki/tls/certs/<hostname>.cern.ch.pem: good" => cert is valid!
    cmd = ["/usr/bin/openssl", "ocsp", "-no_nonce", "-issuer", CERT_CA_TEMP,
           "-cert", pem_cert, "-url", "http://ocsp.cern.ch/ocsp/"]
    if debug or verbose:
        print(f"executing: {' '.join(cmd)}")
    result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8")
    # We don't need the concatenated file anymore
    try:
        os.remove(CERT_CA_TEMP)
    except FileNotFoundError:
        pass
    print_debug(f"stdout for checking certificate expiration:\n{result.stdout}", debug)
    print_debug(f"stderr for checking certificate expiration:\n{result.stderr}", debug)

    if re.findall(f"^{pem_cert}: good", result.stdout):
        is_verified = True
        print_verbose("The certificate is valid. Continue...", verbose)
    else:
        print_verbose(f"The certificate is not verified because of:\n{result.stdout}", verbose)

    return is_verified


def checkcertdays(pem_cert, verbose, debug):
    '''
        checkcertdays() uses .pem file to find its expiry date (using openssl bash command)
        It returns the amount of days from today until the expiration date.
        ARG: filepath for the existing .pem file
        RET: The amount of days from today until the expiry date.
    '''
    if debug:
        cmd_verb = "-verbose"
        cmd = ["/usr/bin/openssl", "x509", "-inform", "PEM",
               "-noout", "-enddate", "-in", pem_cert, cmd_verb]
        print_debug(f"executing: {' '.join(cmd)}", debug)
    else:
        cmd = ["/usr/bin/openssl", "x509", "-inform", "PEM",
               "-noout", "-enddate", "-in", pem_cert]
        print_verbose(f"executing: {' '.join(cmd)}", verbose)
    result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                            encoding="utf-8")
    print_debug(f"stdout for checking certificate expiration:\n{result.stdout}", debug)
    print_debug(f"stderr for checking certificate expiration:\n{result.stderr}", debug)
    if result.returncode != 0:
        errorout(f"Could not check existing certificate due to:\n{result.stderr}")

    if not re.findall("^notAfter=", result.stdout):
        errorout("malformed content from openssl")

    stdout_date = result.stdout.split("=")
    expiry_date = parse(stdout_date[1].strip(), ignoretz=True)
    current_date = datetime.now()
    time_diff = expiry_date - current_date

    return time_diff.days


def integrate_cert(cert_content, cert_parent_name, verbose, debug):
    '''
        integrate_cert() copies the content of the certificate (as received from CA) and creates
        a .pem file (marked as temporary). Then openssl command and .pem to create a .crt file.
        ARG: certificate content (as received from CA), filepath template for .pem and .cert
        RET: - (If success, files .pem and .crt are created. If fail, it exits with a message)
    '''
    pem_temp = cert_parent_name + ".pem" + TMPSUFF
    print_verbose(f"writing certificate data to:\t{pem_temp}", verbose)
    try:
        with open(pem_temp, "w", encoding="utf-8") as out:
            out.write(cert_content)
    except FileNotFoundError:
        errorout(f"Could not create: {pem_temp}")
    os.chmod(pem_temp, 0o644)

    crt_temp = cert_parent_name + ".crt" + TMPSUFF
    print_verbose(f"writing certificate data to:\t{crt_temp}", verbose)
    cmd = ["/usr/bin/openssl", "x509", "-inform", "PEM", "-in", pem_temp,
            "-outform", "DER", "-out", crt_temp]
    if debug:
        print_debug(f"executing: {' '.join(cmd)}", debug)
    else:
        print_verbose(f"executing: {' '.join(cmd)}", verbose)
    result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                            encoding="utf-8")
    print_debug(f"stdout for creating a new CSR:\n{result.stdout}", debug)
    print_debug(f"stderr for creating a new CSR:\n{result.stderr}", debug)
    if result.returncode != 0:
        errorout(f"Could not run openssl_cmd_cert.sh due to:\n{result.stderr}")
    else:
        print_debug(f"stdout =\n{result.stdout}", debug)

    if os.path.isfile(crt_temp):
        os.chmod(crt_temp, 0o644)
        print_verbose(f"Succesfully created: {crt_temp}", verbose)
    else:
        errorout(f"Could not create: {crt_temp}")


def manage_files(csrfile, keyfile, pem_cert, crt_cert, chown_uid, chown_gid, verbose):
    '''
        manage_files() removes .csr (certificate is here. CSR is unnecessary) and
        makes .key, .pem and .crt files permanent (not "._tmp_")
        ARG: All 4 files related to certificate creation (.csr, .key, .pem and .crt)
        RET: - (If success, files .pem and .crt are permanent)
    '''

    def rename_and_fixselinux(temp_file, perm_file):
        if os.path.isfile(temp_file):
            print_verbose(f"Moving: {temp_file} to {perm_file}", verbose)
            os.rename(temp_file, perm_file)
            fixselinux(perm_file, verbose)

    csr_temp = csrfile + TMPSUFF
    if os.path.isfile(csr_temp):
        print_verbose(f"Removing: {csr_temp}", verbose)
        os.remove(csr_temp)

    rename_and_fixselinux(keyfile + TMPSUFF, keyfile)
    rename_and_fixselinux(pem_cert + TMPSUFF, pem_cert)
    rename_and_fixselinux(crt_cert + TMPSUFF, crt_cert)

    chown_files([keyfile, pem_cert, crt_cert], chown_uid, chown_gid, verbose)


def fixselinux(file, verbose):
    """
        fixselinux() ensures selinux context of a file.
        ARG: The file to be processed and fix selinux.
        RET: - (If success, the file's selinux context is changed.)
    """
    chcon = "/usr/bin/chcon"
    if (os.path.exists("/sys/fs/selinux") or os.path.exists("/selinux/status")) and\
                                                            os.access(chcon, os.R_OK):
        do_execute(f"{chcon} system_u:object_r:cert_t:s0 {file}", verbose)

def chown_files(files_list, uid, gid, verbose):
    """
        Change ownership of certificate files
        ARG: List of files, username, groupname, verbosity flag
        RET: - (Changes ownership or prints error)
    """

    if (uid is None or gid is None) or (uid == 0 and gid == 0):
        return
    for file_path in files_list:
        if os.path.isfile(file_path):
            try:
                os.chown(file_path, uid, gid)
                print_verbose(f"Changed ownership of {file_path} to {uid}:{gid}", verbose)
            except (OSError, PermissionError) as e:
                print(f"Warning: Could not change ownership of {file_path}: {e}")

def kinit(principal, keytab, verbose):
    """
        kinit() runs 'kinit' bash command for specific principal and keytab file.
        ARG: The principal and the keytab files to be used.
        RET: True if kinit is executed succesfully, False if it fails.
    """
    if not (os.path.isfile(keytab) and os.access(keytab, os.R_OK)):
        print_verbose("keytab file not readable.", verbose)
        return False
    cmd = f"/usr/bin/kinit -k -t {keytab} {principal}"
    if do_execute(cmd, verbose):
        return True
    return False


def do_execute(cmd, verbose, return_output=False):
    """
        do_execute() executes a given bash command.
        ARG: The command to execute. Variables that define verbosity.
        RET: Different according to arguments.
    """
    # pylint: disable=consider-using-with
    process = subprocess.Popen(cmd, stderr=subprocess.PIPE, shell=True, stdout=subprocess.PIPE)
    out, err = process.communicate()
    if len(err) > 0 and verbose:
        print(err.decode().strip())
    if return_output:
        return out.decode().strip()
    return True


def generate_temp_file(prefix):
    '''
        generate_temp_file() is creating a temporary file that has a name based on the given prefix.
        ARG: The prefix the file will have.
        RET: The filename of the temporary file.
    '''
    try:
        # pylint: disable=consider-using-with
        tfh = tempfile.NamedTemporaryFile(mode="w", prefix=prefix, dir="/tmp")
        filename = tfh.name
    except PermissionError:
        print(f"cannot create temporary {prefix} config file.\nExiting...")
        sys.exit(0)
    return filename


def errorout(msg, code=1):
    '''
        errorout() prints a message and exits according to specific error code.
        ARG: The message to print and the code to exit.
        RET: -
    '''
    if code > 0:
        print(f"Error: {msg}")
    else:
        print(f"{msg}")

    sys.exit(code)


def check_prop(prop_type, prop_key, prop_val, cfgfile, default_val):
    '''
        check_prop() checks if the given string/filepath points to an existing file or dir
        ARG: The value to check, prop_key = "FILE" or "DIR", The property name,
             the config file that was used, the default value for this property.
        RET: If success, the filename. Else, it exits using errorout() function.
    '''
    ret_val = default_val
    if prop_val:
        ret_val = prop_val

    if prop_type == "FILE":
        if not (os.path.isfile(ret_val) and os.access(ret_val, os.R_OK)):
            errorout(f"error in config file {cfgfile}:\
                     {prop_key} file/path ({ret_val}) NOT readable.")
        return ret_val

    if prop_type == "DIR":
        if not os.path.isdir(ret_val):
            errorout(f"error in config file {cfgfile}:\
                     {prop_key} file/path ({ret_val}) NOT readable.")
        return ret_val


def fallback(prop_val, default_val):
    '''
        fallback() checks if a variable exists. If not,  it returns a default value.
        fallback in ConfigParser REFUSES TO WORK!!! (and I am mad as hell!)
        ARG: The value to check, The property name, the config file that was used.
        RET: returns either the variable from conf file or a default value.
    '''
    val = default_val
    if prop_val:
        val = prop_val

    return val


def print_verbose(msg, verbose):
    """Prints verbose messages"""
    if verbose:
        print(f"VERBOSE: {msg}")


def print_debug(msg, debug):
    """Prints debug messages"""
    if debug:
        print(f"DEBUG: {msg}")


if __name__ == "__main__":
    main()
