diff --git a/bin/confetty b/bin/confetty index eb32bc50..3ef592bf 100755 --- a/bin/confetty +++ b/bin/confetty @@ -313,7 +313,7 @@ def setvalues(attribs): else: # an actual resource resource = attribs[0] attribs = attribs[1:] - keydata = paramaterize_attribs(attribs) + keydata = parameterize_attribs(attribs) if not keydata: return targpath = fullpath_target(resource) diff --git a/confluent/auth.py b/confluent/auth.py index 52d98f05..e9cced74 100644 --- a/confluent/auth.py +++ b/confluent/auth.py @@ -92,26 +92,6 @@ def authorize(name, element, tenant=False, access='rw'): return None -def set_user_passphrase(name, passphrase, tenant=None): - """Set user passphrase - - :param name: The unique shortname of the user - :param passphrase: The passphrase 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)] = passphrase - salt = os.urandom(8) - crypted = kdf.PBKDF2(passphrase, salt, 32, 10000, - lambda p, s: hash.HMAC.new(p, s, hash.SHA256).digest() - ) - cfm = configmanager.ConfigManager(tenant) - cfm.set_user(name, {'cryptpass': (salt, crypted)}) - - def check_user_passphrase(name, passphrase, element=None, tenant=False): """Check a a login name and passphrase for authenticity and authorization diff --git a/confluent/config/attributes.py b/confluent/config/attributes.py index 7578d104..6e24a274 100644 --- a/confluent/config/attributes.py +++ b/confluent/config/attributes.py @@ -61,6 +61,11 @@ nic = { }, } +user = { + 'passphrase': { + 'description': 'The passphrase used to authenticate this user' + }, +} # 'node', which can be considered a 'system' or a 'vm' node = { diff --git a/confluent/config/configmanager.py b/confluent/config/configmanager.py index 5aa5828f..d47c7c62 100644 --- a/confluent/config/configmanager.py +++ b/confluent/config/configmanager.py @@ -98,6 +98,7 @@ def _mkpath(pathname): def _derive_keys(passphrase, salt): #implement our specific combination of pbkdf2 transforms to get at #key. We bump the iterations up because we can afford to + #TODO: WORKERPOOL PBKDF2 is expensive tmpkey = kdf.PBKDF2(passphrase, salt, 32, 50000, lambda p, s: HMAC.new(p, s, SHA256).digest()) finalkey = kdf.PBKDF2(tmpkey, salt, 32, 50000, @@ -246,7 +247,8 @@ def set_global(globalname, value): def _mark_dirtykey(category, key, tenant=None): - key = key.encode('utf-8') + if type(key) in (str, unicode): + key = key.encode('utf-8') with _dirtylock: if 'dirtykeys' not in _cfgstore: _cfgstore['dirtykeys'] = {} @@ -261,11 +263,11 @@ def _generate_new_id(): # generate a random id outside the usual ranges used for norml users in # /etc/passwd. Leave an equivalent amount of space near the end disused, # just in case - id = confluent.util.securerandomnumber(65537, 4294901759) - if 'idmap' not in _cfgstore: + id = str(confluent.util.securerandomnumber(65537, 4294901759)) + if 'idmap' not in _cfgstore['main']: return id - while id in _cfgstore['idmap']: - id = confluent.util.securerandomnumber(65537, 4294901759) + while id in _cfgstore['main']['idmap']: + id = str(confluent.util.securerandomnumber(65537, 4294901759)) return id class _ExpressionFormat(string.Formatter): @@ -483,6 +485,9 @@ class ConfigManager(object): del self._notifierids[watcher] + def list_users(self): + return self._cfgstore['users'].iterkeys() + def get_user(self, name): """Get user information from DB @@ -509,11 +514,21 @@ class ConfigManager(object): user = self._cfgstore['users'][name] _mark_dirtykey('users', name, self.tenant) for attribute in attributemap: - user[attribute] = attributemap[attribute] + if attribute == 'passphrase': + salt = os.urandom(8) + #TODO: WORKERPOOL, offload password set to a worker + crypted = kdf.PBKDF2( + attributemap[attribute], salt, 32, 10000, + lambda p, s: HMAC.new(p, s, SHA256).digest() + ) + user['cryptpass'] = (salt, crypted) + else: + user[attribute] = attributemap[attribute] self._bg_sync_to_file() def create_user(self, name, - role="Administrator", id=None, displayname=None): + role="Administrator", id=None, displayname=None, + attributemap=None): """Create a new user :param name: The login name of the user @@ -526,7 +541,7 @@ class ConfigManager(object): if id is None: id = _generate_new_id() else: - if id in _cfgstore['idmap']: + if id in _cfgstore['main']['idmap']: raise Exception("Duplicate id requested") if 'users' not in self._cfgstore: self._cfgstore['users'] = { } @@ -537,13 +552,15 @@ class ConfigManager(object): self._cfgstore['users'][name] = {'id': id} if displayname is not None: self._cfgstore['users'][name]['displayname'] = displayname - if 'idmap' not in _cfgstore: - _cfgstore['idmap'] = {} + if 'idmap' not in _cfgstore['main']: + _cfgstore['main']['idmap'] = {} _mark_dirtykey('idmap', id) - _cfgstore['idmap'][id] = { + _cfgstore['main']['idmap'][id] = { 'tenant': self.tenant, 'username': name } + if attributemap is not None: + self.set_user(name, attributemap) self._bg_sync_to_file() def is_node(self, node): @@ -555,7 +572,7 @@ class ConfigManager(object): def get_groups(self): return self._cfgstore['groups'].iterkeys() - def get_nodes(self): + def list_nodes(self): return self._cfgstore['nodes'].iterkeys() def get_nodegroup_attributes(self, nodegroup, attributes=[]): diff --git a/confluent/messages.py b/confluent/messages.py index 6a512b5d..1ded84b6 100644 --- a/confluent/messages.py +++ b/confluent/messages.py @@ -164,7 +164,7 @@ class ChildCollection(LinkRelation): def get_input_message(path, operation, inputdata, nodes=None): if path[0] == 'power' and path[1] == 'state' and operation != 'retrieve': return InputPowerMessage(path, nodes, inputdata) - elif path[0] == 'attributes' and operation != 'retrieve': + elif path[0] in ('attributes', 'users') and operation != 'retrieve': return InputAttributes(path, inputdata, nodes) elif path == ['boot', 'nextdevice'] and operation != 'retrieve': return InputBootDevice(path, nodes, inputdata) @@ -326,7 +326,7 @@ class PowerState(ConfluentChoiceMessage): class Attributes(ConfluentMessage): - def __init__(self, node=None, kv=None, desc=''): + def __init__(self, name=None, kv=None, desc=''): self.desc = desc nkv = {} for key in kv.iterkeys(): @@ -334,11 +334,11 @@ class Attributes(ConfluentMessage): nkv[key] = {'value': kv[key]} else: nkv[key] = kv[key] - if node is None: + if name is None: self.kvpairs = nkv else: self.kvpairs = { - node: nkv + name: nkv } diff --git a/confluent/pluginapi.py b/confluent/pluginapi.py index 4f7f2099..6ecfa9bb 100644 --- a/confluent/pluginapi.py +++ b/confluent/pluginapi.py @@ -32,6 +32,7 @@ # functions. Console is special and just get's passed through # see API.txt +import confluent.config.attributes as attrscheme import confluent.interface.console as console import confluent.exceptions as exc import confluent.messages as msg @@ -73,7 +74,7 @@ def load_plugins(): pluginmap[plugin] = tmpmod -rootcollections = ['nodes/', 'groups/'] +rootcollections = ['nodes/', 'groups/', 'users/'] class PluginRoute(object): @@ -117,15 +118,47 @@ nodegroupresources = { } +def create_user(inputdata, configmanager): + try: + username = inputdata['name'] + del inputdata['name'] + except (KeyError, ValueError): + raise exc.InvalidArgumentException() + configmanager.create_user(username, attributemap=inputdata) + + +def update_user(name, attribmap, configmanager): + try: + configmanager.set_user(name, attribmap) + except ValueError: + raise exc.InvalidArgumentException() + +def show_user(name, configmanager): + userobj = configmanager.get_user(name) + rv = {} + for attr in attrscheme.user.iterkeys(): + rv[attr] = None + if attr == 'passphrase': + if 'cryptpass' in userobj: + rv['passphrase'] = {'cryptvalue': True} + yield msg.CryptedAttributes(kv={'passphrase': rv['passphrase']}, + desc=attrscheme.user[attr]['description']) + else: + if attr in userobj: + rv[attr] = userobj[attr] + yield msg.Attributes(kv={attr: rv[attr]}, + desc=attrscheme.user[attr]['description']) + + def stripnode(iterablersp, node): for i in iterablersp: i.strip_node(node) yield i -def iterate_collections(iterable): +def iterate_collections(iterable, forcecollection=True): for coll in iterable: - if coll[-1] != '/': + if forcecollection and coll[-1] != '/': coll = coll + '/' yield msg.ChildCollection(coll, candelete=True) @@ -166,7 +199,7 @@ def enumerate_nodegroup_collection(collectionpath, configmanager): def enumerate_node_collection(collectionpath, configmanager): if collectionpath == ['nodes']: # it is just '/node/', need a list of nodes - return iterate_collections(configmanager.get_nodes()) + return iterate_collections(configmanager.list_nodes()) node = collectionpath[1] if not configmanager.is_node(node): raise exc.NotFoundException("Invalid element requested") @@ -256,7 +289,7 @@ def handle_path(path, operation, configmanager, inputdata=None): if operation == "create": inputdata = msg.InputAttributes(pathcomponents, inputdata) create_node(inputdata.attribs, configmanager) - return iterate_collections(configmanager.get_nodes()) + return iterate_collections(configmanager.list_nodes()) if len(pathcomponents) == 2: iscollection = True if iscollection: @@ -296,5 +329,24 @@ def handle_path(path, operation, configmanager, inputdata=None): return passvalue else: return stripnode(passvalue, node) + elif pathcomponents[0] == 'users': + try: + user = pathcomponents[1] + except IndexError: # it's just users/ + if operation == 'create': + inputdata = msg.get_input_message( + pathcomponents, operation, inputdata) + create_user(inputdata.attribs, configmanager) + return iterate_collections(configmanager.list_users(), forcecollection=False) + inputdata = msg.get_input_message( + pathcomponents, operation, inputdata) + if operation == 'retrieve': + return show_user(user, configmanager) + elif operation == 'delete': + delete_user(user, configmanager) + return show_user(user, configmanager) + elif operation == 'update': + update_user(user, inputdata.attribs, configmanager) + return show_user(user, configmanager) else: raise exc.NotFoundException()