#!/usr/bin/python3.3

# Copyright (C) 2010  Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN COMMAND OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS COMMAND, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

'''
This tool implements user management for b10-cmdctl. It is used to
add and remove users from the accounts file.
'''
import sys; sys.path.append ('/usr/lib/python3.3/site-packages')
from bind10_config import SYSCONFPATH
from collections import OrderedDict
import random
from hashlib import sha1
import csv
import getpass
from optparse import OptionParser, OptionValueError
import os
import isc.util.process

isc.util.process.rename()

VERSION_STRING = "b10-cmdctl-usermgr 1.1.0"
DEFAULT_FILE = SYSCONFPATH + "/cmdctl-accounts.csv"

# Actions that can be performed (used for argument parsing,
# code paths, and output)
COMMAND_ADD = "add"
COMMAND_DELETE = "delete"

# Non-zero return codes, used in tests
BAD_ARGUMENTS = 1
FILE_ERROR = 2
USER_EXISTS = 3
USER_DOES_NOT_EXIST = 4

class UserManager:
    def __init__(self, options, args):
        self.options = options
        self.args = args

    def __print(self, msg):
        if not self.options.quiet:
            print(msg)

    def __gen_password_hash(self, password):
        salt = "".join(chr(random.randint(ord('!'), ord('~')))\
                    for x in range(64))
        saltedpwd = sha1((password + salt).encode()).hexdigest()
        return salt, saltedpwd

    def __read_user_info(self):
        """
        Read the existing user info
        Raises an IOError if the file can't be read
        """
        # Currently, this is done quite naively (there is no
        # check that the file is modified between read and write)
        # But taking multiple simultaneous users of this tool on the
        # same file seems unnecessary at this point.
        self.user_info = OrderedDict()
        if os.path.exists(self.options.output_file):
            # Just let any file read error bubble up; it will
            # be caught in the run() method
            with open(self.options.output_file, newline='') as csvfile:
                reader = csv.reader(csvfile)
                for row in reader:
                    self.user_info[row[0]] = row

    def __save_user_info(self):
        """
        Write out the (modified) user info
        Raises an IOError if the file can't be written
        """
        # Just let any file write error bubble up; it will
        # be caught in the run() method
        with open(self.options.output_file, 'w',
                  newline='') as csvfile:
            writer = csv.writer(csvfile)
            for row in self.user_info.values():
                writer.writerow(row)

    def __add_user(self, name, password):
        """
        Add the given username/password combination to the stored user_info.
        First checks if the username exists, and returns False if so.
        If not, it is added, and this method returns True.
        """
        if name in self.user_info:
            return False
        salt, pw = self.__gen_password_hash(password)
        self.user_info[name] = [name, pw, salt]
        return True

    def __delete_user(self, name):
        """
        Removes the row with the given name from the stored user_info
        First checks if the username exists, and returns False if not.
        Otherwise, it is removed, and this mehtod returns True
        """
        if name not in self.user_info:
            return False
        del self.user_info[name]
        return True

    # overridable input() call, used in testing
    def _input(self, prompt):
        return input(prompt)

    # in essence this is private, but made 'protected' for ease
    # of testing
    def _prompt_for_username(self, command):
        # Note, direct prints here are intentional
        while True:
            name = self._input("Username to " + command + ": ")
            if name == "":
                print("Error username can't be empty")
                continue

            if command == COMMAND_ADD and name in self.user_info:
                 print("user already exists")
                 continue
            elif command == COMMAND_DELETE and name not in self.user_info:
                 print("user does not exist")
                 continue

            return name

    # in essence this is private, but made 'protected' for ease
    # of testing
    def _prompt_for_password(self):
        # Note, direct prints here are intentional
        while True:
            pwd1 = getpass.getpass("Choose a password: ")
            if pwd1 == "":
                print("Error: password cannot be empty")
                continue
            pwd2 = getpass.getpass("Re-enter password: ")
            if pwd1 != pwd2:
                print("passwords do not match, try again")
                continue
            return pwd1

    def __verify_options_and_args(self):
        """
        Basic sanity checks on command line arguments.
        Returns False if there is a problem, True if everything seems OK.
        """
        if len(self.args) < 1:
            self.__print("Error: no command specified")
            return False
        if len(self.args) > 3:
            self.__print("Error: extraneous arguments")
            return False
        if self.args[0] not in [ COMMAND_ADD, COMMAND_DELETE ]:
            self.__print("Error: command must be either add or delete")
            return False
        if self.args[0] == COMMAND_DELETE and len(self.args) > 2:
            self.__print("Error: delete only needs username, not a password")
            return False
        return True

    def run(self):
        if not self.__verify_options_and_args():
            return BAD_ARGUMENTS

        try:
            self.__print("Using accounts file: " + self.options.output_file)
            self.__read_user_info()

            command = self.args[0]

            if len(self.args) > 1:
                username = self.args[1]
            else:
                username = self._prompt_for_username(command)

            if command == COMMAND_ADD:
                if len(self.args) > 2:
                    password = self.args[2]
                else:
                    password = self._prompt_for_password()
                if not self.__add_user(username, password):
                    print("Error: username exists")
                    return USER_EXISTS
            elif command == COMMAND_DELETE:
                if not self.__delete_user(username):
                    print("Error: username does not exist")
                    return USER_DOES_NOT_EXIST

            self.__save_user_info()
            return 0
        except IOError as ioe:
            self.__print("Error accessing " + ioe.filename +\
                         ": " + str(ioe.strerror))
            return FILE_ERROR
        except csv.Error as csve:
            self.__print("Error parsing csv file: " + str(csve))
            return FILE_ERROR

def set_options(parser):
    parser.add_option("-f", "--file",
                      dest="output_file", default=DEFAULT_FILE,
                      help="Accounts file to modify"
                     )
    parser.add_option("-q", "--quiet",
                      dest="quiet", action="store_true", default=False,
                      help="Quiet mode, don't print any output"
                     )

def main():
    usage = "usage: %prog [options] <command> [username] [password]\n\n"\
            "Arguments:\n"\
            "  command\t\teither 'add' or 'delete'\n"\
            "  username\t\tthe username to add or delete\n"\
            "  password\t\tthe password to set for the added user\n"\
            "\n"\
            "If username or password are not specified, %prog will\n"\
            "prompt for them. It is recommended practice to let the\n"\
            "tool prompt for the password, as command-line\n"\
            "arguments can be visible through history or process\n"\
            "viewers."
    parser = OptionParser(usage=usage, version=VERSION_STRING)
    set_options(parser)
    (options, args) = parser.parse_args()

    usermgr = UserManager(options, args)
    return usermgr.run()

if __name__ == '__main__':
    sys.exit(main())

