# -*- coding: utf-8 -*-
# Original version (pam-0.1.3) ©2007 Chris AtLee <chris@atlee.ca>
# This version (modifications) © 2013 Liftoff Software Corporation
# Licensed under the MIT license:
#   http://www.opensource.org/licenses/mit-license.php
# This is a modified version of pam-0.1.3 that adds support for
# pam_set_item (specificallly, to support setting a PAM_TTY)
# Meta
__license__ = "MIT"
__author__ = 'Dan McDougall <daniel.mcdougall@liftoffsoftware.com>'
__doc__ = """\
.. _gopam.py:
PAM Authentication Module for Python
====================================
Provides an authenticate function that will allow the caller to authenticate
a user against the Pluggable Authentication Modules (PAM) on the system.
Implemented using ctypes, so no compilation is necessary.
"""
__all__ = ['authenticate']
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, pointer, sizeof
from ctypes import c_void_p, c_uint, c_char_p, c_char, c_int
from ctypes.util import find_library
LIBPAM = CDLL(find_library("pam"))
LIBC = CDLL(find_library("c"))
CALLOC = LIBC.calloc
CALLOC.restype = c_void_p
CALLOC.argtypes = [c_uint, c_uint]
STRDUP = LIBC.strdup
STRDUP.argstypes = [c_char_p]
STRDUP.restype = POINTER(c_char) # NOT c_char_p !!!!
# Various constants
PAM_PROMPT_ECHO_OFF = 1
PAM_PROMPT_ECHO_ON = 2
PAM_ERROR_MSG = 3
PAM_TEXT_INFO = 4
# pam_set_item and pam_get_item constants:
PAM_SERVICE =       1    # The service name
PAM_USER =          2    # The user name
PAM_TTY =           3    # The tty name
PAM_RHOST =         4    # The remote host name
PAM_CONV =          5    # The pam_conv structure
PAM_AUTHTOK =       6    # The authentication token (password)
PAM_OLDAUTHTOK =    7    # The old authentication token
PAM_RUSER =         8    # The remote user name
PAM_USER_PROMPT =   9    # the prompt for getting a username
# These are Linux-specific pam_set_item/pam_get_item constants:
PAM_FAIL_DELAY =   10    # app supplied function to override failure
PAM_XDISPLAY =     11    # X display name
PAM_XAUTHDATA =    12    # X server authentication data
PAM_AUTHTOK_TYPE = 13    # The type for pam_get_authtok
class PamHandle(Structure):
    """wrapper class for pam_handle_t"""
    _fields_ = [("handle", c_void_p)]
    def __init__(self):
        Structure.__init__(self)
        self.handle = 0
class PamMessage(Structure):
    """wrapper class for pam_message structure"""
    _fields_ = [("msg_style", c_int), ("msg", c_char_p)]
    def __repr__(self):
        return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
class PamResponse(Structure):
    """wrapper class for pam_response structure"""
    _fields_ = [("resp", c_char_p), ("resp_retcode", c_int)]
    def __repr__(self):
        return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
CONV_FUNC = CFUNCTYPE(
    c_int,
    c_int,
    POINTER(POINTER(PamMessage)),
    POINTER(POINTER(PamResponse)),
    c_void_p)
class PamConv(Structure):
    """wrapper class for pam_conv structure"""
    _fields_ = [("conv", CONV_FUNC), ("appdata_ptr", c_void_p)]
PAM_START = LIBPAM.pam_start
PAM_START.restype = c_int
PAM_START.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
PAM_AUTHENTICATE = LIBPAM.pam_authenticate
PAM_AUTHENTICATE.restype = c_int
PAM_AUTHENTICATE.argtypes = [PamHandle, c_int]
PAM_SET_ITEM = LIBPAM.pam_set_item
PAM_SET_ITEM.restype = c_int
PAM_SET_ITEM.argtypes = [PamHandle, c_int, c_char_p]
[docs]def authenticate(username, password, service='login', tty="console", **kwargs):
    """
    Returns True if the given username and password authenticate for the
    given service.  Returns False otherwise.
    :param string username: The username to authenticate.
    :param string password: The password in plain text.
    :param string service:
        The PAM service to authenticate against.  Defaults to 'login'.
    :param string tty:
        Name of the TTY device to use when authenticating.  Defaults to
        'console' (to allow root).
    If additional keyword arguments are provided they will be passed to
    PAM_SET_ITEM() like so::
        PAM_SET_ITEM(handle, <keyword mapped to PAM_whatever>, <value>)
    Where the keyword will be automatically converted to a PAM_whatever constant
    if present in this file.  Example::
        authenticate(user, pass, PAM_RHOST="myhost")
    ...would result in::
        PAM_SET_ITEM(handle, 4, "myhost") # PAM_RHOST (4) taken from the global
    """
    encoding = 'utf-8'
    if not isinstance(password, bytes):
        password = password.encode(encoding)
    if not isinstance(username, bytes):
        username = username.encode(encoding)
    if not isinstance(service, bytes):
        service = service.encode(encoding)
    if not isinstance(tty, bytes):
        tty = tty.encode(encoding)
    @CONV_FUNC
    def my_conv(n_messages, messages, p_response, app_data):
        """
        Simple conversation function that responds to any prompt where the echo
        is off with the supplied password.
        """
        # Create an array of n_messages response objects
        addr = CALLOC(n_messages, sizeof(PamResponse))
        p_response[0] = cast(addr, POINTER(PamResponse))
        for i in range(n_messages):
            if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF:
                pw_copy = STRDUP(password)
                p_response.contents[i].resp = cast(pw_copy, c_char_p)
                p_response.contents[i].resp_retcode = 0
        return 0
    handle = PamHandle()
    conv = PamConv(my_conv, 0)
    retval = PAM_START(service, username, pointer(conv), pointer(handle))
    PAM_SET_ITEM(handle, PAM_TTY, tty)
    for key, value in kwargs.items():
        if key.startswith('PAM_') and key in globals():
            if isinstance(value, str):
                value = value.encode(encoding)
            PAM_SET_ITEM(handle, globals()[key], value)
    if retval != 0:
        # TODO: This is not an authentication error, something
        # has gone wrong starting up PAM
        return False
    retval = PAM_AUTHENTICATE(handle, 0)
    return retval == 0 
def pam_service_exists(service):
    """
    Returns ``True`` if the given *service* can be found in the system's PAM
    configuration.
    """
    if os.path.isdir('/etc/pam.d'):
        # Modern PAM implementation.  Services are named after files.
        if service in os.listdir('/etc/pam.d/'):
            return True
    else:
        # Old-school PAM implementation (Solaris, AIX, etc).
        services = [] # He's making a list, and checkin' it twice.
        for line in open('/etc/pam.conf'):
            if line.startswith('#'): # It's a comment
                continue
            _service = line.split()[0]
            if _service not in services:
                services.append(_service)
        if service in services:
            return True
    return False
if __name__ == "__main__":
    # Do a little test.  Make a little love.  Get down tonight!
    import os, sys, getpass
    print("\x1b[1mTesting PAM authentication\x1b[0m")
    if os.getuid() != 0:
        print( # Print in bold/yellow
            "\x1b[1;33mWarning: You're not root.  This means you'll only be "
            "able to test authenticating your own user ({0}).\x1b[0m"
            .format(getpass.getuser()))
    service = raw_input("PAM Service [login]: ")
    if not service:
        service = 'login'
    if not pam_service_exists(service):
        print(
            "\x1b[1;33mWarning: The given service, '{0}' could not be found in "
            "this system's PAM configuration.  This means the 'other' service "
            "will be used.\x1b[0m".format(service))
    getting_user = True
    while getting_user:
        user = raw_input("Username [{0}]: ".format(getpass.getuser()))
        getting_user = False
        if not user:
            user = getpass.getuser()
        if os.getuid() != 0 and user != getpass.getuser():
            getting_user = True
            print(
                "ERROR: I told you that you can only authenticate as yourself "
                "(since you're not root).")
            print(
                "Try again but this time just hit the enter key or actually "
                "type out your own username.")
    password = getpass.getpass()
    try:
        result = authenticate(user, password)
        if result:
            print("SUCCESS:  PAM authentication definitely works.")
        else:
            print(
                "FAIL:  Authentication failed.  Did you enter your password "
                "correctly?")
            print(
                "If this keeps happening you either need some caffeine or you "
                "need to check the system logs to see why the authentication "
                "is failing.")
    except Exception as e:
        print("EPIC FAIL:  Something horrible went wrong.  Exception message:")
        print(e)
        print("Here's the traceback:")
        import traceback
        traceback.print_exc(file=sys.stdout)