########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Common/ClAuthenticate.py,v 1.8 2004/09/03 17:11:20 mbrown Exp $
"""
Repository client (4ss, 4ss_manager) common login routines

Copyright 2004 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

import os, sys, sha, getpass

from Ft.Server import FtServerBaseException
import MessageSource

__all__ = [ 'FtServerAuthenticationException', 'Error',
            'GetUserName', 'GetPass', 'GetHostName', 'GetPort',
            'HashPasswd',
            'PasswordFileManager',
            'UserAuthenticate',
           ]


class FtServerAuthenticationException(FtServerBaseException):
    MessageSource = MessageSource

Error = MessageSource.Error


def _raw_input(prompt=''):
    # _raw_input() from getpass: like raw_input(), but doesn't save
    # the string in the GNU readline history. Modified to use stderr.
    prompt = str(prompt)
    if prompt:
        sys.stderr.write(prompt)
        sys.stderr.flush()
    line = sys.stdin.readline()
    if not line:
        raise EOFError
    if line[-1] == '\n':
        line = line[:-1]
    return line


def GetUserName(prompt='Username: ', emptyOK=True):
    """
    Prompts for a username on stderr.
    Keeps prompting if emptyOK is false and no username is entered.
    """
    if emptyOK:
        return _raw_input(prompt)
    else:
        username = ''
        while not username:
            username = _raw_input(prompt)
        return username


def GetPass(prompt='Password: '):
    """
    Wrapper for getpass.getpass(), with the only difference being that
    on Unix, the prompt will be sent to stderr rather than stdout.
    """
    if os.name == 'posix':
        try:
            stdout = sys.stdout
            sys.stdout = sys.stderr
            passwd = getpass.getpass(prompt)
        finally:
            sys.stdout = stdout
    else:
        passwd = getpass.getpass(prompt)
    return passwd


def GetHostName(prompt='Host: '):
    """
    Prompts for a hostname on stderr.
    """
    return _raw_input(prompt)


def GetPort(prompt='Port: '):
    """
    Prompts for a port on stderr.
    """
    return _raw_input(prompt)


def HashPasswd(passwd):
    """
    Returns a hash of the given password string.
    """
    return sha.new(passwd).hexdigest()


default_passwdFilename = '.4ss.passwd'

class PasswordFileManager:
    """
    A collection of functions to make reading and writing from
    4Suite repository server password files more convenient.

    A password file maps a username to a password hash, hostname,
    and port. The location of the file is determined by the
    FTSS_PASSWORD_FILE environment variable. If not set, then the
    location is assumed to be a file named '%s'
    in the directory determined by:
    1. Env. variable HOME (on Unix) or HOMEPATH (on Windows);
    2. Env. variable WINDIR (on Windows).
    If none of the variables are set, an exception is raised.
    If the password file does not exist, it will be created when the
    first entry is stored. The location must be writable, of course.
    """ % default_passwdFilename
    def __init__(self):
        self._getFileLocation()
        return

    def setUserEntry(self, userName, passwdHash, hostName, port):
        """
        Stores the password hash, hostname, and port for the given username
        in the password file, overwriting any existing entry for the user.
        Raises an exception if any of the values are inappropriate for
        storage or if there was a problem storing them in the file.
        """
        if not userName or ':' in userName:
            raise ValueError('Invalid username %r' % userName)
        if not passwdHash or ':' in passwdHash or ', ' in passwdHash:
            raise ValueError('Invalid password hash %r' % passwdHash)
        if hostName and ', ' in hostName:
            raise ValueError('Invalid hostname %r' % hostName)
        if port:
            try:
                port = int(port)
            except ValueError:
                raise ValueError('Invalid port %r' % port)
        entries = self._readEntries()
        entries[userName] = (passwdHash, hostName, port)
        self._writeEntries(entries)
        return

    def getUserEntry(self, userName):
        """
        Gets the password hash, hostname, and port for the given username
        in the password file. Returns (None, None, 0) if there is no entry
        for the username, or if the password file is not readable.
        """
        entries = self._readEntries()
        return entries.get(userName, (None, None, 0))

    def _getFileLocation(self):
        # environment var is preferred
        passwdFileLocation = os.environ.get('FTSS_PASSWORD_FILE')
        if not passwdFileLocation:
            # Get the home directory
            home_dir = ''
            # Unix, cygwin shells usually have $HOME
            if os.name == 'posix':
                home_dir = os.environ.get('HOME')
            # Windows shell may have %HOMEPATH%
            elif sys.platform[:3] == 'win':
                home_dir = os.environ.get('HOMEPATH')
                if not home_dir:
                    home_dir = os.environ.get('WINDIR')
            if not home_dir:
                raise FtServerAuthenticationException(Error.PASSWDFILE_UNDEFINED)
            else:
                self._passwdFileLocation = os.path.join(home_dir,
                    default_passwdFilename)
        return

    def _readEntries(self):
        if not os.path.exists(self._passwdFileLocation): return {}
        passwdfile = open(self._passwdFileLocation, 'r')
        #print "!!reading password file"
        entries = {}
        for line in passwdfile:
            fields = line.split(':')
            if len(fields) != 2:
                continue
            data = fields[1][:-1].split(', ')
            if len(data) != 3:
                continue
            entries[fields[0]] = (data[0], data[1], int(data[2]))
        passwdfile.close()
        return entries

    def _writeEntries(self, entries):
        passwdfile = open(self._passwdFileLocation, 'w')
        #print "!!writing password file"
        for name, (passwdhash, host, port) in entries.items():
            passwdfile.write("%s:%s, %s, %d\n"%(name, passwdhash, host, port))
        passwdfile.close()
        return


def UserAuthenticate(options=None, prompt="User", promptOnly=0):
    """
    Obtains a username and password hash, prompting for them, if necessary.
    Returns a tuple (username, password), which can be (None, None).

    Username sources normally checked are, in order:
    1. the options dictionary;
    2. the agent referenced in the FTSS_AGENT environment variable;
    3. the FTSS_USERNAME environment variable;
    4. a prompt for user input.

    Password sources normally checked are, in order:
    1. the password file referenced in environment var. FTSS_PASSWORD_FILE;
    2. the password file $HOME/.4ss.passwd or %WINDIR%\.4ss.passwd;
    3. the password file referenced in the deprecated environment variable
       FTSERVER_PASSWORD_FILE;
    4. a prompt for user input.

    Optional arguments:

    options - a dictionary of options parsed from the command line
    (see FancyGetOpt). The key 'anonymous' is taken to indicate that no
    authentication is necessary and (None, None) should be returned.
    If the key 'username' is present, the associated value is returned as
    the username and no other potential sources for username are checked.

    prompt - if the user is to be prompted for their username, this is the
    string that indicates the type of user to be mentioned in the prompt.
    It is 'User' or 'Manager', typically.

    promptOnly - if set, this flag forces the username and password to be
    obtained by prompting the user; other sources are not checked.
    """
    options = options or {}

    #See if there is an overide on the command line
    if options.has_key('anonymous') and not promptOnly:
        return (None, None)

    username = options.get('username')

    #See if it is in an agent
    if not username and os.environ.has_key('FTSS_AGENT') and not promptOnly:
        return eval(os.environ['FTSS_AGENT'])

    #See if it is in the environment
    if not username and not promptOnly:
        username = os.environ.get('FTSS_USERNAME')

    #Nope, ask for it
    if not username:
        username = GetUserName("4SS %s Name: " % prompt)

    #Now look for it in the passwd file
    passwordHash = None

    if not promptOnly:
        pm = PasswordFileManager()
        passwordHash, t, t = pm.getUserEntry(username)

    if not passwordHash:
        password = GetPass("Password for %s: " % username)
        passwordHash = HashPasswd(password)

    return (username, passwordHash)
