diff --git a/TODO b/TODO index df1db5c6..6af0e4bb 100644 --- a/TODO +++ b/TODO @@ -50,4 +50,15 @@ Traceback (most recent call last): AttributeError: 'str' object has no attribute 'iterkeys' -/detected/ collection -SLP snooping: snoop for srvrequests, likel --show power/state while session is believed to be good but will timeout \ No newline at end of file +-Config backup/restore + -seal to password +-Scenario was somehow solconnection object is 'broken' (broken=True) but the supervising console manager + never notices. The 'write()' doesn't raise an exception, just returns. + -pyghmi should except on attempt to write to broken console I think... + -need to investigate what path does not end up getting immediate notification + for disconnection + -last observed when a switch between confluent and imm was resetting in a loop + while it was trying to keep console open as much as possibl + -cs._handled_consoles[('n4', None)]._console.handle_data({'error':1}) is how I was able to + kick the session state back to working order after whatever event that was missed above + occurred. \ No newline at end of file diff --git a/confluent_server/VERSION b/confluent_server/VERSION index 0c62199f..ee1372d3 100644 --- a/confluent_server/VERSION +++ b/confluent_server/VERSION @@ -1 +1 @@ -0.2.1 +0.2.2 diff --git a/confluent_server/confluent/config/configmanager.py b/confluent_server/confluent/config/configmanager.py index 292cdca3..8a08c64d 100644 --- a/confluent_server/confluent/config/configmanager.py +++ b/confluent_server/confluent/config/configmanager.py @@ -62,11 +62,13 @@ from Crypto.Hash import HMAC from Crypto.Hash import SHA256 import anydbm as dbm import ast +import base64 import confluent.config.attributes as allattributes import confluent.util import copy import cPickle import errno +import json import operator import os import random @@ -79,7 +81,7 @@ import threading _masterkey = None _masterintegritykey = None _dirtylock = threading.RLock() - +_config_areas = ('nodegroups', 'nodes', 'usergroups', 'users') def _mkpath(pathname): try: @@ -91,62 +93,62 @@ def _mkpath(pathname): raise -def _derive_keys(passphrase, salt): +def _derive_keys(password, 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, + tmpkey = KDF.PBKDF2(password, salt, 32, 50000, lambda p, s: HMAC.new(p, s, SHA256).digest()) finalkey = KDF.PBKDF2(tmpkey, salt, 32, 50000, lambda p, s: HMAC.new(p, s, SHA256).digest()) return finalkey[:32], finalkey[32:] -def _get_protected_key(keydict, passphrase): +def _get_protected_key(keydict, password): if keydict['unencryptedvalue']: return keydict['unencryptedvalue'] # TODO(jbjohnso): check for TPM sealing if 'passphraseprotected' in keydict: - if passphrase is None: - raise Exception("Passphrase protected secret requires passhrase") + if password is None: + raise Exception("Passphrase protected secret requires password") for pp in keydict['passphraseprotected']: salt = pp[0] - privkey, integkey = _derive_keys(passphrase, salt) + privkey, integkey = _derive_keys(password, salt) return decrypt_value(pp[1:], key=privkey, integritykey=integkey) else: raise Exception("No available decryption key") -def _format_key(key, passphrase=None): - if passphrase is not None: +def _format_key(key, password=None): + if password is not None: salt = os.urandom(32) - privkey, integkey = _derive_keys(passphrase, salt) + privkey, integkey = _derive_keys(password, salt) cval = crypt_value(key, key=privkey, integritykey=integkey) return {"passphraseprotected": cval} else: return {"unencryptedvalue": key} -def init_masterkey(passphrase=None): +def init_masterkey(password=None): global _masterkey global _masterintegritykey cfgn = get_global('master_privacy_key') if cfgn: - _masterkey = _get_protected_key(cfgn, passphrase=passphrase) + _masterkey = _get_protected_key(cfgn, password=password) else: _masterkey = os.urandom(32) set_global('master_privacy_key', _format_key( _masterkey, - passphrase=passphrase)) + password=password)) cfgn = get_global('master_integrity_key') if cfgn: - _masterintegritykey = _get_protected_key(cfgn, passphrase=passphrase) + _masterintegritykey = _get_protected_key(cfgn, password=password) else: _masterintegritykey = os.urandom(64) set_global('master_integrity_key', _format_key( _masterintegritykey, - passphrase=passphrase)) + password=password)) def decrypt_value(cryptvalue, @@ -258,7 +260,7 @@ def _mark_dirtykey(category, key, tenant=None): def _generate_new_id(): - # generate a random id outside the usual ranges used for norml users in + # generate a random id outside the usual ranges used for normal users in # /etc/passwd. Leave an equivalent amount of space near the end disused, # just in case uid = str(confluent.util.securerandomnumber(65537, 4294901759)) @@ -560,7 +562,7 @@ class ConfigManager(object): self._cfgstore['usergroups'][attribute] = attributemap[attribute] _mark_dirtykey('usergroups', groupname, self.tenant) - def create_usergroup(selfself, groupname, role="Administrator"): + def create_usergroup(self, groupname, role="Administrator"): if 'usergroups' not in self._cfgstore: self._cfgstore['usergroups'] = {} groupname = groupname.encode('utf-8') @@ -569,7 +571,6 @@ class ConfigManager(object): self._cfgstore['usergroups'][groupname] = {'role': role} _mark_dirtykey('usergroups', groupname, self.tenant) - def set_user(self, name, attributemap): """Set user attribute(s) @@ -1054,30 +1055,69 @@ class ConfigManager(object): self._bg_sync_to_file() #TODO: wait for synchronization to suceed/fail??) + def _dump_to_json(self, redact=None): + """Dump the configuration in json form to output + + password is used to protect the 'secret' attributes in liue of the + actual in-configuration master key (which will have no clear form + in the dump + + :param redact: If True, then sensitive password data will be redacted. + Other values may be used one day to redact in more + complex and interesting ways for non-secret + data. + + """ + dumpdata = {} + for confarea in _config_areas: + if confarea not in self._cfgstore: + continue + dumpdata[confarea] = {} + for element in self._cfgstore[confarea].iterkeys(): + dumpdata[confarea][element] = \ + copy.deepcopy(self._cfgstore[confarea][element]) + for attribute in self._cfgstore[confarea][element].iterkeys(): + if 'inheritedfrom' in dumpdata[confarea][element][attribute]: + del dumpdata[confarea][element][attribute] + elif (attribute == 'cryptpass' or + 'cryptvalue' in + dumpdata[confarea][element][attribute]): + if redact is not None: + dumpdata[confarea][element][attribute] = '*REDACTED*' + else: + if attribute == 'cryptpass': + target = dumpdata[confarea][element][attribute] + else: + target = dumpdata[confarea][element][attribute]['cryptvalue'] + cryptval = [] + for value in target: + cryptval.append(base64.b64encode(value)) + if attribute == 'cryptpass': + dumpdata[confarea][element][attribute] = '!'.join(cryptval) + else: + dumpdata[confarea][element][attribute]['cryptvalue'] = '!'.join(cryptval) + elif isinstance(dumpdata[confarea][element][attribute], set): + dumpdata[confarea][element][attribute] = \ + list(dumpdata[confarea][element][attribute]) + return json.dumps( + dumpdata, sort_keys=True, indent=4, separators=(',', ': ')) + + + @classmethod def _read_from_path(cls): global _cfgstore _cfgstore = {} rootpath = cls._cfgdir _load_dict_from_dbm(['globals'], rootpath + "/globals") - _load_dict_from_dbm(['main', 'nodes'], rootpath + "/nodes") - _load_dict_from_dbm(['main', 'users'], rootpath + "/users") - _load_dict_from_dbm(['main', 'nodegroups'], rootpath + "/nodegroups") - _load_dict_from_dbm(['main', 'usergroups'], rootpath + "/usergroups") + for confarea in _config_areas: + _load_dict_from_dbm(['main', confarea], rootpath + "/" + confarea) try: for tenant in os.listdir(rootpath + '/tenants/'): - _load_dict_from_dbm( - ['main', tenant, 'nodes'], - "%s/%s/nodes" % (rootpath, tenant)) - _load_dict_from_dbm( - ['main', tenant, 'nodegroups'], - "%s/%s/groups" % (rootpath, tenant)) - _load_dict_from_dbm( - ['main', tenant, 'users'], - "%s/%s/users" % (rootpath, tenant)) - _load_dict_from_dbm( - ['main', tenant, 'usergroups'], - "%s/%s/usergroups" % (rootpath, tenant)) + for confarea in _config_areas: + _load_dict_from_dbm( + ['main', tenant, confarea], + "%s/%s/%s" % (rootpath, tenant, confarea)) except OSError: pass @@ -1149,6 +1189,33 @@ class ConfigManager(object): self._recalculate_expressions(cfgobj[key], formatter, node, changeset) +def _dump_keys(password): + if _masterkey is None or _masterintegritykey is None: + init_masterkey() + cryptkey = _format_key(_masterkey, password=password) + cryptkey = '!'.join(map(base64.b64encode, cryptkey['passphraseprotected'])) + integritykey = _format_key(_masterintegritykey, password=password) + integritykey = '!'.join(map(base64.b64encode, integritykey['passphraseprotected'])) + return json.dumps({'cryptkey': cryptkey, 'integritykey': integritykey}, + sort_keys=True, indent=4, separators=(',', ': ')) + + +def dump_db_to_directory(location, password, redact=None): + with open(os.path.join(location, 'keys.json'), 'w') as cfgfile: + cfgfile.write(_dump_keys(password)) + cfgfile.write('\n') + with open(os.path.join(location, 'main.json'), 'w') as cfgfile: + cfgfile.write(ConfigManager(tenant=None)._dump_to_json(redact=redact)) + cfgfile.write('\n') + try: + for tenant in os.listdir(ConfigManager._cfgdir + '/tenants/'): + with open(os.path.join(location, tenant + '.json'), 'w') as cfgfile: + cfgfile.write(ConfigManager(tenant=tenant)._dump_to_json( + redact=redact)) + cfgfile.write('\n') + except OSError: + pass + try: ConfigManager._read_from_path()