mirror of
https://github.com/xcat2/confluent.git
synced 2025-02-16 10:39:23 +00:00
Implement session cookie for optional use by clients to guard against attempts to slow session
Implement http and console session reaping in httpapi layer
This commit is contained in:
parent
dd111ffef5
commit
7f572fd164
@ -1,6 +1,7 @@
|
||||
# authentication and authorization routines for confluent
|
||||
# authentication scheme caches password values to help HTTP Basic auth
|
||||
# the PBKDF2 transform is skipped if a user has been idle for sufficient time
|
||||
# authentication scheme caches passphrase values to help HTTP Basic auth
|
||||
# the PBKDF2 transform is skipped unless a user has been idle for sufficient
|
||||
# time
|
||||
|
||||
import confluent.config as config
|
||||
import eventlet
|
||||
@ -35,15 +36,16 @@ def _get_usertenant(name, tenant=False):
|
||||
administrator account a tenant gets.
|
||||
Otherwise, just assume a user in the default tenant
|
||||
"""
|
||||
if isinstance(tenant,bool):
|
||||
if not isinstance(tenant,bool):
|
||||
# if not boolean, it must be explicit tenant
|
||||
user = name
|
||||
tenant = None
|
||||
elif '/' in name:
|
||||
elif '/' in name: # tenant scoped name
|
||||
tenant, user = name.split('/', 1)
|
||||
elif config.is_tenant(name):
|
||||
# the account is the implicit tenant owner account
|
||||
user = name
|
||||
tenant = name
|
||||
else:
|
||||
else: # assume it is a non-tenant user account
|
||||
user = name
|
||||
tenant = None
|
||||
yield user
|
||||
@ -73,7 +75,7 @@ def authorize(name, element, tenant=False, access='rw'):
|
||||
return None
|
||||
|
||||
|
||||
def set_user_password(name, passphrase, tenant=None):
|
||||
def set_user_passphrase(name, passphrase, tenant=None):
|
||||
"""Set user passphrase
|
||||
|
||||
:param name: The unique shortname of the user
|
||||
@ -92,7 +94,27 @@ def set_user_password(name, passphrase, tenant=None):
|
||||
cfm.set_user(name, { 'cryptpass': (salt, crypted) })
|
||||
|
||||
|
||||
def check_user_passphrase(name, passphrase, tenant=None):
|
||||
def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
"""Check a a login name and passphrase for authenticity and authorization
|
||||
|
||||
The function combines authentication and authorization into one function.
|
||||
It is highly recommended for a session layer to provide some secure means
|
||||
of protecting a session once this function works once and calling
|
||||
authorize() in order to provide best performance regardless of
|
||||
circumstance. The function makes effort to provide good performance
|
||||
in repeated invocation, but that facility will slow down to deter
|
||||
detected passphrase guessing activity when such activity is detected.
|
||||
|
||||
:param name: The login name provided by client
|
||||
:param passhprase: The passphrase provided by client
|
||||
:param element: Optional specification of a particular destination
|
||||
:param tenant: Optional explicit indication of tenant (defaults to
|
||||
embedded in name)
|
||||
"""
|
||||
# If there is any sign of guessing on a user, all valid and
|
||||
# invalid attempts are equally slowed to no more than 20 per second
|
||||
# for that particular user.
|
||||
# similarly, guessing usernames is throttled to 20/sec
|
||||
user, tenant = _get_usertenant(name, tenant)
|
||||
while (user,tenant) in _passchecking:
|
||||
# Want to serialize passphrase checking activity
|
||||
@ -102,18 +124,18 @@ def check_user_passphrase(name, passphrase, tenant=None):
|
||||
eventlet.sleep(0.5)
|
||||
if (user,tenant) in _passcache:
|
||||
if passphrase == _passcache[(user,tenant)]:
|
||||
return True
|
||||
return authorize(user, element, tenant)
|
||||
else:
|
||||
# In case of someone trying to guess,
|
||||
# while someone is legitimately logged in
|
||||
# invalidate cache and force the slower check
|
||||
del _passcache[(user, tenant)]
|
||||
return False
|
||||
eventlet.sleep(0.1) # limit throughput of remote guessing
|
||||
return None
|
||||
cfm = config.ConfigManager(tenant)
|
||||
ucfg = cfm.get_user(user)
|
||||
if ucfg is None or 'cryptpass' not in ucfg:
|
||||
return False
|
||||
eventlet.sleep(0.05) #stall even on test for existance of a username
|
||||
return None
|
||||
_passchecking[(user, tenant)] = True
|
||||
# TODO(jbjohnso): WORKERPOOL
|
||||
# PBKDF2 is, by design, cpu intensive
|
||||
@ -122,8 +144,10 @@ def check_user_passphrase(name, passphrase, tenant=None):
|
||||
crypted = kdf.PBKDF2(passphrase, salt, 32, 10000,
|
||||
lambda p, s: hash.HMAC.new(p, s, hash.SHA256).digest())
|
||||
del _passchecking[(user, tenant)]
|
||||
eventlet.sleep(0.05) #either way, we want to stall so that client can't
|
||||
# determine failure because there is a delay, valid response will
|
||||
# delay as well
|
||||
if crypt == crypted:
|
||||
_passcache[(user, tenant)] = passphrase
|
||||
return True
|
||||
return False
|
||||
|
||||
return authorize(user, element, tenant)
|
||||
return None
|
||||
|
@ -4,6 +4,7 @@
|
||||
# It additionally manages httprequest console sessions as supported by
|
||||
# shillinabox javascript
|
||||
import base64
|
||||
import Cookie
|
||||
import confluent.console as console
|
||||
import confluent.auth as auth
|
||||
import confluent.util as util
|
||||
@ -11,16 +12,34 @@ import eventlet
|
||||
import json
|
||||
import os
|
||||
import string
|
||||
import time
|
||||
import urlparse
|
||||
import eventlet.wsgi
|
||||
#scgi = eventlet.import_patched('flup.server.scgi')
|
||||
|
||||
|
||||
consolesessions = {}
|
||||
httpsessions = {}
|
||||
|
||||
|
||||
def _get_query_dict(qstring, reqbody, reqtype):
|
||||
def _sessioncleaner():
|
||||
while (1):
|
||||
currtime = time.time()
|
||||
for session in httpsessions.keys():
|
||||
if httpsessions[session]['expiry'] < currtime:
|
||||
del httpsessions[session]
|
||||
for session in consolesessions.keys():
|
||||
if consolesessions[session]['expiry'] < currtime:
|
||||
del consolesessions[session]
|
||||
eventlet.sleep(10)
|
||||
|
||||
|
||||
def _get_query_dict(env, reqbody, reqtype):
|
||||
qdict = {}
|
||||
try:
|
||||
qstring = env['QUERY_STRING']
|
||||
except KeyError:
|
||||
qstring = None
|
||||
if qstring:
|
||||
for qpair in qstring.split('&'):
|
||||
qkey, qvalue = qpair.split('=')
|
||||
@ -37,17 +56,37 @@ def _authorize_request(env):
|
||||
"""Grant/Deny access based on data from wsgi env
|
||||
|
||||
"""
|
||||
if 'REMOTE_USER' in env: # HTTP Basic auth passed
|
||||
user = env['REMOTE_USER']
|
||||
#TODO: actually pass in the element
|
||||
authdata = auth.authorize(user, element=None)
|
||||
if authdata is None:
|
||||
return {'code': 401}
|
||||
else:
|
||||
return {'code': 200,
|
||||
'cfgmgr': authdata[1],
|
||||
'userdata': authdata[0]}
|
||||
|
||||
authdata = False
|
||||
cookie = Cookie.SimpleCookie()
|
||||
if 'HTTP_COOKIE' in env:
|
||||
#attempt to use the cookie. If it matches
|
||||
cc = Cookie.SimpleCookie()
|
||||
cc.load(env['HTTP_COOKIE'])
|
||||
if 'confluentsessionid' in cc:
|
||||
sessionid = cc['confluentsessionid'].value
|
||||
if sessionid in httpsessions:
|
||||
httpsessions[sessionid]['expiry'] = time.time() + 90
|
||||
name = httpsessions[sessionid]['name']
|
||||
authdata = auth.authorize(name, element=None)
|
||||
if authdata is False and 'HTTP_AUTHORIZATION' in env:
|
||||
name, passphrase = base64.b64decode(
|
||||
env['HTTP_AUTHORIZATION'].replace('Basic ','')).split(':',1)
|
||||
authdata = auth.check_user_passphrase(name, passphrase, element=None)
|
||||
sessid = util.randomstring(32)
|
||||
while sessid in httpsessions:
|
||||
sessid = util.randomstring(32)
|
||||
httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90}
|
||||
cookie['confluentsessionid']=sessid
|
||||
cookie['confluentsessionid']['secure'] = 1
|
||||
cookie['confluentsessionid']['httponly'] = 1
|
||||
cookie['confluentsessionid']['path'] = '/'
|
||||
if authdata:
|
||||
return {'code': 200,
|
||||
'cookie': cookie,
|
||||
'cfgmgr': authdata[1],
|
||||
'userdata': authdata[0]}
|
||||
else:
|
||||
return {'code': 401}
|
||||
# TODO(jbjohnso): actually evaluate the request for authorization
|
||||
# In theory, the x509 or http auth stuff will get translated and then
|
||||
# passed on to the core authorization function in an appropriate form
|
||||
@ -77,10 +116,11 @@ def _pick_mimetype(env):
|
||||
|
||||
|
||||
def _assign_consessionid(consolesession):
|
||||
sessid = util.randomstring(20)
|
||||
sessid = util.randomstring(32)
|
||||
while sessid in consolesessions.keys():
|
||||
sessid = util.randomstring(20)
|
||||
consolesessions[sessid] = consolesession
|
||||
sessid = util.randomstring(32)
|
||||
consolesessions[sessid] = {'session': consolesession,
|
||||
'expiry': time.time() + 60}
|
||||
return sessid
|
||||
|
||||
def resourcehandler(env, start_response):
|
||||
@ -96,17 +136,20 @@ def resourcehandler(env, start_response):
|
||||
if authorized['code'] == 401:
|
||||
start_response('401 Authentication Required',
|
||||
[('Content-type', 'text/plain'),
|
||||
('WWW-Authenticate', 'Basic realm="confluent"')])
|
||||
('WWW-Authenticate', 'Basic realm="confluent"')])
|
||||
return 'authentication required'
|
||||
if authorized['code'] == 403:
|
||||
start_response('403 Forbidden',
|
||||
[('Content-type', 'text/plain'),
|
||||
('WWW-Authenticate', 'Basic realm="confluent"')])
|
||||
('WWW-Authenticate', 'Basic realm="confluent"')])
|
||||
return 'authorization failed'
|
||||
if authorized['code'] != 200:
|
||||
raise Exception("Unrecognized code from auth engine")
|
||||
headers = [('Content-Type', 'application/json; charset=utf-8')]
|
||||
headers.extend(("Set-Cookie", m.OutputString())
|
||||
for m in authorized['cookie'].values())
|
||||
cfgmgr = authorized['cfgmgr']
|
||||
querydict = _get_query_dict(env['QUERY_STRING'], reqbody, reqtype)
|
||||
querydict = _get_query_dict(env, reqbody, reqtype)
|
||||
if '/console/session' in env['PATH_INFO']:
|
||||
#hard bake JSON into this path, do not support other incarnations
|
||||
prefix, _, _ = env['PATH_INFO'].partition('/console/session')
|
||||
@ -116,11 +159,10 @@ def resourcehandler(env, start_response):
|
||||
consession = console.ConsoleSession(node=nodename,
|
||||
configmanager=cfgmgr)
|
||||
if not consession:
|
||||
start_response("500 Internal Server Error", [])
|
||||
start_response("500 Internal Server Error", headers)
|
||||
return
|
||||
sessid = _assign_consessionid(consession)
|
||||
start_response('200 OK', [('Content-Type',
|
||||
'application/json; charset=utf-8')])
|
||||
start_response('200 OK', headers)
|
||||
return ['{"session":"%s","data":""}' % sessid]
|
||||
elif 'keys' in querydict.keys():
|
||||
# client wishes to push some keys into the remote console
|
||||
@ -128,24 +170,24 @@ def resourcehandler(env, start_response):
|
||||
for idx in xrange(0, len(querydict['keys']), 2):
|
||||
input += chr(int(querydict['keys'][idx:idx+2],16))
|
||||
sessid = querydict['session']
|
||||
consolesessions[sessid].write(input)
|
||||
start_response('200 OK', [('Content-Type',
|
||||
'application/json; charset=utf-8')])
|
||||
consolesessions[sessid]['expiry'] = time.time() + 90
|
||||
consolesessions[sessid]['session'].write(input)
|
||||
start_response('200 OK', headers)
|
||||
return [] # client has requests to send or receive, not both...
|
||||
else: #no keys, but a session, means it's hooking to receive data
|
||||
sessid = querydict['session']
|
||||
outdata = consolesessions[sessid].get_next_output(timeout=45)
|
||||
consolesessions[sessid]['expiry'] = time.time() + 90
|
||||
outdata = consolesessions[sessid]['session'].get_next_output(timeout=45)
|
||||
try:
|
||||
rsp = json.dumps({'session': querydict['session'], 'data': outdata})
|
||||
except UnicodeDecodeError:
|
||||
rsp = json.dumps({'session': querydict['session'], 'data': outdata}, encoding='cp437')
|
||||
except UnicodeDecodeError:
|
||||
rsp = json.dumps({'session': querydict['session'], 'data': 'DECODEERROR'})
|
||||
start_response('200 OK', [('Content-Type',
|
||||
'application/json; charset=utf-8')])
|
||||
start_response('200 OK', headers)
|
||||
return [rsp]
|
||||
start_response('404 Not Found', [])
|
||||
return ["Unrecognized directive (404)"]
|
||||
start_response('404 Not Found', headers)
|
||||
return ["404 Unrecognized resource"]
|
||||
|
||||
|
||||
def serve():
|
||||
@ -166,6 +208,8 @@ class HttpApi(object):
|
||||
def start(self):
|
||||
self.server = eventlet.spawn(serve)
|
||||
|
||||
_cleaner = eventlet.spawn(_sessioncleaner)
|
||||
|
||||
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user