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