Switch client to JSON-RPC

Modify ipalib.rpc to support JSON-RPC in addition to XML-RPC.
This is done by subclassing and extending xmlrpclib, because
our existing code relies on xmlrpclib internals.

The URI to use is given in the new jsonrpc_uri env variable. When
it is not given, it is generated from xmlrpc_uri by replacing
/xml with /json.

The rpc_json_uri env variable existed before, but was unused,
undocumented and not set the install scripts.
This patch removes it in favor of jsonrpc_uri (for consistency
with xmlrpc_uri).

Add the rpc_protocol env variable to control the protocol
IPA uses. rpc_protocol defaults to 'jsonrpc', but may be changed
to 'xmlrpc'.
Make backend.Executioner and tests use the backend specified by
rpc_protocol.

For compatibility with unwrap_xml, decoding JSON now gives tuples
instead of lists.

Design: http://freeipa.org/page/V3/JSON-RPC
Ticket: https://fedorahosted.org/freeipa/ticket/3299
This commit is contained in:
Petr Viktorin
2012-12-19 04:25:24 -05:00
parent a1165ffbb8
commit 1e836d2d0c
18 changed files with 329 additions and 176 deletions

View File

@@ -25,27 +25,29 @@ Also see the `ipalib.rpc` module.
from xml.sax.saxutils import escape
from xmlrpclib import Fault
from wsgiref.util import shift_path_info
import base64
import os
import string
import datetime
from decimal import Decimal
import urlparse
import time
import json
from ipalib import plugable, capabilities
from ipalib.backend import Executioner
from ipalib.errors import PublicError, InternalError, CommandError, JSONError, ConversionError, CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError, ExecutionError
from ipalib.request import context, Connection, destroy_context
from ipalib.rpc import xml_dumps, xml_loads
from ipalib.errors import (PublicError, InternalError, CommandError, JSONError,
CCacheError, RefererError, InvalidSessionPassword, NotFound, ACIError,
ExecutionError)
from ipalib.request import context, destroy_context
from ipalib.rpc import (xml_dumps, xml_loads,
json_encode_binary, json_decode_binary)
from ipalib.util import parse_time_duration, normalize_name
from ipapython.dn import DN
from ipaserver.plugins.ldap2 import ldap2
from ipalib.session import session_mgr, AuthManager, get_ipa_ccache_name, load_ccache_data, bind_ipa_ccache, release_ipa_ccache, fmt_time, default_max_session_duration
from ipalib.session import (session_mgr, AuthManager, get_ipa_ccache_name,
load_ccache_data, bind_ipa_ccache, release_ipa_ccache, fmt_time,
default_max_session_duration)
from ipalib.backend import Backend
from ipalib.krb_utils import krb5_parse_ccache, KRB5_CCache, krb_ticket_expiration_threshold, krb5_format_principal_name
from ipalib.krb_utils import (
KRB5_CCache, krb_ticket_expiration_threshold, krb5_format_principal_name)
from ipapython import ipautil
from ipapython.version import VERSION
from ipalib.text import _
@@ -397,99 +399,6 @@ class WSGIExecutioner(Executioner):
raise NotImplementedError('%s.marshal()' % self.fullname)
def json_encode_binary(val):
'''
JSON cannot encode binary values. We encode binary values in Python str
objects and text in Python unicode objects. In order to allow a binary
object to be passed through JSON we base64 encode it thus converting it to
text which JSON can transport. To assure we recognize the value is a base64
encoded representation of the original binary value and not confuse it with
other text we convert the binary value to a dict in this form:
{'__base64__' : base64_encoding_of_binary_value}
This modification of the original input value cannot be done "in place" as
one might first assume (e.g. replacing any binary items in a container
(e.g. list, tuple, dict) with the base64 dict because the container might be
an immutable object (i.e. a tuple). Therefore this function returns a copy
of any container objects it encounters with tuples replaced by lists. This
is O.K. because the JSON encoding will map both lists and tuples to JSON
arrays.
'''
if isinstance(val, dict):
new_dict = {}
for k,v in val.items():
new_dict[k] = json_encode_binary(v)
return new_dict
elif isinstance(val, (list, tuple)):
new_list = [json_encode_binary(v) for v in val]
return new_list
elif isinstance(val, str):
return {'__base64__' : base64.b64encode(val)}
elif isinstance(val, Decimal):
return {'__base64__' : base64.b64encode(str(val))}
elif isinstance(val, DN):
return str(val)
else:
return val
def json_decode_binary(val):
'''
JSON cannot transport binary data. In order to transport binary data we
convert binary data to a form like this:
{'__base64__' : base64_encoding_of_binary_value}
see json_encode_binary()
After JSON had decoded the JSON stream back into a Python object we must
recursively scan the object looking for any dicts which might represent
binary values and replace the dict containing the base64 encoding of the
binary value with the decoded binary value. Unlike the encoding problem
where the input might consist of immutable object, all JSON decoded
container are mutable so the conversion could be done in place. However we
don't modify objects in place because of side effects which may be
dangerous. Thus we elect to spend a few more cycles and avoid the
possibility of unintended side effects in favor of robustness.
'''
if isinstance(val, dict):
if val.has_key('__base64__'):
return base64.b64decode(val['__base64__'])
else:
new_dict = {}
for k,v in val.items():
if isinstance(v, dict) and v.has_key('__base64__'):
new_dict[k] = base64.b64decode(v['__base64__'])
else:
new_dict[k] = json_decode_binary(v)
return new_dict
elif isinstance(val, list):
new_list = []
n = len(val)
i = 0
while i < n:
v = val[i]
if isinstance(v, dict) and v.has_key('__base64__'):
binary_val = base64.b64decode(v['__base64__'])
new_list.append(binary_val)
else:
new_list.append(json_decode_binary(v))
i += 1
return new_list
else:
if isinstance(val, basestring):
try:
return val.decode('utf-8')
except UnicodeDecodeError:
raise ConversionError(
name=val,
error='incorrect type'
)
else:
return val
class jsonserver(WSGIExecutioner, HTTP_Status):
"""
JSON RPC server.