diff --git a/oslo_utils/strutils.py b/oslo_utils/strutils.py index 342b398..af55d9d 100644 --- a/oslo_utils/strutils.py +++ b/oslo_utils/strutils.py @@ -287,6 +287,79 @@ def mask_password(message, secret="***"): # nosec return message +def mask_dict_password(dictionary, secret="***"): # nosec + """Replace password with *secret* in a dictionary recursively. + + :param dictionary: The dictionary which includes secret information. + :param secret: value with which to replace secret information. + :returns: The dictionary with string substitutions. + + A dictionary (which may contain nested dictionaries) contains + information (such as passwords) which should not be revealed, and + this function helps detect and replace those with the 'secret' + provided (or '***' if none is provided). + + Substitution is performed in one of three situations: + + If the key is something that is considered to be indicative of a + secret, then the corresponding value is replaced with the secret + provided (or '***' if none is provided). + + If a value in the dictionary is a string, then it is masked + using the mask_password() function. + + Finally, if a value is a dictionary, this function will + recursively mask that dictionary as well. + + For example: + + >>> mask_dict_password({'password': 'd81juxmEW_', + >>> 'user': 'admin', + >>> 'home-dir': '/home/admin'}, + >>> '???') + {'password': '???', 'user': 'admin', 'home-dir': '/home/admin'} + + For example (the value is masked using mask_password()) + + >>> mask_dict_password({'password': '--password d81juxmEW_', + >>> 'user': 'admin', + >>> 'home-dir': '/home/admin'}, + >>> '???') + {'password': '--password ???', 'user': 'admin', + 'home-dir': '/home/admin'} + + + For example (a nested dictionary is masked): + + >>> mask_dict_password({"nested": {'password': 'd81juxmEW_', + >>> 'user': 'admin', + >>> 'home': '/home/admin'}}, + >>> '???') + {"nested": {'password': '???', 'user': 'admin', 'home': '/home/admin'}} + + .. versionadded:: 3.4 + + """ + + if not isinstance(dictionary, dict): + raise TypeError("Expected a dictionary, got %s instead." + % type(dictionary)) + + out = {} + + for k, v in dictionary.items(): + if isinstance(v, dict): + v = mask_dict_password(v, secret=secret) + elif k in _SANITIZE_KEYS: + v = secret + elif isinstance(v, six.string_types): + v = mask_password(v, secret=secret) + + out[k] = v + + return out + + def is_int_like(val): """Check if a value looks like an integer with base 10. diff --git a/oslo_utils/tests/test_strutils.py b/oslo_utils/tests/test_strutils.py index 7864c39..eba10c9 100644 --- a/oslo_utils/tests/test_strutils.py +++ b/oslo_utils/tests/test_strutils.py @@ -587,6 +587,55 @@ class MaskPasswordTestCase(test_base.BaseTestCase): self.assertEqual(expected, strutils.mask_password(payload)) +class MaskDictionaryPasswordTestCase(test_base.BaseTestCase): + + def test_dictionary(self): + payload = {'password': 'mypassword'} + expected = {'password': '***'} + self.assertEqual(expected, + strutils.mask_dict_password(payload)) + + payload = {'user': 'admin', 'password': 'mypassword'} + expected = {'user': 'admin', 'password': '***'} + self.assertEqual(expected, + strutils.mask_dict_password(payload)) + + payload = {'strval': 'somestring', + 'dictval': {'user': 'admin', 'password': 'mypassword'}} + expected = {'strval': 'somestring', + 'dictval': {'user': 'admin', 'password': '***'}} + self.assertEqual(expected, + strutils.mask_dict_password(payload)) + + payload = {'strval': '--password abc', + 'dont_change': 'this is fine', + 'dictval': {'user': 'admin', 'password': b'mypassword'}} + expected = {'strval': '--password ***', + 'dont_change': 'this is fine', + 'dictval': {'user': 'admin', 'password': '***'}} + self.assertEqual(expected, + strutils.mask_dict_password(payload)) + + def test_do_no_harm(self): + payload = {} + expected = {} + self.assertEqual(expected, + strutils.mask_dict_password(payload)) + + payload = {'somekey': 'somevalue', + 'anotherkey': 'anothervalue'} + expected = {'somekey': 'somevalue', + 'anotherkey': 'anothervalue'} + self.assertEqual(expected, + strutils.mask_dict_password(payload)) + + def test_mask_values(self): + payload = {'somekey': 'test = cmd --password my\xe9\x80\x80pass'} + expected = {'somekey': 'test = cmd --password ***'} + self.assertEqual(expected, + strutils.mask_dict_password(payload)) + + class IsIntLikeTestCase(test_base.BaseTestCase): def test_is_int_like_true(self): self.assertTrue(strutils.is_int_like(1))