commit 560ef132c91c3d1d55cbb59198afc12a4790e0d3
Author: Santi Raffa <rsanti@google.com>
Date:   Fri Feb 7 09:52:05 2014 +0100

    serializer: emit and encode Private values
    
    For inbound data the simplest, safest thing to do is to traverse all
    JSON right after encoding and search for private parameters by key.
    
    This ensures that all consumers of this data get Private values
    transparently and consistently; the serializing methods don't have to
    worry about trying to understand what kind of JSON it is receiving.
    It also minimizes the surface area of codebase that is exposed to the
    values directly.
    
    The downside is it adds a ~5% time overhead to all JSON decoding as
    measured through:
    
    $ time make hs-test-py_compat_types
    
    As far as encoding to JSON is concerned, the serializer will redact
    Private values unless told to do so explicitly (e.g., for tests).
    
    Signed-off-by: Santi Raffa <rsanti@google.com>
    Reviewed-by: Jose A. Lopes <jabolopes@google.com>

diff --git a/lib/serializer.py b/lib/serializer.py
index 40edff3..f98db48 100644
--- a/lib/serializer.py
+++ b/lib/serializer.py
@@ -40,19 +40,25 @@ import simplejson
 
 from ganeti import errors
 from ganeti import utils
-
+from ganeti import constants
 
 _RE_EOLSP = re.compile("[ \t]+$", re.MULTILINE)
 
 
-def DumpJson(data):
+def DumpJson(data, private_encoder=None):
   """Serialize a given object.
 
   @param data: the data to serialize
   @return: the string representation of data
+  @param private_encoder: specify L{serializer.EncodeWithPrivateFields} if you
+                          require the produced JSON to also contain private
+                          parameters. Otherwise, they will encode to null.
 
   """
-  encoded = simplejson.dumps(data)
+  if private_encoder is None:
+    # Do not leak private fields by default.
+    private_encoder = EncodeWithoutPrivateFields
+  encoded = simplejson.dumps(data, default=private_encoder)
 
   txt = _RE_EOLSP.sub("", encoded)
   if not txt.endswith("\n"):
@@ -69,20 +75,64 @@ def LoadJson(txt):
   @raise JSONDecodeError: if L{txt} is not a valid JSON document
 
   """
-  return simplejson.loads(txt)
+  values = simplejson.loads(txt)
+
+  # Hunt and seek for Private fields and wrap them.
+  WrapPrivateValues(values)
+
+  return values
+
 
+def WrapPrivateValues(json):
+  """Crawl a JSON decoded structure for private values and wrap them.
+
+  @param json: the json-decoded value to protect.
+
+  """
+  # This function used to be recursive. I use this list to avoid actual
+  # recursion, however, since this is a very high-traffic area.
+  todo = [json]
+
+  while todo:
+    data = todo.pop()
+
+    if isinstance(data, list): # Array
+      for item in data:
+        todo.append(item)
+    elif isinstance(data, dict): # Object
+
+      # This is kind of a kludge, but the only place where we know what should
+      # be protected is in ganeti.opcodes, and not in a way that is helpful to
+      # us, especially in such a high traffic method; on the other hand, the
+      # Haskell `py_compat_fields` test should complain whenever this check
+      # does not protect fields properly.
+      for field in data:
+        value = data[field]
+        if field in constants.PRIVATE_PARAMETERS_BLACKLIST:
+          if not field.endswith("_cluster"):
+            data[field] = PrivateDict(value)
+          else:
+            for os in data[field]:
+              value[os] = PrivateDict(value[os])
+        else:
+          todo.append(value)
+    else: # Values
+      pass
 
-def DumpSignedJson(data, key, salt=None, key_selector=None):
+
+def DumpSignedJson(data, key, salt=None, key_selector=None,
+                   private_encoder=None):
   """Serialize a given object and authenticate it.
 
   @param data: the data to serialize
   @param key: shared hmac key
   @param key_selector: name/id that identifies the key (in case there are
     multiple keys in use, e.g. in a multi-cluster environment)
+  @param private_encoder: see L{DumpJson}
   @return: the string representation of data signed by the hmac key
 
   """
-  txt = DumpJson(data)
+  txt = DumpJson(data, private_encoder=private_encoder)
   if salt is None:
     salt = ""
   signed_dict = {
@@ -113,6 +163,9 @@ def LoadSignedJson(txt, key):
 
   """
   signed_dict = LoadJson(txt)
+
+  WrapPrivateValues(signed_dict)
+
   if not isinstance(signed_dict, dict):
     raise errors.SignatureError("Invalid external message")
   try:
@@ -319,3 +372,15 @@ class PrivateDict(dict):
     for key in self:
       returndict[key] = self[key].Get()
     return returndict
+
+
+def EncodeWithoutPrivateFields(obj):
+  if isinstance(obj, Private):
+    return None
+  raise TypeError(repr(obj) + " is not JSON serializable")
+
+
+def EncodeWithPrivateFields(obj):
+  if isinstance(obj, Private):
+    return obj.Get()
+  raise TypeError(repr(obj) + " is not JSON serializable")
