Revision 560ef132

b/lib/rpc/client.py
120 120
    request[KEY_VERSION] = version
121 121

  
122 122
  # Serialize the request
123
  return serializer.DumpJson(request)
123
  return serializer.DumpJson(request,
124
                             private_encoder=serializer.EncodeWithPrivateFields)
124 125

  
125 126

  
126 127
def CallRPCMethod(transport_cb, method, args, version=None):
b/lib/serializer.py
40 40

  
41 41
from ganeti import errors
42 42
from ganeti import utils
43

  
43
from ganeti import constants
44 44

  
45 45
_RE_EOLSP = re.compile("[ \t]+$", re.MULTILINE)
46 46

  
47 47

  
48
def DumpJson(data):
48
def DumpJson(data, private_encoder=None):
49 49
  """Serialize a given object.
50 50

  
51 51
  @param data: the data to serialize
52 52
  @return: the string representation of data
53
  @param private_encoder: specify L{serializer.EncodeWithPrivateFields} if you
54
                          require the produced JSON to also contain private
55
                          parameters. Otherwise, they will encode to null.
53 56

  
54 57
  """
55
  encoded = simplejson.dumps(data)
58
  if private_encoder is None:
59
    # Do not leak private fields by default.
60
    private_encoder = EncodeWithoutPrivateFields
61
  encoded = simplejson.dumps(data, default=private_encoder)
56 62

  
57 63
  txt = _RE_EOLSP.sub("", encoded)
58 64
  if not txt.endswith("\n"):
......
69 75
  @raise JSONDecodeError: if L{txt} is not a valid JSON document
70 76

  
71 77
  """
72
  return simplejson.loads(txt)
78
  values = simplejson.loads(txt)
79

  
80
  # Hunt and seek for Private fields and wrap them.
81
  WrapPrivateValues(values)
82

  
83
  return values
84

  
73 85

  
86
def WrapPrivateValues(json):
87
  """Crawl a JSON decoded structure for private values and wrap them.
88

  
89
  @param json: the json-decoded value to protect.
90

  
91
  """
92
  # This function used to be recursive. I use this list to avoid actual
93
  # recursion, however, since this is a very high-traffic area.
94
  todo = [json]
95

  
96
  while todo:
97
    data = todo.pop()
98

  
99
    if isinstance(data, list): # Array
100
      for item in data:
101
        todo.append(item)
102
    elif isinstance(data, dict): # Object
103

  
104
      # This is kind of a kludge, but the only place where we know what should
105
      # be protected is in ganeti.opcodes, and not in a way that is helpful to
106
      # us, especially in such a high traffic method; on the other hand, the
107
      # Haskell `py_compat_fields` test should complain whenever this check
108
      # does not protect fields properly.
109
      for field in data:
110
        value = data[field]
111
        if field in constants.PRIVATE_PARAMETERS_BLACKLIST:
112
          if not field.endswith("_cluster"):
113
            data[field] = PrivateDict(value)
114
          else:
115
            for os in data[field]:
116
              value[os] = PrivateDict(value[os])
117
        else:
118
          todo.append(value)
119
    else: # Values
120
      pass
74 121

  
75
def DumpSignedJson(data, key, salt=None, key_selector=None):
122

  
123
def DumpSignedJson(data, key, salt=None, key_selector=None,
124
                   private_encoder=None):
76 125
  """Serialize a given object and authenticate it.
77 126

  
78 127
  @param data: the data to serialize
79 128
  @param key: shared hmac key
80 129
  @param key_selector: name/id that identifies the key (in case there are
81 130
    multiple keys in use, e.g. in a multi-cluster environment)
131
  @param private_encoder: see L{DumpJson}
82 132
  @return: the string representation of data signed by the hmac key
83 133

  
84 134
  """
85
  txt = DumpJson(data)
135
  txt = DumpJson(data, private_encoder=private_encoder)
86 136
  if salt is None:
87 137
    salt = ""
88 138
  signed_dict = {
......
113 163

  
114 164
  """
115 165
  signed_dict = LoadJson(txt)
166

  
167
  WrapPrivateValues(signed_dict)
168

  
116 169
  if not isinstance(signed_dict, dict):
117 170
    raise errors.SignatureError("Invalid external message")
118 171
  try:
......
319 372
    for key in self:
320 373
      returndict[key] = self[key].Get()
321 374
    return returndict
375

  
376

  
377
def EncodeWithoutPrivateFields(obj):
378
  if isinstance(obj, Private):
379
    return None
380
  raise TypeError(repr(obj) + " is not JSON serializable")
381

  
382

  
383
def EncodeWithPrivateFields(obj):
384
  if isinstance(obj, Private):
385
    return obj.Get()
386
  raise TypeError(repr(obj) + " is not JSON serializable")
b/src/Ganeti/Constants.hs
4773 4773

  
4774 4774
instanceCommunicationNicPrefix :: String
4775 4775
instanceCommunicationNicPrefix = "ganeti:communication:"
4776

  
4777
-- | Parameters that should be protected
4778
--
4779
-- Python does not have a type system and can't automatically infer what should
4780
-- be the resulting type of a JSON request. As a result, it must rely on this
4781
-- list of parameter names to protect values correctly.
4782
--
4783
-- Names ending in _cluster will be treated as dicts of dicts of private values.
4784
-- Otherwise they are considered dicts of private values.
4785
privateParametersBlacklist :: [String]
4786
privateParametersBlacklist = [ "osparams_private"
4787
                             , "osparams_secret"
4788
                             , "osparams_private_cluster"
4789
                             ]
4790

  
4791
-- | Warn the user that the logging level is too low for production use.
4792
debugModeConfidentialityWarning :: String
4793
debugModeConfidentialityWarning =
4794
  "ALERT: %s started in debug mode.\n\
4795
  \ Private and secret parameters WILL be logged!\n"
b/test/hs/Test/Ganeti/OpCodes.hs
519 519
               \  op.Validate(True)\n\
520 520
               \encoded = [(op.Summary(), op.__getstate__())\n\
521 521
               \           for op in decoded]\n\
522
               \print serializer.Dump(encoded)" serialized
522
               \print serializer.Dump(\
523
               \  encoded,\
524
               \  private_encoder=serializer.EncodeWithPrivateFields)"
525
               serialized
523 526
     >>= checkPythonResult
524 527
  let deserialised =
525 528
        J.decode py_stdout::J.Result [(String, OpCodes.MetaOpCode)]
b/test/py/cmdlib/node_unittest.py
229 229
    self.ExecOpCodeExpectOpPrereqError(op, "Readded node doesn't have the same"
230 230
                                       " IP address configuration as before")
231 231

  
232

  
233 232
  def testNodeHasSecondaryIpButNotMaster(self):
234 233
    self.master.secondary_ip = self.master.primary_ip
235 234

  
b/test/py/ganeti.serializer_unittest.py
22 22
"""Script for unittesting the serializer module"""
23 23

  
24 24

  
25
import doctest
25 26
import unittest
26 27

  
27
from ganeti import serializer
28 28
from ganeti import errors
29 29
from ganeti import ht
30
from ganeti import objects
31
from ganeti import serializer
30 32

  
31 33
import testutils
32 34

  
......
39 41
    255,
40 42
    [1, 2, 3],
41 43
    (1, 2, 3),
42
    { "1": 2, "foo": "bar", },
44
    {"1": 2,
45
     "foo": "bar"},
43 46
    ["abc", 1, 2, 3, 999,
44 47
      {
45 48
        "a1": ("Hello", "World"),
46 49
        "a2": "This is only a test",
47 50
        "a3": None,
48
        },
49
      {
50
        "foo": "bar",
51
        },
52
      ]
51
        "osparams:": serializer.PrivateDict({
52
                      "foo": 5,
53
                     })
54
      }
53 55
    ]
56
  ]
54 57

  
55 58
  def _TestSerializer(self, dump_fn, load_fn):
59
    _dump_fn = lambda data: dump_fn(
60
      data,
61
      private_encoder=serializer.EncodeWithPrivateFields
62
    )
56 63
    for data in self._TESTDATA:
57
      self.failUnless(dump_fn(data).endswith("\n"))
58
      self.assertEqualValues(load_fn(dump_fn(data)), data)
64
      self.failUnless(_dump_fn(data).endswith("\n"))
65
      self.assertEqualValues(load_fn(_dump_fn(data)), data)
59 66

  
60 67
  def testGeneric(self):
61 68
    self._TestSerializer(serializer.Dump, serializer.Load)
......
70 77
    self._TestSigned(serializer.DumpSignedJson, serializer.LoadSignedJson)
71 78

  
72 79
  def _TestSigned(self, dump_fn, load_fn):
80
    _dump_fn = lambda *args, **kwargs: dump_fn(
81
      *args,
82
      private_encoder=serializer.EncodeWithPrivateFields,
83
      **kwargs
84
    )
73 85
    for data in self._TESTDATA:
74
      self.assertEqualValues(load_fn(dump_fn(data, "mykey"), "mykey"),
86
      self.assertEqualValues(load_fn(_dump_fn(data, "mykey"), "mykey"),
75 87
                             (data, ""))
76
      self.assertEqualValues(load_fn(dump_fn(data, "myprivatekey",
77
                                             salt="mysalt"),
88
      self.assertEqualValues(load_fn(_dump_fn(data, "myprivatekey",
89
                                              salt="mysalt"),
78 90
                                     "myprivatekey"),
79 91
                             (data, "mysalt"))
80 92

  
81 93
      keydict = {
82 94
        "mykey_id": "myprivatekey",
83 95
        }
84
      self.assertEqualValues(load_fn(dump_fn(data, "myprivatekey",
85
                                             salt="mysalt",
86
                                             key_selector="mykey_id"),
96
      self.assertEqualValues(load_fn(_dump_fn(data, "myprivatekey",
97
                                              salt="mysalt",
98
                                              key_selector="mykey_id"),
87 99
                                     keydict.get),
88 100
                             (data, "mysalt"))
89 101
      self.assertRaises(errors.SignatureError, load_fn,
90
                        dump_fn(data, "myprivatekey",
91
                                salt="mysalt",
92
                                key_selector="mykey_id"),
102
                        _dump_fn(data, "myprivatekey",
103
                                 salt="mysalt",
104
                                 key_selector="mykey_id"),
93 105
                        {}.get)
94 106

  
95 107
    self.assertRaises(errors.SignatureError, load_fn,
96
                      dump_fn("test", "myprivatekey"),
108
                      _dump_fn("test", "myprivatekey"),
97 109
                      "myotherkey")
98 110

  
99 111
    self.assertRaises(errors.SignatureError, load_fn,
......
131 143
    self.assertEqual(serializer.LoadAndVerifyJson("\"Foo\"", ht.TAny), "Foo")
132 144

  
133 145

  
146
class TestPrivate(unittest.TestCase):
147

  
148
  def testEquality(self):
149
    pDict = serializer.PrivateDict()
150
    pDict["bar"] = "egg"
151
    nDict = {"bar": "egg"}
152
    self.assertEqual(pDict, nDict, "PrivateDict-dict equality failure")
153

  
154
  def testPrivateDictUnprivate(self):
155
    pDict = serializer.PrivateDict()
156
    pDict["bar"] = "egg"
157
    uDict = pDict.Unprivate()
158
    nDict = {"bar": "egg"}
159
    self.assertEquals(type(uDict), dict,
160
                      "PrivateDict.Unprivate() did not return a dict")
161
    self.assertEqual(pDict, uDict, "PrivateDict.Unprivate() equality failure")
162
    self.assertEqual(nDict, uDict, "PrivateDict.Unprivate() failed to return")
163

  
164
  def testAttributeTransparency(self):
165
    class Dummy(object):
166
      pass
167

  
168
    dummy = Dummy()
169
    dummy.bar = "egg"
170
    pDummy = serializer.Private(dummy)
171
    self.assertEqual(pDummy.bar, "egg", "Failed to access attribute of Private")
172

  
173
  def testCallTransparency(self):
174
    foo = serializer.Private("egg")
175
    self.assertEqual(foo.upper(), "EGG", "Failed to call Private instance")
176

  
177
  def testFillDict(self):
178
    pDict = serializer.PrivateDict()
179
    pDict["bar"] = "egg"
180
    self.assertEqual(pDict, objects.FillDict({}, pDict))
181

  
182
  def testLeak(self):
183
    pDict = serializer.PrivateDict()
184
    pDict["bar"] = "egg"
185
    self.assertNotIn("egg", str(pDict), "Value leaked in str(PrivateDict)")
186
    self.assertNotIn("egg", repr(pDict), "Value leaked in repr(PrivateDict)")
187
    self.assertNotIn("egg", "{0}".format(pDict),
188
                     "Value leaked in PrivateDict.__format__")
189
    self.assertNotIn("egg", serializer.Dump(pDict),
190
                     "Value leaked in serializer.Dump(PrivateDict)")
191

  
192
  def testProperAccess(self):
193
    pDict = serializer.PrivateDict()
194
    pDict["bar"] = "egg"
195

  
196
    self.assertIs("egg", pDict["bar"].Get(),
197
                  "Value not returned by Private.Get()")
198
    self.assertIs("egg", pDict.GetPrivate("bar"),
199
                  "Value not returned by Private.GetPrivate()")
200
    self.assertIs("egg", pDict.Unprivate()["bar"],
201
                  "Value not returned by PrivateDict.Unprivate()")
202
    self.assertIn(
203
      "egg",
204
      serializer.Dump(pDict,
205
                      private_encoder=serializer.EncodeWithPrivateFields)
206
    )
207

  
208
  def testDictGet(self):
209
    self.assertIs("tar", serializer.PrivateDict().GetPrivate("bar", "tar"),
210
                  "Private.GetPrivate() did not handle the default case")
211

  
212
  def testZeronessPrivate(self):
213
    self.assertTrue(serializer.Private("foo"),
214
                    "Private of non-empty string is false")
215
    self.assertFalse(serializer.Private(""), "Private empty string is true")
216

  
217

  
218
class TestCheckDoctests(unittest.TestCase):
219

  
220
  def testCheckSerializer(self):
221
    results = doctest.testmod(serializer)
222
    self.assertEquals(results.failed, 0, "Doctest failures detected")
223

  
134 224
if __name__ == "__main__":
135 225
  testutils.GanetiTestProgram()

Also available in: Unified diff