diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index ed73ab7e..4bdfa885 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -28,8 +28,9 @@ import hashlib import hmac import multiprocessing import confluent.userutil as userutil +pam = None try: - import PAM + import confluent.pam as pam except ImportError: pass import time @@ -94,23 +95,6 @@ _deniedbyrole = { ] } } -class Credentials(object): - def __init__(self, username, passphrase): - self.username = username - self.passphrase = passphrase - self.haspam = False - - def pam_conv(self, auth, query_list): - # use stored credentials in a pam conversation - self.haspam = True - resp = [] - for query_entry in query_list: - query, pamtype = query_entry - if query.startswith('Password'): - resp.append((self.passphrase, 0)) - else: - return None - return resp def _prune_passcache(): @@ -227,7 +211,6 @@ def check_user_passphrase(name, passphrase, operation=None, element=None, tenant # would normally make an event and wait # but here there's no need for that eventlet.sleep(0.5) - credobj = Credentials(user, passphrase) cfm = configmanager.ConfigManager(tenant, username=user) ucfg = cfm.get_user(user) if ucfg is None: @@ -278,22 +261,16 @@ def check_user_passphrase(name, passphrase, operation=None, element=None, tenant if crypt == crypted: _passcache[(user, tenant)] = hashlib.sha256(passphrase).digest() return authorize(user, element, tenant, operation) - try: - pammy = PAM.pam() - pammy.start(_pamservice, user, credobj.pam_conv) - pammy.authenticate() - pammy.acct_mgmt() + if pam: + pammy = pam.pam() + usergood = pammy.authenticate(user, passphrase) del pammy - _passcache[(user, tenant)] = hashlib.sha256(passphrase).digest() - return authorize(user, element, tenant, operation, skipuserobj=False) - except NameError: - pass - except PAM.error: - pass + if usergood: + _passcache[(user, tenant)] = hashlib.sha256(passphrase).digest() + return authorize(user, element, tenant, operation, skipuserobj=False) eventlet.sleep(0.05) # stall even on test for existence of a username return None - def _apply_pbkdf(passphrase, salt): return KDF.PBKDF2(passphrase, salt, 32, 10000, lambda p, s: hmac.new(p, s, hashlib.sha256).digest()) diff --git a/confluent_server/confluent/pam.py b/confluent_server/confluent/pam.py new file mode 100644 index 00000000..9bf28eaf --- /dev/null +++ b/confluent_server/confluent/pam.py @@ -0,0 +1,235 @@ +# Pulled from: +# https://raw.githubusercontent.com/FirefighterBlu3/python-pam/fe44b334970f421635d9e373b563c9e6566613bd/pam.py +# and https://github.com/FirefighterBlu3/python-pam/pull/16/files +# (c) 2007 Chris AtLee +# Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php +# +# Original author: Chris AtLee +# +# Modified by David Ford, 2011-12-6 +# added py3 support and encoding +# added pam_end +# added pam_setcred to reset credentials after seeing Leon Walker's remarks +# added byref as well +# use readline to prestuff the getuser input + +''' +PAM 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__ = ['pam'] +__version__ = '1.8.4' +__author__ = 'David Ford ' +__released__ = '2018 June 15' + +import sys + +from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, byref, sizeof +from ctypes import c_void_p, c_size_t, c_char_p, c_char, c_int +from ctypes import memmove +from ctypes.util import find_library + +class PamHandle(Structure): + """wrapper class for pam_handle_t pointer""" + _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 "" % (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 "" % (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) ] + +# Various constants +PAM_PROMPT_ECHO_OFF = 1 +PAM_PROMPT_ECHO_ON = 2 +PAM_ERROR_MSG = 3 +PAM_TEXT_INFO = 4 +PAM_REINITIALIZE_CRED = 8 + +libc = CDLL(find_library("c")) +libpam = CDLL(find_library("pam")) + +calloc = libc.calloc +calloc.restype = c_void_p +calloc.argtypes = [c_size_t, c_size_t] + +# bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this function +if hasattr(libpam, 'pam_end'): + pam_end = libpam.pam_end + pam_end.restype = c_int + pam_end.argtypes = [PamHandle, c_int] + +pam_start = libpam.pam_start +pam_start.restype = c_int +pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)] + +pam_acct_mgmt = libpam.pam_acct_mgmt +pam_acct_mgmt.restype = c_int +pam_acct_mgmt.argtypes = [PamHandle, c_int] + +pam_setcred = libpam.pam_setcred +pam_setcred.restype = c_int +pam_setcred.argtypes = [PamHandle, c_int] + +pam_strerror = libpam.pam_strerror +pam_strerror.restype = c_char_p +pam_strerror.argtypes = [PamHandle, c_int] + +pam_authenticate = libpam.pam_authenticate +pam_authenticate.restype = c_int +pam_authenticate.argtypes = [PamHandle, c_int] + +class pam(): + code = 0 + reason = None + + def __init__(self): + pass + + def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True): + """username and password authentication for the given service. + + Returns True for success, or False for failure. + + self.code (integer) and self.reason (string) are always stored and may + be referenced for the reason why authentication failed. 0/'Success' will + be stored for success. + + Python3 expects bytes() for ctypes inputs. This function will make + necessary conversions using the supplied encoding. + + Inputs: + username: username to authenticate + password: password in plain text + service: PAM service to authenticate against, defaults to 'login' + + Returns: + success: True + failure: False + """ + + @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)) + response = cast(addr, POINTER(PamResponse)) + p_response[0] = response + for i in range(n_messages): + if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF: + dst = calloc(len(password)+1, sizeof(c_char)) + memmove(dst, cpassword, len(password)) + response[i].resp = dst + response[i].resp_retcode = 0 + return 0 + + # python3 ctypes prefers bytes + if sys.version_info >= (3,): + if isinstance(username, str): username = username.encode(encoding) + if isinstance(password, str): password = password.encode(encoding) + if isinstance(service, str): service = service.encode(encoding) + else: + if isinstance(username, unicode): + username = username.encode(encoding) + if isinstance(password, unicode): + password = password.encode(encoding) + if isinstance(service, unicode): + service = service.encode(encoding) + + if b'\x00' in username or b'\x00' in password or b'\x00' in service: + self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM + self.reason = 'strings may not contain NUL' + return False + + # do this up front so we can safely throw an exception if there's + # anything wrong with it + cpassword = c_char_p(password) + + handle = PamHandle() + conv = PamConv(my_conv, 0) + retval = pam_start(service, username, byref(conv), byref(handle)) + + if retval != 0: + # This is not an authentication error, something has gone wrong starting up PAM + self.code = retval + self.reason = "pam_start() failed" + return False + + retval = pam_authenticate(handle, 0) + auth_success = retval == 0 + + if auth_success: + retval = pam_acct_mgmt(handle, 0) + auth_success = retval == 0 + + if auth_success and resetcreds: + retval = pam_setcred(handle, PAM_REINITIALIZE_CRED) + + # store information to inform the caller why we failed + self.code = retval + self.reason = pam_strerror(handle, retval) + if sys.version_info >= (3,): + self.reason = self.reason.decode(encoding) + + if hasattr(libpam, 'pam_end'): + pam_end(handle, retval) + + return auth_success + + +def authenticate(*vargs, **dargs): + """ + Compatibility function for older versions of python-pam. + """ + return pam().authenticate(*vargs, **dargs) + + +if __name__ == "__main__": + import readline, getpass + + def input_with_prefill(prompt, text): + def hook(): + readline.insert_text(text) + readline.redisplay() + readline.set_pre_input_hook(hook) + + if sys.version_info >= (3,): + result = input(prompt) + else: + result = raw_input(prompt) + + readline.set_pre_input_hook() + return result + + pam = pam() + + username = input_with_prefill('Username: ', getpass.getuser()) + + # enter a valid username and an invalid/valid password, to verify both failure and success + pam.authenticate(username, getpass.getpass()) + print('{} {}'.format(pam.code, pam.reason)) \ No newline at end of file