From 9b39c96135ec421120686f4071abec60bdf197a5 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 24 May 2022 10:22:34 -0400 Subject: [PATCH 1/9] Begin work on webauthn support Provide appropriate registration options as a first step. --- confluent_server/confluent/auth.py | 2 + .../confluent/config/configmanager.py | 6 +-- confluent_server/confluent/httpapi.py | 12 ++++- confluent_server/confluent/webauthn.py | 51 +++++++++++++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 confluent_server/confluent/webauthn.py diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 9e675fbb..7e3e269e 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -162,6 +162,8 @@ def authorize(name, element, tenant=False, operation='create', return False manager = configmanager.ConfigManager(tenant, username=user) userobj = manager.get_user(user) + if userobj and userobj.get('role', None) == 'Stub': + userobj = None if not userobj: for group in userutil.grouplist(user): userobj = manager.get_usergroup(group) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 5c21d89f..c9a8d937 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -113,7 +113,7 @@ _attraliases = { 'bmcpass': 'secret.hardwaremanagementpassword', 'switchpass': 'secret.hardwaremanagementpassword', } -_validroles = ('Administrator', 'Operator', 'Monitor') +_validroles = ('Administrator', 'Operator', 'Monitor', 'Stub') membership_callback = None @@ -2777,8 +2777,8 @@ def dump_db_to_directory(location, password, redact=None, skipkeys=False): cfgfile.write('\n') bkupglobals = get_globals() if bkupglobals: - json.dump(bkupglobals, open(os.path.join(location, 'globals.json'), - 'w')) + with open(os.path.join(location, 'globals.json'), 'w') as globout: + json.dump(bkupglobals, globout) try: for tenant in os.listdir( os.path.join(ConfigManager._cfgdir, '/tenants/')): diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index d6b4af5d..6fc2aee2 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -22,9 +22,9 @@ try: except ModuleNotFoundError: import http.cookies as Cookie try: - import pywarp + import confluent.webauthn as webauthn except ImportError: - pywarp = None + webauthn = None import confluent.auth as auth import confluent.config.attributes as attribs import confluent.consoleserver as consoleserver @@ -834,6 +834,14 @@ def resourcehandler_backend(env, start_response): tlvdata.unicode_dictvalues(sessinfo) yield json.dumps(sessinfo) return + elif url.startswith('/sessions/current/webauthn/'): + if not webauthn: + start_response('501 Not Implemented', headers) + yield '' + return + for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers): + yield rsp + return resource = '.' + url[url.rindex('/'):] lquerydict = copy.deepcopy(querydict) try: diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py new file mode 100644 index 00000000..055cc1cf --- /dev/null +++ b/confluent_server/confluent/webauthn.py @@ -0,0 +1,51 @@ +import confluent.util as util +import json +import pywarp +import pywarp.backends + +creds = {} +challenges = {} + +class TestBackend(pywarp.backends.CredentialStorageBackend): + def __init__(self): + pass + + def get_credential_by_email(self, email): + return creds[email] + + def save_credential_for_user(self, email, credential): + creds[email] = credential + + def save_challenge_for_user(self, email, challenge, type): + challenges[email] = challenge + + def get_challenge_for_user(self, email, type): + return challenges[email] + + +def handle_api_request(url, env, start_response, username, cfm, headers): + if env['REQUEST_METHOD'] != 'POST': + raise Exception('Only POST supported for webauthn operations') + url = url.replace('/sessions/current/webauthn', '') + if'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0: + reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) + reqtype = env['CONTENT_TYPE'] + if url == '/registration_options': + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + userinfo = cfm.get_user(username) + if not userinfo: + cfm.create_user(username, role='Stub') + userinfo = cfm.get_user(username) + authid = userinfo.get('authid', None) + if not authid: + authid = util.randomstring(64) + cfm.set_user(username, {'authid': authid}) + opts = rp.get_registration_options(username) + # pywarp generates an id derived + # from username, which is a 'must not' in the spec + # we replace that with a complying approach + opts['user']['id'] = authid + if 'icon' in opts['user']: + del opts['user']['icon'] + start_response('200 OK', headers) + yield json.dumps(opts) From c93f09bc9177371e3c829dbc84fe37332f548b93 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 24 May 2022 19:17:31 -0400 Subject: [PATCH 2/9] Provide hook to get registered credentials This has to relax the session in getting and requesting validation. --- confluent_server/confluent/auth.py | 2 ++ confluent_server/confluent/httpapi.py | 23 +++++++++----- confluent_server/confluent/webauthn.py | 43 +++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 7e3e269e..02828f36 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -162,6 +162,8 @@ def authorize(name, element, tenant=False, operation='create', return False manager = configmanager.ConfigManager(tenant, username=user) userobj = manager.get_user(user) + if element in ('/sessions/current/webauthn/registered_credentials', '/sessions/current/webauthn/validate'): + return userobj, manager, user, tenant, skipuserobj if userobj and userobj.get('role', None) == 'Stub': userobj = None if not userobj: diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index 6fc2aee2..ece18980 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -211,6 +211,8 @@ def _should_skip_authlog(env): if '/sessions/current/async' in env['PATH_INFO']: # this is effectively invisible return True + if '/sessions/current/webauthn/registered_credentials' in env['PATH_INFO']: + return True if (env['REQUEST_METHOD'] == 'GET' and ('/sensors/' in env['PATH_INFO'] or '/health/' in env['PATH_INFO'] or @@ -274,11 +276,18 @@ def _authorize_request(env, operation): authdata = None name = '' sessionid = None + sessid = None cookie = Cookie.SimpleCookie() element = env['PATH_INFO'] if element.startswith('/sessions/current/'): - element = None - if 'HTTP_COOKIE' in env: + if (element.startswith('/sessions/current/webauthn/registered_credentials/') + or element.startswith('/sessions/current/webauthn/validate/')): + username = element.rsplit('/')[-1] + element = element.replace('/' + username, '') + authdata = auth.authorize(name, element=element, operation=operation) + else: + element = None + if (not authdata) and 'HTTP_COOKIE' in env: cidx = (env['HTTP_COOKIE']).find('confluentsessionid=') if cidx >= 0: sessionid = env['HTTP_COOKIE'][cidx+19:cidx+51] @@ -356,11 +365,11 @@ def _authorize_request(env, operation): auditmsg['user'] = util.stringify(authdata[2]) if sessid is not None: authinfo['sessionid'] = sessid + if 'csrftoken' in httpsessions[sessid]: + authinfo['authtoken'] = httpsessions[sessid]['csrftoken'] + httpsessions[sessid]['cfgmgr'] = authdata[1] if not skiplog: auditlog.log(auditmsg) - if 'csrftoken' in httpsessions[sessid]: - authinfo['authtoken'] = httpsessions[sessid]['csrftoken'] - httpsessions[sessid]['cfgmgr'] = authdata[1] return authinfo elif authdata is None: return {'code': 401} @@ -636,7 +645,7 @@ def resourcehandler_backend(env, start_response): raise Exception("Unrecognized code from auth engine") headers.extend( ("Set-Cookie", m.OutputString()) - for m in authorized['cookie'].values()) + for m in authorized.get('cookie', {}).values()) cfgmgr = authorized['cfgmgr'] if (operation == 'create') and env['PATH_INFO'] == '/sessions/current/async': pagecontent = "" @@ -839,7 +848,7 @@ def resourcehandler_backend(env, start_response): start_response('501 Not Implemented', headers) yield '' return - for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers): + for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers, reqbody): yield rsp return resource = '.' + url[url.rindex('/'):] diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 055cc1cf..9c5c362a 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -1,3 +1,4 @@ +import base64 import confluent.util as util import json import pywarp @@ -11,9 +12,13 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): pass def get_credential_by_email(self, email): + if not isinstance(email, str): + email = email.decode('utf8') return creds[email] def save_credential_for_user(self, email, credential): + if not isinstance(email, str): + email = email.decode('utf8') creds[email] = credential def save_challenge_for_user(self, email, challenge, type): @@ -23,15 +28,12 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): return challenges[email] -def handle_api_request(url, env, start_response, username, cfm, headers): +def handle_api_request(url, env, start_response, username, cfm, headers, reqbody): if env['REQUEST_METHOD'] != 'POST': raise Exception('Only POST supported for webauthn operations') url = url.replace('/sessions/current/webauthn', '') - if'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0: - reqbody = env['wsgi.input'].read(int(env['CONTENT_LENGTH'])) - reqtype = env['CONTENT_TYPE'] if url == '/registration_options': - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) userinfo = cfm.get_user(username) if not userinfo: cfm.create_user(username, role='Stub') @@ -47,5 +49,36 @@ def handle_api_request(url, env, start_response, username, cfm, headers): opts['user']['id'] = authid if 'icon' in opts['user']: del opts['user']['icon'] + if 'id' in opts['rp']: + del opts['rp']['id'] start_response('200 OK', headers) yield json.dumps(opts) + elif url.startswith('/registered_credentials/'): + username = url.rsplit('/', 1)[-1] + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + if not isinstance(username, bytes): + username = username.encode('utf8') + opts = rp.get_authentication_options(username) + opts['challenge'] = base64.b64encode(opts['challenge']).decode('utf8') + start_response('200 OK', headers) + yield json.dumps(opts) + elif url == '/validate': + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + req = json.loads(reqbody) + for x in req: + req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) + req['email'] = username + rsp = rp.verify(**req) + start_response('200 OK') + yield json.dumps(rsp) + elif url == '/register_credential': + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) + req = json.loads(reqbody) + for x in req: + req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) + if not isinstance(username, bytes): + username = username.encode('utf8') + req['email'] = username + rsp = rp.register(**req) + start_response('200 OK', headers) + yield json.dumps(rsp) \ No newline at end of file From e0079b5a86185c774ea7eda73f139c8a457e2435 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 25 May 2022 10:58:02 -0400 Subject: [PATCH 3/9] Amend webauthn validation api --- confluent_server/confluent/webauthn.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 9c5c362a..731b33e5 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -62,7 +62,10 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody opts['challenge'] = base64.b64encode(opts['challenge']).decode('utf8') start_response('200 OK', headers) yield json.dumps(opts) - elif url == '/validate': + elif url.startswith('/validate/'): + username = url.rsplit('/', 1)[-1] + if not isinstance(username, bytes): + username = username.encode('utf8') rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) req = json.loads(reqbody) for x in req: From f6a17b5f32fdb21e5ad47a18df0d79d2c72a290a Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 25 May 2022 15:58:20 -0400 Subject: [PATCH 4/9] Have validate serve as session info request This should facilitate login. Further, provide a quick persistence for the credential test backend --- confluent_server/confluent/auth.py | 2 +- confluent_server/confluent/httpapi.py | 43 ++++++++++++++++---------- confluent_server/confluent/webauthn.py | 36 ++++++++++++++++++--- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 02828f36..6ded2e70 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -162,7 +162,7 @@ def authorize(name, element, tenant=False, operation='create', return False manager = configmanager.ConfigManager(tenant, username=user) userobj = manager.get_user(user) - if element in ('/sessions/current/webauthn/registered_credentials', '/sessions/current/webauthn/validate'): + if element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/'): return userobj, manager, user, tenant, skipuserobj if userobj and userobj.get('role', None) == 'Stub': userobj = None diff --git a/confluent_server/confluent/httpapi.py b/confluent_server/confluent/httpapi.py index ece18980..ec80a095 100644 --- a/confluent_server/confluent/httpapi.py +++ b/confluent_server/confluent/httpapi.py @@ -269,7 +269,7 @@ def _csrf_valid(env, session): env['HTTP_CONFLUENTAUTHTOKEN'] == session['csrftoken']) -def _authorize_request(env, operation): +def _authorize_request(env, operation, reqbody): """Grant/Deny access based on data from wsgi env """ @@ -282,8 +282,7 @@ def _authorize_request(env, operation): if element.startswith('/sessions/current/'): if (element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/')): - username = element.rsplit('/')[-1] - element = element.replace('/' + username, '') + name = element.rsplit('/')[-1] authdata = auth.authorize(name, element=element, operation=operation) else: element = None @@ -335,18 +334,13 @@ def _authorize_request(env, operation): return {'code': 403} elif not authdata: return {'code': 401} - sessid = util.randomstring(32) - while sessid in httpsessions: - sessid = util.randomstring(32) - httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90, - 'skipuserobject': authdata[4], - 'inflight': set([])} - if 'HTTP_CONFLUENTAUTHTOKEN' in env: - httpsessions[sessid]['csrftoken'] = util.randomstring(32) - cookie['confluentsessionid'] = util.stringify(sessid) - cookie['confluentsessionid']['secure'] = 1 - cookie['confluentsessionid']['httponly'] = 1 - cookie['confluentsessionid']['path'] = '/' + sessid = _establish_http_session(env, authdata, name, cookie) + if authdata and element and element.startswith('/sessions/current/webauthn/validate/'): + if webauthn: + for rsp in webauthn.handle_api_request(element, env, None, authdata[2], authdata[1], None, reqbody, None): + if rsp['verified']: + sessid = _establish_http_session(env, authdata, name, cookie) + break skiplog = _should_skip_authlog(env) if authdata: auditmsg = { @@ -376,6 +370,21 @@ def _authorize_request(env, operation): else: return {'code': 403} +def _establish_http_session(env, authdata, name, cookie): + sessid = util.randomstring(32) + while sessid in httpsessions: + sessid = util.randomstring(32) + httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90, + 'skipuserobject': authdata[4], + 'inflight': set([])} + if 'HTTP_CONFLUENTAUTHTOKEN' in env: + httpsessions[sessid]['csrftoken'] = util.randomstring(32) + cookie['confluentsessionid'] = util.stringify(sessid) + cookie['confluentsessionid']['secure'] = 1 + cookie['confluentsessionid']['httponly'] = 1 + cookie['confluentsessionid']['path'] = '/' + return sessid + def _pick_mimetype(env): """Detect the http indicated mime to send back. @@ -616,7 +625,7 @@ def resourcehandler_backend(env, start_response): if operation != 'retrieve' and 'restexplorerop' in querydict: operation = querydict['restexplorerop'] del querydict['restexplorerop'] - authorized = _authorize_request(env, operation) + authorized = _authorize_request(env, operation, reqbody) if 'logout' in authorized: start_response('200 Successful logout', headers) yield('{"result": "200 - Successful logout"}') @@ -848,7 +857,7 @@ def resourcehandler_backend(env, start_response): start_response('501 Not Implemented', headers) yield '' return - for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers, reqbody): + for rsp in webauthn.handle_api_request(url, env, start_response, authorized['username'], cfgmgr, headers, reqbody, authorized): yield rsp return resource = '.' + url[url.rindex('/'):] diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 731b33e5..a11984c7 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -1,34 +1,51 @@ import base64 +import confluent.tlvdata as tlvdata import confluent.util as util import json import pywarp import pywarp.backends +import pywarp.credentials creds = {} challenges = {} class TestBackend(pywarp.backends.CredentialStorageBackend): def __init__(self): - pass + global creds + try: + with open('/tmp/mycreds.json', 'r') as ji: + creds = json.load(ji) + except Exception: + pass def get_credential_by_email(self, email): if not isinstance(email, str): email = email.decode('utf8') - return creds[email] + cred = creds[email] + cid = base64.b64decode(cred['cid']) + cpk = base64.b64decode(cred['cpk']) + return pywarp.credentials.Credential(credential_id=cid, credential_public_key=cpk) def save_credential_for_user(self, email, credential): if not isinstance(email, str): email = email.decode('utf8') + credential = {'cid': base64.b64encode(credential.id).decode('utf8'), 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} creds[email] = credential + with open('/tmp/mycreds.json', 'w') as jo: + json.dump(creds, jo) def save_challenge_for_user(self, email, challenge, type): + if not isinstance(email, str): + email = email.decode('utf8') challenges[email] = challenge def get_challenge_for_user(self, email, type): + if not isinstance(email, str): + email = email.decode('utf8') return challenges[email] -def handle_api_request(url, env, start_response, username, cfm, headers, reqbody): +def handle_api_request(url, env, start_response, username, cfm, headers, reqbody, authorized): if env['REQUEST_METHOD'] != 'POST': raise Exception('Only POST supported for webauthn operations') url = url.replace('/sessions/current/webauthn', '') @@ -72,8 +89,17 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) req['email'] = username rsp = rp.verify(**req) - start_response('200 OK') - yield json.dumps(rsp) + if start_response: + start_response('200 OK', headers) + sessinfo = {'username': username} + if 'authtoken' in authorized: + sessinfo['authtoken'] = authorized['authtoken'] + if 'sessionid' in authorized: + sessinfo['sessionid'] = authorized['sessionid'] + tlvdata.unicode_dictvalues(sessinfo) + yield json.dumps(sessinfo) + else: + yield rsp elif url == '/register_credential': rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) req = json.loads(reqbody) From 3c3d6bb314688a71981ac10032cb2e7e248ac253 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Wed, 25 May 2022 17:40:35 -0400 Subject: [PATCH 5/9] Fix auth handling of the session/info --- confluent_server/confluent/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_server/confluent/auth.py b/confluent_server/confluent/auth.py index 6ded2e70..ce8cfd49 100644 --- a/confluent_server/confluent/auth.py +++ b/confluent_server/confluent/auth.py @@ -162,7 +162,7 @@ def authorize(name, element, tenant=False, operation='create', return False manager = configmanager.ConfigManager(tenant, username=user) userobj = manager.get_user(user) - if element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/'): + if element and (element.startswith('/sessions/current/webauthn/registered_credentials/') or element.startswith('/sessions/current/webauthn/validate/')): return userobj, manager, user, tenant, skipuserobj if userobj and userobj.get('role', None) == 'Stub': userobj = None From 9b6114f5233165eba8094546923580886d71bb1f Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 26 May 2022 15:01:47 -0400 Subject: [PATCH 6/9] Break if stuck in loop for over a minute --- confluent_server/confluent/plugins/hardwaremanagement/ipmi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py index b5220791..99b94021 100644 --- a/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py +++ b/confluent_server/confluent/plugins/hardwaremanagement/ipmi.py @@ -514,8 +514,11 @@ class IpmiHandler(object): raise exc.TargetEndpointUnreachable(ge.strerror) raise self.ipmicmd = persistent_ipmicmds[(node, tenant)] + giveup = util._monotonic_time() + 60 while not self.ipmicmd.ipmi_session.broken and not self.ipmicmd.ipmi_session.logged: self.ipmicmd.ipmi_session.wait_for_rsp(3) + if util._monotonic_time() > giveup: + self.ipmicmd.ipmi_session.broken = True bootdevices = { 'optical': 'cd' From 7072a85d794813e8fc6086f05179d1962eab2e84 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 26 May 2022 16:34:18 -0400 Subject: [PATCH 7/9] Transition to multi-authenticator support Provide a way to store a plurality of keys for a user. This enables use of 'backup' authenticators. --- confluent_server/confluent/webauthn.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index a11984c7..17d63579 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -18,6 +18,20 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): except Exception: pass + def get_credential_ids_by_email(self, email): + if not isinstance(email, str): + email = email.decode('utf8') + for cid in creds[email]: + yield base64.b64decode(cid) + + def get_credential_by_email_id(self, email, id): + if not isinstance(email, str): + email = email.decode('utf8') + cid = base64.b64encode(id).decode('utf8') + pk = creds[email][cid]['cpk'] + pk = base64.b64decode(pk) + return pywarp.credentials.Credential(credential_id=id, credential_public_key=pk) + def get_credential_by_email(self, email): if not isinstance(email, str): email = email.decode('utf8') @@ -29,8 +43,11 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): def save_credential_for_user(self, email, credential): if not isinstance(email, str): email = email.decode('utf8') - credential = {'cid': base64.b64encode(credential.id).decode('utf8'), 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} - creds[email] = credential + cid = base64.b64encode(credential.id).decode('utf8') + credential = {'cid': cid, 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} + if email not in creds: + creds[email] = {} + creds[email][cid] = credential with open('/tmp/mycreds.json', 'w') as jo: json.dump(creds, jo) From b638bb69f9034695c77bdcca36b6d981dac0de1d Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 26 May 2022 17:41:16 -0400 Subject: [PATCH 8/9] Change webauthn to use confluent user db This allows persistence with the rest of the configuration. --- .../confluent/config/configmanager.py | 8 ++-- confluent_server/confluent/webauthn.py | 40 +++++++++---------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index c9a8d937..e88e9483 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -2447,10 +2447,10 @@ class ConfigManager(object): uid = tmpconfig[confarea].get('id', None) displayname = tmpconfig[confarea].get('displayname', None) self.create_user(user, uid=uid, displayname=displayname) - if 'cryptpass' in tmpconfig[confarea][user]: - self._cfgstore['users'][user]['cryptpass'] = \ - tmpconfig[confarea][user]['cryptpass'] - _mark_dirtykey('users', user, self.tenant) + for attrname in ('authid', 'authenticators', 'cryptpass'): + if attrname in tmpconfig[confarea][user]: + self._cfgstore['users'][user][attrname] = tmpconfig[confarea][user][attrname] + _mark_dirtykey('users', user, self.tenant) if sync: self._bg_sync_to_file() diff --git a/confluent_server/confluent/webauthn.py b/confluent_server/confluent/webauthn.py index 17d63579..7e3b148f 100644 --- a/confluent_server/confluent/webauthn.py +++ b/confluent_server/confluent/webauthn.py @@ -6,36 +6,36 @@ import pywarp import pywarp.backends import pywarp.credentials -creds = {} challenges = {} -class TestBackend(pywarp.backends.CredentialStorageBackend): - def __init__(self): - global creds - try: - with open('/tmp/mycreds.json', 'r') as ji: - creds = json.load(ji) - except Exception: - pass +class ConfluentBackend(pywarp.backends.CredentialStorageBackend): + def __init__(self, cfg): + self.cfg = cfg def get_credential_ids_by_email(self, email): if not isinstance(email, str): email = email.decode('utf8') - for cid in creds[email]: + authenticators = self.cfg.get_user(email).get('authenticators', {}) + if not authenticators: + raise Exception('No authenticators found') + for cid in authenticators: yield base64.b64decode(cid) def get_credential_by_email_id(self, email, id): if not isinstance(email, str): email = email.decode('utf8') + authenticators = self.cfg.get_user(email).get('authenticators', {}) cid = base64.b64encode(id).decode('utf8') - pk = creds[email][cid]['cpk'] + pk = authenticators[cid]['cpk'] pk = base64.b64decode(pk) return pywarp.credentials.Credential(credential_id=id, credential_public_key=pk) def get_credential_by_email(self, email): if not isinstance(email, str): email = email.decode('utf8') - cred = creds[email] + authenticators = self.cfg.get_user(email) + cid = list(authenticators)[0] + cred = authenticators[cid] cid = base64.b64decode(cred['cid']) cpk = base64.b64decode(cred['cpk']) return pywarp.credentials.Credential(credential_id=cid, credential_public_key=cpk) @@ -45,11 +45,9 @@ class TestBackend(pywarp.backends.CredentialStorageBackend): email = email.decode('utf8') cid = base64.b64encode(credential.id).decode('utf8') credential = {'cid': cid, 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')} - if email not in creds: - creds[email] = {} - creds[email][cid] = credential - with open('/tmp/mycreds.json', 'w') as jo: - json.dump(creds, jo) + authenticators = self.cfg.get_user(email).get('authenticators', {}) + authenticators[cid] = credential + self.cfg.set_user(email, {'authenticators': authenticators}) def save_challenge_for_user(self, email, challenge, type): if not isinstance(email, str): @@ -67,7 +65,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody raise Exception('Only POST supported for webauthn operations') url = url.replace('/sessions/current/webauthn', '') if url == '/registration_options': - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm), require_attestation=False) userinfo = cfm.get_user(username) if not userinfo: cfm.create_user(username, role='Stub') @@ -89,7 +87,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody yield json.dumps(opts) elif url.startswith('/registered_credentials/'): username = url.rsplit('/', 1)[-1] - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm)) if not isinstance(username, bytes): username = username.encode('utf8') opts = rp.get_authentication_options(username) @@ -100,7 +98,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody username = url.rsplit('/', 1)[-1] if not isinstance(username, bytes): username = username.encode('utf8') - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend()) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm)) req = json.loads(reqbody) for x in req: req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) @@ -118,7 +116,7 @@ def handle_api_request(url, env, start_response, username, cfm, headers, reqbody else: yield rsp elif url == '/register_credential': - rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=TestBackend(), require_attestation=False) + rp = pywarp.RelyingPartyManager('Confluent Web UI', credential_storage_backend=ConfluentBackend(cfm), require_attestation=False) req = json.loads(reqbody) for x in req: req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/')) From 9c7e23f29e1913b9b2ce93e72d7a3320b58d1676 Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Thu, 26 May 2022 17:41:54 -0400 Subject: [PATCH 9/9] Add missing nodepower statuses --- confluent_client/bin/nodepower | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confluent_client/bin/nodepower b/confluent_client/bin/nodepower index dffa241c..ccbf41bf 100755 --- a/confluent_client/bin/nodepower +++ b/confluent_client/bin/nodepower @@ -55,7 +55,7 @@ if len(args) > 1: if setstate == 'softoff': setstate = 'shutdown' -if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset', 'pdu_status', 'pdu_stat', 'pdu_on', 'pdu_off'): +if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset', 'pdu_status', 'pdu_stat', 'pdu_on', 'pdu_off', 'status', 'stat', 'state'): argparser.print_help() sys.exit(1) session = client.Command()