#!/usr/bin/env python
# vim: tabstop=4 softtabstop=4 shiftwidth=4 textwidth=80 smarttab expandtab
"""
* Copyright (C) 2011   Sangoma Technologies Corp.
* All Rights Reserved.
*
* Author(s)
* William Adam <william.adam@sangoma.com>
*
* This code is Sangoma Technologies Confidential Property.
* Use of and access to this code is covered by a previously executed
* non-disclosure agreement between Sangoma Technologies and the Recipient.
* This code is being supplied for evaluation purposes only and is not to be
* used for any other purpose.
"""
import os
import syslog
import crypt
import sys
import subprocess
import string
import random
import crypt
from optparse import OptionParser
import logging
import logging.handlers


def execute_cmd(cmd, args=None, pipe_args=None):
    cmd_args = [cmd]
    if args:
        cmd_args = cmd_args + args

    try:
        p = subprocess.Popen(cmd_args, 
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT)
        output = p.communicate(pipe_args)[0].split('\n'); 
        error = p.returncode
    except Exception, e:
        error = -1
        output = ['Failed to execute cmd %s: %s' % (' '.join(cmd_args), str(e))]
        pass

    return (error, output)

class sngUser(object):
    # user management binary base directory
    # Required as when invoked under sudo env is stripped (including path)
    bin_dir = '/usr/sbin/'
    # List of system users
    # Read /usr/local/sng/conf/system-users.conf if exists
    # List can be produced/updated with: /usr/local/sng/scripts/sng-system-users.sh script
    system_users = {}

    logger = None
    user = None
    name = None
    info = {}
    options = None
    rc = -1
    output = []
    user_exists = False
    is_root = False

    def __init__(self, logger, options):
        super(sngUser, self).__init__()
        self.user = options.user
        self.options = options
        self.logger = logger
        # Get system users list
        self._system_users()
        self._check_user()

    def _check_user(self):
        # Special case for root
        if "root" == self.user :
            self.user_exists = True
            self.is_root = True
        else:
            # Check not a system user (but root, as pwd can be updated)
            if self.user in self.system_users:
                self.logger.error("User " + self.user + " is system user "
                        + self.system_users[self.user])
                sys.exit(-1)

            # Check user exists
            rc, output = execute_cmd("id", [self.user])
            if 0 == rc:
                self.user_exists = True

        if self.options.name:
            self.name = self.options.name

    def _system_users(self):
        try:
            # Read system users configuration file
            _users = [line.strip() for line in
                    open('/usr/local/sng/conf/system-users.conf')]
            for line in _users:
                user_def = line.split(':')
                if len(user_def) > 1:
                    self.system_users[user_def[0]] = user_def[1]
        except:
            self.logger.error('System users list not present')

    def _info(self):
        # Read system users configuration file
        _users = [line.strip() for line in
                open('/etc/passwd')]
        # /etc/passwd line format:
        # Name:Password:UserID:GroupID:Info:HomeDirectory:Shell
        for line in _users:
            user_def = line.split(':')
            if len(user_def) > 1:
                if user_def[0] == self.user:
                    self.info = {
                        'name': user_def[0],
                        #'password': user_def[1],
                        'uid': user_def[2],
                        'gid': user_def[3],
                        'info': user_def[4],
                        'home': user_def[5],
                        'shell': user_def[6],
                        }
                    return True
        return False

    def _shadow_psw(self):
        # Read /etc/shadow file
        _shadow = [line.strip() for line in open('/etc/shadow')]
        for line in _shadow:
            user_def = line.split(':')
            if len(user_def) > 2 and user_def[0] == self.user:
                # Extract salt and salted psw
                psw_def = user_def[1].split('$')
                # Format should be: $hash type$XX$the salted psw
                # so split result list must have 4 entries
                # where [1] contains the hash type, [2] contains salt
                # and [3] contains salted psw
                # any other list entry will be considered as error
                if 4 == len(psw_def):
                    return [ user_def[1], psw_def[1], psw_def[2], psw_def[3] ]
                else:
                    break

        # hitting this is an error
        self.logger.error("Failed to retrieve password for User " + self.user )
        sys.exit(-1)

    def _password(self, encrypt=True, hash_type="1"):
        if not self.options.password:
            return False

        psw = self.options.password
        if encrypt:
            chars = string.ascii_letters + string.digits + './'
            # Random salt
            _salt = ''.join(random.choice(chars) for x in range(2))
            # hash type (from http://www.akkadia.org/drepper/SHA-crypt.txt)
            # 1 = MD5
            # 5 = SHA256
            # 6 = SHA512
            salt = "$" + hash_type + "$" + _salt + "$"
            psw = crypt.crypt(psw,salt)

        return psw

    def do_add(self):
        if self.user_exists:
            self.logger.error("User " + self.user + " already exists")
            self.rc = -1
        else:
            if not self.name:
                self.name = self.user
            self.logger.info("Add User: " + self.user)
            psw = self._password()
            if not psw:
                self.logger.error("Missing password argument")
                sys.exit(-1)
            # Create usermod arg list
            args = []
            args.extend(['-p', psw])
            if self.name:
                args.extend(['-c', self.name])

            # Add user name
            args.append(self.user)

            self.rc, output = execute_cmd(self.bin_dir + "useradd",args)
            for line in output:
                self.logger.info(line)

        return self.rc

    def do_remove(self):
        if self.is_root:
            self.logger.error("Cannot remove User "+self.user)
            sys.exit(-1)

        if not self.user_exists:
            self.logger.error("User " + self.user + " doesn't exist")
            self.rc = -1
        else:
            # userdel will return 8 if user is logged
            self.logger.info("Remove User: " + self.user)
            self.rc, output = execute_cmd(self.bin_dir + "userdel", ["-r", self.user])
            for line in output:
                self.logger.info(line)

        return self.rc

    def do_update(self):
        if not self.user_exists:
            self.logger.error("User " + self.user + " doesn't exist")
            self.rc = -1
        else:
            self.logger.info("Update User: " + self.user)
            # Create usermod arg list
            args = []
            # Check password arg exists
            psw = self._password()
            if psw:
                args.extend(['-p', psw])
            if self.name:
                args.extend(['-c', self.name])

            # Add user name
            args.append(self.user)

            self.rc, output = execute_cmd(self.bin_dir + "usermod", args)
            for line in output:
                self.logger.info(line)

        return self.rc


    def do_login(self):
        if not self.user_exists:
            self.logger.error("User " + self.user + " doesn't exist")
            self.rc = -1
        else:
            # Based on following solution:
            # http://stackoverflow.com/questions/18035093/given-a-linux-username-and-a-password-how-can-i-test-if-it-is-a-valid-account
            # http://serverfault.com/questions/330069/how-to-create-an-sha-512-hashed-password-for-shadow
            self.logger.info("Login for User: " + self.user)
            # Check password arg exists
            psw = self._password(False)
            # Find shadow definition for user
            shadow_def, shadow_crypt, shadow_salt, shadow_psw = self._shadow_psw()
            # Submit to crypt
            _tmp_shadow_def = crypt.crypt(psw, '$' + shadow_crypt + '$' + shadow_salt)

            # Check crypt outut and shadow_def match
            if shadow_def == _tmp_shadow_def:
                self.rc = 0
                return self.rc

        self.logger.error("Login Failed for User " + self.user )
        sys.exit(-1)

    def do_exist(self):
        if not self.user_exists:
            self.logger.info("User " + self.user + " doesn't exist")
            self.rc = 0
        else:
            self.logger.info("User " + self.user + " exists")
            self.rc = -2

        return self.rc

    def do_logged(self):
        # Get logged users list
        self.rc, output = execute_cmd("users")
        users = output[0].split()
        # check user within the list
        if self.user in users:
            self.logger.info("User " + self.user + " logged in")
            self.rc = -2
        else:
            self.logger.info("User " + self.user + " not logged in")
            self.rc = 0

        return self.rc

    def do_info(self):
        if not self.user_exists:
            self.logger.error("User " + self.user + " doesn't exist")
            sys.exit(-1)

        if self._info():
            self.logger.info(self.info)
            self.rc = 0

        return self.rc

def main():
    #Parse the options
    usage = "usage: %prog [options] arg"
    optParser = OptionParser(usage)
    optParser.add_option('-a', '--action', action='store', type='choice',
                        dest='action', metavar='ACTION',
                        choices=['add', 'remove', 'update', 'login', 'exist',
                            'logged', 'info'],
                        help="Action to perform.")

    optParser.add_option('-u', '--user', action='store', type='string',
                        dest='user', metavar='USER',
                        help="User Name")

    optParser.add_option('-s', '--syslog', action='store_true',
                        dest='syslog', metavar='SYSLOG',
                        help="Log to syslog")

    optParser.add_option('-p', '--password', action='store', type='string',
                        dest='password', metavar='PASSWORD',
                        help="Password")

    optParser.add_option('-n', '--name', action='store', type='string',
                        dest='name', metavar='NAME',
                        help="User Name")

    (options, args) = optParser.parse_args()

    # Create logger
    logger = logging.getLogger(os.path.basename(os.path.abspath(sys.argv[0])))
    logger.setLevel(logging.INFO)
    handler = logging.StreamHandler()
    logger.addHandler(handler)

    # Syslog ?
    if options.syslog:
        syslog_handler = logging.handlers.SysLogHandler(address='/dev/log',
                facility=logging.handlers.SysLogHandler.LOG_USER)
        logger.addHandler(syslog_handler)

    # Check for mandatory arguments: user and action
    if not options.action or not options.user:
        logger.error('Missing mandatory arguments')
        sys.exit(-1)

    # Create user object
    user = sngUser(logger, options)

    # Locate Action to run
    action = getattr(user, 'do_' + options.action)
    if not action:
        logger.error('Invalid action ' + options.action)
        sys.exit(-1)
    # Do it
    rc = action()
    sys.exit(rc)

if __name__ == '__main__':
  main()
