From fd0351aef246f5d36e641209429e2ec093d325f8 Mon Sep 17 00:00:00 2001
From: Michael Hanselmann <hansmi@google.com>
Date: Fri, 23 Sep 2011 16:26:10 +0200
Subject: [PATCH] serializer: Fail if dictionary uses invalid keys
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

JSON only supports a very restricted set of types for dictionary keys,
among them strings, booleans and β€œnull”. Integers and floats are
converted to strings. Since this can cause a lot of confusion in Python,
this check raises an exception if a caller tries to use such types.

Since the pre-Python 2.6 β€œsimplejson” module doesn't support overriding
the function where the conversion takes place this check can only be
done for the newer β€œjson” module.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: RenΓ© Nussbaumer <rn@google.com>
---
 lib/serializer.py                  | 22 +++++++++++++++++++++-
 test/ganeti.serializer_unittest.py | 21 +++++++++++++++++++++
 2 files changed, 42 insertions(+), 1 deletion(-)

diff --git a/lib/serializer.py b/lib/serializer.py
index 5b806759c..090ae00b6 100644
--- a/lib/serializer.py
+++ b/lib/serializer.py
@@ -29,14 +29,18 @@ backend (currently json).
 # C0103: Invalid name, since pylint doesn't see that Dump points to a
 # function and not a constant
 
+_OLD_SIMPLEJSON = False
+
 try:
   import json
 except ImportError:
   # The "json" module was only added in Python 2.6. Earlier versions must use
   # the separate "simplejson" module.
   import simplejson as json
+  _OLD_SIMPLEJSON = True
 
 import re
+import logging
 
 from ganeti import errors
 from ganeti import utils
@@ -47,7 +51,23 @@ _JSON_INDENT = 2
 _RE_EOLSP = re.compile("[ \t]+$", re.MULTILINE)
 
 
-def _GetJsonDumpers(_encoder_class=json.JSONEncoder):
+class _CustomJsonEncoder(json.JSONEncoder):
+  if __debug__ and not _OLD_SIMPLEJSON:
+    try:
+      _orig_fn = json.JSONEncoder._iterencode_dict
+    except AttributeError:
+      raise Exception("Can't override JSONEncoder's '_iterencode_dict'")
+    else:
+      def _iterencode_dict(self, data, *args, **kwargs):
+        for key in data.keys():
+          if not (key is None or isinstance(key, (basestring, bool))):
+            raise ValueError("Key '%s' is of disallowed type '%s'" %
+                             (key, type(key)))
+
+        return self._orig_fn(data, *args, **kwargs)
+
+
+def _GetJsonDumpers(_encoder_class=_CustomJsonEncoder):
   """Returns two JSON functions to serialize data.
 
   @rtype: (callable, callable)
diff --git a/test/ganeti.serializer_unittest.py b/test/ganeti.serializer_unittest.py
index 9d8d65680..3e68efaa6 100755
--- a/test/ganeti.serializer_unittest.py
+++ b/test/ganeti.serializer_unittest.py
@@ -23,9 +23,11 @@
 
 
 import unittest
+import warnings
 
 from ganeti import serializer
 from ganeti import errors
+from ganeti import compat
 
 import testutils
 
@@ -107,5 +109,24 @@ class TestSerializer(testutils.GanetiTestCase):
                       serializer.DumpJson(tdata), "mykey")
 
 
+class TestInvalidDictionaryKey(unittest.TestCase):
+  def _Test(self, data):
+    if serializer._OLD_SIMPLEJSON:
+      # Using old "simplejson", can't really test
+      warnings.warn("This test requires Python 2.6 or above to function"
+                    " correctly")
+      self.assertTrue(serializer.DumpJson(data))
+    else:
+      self.assertRaises(ValueError, serializer.DumpJson, data)
+
+  def test(self):
+    for value in [123, 1.1, -1, -9492.1123, -3234e-4]:
+      self._Test({value: ""})
+
+  def testAllowed(self):
+    for value in ["", "Hello World", None, True, False]:
+      self.assertTrue(serializer.DumpJson({value: ""}))
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
-- 
GitLab