From b8c9e9c535da3720ca038ffb969cc93fb9623c78 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 23 Jun 2021 16:31:42 -0400 Subject: [PATCH] Begin work to support complex PAM conversations For example, TOTP setups need more prompts, this will pass the info to the client for the client to adjust. --- confluent_server/confluent/auth.py | 7 ++++++- confluent_server/confluent/httpapi.py | 16 ++++++++++++++-- confluent_server/confluent/pam.py | 23 +++++++++++++++++++---- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 8bcebac6..dc17af8c 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -283,9 +283,14 @@ def check_user_passphrase(name, passphrase, operation=None, element=None, tenant # pam_unix uses unix_chkpwd which reque os.setuid(pwe.pw_uid) usergood = pam.authenticate(user, passphrase, service=_pamservice) + except pam.PromptsNeeded: + os._exit(2) finally: os._exit(0 if usergood else 1) - usergood = os.waitpid(pid, 0)[1] == 0 + usergood = os.waitpid(pid, 0)[1] + if (usergood >> 8) == 2: + pam.authenticate(user, passphrase, service=_pamservice) + usergood = usergood == 0 else: # We are running as root, we don't need to fork in order to authenticate the # user diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index e1519787..2268983d 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -307,7 +307,12 @@ def _authorize_request(env, operation): return ('logout',) name, passphrase = base64.b64decode( env['HTTP_AUTHORIZATION'].replace('Basic ', '')).split(b':', 1) - authdata = auth.check_user_passphrase(name, passphrase, operation=operation, element=element) + try: + authdata = auth.check_user_passphrase(name, passphrase, operation=operation, element=element) + except Exception as e: + if hasattr(e, 'prompts'): + return {'code': 403, 'prompts': e.prompts} + raise if authdata is False: return {'code': 403} elif not authdata: @@ -519,7 +524,14 @@ def resourcehandler_backend(env, start_response): return if authorized['code'] == 403: start_response('403 Forbidden', badauth) - yield 'Forbidden' + response = {'result': 'Forbidden'} + if 'prompts' in authorized: + response['prompts'] = [] + for prompt in authorized['prompts']: + if not isinstance(prompt, str): + prompt = prompt.decode('utf8') + response['prompts'].append(prompt) + yield json.dumps(response) return if authorized['code'] != 200: raise Exception("Unrecognized code from auth engine") diff --git a/confluent_server/confluent/pam.py b/confluent_server/confluent/pam.py index 9bf28eaf..5528f4f6 100644 --- a/confluent_server/confluent/pam.py +++ b/confluent_server/confluent/pam.py @@ -103,6 +103,12 @@ pam_authenticate = libpam.pam_authenticate pam_authenticate.restype = c_int pam_authenticate.argtypes = [PamHandle, c_int] + +class PromptsNeeded(Exception): + def __init__(self, prompts): + self.prompts = prompts + + class pam(): code = 0 reason = None @@ -110,7 +116,7 @@ class pam(): def __init__(self): pass - def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True): + def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True, answers=None): """username and password authentication for the given service. Returns True for success, or False for failure. @@ -142,8 +148,15 @@ class pam(): 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)) + prompts.add(messages[i].contents.msg) + if answers and messages[i].contents.msg in answers: + currpassword = answers[messages[i].contents.msg] + currcpassword = c_char_p(currpassword) + else: + currpassword = password + currcpassword = cpassword + dst = calloc(len(currpassword)+1, sizeof(c_char)) + memmove(dst, currcpassword, len(currpassword)) response[i].resp = dst response[i].resp_retcode = 0 return 0 @@ -169,6 +182,7 @@ class pam(): # do this up front so we can safely throw an exception if there's # anything wrong with it cpassword = c_char_p(password) + prompts = set([]) handle = PamHandle() conv = PamConv(my_conv, 0) @@ -198,7 +212,8 @@ class pam(): if hasattr(libpam, 'pam_end'): pam_end(handle, retval) - + if answers is None and len(prompts) > 1 and not auth_success: + raise PromptsNeeded(prompts) return auth_success