#!/usr/bin/python

import string
import re
import os
import sys
import pprint
import logging
import ldap
import ldap.sasl
import subprocess
import remctl
import tempfile
import time

###########################################################################
# Set up logging

### Add a "trace" log level
DEBUG_LEVELV_NUM = 5
logging.addLevelName(DEBUG_LEVELV_NUM, "TRACE")
def trace(self, message, *args, **kws):
    # Yes, logger takes its '*args' as 'args'.
    if self.isEnabledFor(DEBUG_LEVELV_NUM):
        self._log(DEBUG_LEVELV_NUM, message, args, **kws)
logging.Logger.trace = trace
###

logger = logging.getLogger()

def setup_logging_filehandler(log_file='/var/log/password-expiration.log'):
    # Time format: 2017-09-14T07:06:20.706347-07:00
    time_format    = '%Y-%m-%dT%H:%M:%S'
    logging_format = '%(asctime)s.%(msecs)03d %(levelname)-6s %(message)s'
    logFormatter   = logging.Formatter(logging_format, time_format)

    #fileHandler = logging.FileHandler(log_file)
    #fileHandler.setFormatter(logFormatter)
    #logger.addHandler(fileHandler)

    consoleHandler = logging.StreamHandler(sys.stdout)
    consoleHandler.setFormatter(logFormatter)
    logger.addHandler(consoleHandler)



# Set the log-level to INFO
logging.getLogger().setLevel(logging.INFO)

pp = pprint.PrettyPrinter(indent=4)

def exit_with_error(msg):
    logger.error(msg)
    raise Exception(msg)

class LDAPProcessor():

    def __init__(self, ldap_server):
        self.ldap_server = ldap_server
        self.ldap        = self.make_ldap_connection(ldap_server)

    #### UTILITY methods ####
    def pprint(self, object):
        pp = pprint.PrettyPrinter(indent=4)
        pp.pprint(object)

    #### LDAP methods ####
    def make_ldap_connection(self, ldap_server):
        connection_successful = False
        number_of_attempts    = 0
        max_try_limit         = 8
        number_seconds_wait   = 60

        while ((not connection_successful) and (number_of_attempts < max_try_limit)):
            try:
                con  = ldap.initialize('ldap://' + ldap_server)
                auth = ldap.sasl.gssapi("")
                con.sasl_interactive_bind_s("", auth)
                logger.debug("ldap: created LDAP connection to %s", ldap_server)
                connection_successful = True
            except Exception as e:
                msg = "connection to ldap server {0} failed: {1}".format(ldap_server, str(e))
                logger.warn(msg)

                # Connection failed, so let's wait a bit and try again.
                connection_successful = False
                number_of_attempts    = number_of_attempts + 1
                if (number_of_attempts < max_try_limit):
                    seconds_to_wait = number_seconds_wait * number_of_attempts
                    msg = "will wait {0} seconds and try again".format(seconds_to_wait)
                    logger.warn(msg)
                    time.sleep(number_seconds_wait * number_of_attempts)
                else:
                    # Nothing to do here except exit while loop
                    pass

        if (connection_successful):
            return con
        else:
            msg = "tried {0} times but could not connect " \
                  "to ldap server {1}".format(number_of_attempts, ldap_server)
            exit_with_error(msg)

    def results_say_is_active(self, result_data):
        counter = 0
        for result_tuple in result_data:
            # The first element of the tuple should be the dn.
            dn = result_tuple[0]
            logger.debug(pp.pformat(result_data))

            # The second element of the tuple contains the
            # attributes as a dict.
            attributes_dict = result_tuple[1]

            kerberos_status_active = False
            account_status_active  = False

            if 'uid' in attributes_dict:
                uid = attributes_dict['uid'][0]
            if 'suAccountStatus' in attributes_dict:
                status = attributes_dict['suAccountStatus'][0]
                logger.debug("ldap: suAccountStatus for {0} is {1}".format(uid, status))
                if (status == 'active'):
                    account_status_active = True

            if 'suKerberosStatus' in attributes_dict:
                status = attributes_dict['suKerberosStatus'][0]
                logger.debug("ldap: suKerberosStatus for {0} is {1}".format(uid, status))
                if (status == 'active'):
                    kerberos_status_active = True

            counter = counter + 1

        if (counter == 0):
            return False
        elif (counter > 1):
            raise "too many results"
        else:
            return account_status_active and kerberos_status_active

    def is_active(self, sunetid):
        basedn          = 'cn=accounts,dc=stanford,dc=edu'
        searchScope     = ldap.SCOPE_SUBTREE
        searchFilter    = "(uid={0})".format(sunetid)
        searchAttribute = [
            "uid",
            "suKerberosStatus",
            "suAccountStatus",
        ]
        logger.debug("ldap: search filter: {0}".format(searchFilter))

        ldap_result_id = self.ldap.search(basedn, searchScope, searchFilter, searchAttribute)
        result_set = []
        while True:
            result_type, result_data = self.ldap.result(ldap_result_id, 0)
            if (result_data == []):
                break
            else:
                if result_type == ldap.RES_SEARCH_ENTRY:
                    return self.results_say_is_active(result_data)
                else:
                    msg = "error getting result from LDAP search"
                    exit_with_error(msg)


def get_all_root_principals(kdc_server, keytab_file, principal_name):
    logger.debug("kdc_server is {0}".format(kdc_server))

    with tempfile.NamedTemporaryFile() as tmp:
        cache_file = tmp.name

        make_krb5_context(cache_file, keytab_file, principal_name)

        cmd = ['list-root-principals']
        try:
            result = remctl.remctl(host = kdc_server, command = cmd)
        except remctl.RemctlProtocolError, error:
            print "Error running remctl command '{0}': {1}".format(
                ' '.join(cmd), str(error))
            sys.exit(1)
        if (result.status != 0):
            msg = "list-root-principals remctl returned non-zero status: {0}".format(str(result.status))
            logger.error(msg)
            raise Exception(msg)
        if result.stderr:
            print "stderr:", result.stderr
            print "exit status:", result.status

        if result.stdout:
            root_principals = result.stdout.strip().split("\n")
            if (len(root_principals) == 0):
                msg = "no root principals from list-root-principals remctl call to {0}!".format(kdc_server)
                logger.error(msg)
                raise Exception(msg)
            else:
                return root_principals
        else:
            msg = "empty string from list-root-principals remctl call to {0}!".format(kdc_server)
            logger.error(msg)
            raise Exception(msg)

def make_krb5_context(cache_file, keytab_file, principal_name):
    cmd = ['kinit', '-c', cache_file, '-t', keytab_file, principal_name]
    logger.debug("about to run command '{0}'".format(' '.join(cmd)))

    try:
        rv = subprocess.check_output(cmd, stderr=subprocess.PIPE)
    except subprocess.CalledProcessError as e:
        print('exit code: {}'.format(e.returncode))
        print('stdout: {}'.format(e.output.decode(sys.getfilesystemencoding())))
        print('stderr: {}'.format(e.stderr.decode(sys.getfilesystemencoding())))
        sys.exit("make_krb5_context failed")

    logger.debug("command completed without error")
    os.environ["KRB5CCNAME"] = cache_file

def extract_sunetid_from_root_principal(root_principal):
    match = re.search("^([^/]+)/root$", root_principal)
    if (match is None):
        return None
    else:
        return match.group(1)


def make_man_page():
    perl_code = 'use Stanford::Orange::Util; Stanford::Orange::Util::print_man_page_from_pod("' + __file__ + '");'
    cmd = ['perl', '-e', perl_code]

    try:
        rv = subprocess.check_output(cmd, stderr=subprocess.PIPE)
    except subprocess.CalledProcessError as e:
        print('exit code: {}'.format(e.returncode))
        print('stdout: {}'.format(e.output.decode(sys.getfilesystemencoding())))
        print('stderr: {}'.format(e.stderr.decode(sys.getfilesystemencoding())))
    else:
        print(rv.decode('ascii'))

def find_all_inactive_accounts(root_principals, keytab_file, principal_name, ldap_server):

    with tempfile.NamedTemporaryFile() as tmp:
        cache_file     = tmp.name
        logger.debug("setting kerberos context for LDAP")
        make_krb5_context(cache_file, keytab_file, principal_name)

        logger.debug("creating LDAPProcessor object")
        ldap_processor = LDAPProcessor(ldap_server)

        # Loop through this list finding out if the user is
        # still active
        inactive_sunetids = []
        for root_principal in root_principals:
            if (root_principal):
                sunetid = extract_sunetid_from_root_principal(root_principal)
                if (sunetid is None):
                    logger.error("root principal '{0}' not parseable".format(root_principal))
                else:
                    logger.debug("processing " + sunetid)
                    if (ldap_processor.is_active(sunetid)):
                        logger.debug("sunetid {0} is active".format(sunetid))
                    else:
                        inactive_sunetids.append(sunetid)
            else:
                logger.warning("root principal is empty")

        return sorted(inactive_sunetids)

######################################################################
import argparse

parser = argparse.ArgumentParser(
  description='Find active root principals for non-active accounts',
  formatter_class=argparse.RawDescriptionHelpFormatter,
  epilog='''
  Example:
  kdc-root-principal-reconcile ldap-test.stanford.edu
 '''
)
#parser.add_argument('--log-file',
#                    action='store',
#                    dest='logfile',
#                    default="/var/log/password-expiration",
#                    help="log file location"
#)
parser.add_argument('kdcserver',
                    help="KDC server to query about existing root principals"
)
parser.add_argument('ldapserver',
                    help="LDAP server to query"
)
parser.add_argument('--verbose',
                    help='show more',
                    action='store_true'
)
parser.add_argument('--manual',
                    help='show man page',
                    action='store_true'
)
parser.add_argument('--keytab-file',
                    dest='keytabfile',
                    default='/etc/root-principal-reconcile/kdc-user-read.keytab',
                    help='the keytab file to use when querying ldapserver',
                    action='store'
)
parser.add_argument('--principal-name',
                    dest='principalname',
                    default='service/kdc-user-read@stanford.edu',
                    help='the principal whose credentials are in the keytab file',
                    action='store'
)
args = parser.parse_args()

if (args.manual):
    make_man_page()
    sys.exit(0)

# Set up the logging file-handler.
setup_logging_filehandler()

# Turn on extra logging if --verbose option used.
if (args.verbose):
    logger.info("main: setting log-level to DEBUG")
    logging.getLogger().setLevel(logging.DEBUG)

# Get all root principals
root_principals = get_all_root_principals(kdc_server=args.kdcserver,
                                          keytab_file=args.keytabfile,
                                          principal_name=args.principalname)
logger.debug("found " + str(len(root_principals)) + " root principals")

# Find all inactive accounts that appear in the list
# root_principals

inactive_accounts = find_all_inactive_accounts(root_principals=root_principals,
                                               keytab_file=args.keytabfile,
                                               principal_name=args.principalname,
                                               ldap_server=args.ldapserver)

for sunetid in inactive_accounts:
    print "root principal {0}/root exists but account sunetid {0} is NOT active".format(sunetid)


####################################################################################
####################################################################################
####################################################################################
####################################################################################

#=pod
#
#=head1 NAME
#
#kdc-root-principal-reconcile - Find inactive accounts with Kerberos root principals
#
#=head1 USAGE
#
#    kdc-root-principal-reconcile B<kdcserver> B<ldapserver>
#    kdc-root-principal-reconcile --help
#
#=head1 DESCRIPTION
#
#This script first gets a list of root principals from
#B<kdcserver>. It then checks against B<ldapserver> to see if the
#sunetid corresponding to this root principal is active or
#inactive. If the account is inacteive, a line will be printed
#out. That is, if C<sunetid>/root is a root principal but C<sunetid> is
#not an active account in the LDAP, a line will be output. This is
#intended to help catch users who have left Stanford (i.e., are
#inactive) but still have a root principal.
#
#The Kerberos credentials must have access to the C<list-root-principals>
#remctl function on B<kdcserver> as well as have the access to read these attributes
#on the C<cn=accounts,dc=stanford,dc=edu> branch of B<ldapserver>:
#
#    uid
#    suKerberosStatus
#    suAccountStatus
#
#
#=head1 REQUIRED ARGUMENTS
#
#You must supply the B<kdcserver> and B<ldapserver> arguments (in the correct
#order).
#
#=head1 OPTIONS
#
#=over 4
#
#=item B<--keytab-file>=path-to-keytab-file
#
#The file containing the Kerberos credentials needed to both run the
#remctl against B<kdcserver> and query B<ldapserver>. If omitted
#defaults to F</etc/root-principal-reconcile/kdc-user-read.keytab>.
#
#=back
#
#=head1 EXIT STATUS
#
#The script will exit with 0 if the script completes and there were no
#failures, 1 for any other reason.
#
#=head1 NOTES
#
#Later.
#
#=head1 EXAMPLE
#
#Generate a list of inactive users who still have root principals (production environment):
#
#    kdc-root-principal-reconcile kdc-prod1.stanford.edu ldap.stanford.edu
#
#=head1 INCOMPATIBILITIES
#
#No known incompatibilities.
#
#=head1 BUGS AND LIMITATIONS
#
#None known.
#
#
#=head1 DIAGNOSTICS
#
#More later.
#
#=head1 CONFIGURATION
#
#Configuration is managed via the options.
#
#=head1 SEE ALSO
#
#ldapsearch(1), remctl(1)
#
#=head1 AUTHOR
#
#Adam Lewenberg <adamhl@stanford.edu>
#
#=head1 LICENSE AND COPYRIGHT
#
#Copyright 2019 The Board of Trustees of the Leland Stanford Junior
#University.  All rights reserved.
#
#Permission to use, copy, modify, and distribute this software and its
#documentation for any purpose and without fee is hereby granted, provided
#that the above copyright notice appear in all copies and that both that
#copyright notice and this permission notice appear in supporting
#documentation, and that the name of Stanford University not be used in
#advertising or publicity pertaining to distribution of the software
#without specific, written prior permission.  Stanford University makes no
#representations about the suitability of this software for any purpose.
#It is provided "as is" without express or implied warranty.
#
#THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
#WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
#MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
#
#=cut
