#!/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
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: $1$XX$the salted psw
                # so split result list must have 4 entries
                # where [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[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):
        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))
            # Force to use MD5
            salt = "$1$" + _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
            self.logger.info("Login for User: " + self.user)
            # Check password arg exists
            psw = self._password(False)
            # Find shadow definition for user
            shadow_def, shadow_salt, shadow_psw = self._shadow_psw()
            # Submit to openssl
            self.rc, output = execute_cmd("openssl", ["passwd", "-1", "-salt",
                shadow_salt, psw])
            # Check openssl outut and shadow_def match
            if 0 == self.rc and len(output)>0 and shadow_def == output[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()
