#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# 2018-08-06 Friedrich Weber <friedrich.weber@netknights.it>
#            Add standalone tool
#
# Copyright (c) 2018, Friedrich Weber
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from this
# software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

__doc__ = """
This script can be used to create a self-contained local privacyIDEA
instance that does not require a web server to run. Instead,
authentication requests are validated via the command line.

The ``create`` command launches a wizard that creates a new instance.
The ``configure`` command starts a local development server that can
be used to setup tokens. This server must not be exposed to the network!
The ``check`` command can then be used to authenticate users."""

import json
import os
import string
import subprocess
from functools import wraps

import sqlalchemy
import sys
import warnings

from flask_script import Manager, Server
from tempfile import NamedTemporaryFile

from privacyidea.app import create_app
from privacyidea.lib.security.default import DefaultSecurityModule

warnings.simplefilter("ignore", category=sqlalchemy.exc.SAWarning)

PI_CFG_TEMPLATE = """import os, logging

INSTANCE_DIRECTORY = os.path.abspath(os.path.dirname(__file__))
PI_ENCFILE = os.path.join(INSTANCE_DIRECTORY, 'encKey')
PI_AUDIT_KEY_PRIVATE = os.path.join(INSTANCE_DIRECTORY, 'private.pem')
PI_AUDIT_KEY_PUBLIC = os.path.join(INSTANCE_DIRECTORY, 'public.pem')
PI_LOGFILE = os.path.join(INSTANCE_DIRECTORY, 'privacyidea.log')
PI_LOGLEVEL = logging.INFO
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(INSTANCE_DIRECTORY, 'privacyidea.sqlite')

SECRET_KEY = b'{secret_key}'
PI_PEPPER = b'{pi_pepper}'

"""

RSA_KEYSIZE = 2048
PEPPER_CHARSET = string.ascii_letters + string.digits + '_'


def invoke_pi_manage(commandline, pi_cfg):
    """
    Invoke ``pi-manage`` with arguments, setting PRIVACYIDEA_CONFIGFILE TO ``pi_cfg``.
    :param commandline: arguments to pass as a list
    :param pi_cfg: location of the privacyIDEA config file
    """
    environment = os.environ.copy()
    environment['PRIVACYIDEA_CONFIGFILE'] = pi_cfg
    subprocess.check_call(['pi-manage'] + commandline, env=environment)


def _app_factory(instance):
    """
    Create a Flask app object with the given privacyIDEA standalone instance.
    """
    config_file = os.path.abspath(os.path.join(instance, 'pi.cfg'))
    app = create_app(config_name='production', config_file=config_file, silent=True)
    app.instance_directory = instance
    return app


def require_instance(f):
    """
    Decorator that marks commands that require an already set-up instance directory
    """
    @wraps(f)
    def deco(*args, **kwargs):
        config_file = os.path.join(manager.app.instance_directory, 'pi.cfg')
        if not os.path.exists(config_file):
            raise RuntimeError(
                "{!r} does not exist! Create a new instance using ``privacyidea-standalone create``.".format(
                    config_file
                ))
        return f(*args, **kwargs)
    return deco


class Configure(Server):
    help = description = "Run a local webserver to configure privacyIDEA"

    @require_instance
    def __call__(self, *args, **kwargs):
        Server.__call__(self, *args, **kwargs)


manager = Manager(_app_factory, with_default_commands=False, description=__doc__)
manager.add_command("configure", Configure())
manager.add_option('-i', '--instance', dest='instance', required=False, default=os.path.expanduser('~/.privacyidea'),
                   help='Location of the privacyIDEA instance (defaults to ~/.privacyidea)')


def read_credentials(file):
    """
    read username and password from a file. This could be sys.stdin.

    The first line specifies the username, the second line specifies the password.

    :param file: a Python file object
    :return: a tuple (user, password)
    """
    username = file.readline().strip()
    password = file.readline().strip()
    return username, password


def create_pepper(length=24, chunk_size=8, charset=PEPPER_CHARSET):
    """
    create a valid PI_PEPPER value of a given length from urandom,
    choosing characters from a given charset
    :param length: pepper length to generate
    :param chunk_size: number of bytes to read from urandom per iteration
    :param charset: list of valid characters
    :return: a bytestring of the specified length
    """
    pepper = b''
    while len(pepper) < length:
        random_bytes = DefaultSecurityModule.random(chunk_size)
        printable_bytes = ''.join(b for b in random_bytes if b in charset)
        pepper += printable_bytes
    return pepper[:length]


def choice(question, choices, case_insensitive=True):
    """
    Ask a question interactively until one of the given choices is selected.
    Return the choice then.
    :param question: Question to ask the user as a string
    :param choices: Dictionary mapping user answers to return values
    :param case_insensitive: Set to true if the answer should be handled case-insensitively.
                             Then, ``choices`` should contain only lowercase keys.
    :return: a value of ``choices``
    """
    while True:
        answer = raw_input(question)
        if case_insensitive:
            answer = answer.lower()
        if answer in choices:
            return choices[answer]
        else:
            print '{!r} is not a valid answer.'.format(answer)


def yesno(question, default):
    """
    Ask a y/n question with a default value.
    :param question: Question to ask the user as a string
    :param default: Default return value (boolean)
    :return: boolean
    """
    return choice(question, {'y': True,
                             'n': False,
                             '': default})


@manager.command
def create():
    """ Create a new privacyIDEA instance """
    instance_dir = os.path.abspath(manager.app.instance_directory)
    if os.path.exists(manager.app.instance_directory):
        raise RuntimeError("Instance at {!r} exists already!".format(manager.app.instance_directory))
    os.makedirs(instance_dir)

    # create SECRET_KEY and PI_PEPPER
    secret_key = DefaultSecurityModule.random(24)
    pi_pepper = create_pepper()

    # create a pi.cfg
    pi_cfg = os.path.join(instance_dir, 'pi.cfg')
    with open(pi_cfg, 'w') as f:
        f.write(PI_CFG_TEMPLATE.format(
            secret_key=secret_key.encode('string-escape'),
            pi_pepper=pi_pepper.encode('string-escape')
        ))

    # create an enckey
    invoke_pi_manage(['create_enckey'], pi_cfg)
    invoke_pi_manage(['create_audit_keys'], pi_cfg)
    invoke_pi_manage(['createdb'], pi_cfg)

    print
    print 'Please enter a password for the new admin `super`.'
    invoke_pi_manage(['admin', 'add', 'super'], pi_cfg)

    # create users
    if yesno('Would you like to create a default resolver and realm (Y/n)? ', True):
        print 'There are two possibilities to create a resolver:'
        print ' 1) We can create a table in the privacyIDEA SQLite database to store the users.'
        print '    You can add users via the privacyIDEA Web UI.'
        print ' 2) We can create a resolver that contains the users from /etc/passwd'
        print
        create_sql_resolver = choice('Please choose (default=1): ', {
            '1': True,
            '2': False,
            '': True
        })
        if create_sql_resolver:
            invoke_pi_manage(['resolver', 'create_internal', 'defresolver'], pi_cfg)
        else:
            with NamedTemporaryFile(delete=False) as f:
                f.write('{"fileName": "/etc/passwd"}')
            invoke_pi_manage(['resolver', 'create', 'defresolver', 'passwdresolver', f.name], pi_cfg)
            os.unlink(f.name)
        invoke_pi_manage(['realm', 'create', 'defrealm', 'defresolver'], pi_cfg)

    print
    print 'Configuration is complete. You can now configure privacyIDEA in the web browser by running'
    print "  privacyidea-standalone -i '{}' configure".format(instance_dir.encode('string-escape'))


@manager.option('-r', '--response', dest='show_response', action='store_true',
                help='Print the JSON response of privacyIDEA to standard output')
@require_instance
def check(show_response=False):
    """
    Check the given username and password against privacyIDEA.
    This command reads two lines from standard input: The first line is
    the username, the second line is the password (which consists of a
    static part and the OTP).

    This commands exits with return code 0 if the user could be authenticated
    successfully.
    """
    user, password = read_credentials(sys.stdin)
    exitcode = 255
    try:
        with manager.app.test_request_context('/validate/check', method='POST',
                                      data={'user': user, 'pass': password}):
            response = manager.app.full_dispatch_request()
            data = json.loads(response.data)
            result = data['result']
            if result['value'] is True:
                exitcode = 0
            else:
                exitcode = 1
            if show_response:
                print response.data
    except Exception, e:
        print repr(e)
    sys.exit(exitcode)


if __name__ == '__main__':
    manager.run()