2
0
mirror of https://github.com/xcat2/confluent.git synced 2024-11-25 11:01:09 +00:00

Merge branch 'webauthn'

This commit is contained in:
Jarrod Johnson 2022-05-26 17:42:15 -04:00
commit ff698ea06b
6 changed files with 191 additions and 30 deletions

View File

@ -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()

View File

@ -162,6 +162,10 @@ def authorize(name, element, tenant=False, operation='create',
return False
manager = configmanager.ConfigManager(tenant, username=user)
userobj = manager.get_user(user)
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
if not userobj:
for group in userutil.grouplist(user):
userobj = manager.get_usergroup(group)

View File

@ -113,7 +113,7 @@ _attraliases = {
'bmcpass': 'secret.hardwaremanagementpassword',
'switchpass': 'secret.hardwaremanagementpassword',
}
_validroles = ('Administrator', 'Operator', 'Monitor')
_validroles = ('Administrator', 'Operator', 'Monitor', 'Stub')
membership_callback = None
@ -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()
@ -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/')):

View File

@ -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
@ -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
@ -267,18 +269,24 @@ 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
"""
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/')):
name = element.rsplit('/')[-1]
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]
@ -326,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 = {
@ -356,17 +359,32 @@ 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}
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.
@ -607,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"}')
@ -636,7 +654,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 = ""
@ -834,6 +852,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, reqbody, authorized):
yield rsp
return
resource = '.' + url[url.rindex('/'):]
lquerydict = copy.deepcopy(querydict)
try:

View File

@ -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'

View File

@ -0,0 +1,128 @@
import base64
import confluent.tlvdata as tlvdata
import confluent.util as util
import json
import pywarp
import pywarp.backends
import pywarp.credentials
challenges = {}
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')
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 = 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')
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)
def save_credential_for_user(self, email, credential):
if not isinstance(email, str):
email = email.decode('utf8')
cid = base64.b64encode(credential.id).decode('utf8')
credential = {'cid': cid, 'cpk': base64.b64encode(bytes(credential.public_key)).decode('utf8')}
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):
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, authorized):
if env['REQUEST_METHOD'] != 'POST':
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=ConfluentBackend(cfm), require_attestation=False)
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']
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=ConfluentBackend(cfm))
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.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=ConfluentBackend(cfm))
req = json.loads(reqbody)
for x in req:
req[x] = base64.b64decode(req[x].replace('-', '+').replace('_', '/'))
req['email'] = username
rsp = rp.verify(**req)
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=ConfluentBackend(cfm), 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)