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:
commit
ff698ea06b
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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/')):
|
||||
|
@ -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:
|
||||
|
@ -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'
|
||||
|
128
confluent_server/confluent/webauthn.py
Normal file
128
confluent_server/confluent/webauthn.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user