#!/usr/bin/env python
##
#
# Vaisala software source code file
#
# Copyright (c) Vaisala Oyj 2015. All rights reserved.
#
##
"""
Programmatically set a new master password for GeoServer.
"""
from __future__ import print_function

from os import path
import ConfigParser
import argparse
import codecs
import collections
import datetime
import grp
import json
import logging
import os
import pwd
import random
import requests
import shutil
import string
import subprocess
import sys
import traceback
import xml.etree.ElementTree as ET


CONFIG_DIRECTORY = os.environ.get("VAISALA_RADARSW_CONFIG_DIR",
                                  '/etc/vaisala/radarsw/configuration')

GIS_CONFIG_FILE = path.join(CONFIG_DIRECTORY, 'gis-override.ini')
GIS_TEMP_FILE = path.join(CONFIG_DIRECTORY, 'gis-override.ini.tmp')
INI_SECTION = 'GEOSERVER'


GeoServerConfig = collections.namedtuple('GeoServerConfig',
                                         ['master_password', 'admin_password'])


logger = logging.getLogger('rsw-geoserver-password-tool')


def read_config(config_file=GIS_CONFIG_FILE):
    '''Reads in configuration INI files such as:

    [GEOSERVER]
    geoserver.master.password = password
    geoserver.admin.password = password

    Returns:
        A GeoServerConfig instance.
    '''
    if path.isfile(config_file) is not True:
        raise Exception(u"Configuration file {} doesn't exist!".format(config_file))

    master_password, admin_password = None, None
    config = ConfigParser.RawConfigParser()
    config.read(config_file)

    master_password_option = INI_SECTION, 'geoserver.master.password'
    if master_password is None and config.has_option(*master_password_option):
        master_password = config.get(*master_password_option)

    admin_password_option = INI_SECTION, 'geoserver.admin.password'
    if admin_password is None and config.has_option(*admin_password_option):
        admin_password = config.get(*admin_password_option)

    result = GeoServerConfig(master_password=master_password, admin_password=admin_password)
    return result


def make_config(master_password, admin_password, old_config_file=GIS_CONFIG_FILE):
    conf = ConfigParser.RawConfigParser()
    conf.read(old_config_file)

    if not conf.has_section(INI_SECTION):
        conf.add_section(INI_SECTION)

    conf.set(INI_SECTION, 'geoserver.master.password', master_password)
    conf.set(INI_SECTION, 'geoserver.admin.password', admin_password)

    return conf


def write_to_temp_file(configuration, temp_file):
    with open(temp_file, "wb") as f:
        configuration.write(f)


def back_up_current_conf(conf_file):
    now = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
    backup_file = conf_file + "." + now
    shutil.copyfile(conf_file, backup_file)


def move_temp_to_conf(temp_file, conf_file):
    os.rename(temp_file, conf_file)


def assign_permissions(file_path):
    # Check for Python3
    if sys.version_info >= (3, 0):
        read_and_write_permissions = 0o600
    else:
        read_and_write_permissions = 0600

    os.chown(file_path, pwd.getpwnam("root")[2], grp.getgrnam("radarsw")[2])
    os.chmod(file_path, read_and_write_permissions)


def assign_geoserver_permissions(file_path):
    # Check for Python3
    if sys.version_info >= (3, 0):
        read_and_write_permissions = 0o600
    else:
        read_and_write_permissions = 0600

    os.chown(file_path, pwd.getpwnam("radargeo")[2], grp.getgrnam("radarsw")[2])
    os.chmod(file_path, read_and_write_permissions)


def random_password(length=16):
    chars = string.ascii_uppercase + string.digits + string.ascii_lowercase
    password = ''
    rng = random.SystemRandom()
    for i in range(length):
        password += rng.choice(chars)
    return password


def random_word_password(word_count=4):
    dictionary = subprocess.check_output("strings /usr/share/cracklib/cracklib-small.pwd",
                                         shell=True).split('\n')
    dictionary = [word for word in dictionary
                  if all([c in string.ascii_lowercase for c in word])]

    words = []
    rng = random.SystemRandom()
    for i in range(word_count):
        words.append(rng.choice(dictionary))

    return " ".join(words)


def master_password_resource_url(geoserver_rest_base):
    return u"{}security/masterpw.xml".format(geoserver_rest_base)


def users_xml_with_admin_password_changed(file_path, new_password):
    def find_admin_elem(document):
        for main_level_elem in document:
            if str(main_level_elem.tag).endswith('users'):
                for user_elem in main_level_elem:
                    if user_elem.attrib['name'] == 'admin':
                        return user_elem

    with codecs.open(file_path, 'r', encoding='utf-8') as f:
        document = ET.fromstring(f.read())

    admin_elem = find_admin_elem(document)
    admin_elem.attrib['password'] = u"plain:{}".format(new_password)

    # Register this namespace here this way so that the serialized XML doesn't
    # have the extraneous 'ns0' namespace prefix for every element.
    ET.register_namespace("","http://www.geoserver.org/security/users")
    return ET.tostring(document, 'utf-8')


def resolve_old_master_password(geoserver_rest_base, admin_password='geoserver'):
    logger.debug("Resolving old master password.")
    if not geoserver_rest_base.endswith('/'):
        geoserver_rest_base += '/'
    r = requests.get(master_password_resource_url(geoserver_rest_base),
                     auth=('admin', admin_password))

    if r.status_code != 200:
        logger.error(u"Response status: {}, content from Geoserver: {}".format(
            str(r.status_code), repr(r.text)))
        if r.status_code == 401:
            logger.info(u"Is current admin password set up in {} correct?".format(GIS_CONFIG_FILE))
        raise Exception("Error resolving password")

    document = ET.fromstring(r.text)
    old_root_password_elem = document[0]
    assert old_root_password_elem.tag == 'oldMasterPassword'
    logger.debug("Old master password resolved.")
    return old_root_password_elem.text


def set_new_master_password(geoserver_rest_base, admin_password, old_password, new_password):
    document = u"""
<masterPassword>
   <oldMasterPassword>{}</oldMasterPassword>
   <newMasterPassword>{}</newMasterPassword>
</masterPassword>
""".format(old_password, new_password)
    logger.debug("Setting new master password.")
    r = requests.put(master_password_resource_url(geoserver_rest_base),
                     data=document,
                     headers={'Content-type': 'text/xml'},
                     auth=('admin', admin_password))
    if r.status_code != 200:
        logger.error(u"# Response status: {}, content from Geoserver: {}".format(r.status_code, repr(r.text)))
        raise Exception("Error changing password")
    logger.info("Master password set via GeoServer REST API.")


def reset_master_password(args):
    new_master_password = random_password(length=16)
    config = read_config()
    try:
        old_master_password = resolve_old_master_password(args.rest_url, config.admin_password)
        set_new_master_password(args.rest_url, config.admin_password,
                                old_master_password, new_master_password)
    except Exception, e:
        try:
            old_master_password = resolve_old_master_password(args.rest_url, 'geoserver')
            set_new_master_password(args.rest_url, 'geoserver',
                                    old_master_password, new_master_password)
            logger.warn("Used GeoServer default admin password instead of the one in file.")
        except Exception, e2:
            logger.warn(
                u"Encountered exception {} when trying to use GeoServer default password:\n{}".format(
                    str(e2), traceback.format_exc()))
            raise e

    new_conf = make_config(new_master_password, config.admin_password)
    write_to_temp_file(new_conf, GIS_TEMP_FILE)
    assign_permissions(GIS_TEMP_FILE)
    back_up_current_conf(GIS_CONFIG_FILE)
    move_temp_to_conf(GIS_TEMP_FILE, GIS_CONFIG_FILE)
    logger.info(u"Config file written to '{}'.".format(GIS_CONFIG_FILE))


def reset_admin_password(args):
    users_xml_path = path.join(args.data_dir, 'security/usergroup/default/users.xml')
    users_xml_tmp_path = users_xml_path + '.tmp'

    new_password = random_word_password()
    with codecs.open(users_xml_tmp_path, 'wb', encoding='utf-8') as f:
        print(users_xml_with_admin_password_changed(users_xml_path, new_password),
              file=f)

    config = read_config()
    new_conf = make_config(config.master_password, new_password)
    write_to_temp_file(new_conf, GIS_TEMP_FILE)
    assign_permissions(GIS_TEMP_FILE)
    back_up_current_conf(GIS_CONFIG_FILE)

    assign_geoserver_permissions(users_xml_tmp_path)
    back_up_current_conf(users_xml_path)

    move_temp_to_conf(GIS_TEMP_FILE, GIS_CONFIG_FILE)
    move_temp_to_conf(users_xml_tmp_path, users_xml_path)
    logger.info(u"Config files written to '{}' and '{}'.".format(users_xml_path, GIS_CONFIG_FILE))


def show(args):
    config = read_config()
    print(json.dumps(config._asdict()))


def main():
    functions = ("reset-master-password", "reset-admin-password")
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    subparsers = parser.add_subparsers(title="Commands", description="Supported commands",
                                       dest='subparser_name')
    reset_master_password_parser = subparsers.add_parser('reset-master-password',
                                                         help='Reset GeoServer master password.')
    reset_master_password_parser.add_argument("--geoserver-rest-url", dest="rest_url", metavar="URL",
                                              help="GeoServer REST URL", required=False,
                                              default='http://localhost:34180/geoserver/rest/')

    reset_admin_password_parser = subparsers.add_parser('reset-admin-password',
                                                        help='Reset GeoServer admin password.')
    reset_admin_password_parser.add_argument("--geoserver-data-dir", dest="data_dir", metavar="PATH",
                                             help="GeoServer data directory", required=False,
                                             default='/srv/vaisala/radarsw/geoserver/data')

    show_details_parser = subparsers.add_parser('show', help=argparse.SUPPRESS)

    def add_common_arguments(parser):
        group = parser.add_argument_group("common options")
        group.add_argument("-d", "--debug", dest="debug", action="store_true",
                        help="Enable debug logging.")

    add_common_arguments(reset_master_password_parser)
    add_common_arguments(reset_admin_password_parser)
    add_common_arguments(show_details_parser)
    args = parser.parse_args()

    if pwd.getpwuid(os.getuid()).pw_name != 'root':
        sys.exit("This script must be run as root.")

    def setup_logging(debug_enabled, subparser_name):
        root = logging.getLogger()
        if subparser_name == 'show':
            root.setLevel(logging.ERROR)
        elif debug_enabled:
            root.setLevel(logging.DEBUG)
        else:
            root.setLevel(logging.INFO)
            logging.getLogger("urllib3.connectionpool").setLevel(logging.WARN)

        ch = logging.StreamHandler(sys.stdout)
        ch.setLevel(logging.DEBUG)
        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        ch.setFormatter(formatter)
        root.addHandler(ch)
    setup_logging(args.debug, args.subparser_name)

    if args.subparser_name == 'reset-master-password':
        reset_master_password(args)
    elif args.subparser_name == 'reset-admin-password':
        reset_admin_password(args)
    elif args.subparser_name == 'show':
        show(args)


if __name__ == '__main__':
    main()
