jsonrpc: Don't allow objects at the root

This commit is contained in:
Stein Magnus Jodal 2012-11-25 21:40:12 +01:00
parent b33df8200a
commit 8f604204da
2 changed files with 47 additions and 65 deletions

View File

@ -15,43 +15,24 @@ class JsonRpcWrapper(object):
processing of JSON-RPC 2.0 messages. The transport of the messages over
HTTP, WebSocket, TCP, or whatever is of no concern to this class.
The wrapper supports exporting the methods of multiple objects. If so, they
must be exported with different prefixes, called "mounts".
The wrapper supports exporting the methods of one or more objects. Either
way, the objects must be exported with method name prefixes, called
"mounts".
- To expose a single object, add it to the objects mapping using the empty
string as the mount::
To expose objects, add them all to the objects mapping. The key in the
mapping is used as the object's mounting point in the exposed API::
jrw = JsonRpcWrapper(objects={'': my_object})
jrw = JsonRpcWrapper(objects={
'foo': foo,
'hello': lambda: 'Hello, world!',
})
If ``my_object`` has a method named ``my_method()`` will be exported as
the JSON-RPC 2.0 method name ``my_method``.
This will export the Python callables on the left as the JSON-RPC 2.0
method names on the right::
- To expose multiple objects, add them all to the objects mapping. The key
in the mapping is used as the object's mounting point in the exposed
API::
jrw = JsonRpcWrapper(objects={
'': foo,
'hello': lambda: 'Hello, world!',
'abc': abc,
})
This will export the Python callables on the left as the JSON-RPC 2.0
method names on the right::
foo.bar() -> bar
foo.baz() -> baz
lambda -> hello
abc.def() -> abc.def
If the ``foo`` object mounted at the root also got a method named
``hello``, there will be a name collision between ``foo.hello()`` and the
lambda function mounted at ``hello``. In that case, the JSON-RPC 2.0
method name ``hello`` will refer to the lambda function, because it was
mounted explicitly as ``hello``.
It is recommended to avoid name collisions entirely by using non-empty
mounts for all objects.
foo.bar() -> foo.bar
foo.baz() -> foo.baz
lambda -> hello
Only the public methods of the mounted objects, or functions/methods
included directly in the mapping, will be exposed.
@ -73,6 +54,9 @@ class JsonRpcWrapper(object):
"""
def __init__(self, objects, decoders=None, encoders=None):
if '' in objects.keys():
raise AttributeError(
'The empty string is not allowed as an object mount')
self.objects = objects
self.decoder = get_combined_json_decoder(decoders or [])
self.encoder = get_combined_json_encoder(encoders or [])
@ -305,18 +289,13 @@ class JsonRpcInspector(object):
Inspects a group of classes and functions to create a description of what
methods they can expose over JSON-RPC 2.0.
To inspect a single class, add it to the objects mapping using the empty
string as the key::
jri = JsonRpcInspector(objects={'': MyClass})
To inspect multiple classes, add them all to the objects mapping. The key
in the mapping is used as the classes' mounting point in the exposed API::
To inspect one or more classes, add them all to the objects mapping. The
key in the mapping is used as the classes' mounting point in the exposed
API::
jri = JsonRpcInspector(objects={
'': Foo,
'foo': Foo,
'hello': lambda: 'Hello, world!',
'abc': Abc,
})
Since the inspector is based on inspecting classes and not instances, it
@ -328,6 +307,9 @@ class JsonRpcInspector(object):
"""
def __init__(self, objects):
if '' in objects.keys():
raise AttributeError(
'The empty string is not allowed as an object mount')
self.objects = objects
def describe(self):

View File

@ -44,10 +44,10 @@ class JsonRpcTestBase(unittest.TestCase):
self.jrw = jsonrpc.JsonRpcWrapper(
objects={
'hello': lambda: 'Hello, world!',
'calc': Calculator(),
'core': self.core,
'core.playback': self.core.playback,
'core.tracklist': self.core.tracklist,
'': Calculator(),
},
encoders=[models.ModelJSONEncoder],
decoders=[models.model_json_decoder])
@ -56,6 +56,12 @@ class JsonRpcTestBase(unittest.TestCase):
pykka.ActorRegistry.stop_all()
class JsonRpcSetupTest(JsonRpcTestBase):
def test_empty_object_mounts_is_not_allowed(self):
test = lambda: jsonrpc.JsonRpcWrapper(objects={'': Calculator()})
self.assertRaises(AttributeError, test)
class JsonRpcSerializationTest(JsonRpcTestBase):
def test_handle_json_converts_from_and_to_json(self):
self.jrw.handle_data = mock.Mock()
@ -132,10 +138,10 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase):
self.assertNotIn('error', response)
self.assertEqual(response['result'], 'Hello, world!')
def test_call_method_on_plain_object_as_root(self):
def test_call_method_on_plain_object(self):
request = {
'jsonrpc': '2.0',
'method': 'model',
'method': 'calc.model',
'id': 1,
}
response = self.jrw.handle_data(request)
@ -148,7 +154,7 @@ class JsonRpcSingleCommandTest(JsonRpcTestBase):
def test_call_method_which_returns_dict_from_plain_object(self):
request = {
'jsonrpc': '2.0',
'method': 'describe',
'method': 'calc.describe',
'id': 1,
}
response = self.jrw.handle_data(request)
@ -510,6 +516,10 @@ class JsonRpcBatchErrorTest(JsonRpcTestBase):
class JsonRpcInspectorTest(JsonRpcTestBase):
def test_empty_object_mounts_is_not_allowed(self):
test = lambda: jsonrpc.JsonRpcInspector(objects={'': Calculator})
self.assertRaises(AttributeError, test)
def test_can_describe_method_on_root(self):
inspector = jsonrpc.JsonRpcInspector({
'hello': lambda: 'Hello, world!',
@ -520,24 +530,24 @@ class JsonRpcInspectorTest(JsonRpcTestBase):
self.assertIn('hello', methods)
self.assertEqual(len(methods['hello']['params']), 0)
def test_inspector_can_describe_methods_on_the_root_class(self):
def test_inspector_can_describe_an_object_with_methods(self):
inspector = jsonrpc.JsonRpcInspector({
'': Calculator,
'calc': Calculator,
})
methods = inspector.describe()
self.assertIn('add', methods)
self.assertIn('calc.add', methods)
self.assertEqual(
methods['add']['description'],
methods['calc.add']['description'],
'Returns the sum of the given numbers')
self.assertIn('sub', methods)
self.assertIn('take_it_all', methods)
self.assertNotIn('_secret', methods)
self.assertNotIn('__init__', methods)
self.assertIn('calc.sub', methods)
self.assertIn('calc.take_it_all', methods)
self.assertNotIn('calc._secret', methods)
self.assertNotIn('calc.__init__', methods)
method = methods['take_it_all']
method = methods['calc.take_it_all']
self.assertIn('params', method)
params = method['params']
@ -559,16 +569,6 @@ class JsonRpcInspectorTest(JsonRpcTestBase):
self.assertNotIn('default', params[4])
self.assertEqual(params[4]['kwargs'], True)
def test_inspector_can_describe_methods_not_on_the_root(self):
inspector = jsonrpc.JsonRpcInspector({
'calc': Calculator,
})
methods = inspector.describe()
self.assertIn('calc.add', methods)
self.assertIn('calc.sub', methods)
def test_inspector_can_describe_a_bunch_of_large_classes(self):
inspector = jsonrpc.JsonRpcInspector({
'core.library': core.LibraryController,