2
0
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:
Jarrod Johnson 2013-10-09 16:17:37 -04:00
parent dd111ffef5
commit 7f572fd164
2 changed files with 112 additions and 44 deletions

View File

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

View File

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