Add SensitiveString field type

There are objects that define a String field to hold a serialized json
blob and that blob can have sensitive information in it (tokens,
passwords, etc). When the object is logged, repr will log that sensitive
information. This puts a burden on the code that's doing the logging to
remember to sanitize the object's string form before logging it.

It'd be simpler and cleaner to just have a field type that extends
String so it's the same in all respects except for when it's
stringified. So in SensitiveString's stringify method it just masks the
password on the field value before returning it.

And no SensitiveString does not play an acoustic guitar at your local
coffee shop, he's actually super tough and emotionally walled up, that's
why he doesn't like exposing too much information about himself in the
logs.

Closes-Bug: #1321785

Change-Id: I56466eb8b5d8000ef010f2015f453a9e74dc66b8
This commit is contained in:
Matt Riedemann
2015-10-09 09:05:33 -07:00
parent e8a758907b
commit bd977f400a
3 changed files with 46 additions and 0 deletions

View File

@@ -253,6 +253,16 @@ class String(FieldType):
return '\'%s\'' % value
class SensitiveString(String):
"""A string field type that may contain sensitive (password) information.
Passwords in the string value are masked when stringified.
"""
def stringify(self, value):
return super(SensitiveString, self).stringify(
strutils.mask_password(value))
class VersionPredicate(String):
@staticmethod
def coerce(obj, attr, value):
@@ -547,6 +557,11 @@ class StringField(AutoTypedField):
AUTO_TYPE = String()
class SensitiveStringField(AutoTypedField):
"""Field type that masks passwords when the field is stringified."""
AUTO_TYPE = SensitiveString()
class VersionPredicateField(AutoTypedField):
AUTO_TYPE = VersionPredicate()

View File

@@ -119,6 +119,17 @@ class TestString(TestField):
self.assertEqual("'123'", self.field.stringify(123))
class TestSensitiveString(TestString):
def setUp(self):
super(TestSensitiveString, self).setUp()
self.field = fields.SensitiveStringField()
def test_stringify(self):
payload = """{'admin_password':'mypassword'}"""
expected = """'{'admin_password':'***'}'"""
self.assertEqual(expected, self.field.stringify(payload))
class TestVersionPredicate(TestString):
def setUp(self):
super(TestVersionPredicate, self).setUp()

View File

@@ -144,6 +144,18 @@ class MyObj2(base.VersionedObject):
pass
@base.VersionedObjectRegistry.register_if(False)
class MySensitiveObj(base.VersionedObject):
VERSION = '1.0'
fields = {
'data': fields.SensitiveStringField(nullable=True)
}
@base.remotable_classmethod
def query(cls, *args, **kwargs):
pass
class RandomMixInWithNoFields(object):
"""Used to test object inheritance using a mixin that has no fields."""
pass
@@ -1123,6 +1135,14 @@ class _TestObject(object):
'rel_object=<?>,rel_objects=<?>,timestamp=<?>)',
repr(obj))
def test_obj_repr_sensitive(self):
obj = MySensitiveObj(data="""{'admin_password':'mypassword'}""")
self.assertEqual(
'MySensitiveObj(data=\'{\'admin_password\':\'***\'}\')', repr(obj))
obj2 = MySensitiveObj()
self.assertEqual('MySensitiveObj(data=<?>)', repr(obj2))
def test_obj_make_obj_compatible_with_relationships(self):
subobj = MyOwnedObject(baz=1)
obj = MyObj(rel_object=subobj)