From b0855f7c8838227d399b057690f7f2bc75ba2feb Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 8 Oct 2013 17:49:38 -0400 Subject: [PATCH] Flesh out user password management --- confluent/auth.py | 96 +++++++++++++++++++++++++++++++++++++++------ confluent/config.py | 14 ++++++- 2 files changed, 97 insertions(+), 13 deletions(-) diff --git a/confluent/auth.py b/confluent/auth.py index d027889a..36722ed5 100644 --- a/confluent/auth.py +++ b/confluent/auth.py @@ -1,8 +1,39 @@ # 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 import confluent.config as config +import eventlet +import Crypto.Protocol.KDF as kdf +import Crypto.Hash as hash -def authorize(name, element, tenant=None, access='rw'): +_passcache = {} +_passchecking = {} + +def _get_usertenant(name, tenant=False): + """_get_usertenant + + Convenience function to parse name into username and tenant. + If tenant is explicitly passed in, then name must be the username + tenant name with '/' is forbidden. If '/' is seen in name, tenant + is assumed to preface the /. + If the username is a tenant name, then it is to be the implied + administrator account a tenant gets. + Otherwise, just assume a user in the default tenant + """ + if isinstance(tenant,bool): + user = name + tenant = None + elif '/' in name: + tenant, user = name.split('/', 1) + elif config.is_tenant(name): + user = name + tenant = name + else: + user = name + tenant = None + +def authorize(name, element, tenant=False, access='rw'): #TODO: actually use the element to ascertain if this user is good enough """Determine whether the given authenticated name is authorized. @@ -16,20 +47,61 @@ def authorize(name, element, tenant=None, access='rw'): and the relevant ConfigManager object for the context of the request. """ - if tenant is not None: - user = name - elif '/' in name: - tenant, user = name.split('/', 1) - elif config.is_tenant(name): - user = name - tenant = name - else: - user = name - tenant = 0 - if not config.is_tenant(tenant): + user, tenant = _get_usertenant(name, tenant) + if tenant is not None and not config.is_tenant(tenant): return None configmanager = config.ConfigManager(tenant) userobj = configmanager.get_user(user) if userobj: #returning return (userobj, configmanager) return None + + +def set_user_password(name, password, tenant=None): + """Set user password + + :param name: The unique shortname of the user + :param password: The password to set for given user + :param tenant: The tenant to which the user belongs. + """ + # TODO(jbjohnso): WORKERPOOL + # When worker pool implemented, hand off the + # PBKDF2 to a worker instead of blocking + user, tenant = _get_usertenant(name, tenant) + _passcache[(user, tenant)] = password + salt = os.urandom(8) + crypted = kdf.PBKDF2(passphrase, salt, 32, 10000, + lambda p, s: hash.HMAC.new(p, s, hash.SHA256).digest()) + cfm = config.ConfigManager(tenant) + cfm.set_user(name, { 'cryptpass': (salt, crypted) }) + + +def check_user_passphrase(name, passphrase, tenant=None): + user, tenant = _get_usertenant(name, tenant) + if (user,tenant) in _passcache: + if passphrase == passcache[(user,tenant)]: + return True + else: + # In case of someone trying to guess, + # while someone is legitimately logged in + # invalidate cache and force the slower check + del _passchache[(user, tenant)] + return False + eventlet.sleep(0.1) # limit throughput of remote guessing + while (user,tenant) in _passchecking: + # Want to serialize passphrase checking activity + # by a user, which might be malicious + # would normally make an event and wait + # but here there's no need for that + eventlet.sleep(0.5) + cfm = config.ConfigManager(tenant) + ucfg = cfm.get_user(user) + if ucfg is None or 'cryptpass' not in ucfg: + return False + _passchecking[(user, tenant)] = True + # TODO(jbjohnso): WORKERPOOL + # PBKDF2 is, by design, cpu intensive + # throw it at the worker pool when implemented + salt, crypt = ucfg['cryptpass'] + crypted = kdf.PBKDF2(passphrase, salt, 32, 10000, + lambda p, s: hash.HMAC.new(p, s, hash.SHA256).digest()) diff --git a/confluent/config.py b/confluent/config.py index ff2d973d..ab3efc81 100644 --- a/confluent/config.py +++ b/confluent/config.py @@ -235,7 +235,19 @@ class ConfigManager(object): return None - def create_user(self, name, role="Administrator", id=None, displayname=None): + def set_user(self, name, attributemap): + """Set user attribute(s) + + :param name: The login name of the user + :param attributemap: A dict of key values to set + """ + user = self._cfgstore['users'][name] + for attribute in attributemap: + user[attribute] = attributemap[attribute] + self._bg_sync_to_file() + + def create_user(self, name, + role="Administrator", id=None, displayname=None): """Create a new user :param name: The login name of the user