diff options
Diffstat (limited to 'ipa-server/xmlrpc-server/ipaxmlrpc.py')
-rw-r--r-- | ipa-server/xmlrpc-server/ipaxmlrpc.py | 394 |
1 files changed, 394 insertions, 0 deletions
diff --git a/ipa-server/xmlrpc-server/ipaxmlrpc.py b/ipa-server/xmlrpc-server/ipaxmlrpc.py new file mode 100644 index 00000000..5e13611a --- /dev/null +++ b/ipa-server/xmlrpc-server/ipaxmlrpc.py @@ -0,0 +1,394 @@ +# mod_python script + +# ipaxmlrpc - an XMLRPC interface for ipa. +# Copyright (c) 2007 Red Hat +# +# IPA is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; +# version 2.1 of the License. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this software; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Based on kojixmlrpc - an XMLRPC interface for koji by +# Mike McLean <mikem@redhat.com> +# +# Authors: +# Rob Crittenden <rcritten@redhat.com> + +import sys + + +import time +import traceback +import pprint +from xmlrpclib import Marshaller,loads,dumps,Fault +from mod_python import apache +import logging + +from ipaserver import funcs +from ipa import ipaerror, ipautil +import ldap + +import string +import base64 + +# +# An override so we can base64 encode all outgoing values. +# This is set by calling: Marshaller._Marshaller__dump = xmlrpclib_dump +# +# Not currently used. +# +def xmlrpclib_escape(s, replace = string.replace): + """ + xmlrpclib only handles certain characters. Lets encode the whole + blob + """ + + return base64.encodestring(s) + +def xmlrpclib_dump(self, value, write): + """ + xmlrpclib cannot marshal instances of subclasses of built-in + types. This function overrides xmlrpclib.Marshaller.__dump so that + any value that is an instance of one of its acceptable types is + marshalled as that type. + + xmlrpclib also cannot handle invalid 7-bit control characters. See + above. + """ + + # Use our escape function + args = [self, value, write] + if isinstance(value, (str, unicode)): + args.append(xmlrpclib_escape) + + try: + # Try for an exact match first + f = self.dispatch[type(value)] + except KeyError: + # Try for an isinstance() match + for Type, f in self.dispatch.iteritems(): + if isinstance(value, Type): + f(*args) + return + raise TypeError, "cannot marshal %s objects" % type(value) + else: + f(*args) + + +class ModXMLRPCRequestHandler(object): + """Simple XML-RPC handler for mod_python environment""" + + def __init__(self): + self.funcs = {} + self.traceback = False + #introspection functions + self.register_function(self.ping, name="ping") + self.register_function(self.list_api, name="_listapi") + self.register_function(self.system_listMethods, name="system.listMethods") + self.register_function(self.system_methodSignature, name="system.methodSignature") + self.register_function(self.system_methodHelp, name="system.methodHelp") + self.register_function(self.multiCall) + + def register_function(self, function, name = None): + if name is None: + name = function.__name__ + self.funcs[name] = function + + def register_module(self, instance, prefix=None): + """Register all the public functions in an instance with prefix prepended + + For example + h.register_module(exports,"pub.sys") + will register the methods of exports with names like + pub.sys.method1 + pub.sys.method2 + ...etc + """ + for name in dir(instance): + if name.startswith('_'): + continue + function = getattr(instance, name) + if not callable(function): + continue + if prefix is not None: + name = "%s.%s" %(prefix,name) + self.register_function(function, name=name) + + def register_instance(self,instance): + self.register_module(instance) + + def _marshaled_dispatch(self, data, req): + """Dispatches an XML-RPC method from marshalled (XML) data.""" + + params, method = loads(data) + pythonopts = req.get_options() + + # Populate the Apache environment variables + req.add_common_vars() + + opts={} + opts['remoteuser'] = req.user + + if req.subprocess_env.get("KRB5CCNAME") is not None: + opts['krbccache'] = req.subprocess_env.get("KRB5CCNAME") + else: + response = dumps(Fault(5, "Did not receive Kerberos credentials.")) + return response + + debuglevel = logging.INFO + if pythonopts.get("IPADebug"): + opts['ipadebug'] = pythonopts.get("IPADebug").lower() + + if opts['ipadebug'] == "on": + debuglevel = logging.DEBUG + + if not opts.get('ipadebug'): + opts['ipadebug'] = "off" + + logging.basicConfig(level=debuglevel, + format='[%(asctime)s] [%(levelname)s] %(message)s', + datefmt='%a %b %d %H:%M:%S %Y', + stream=sys.stderr) + +# if opts['ipadebug'] == "on": +# for o in opts: +# logging.debug("IPA: setting option %s: %s" % (o, opts[o])) +# for e in req.subprocess_env: +# logging.debug("IPA: environment %s: %s" % (e, req.subprocess_env[e])) + + # Tack onto the end of the passed-in arguments any options we also + # need + params = params + (opts,) + + # special case +# if method == "get_user": +# Marshaller._Marshaller__dump = xmlrpclib_dump + + start = time.time() + # generate response + try: + response = self._dispatch(method, params) + # wrap response in a singleton tuple + response = (response,) + response = dumps(response, methodresponse=1, allow_none=1) + except ipaerror.IPAError, e: + self.traceback = True + + if (isinstance(e.detail, ldap.LDAPError) and len(e.detail[0].get('desc')) > 1): + err = ": %s: %s" % (e.detail[0].get('desc'), e.detail[0].get('info','')) + response = dumps(Fault(e.code, str(e) + err)) + else: + response = dumps(Fault(e.code, str(e))) + except: + self.traceback = True + # report exception back to server + e_class, e = sys.exc_info()[:2] + faultCode = getattr(e_class,'faultCode',1) + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + faultString = tb_str + response = dumps(Fault(faultCode, faultString)) + + return response + + def _dispatch(self,method,params): + func = self.funcs.get(method,None) + if func is None: + raise Fault(1, "Invalid method: %s" % method) + + args = list(ipautil.unwrap_binary_data(params)) + for i in range(len(args)): + if args[i] == '__NONE__': + args[i] = None + + ret = func(*args) + + return ipautil.wrap_binary_data(ret) + + def multiCall(self, calls): + """Execute a multicall. Execute each method call in the calls list, collecting + results and errors, and return those as a list.""" + results = [] + for call in calls: + try: + result = self._dispatch(call['methodName'], call['params']) + except Fault, fault: + results.append({'faultCode': fault.faultCode, 'faultString': fault.faultString}) + except: + # transform unknown exceptions into XML-RPC Faults + # don't create a reference to full traceback since this creates + # a circular reference. + exc_type, exc_value = sys.exc_info()[:2] + faultCode = getattr(exc_type, 'faultCode', 1) + faultString = ', '.join(exc_value.args) + trace = traceback.format_exception(*sys.exc_info()) + # traceback is not part of the multicall spec, but we include it for debugging purposes + results.append({'faultCode': faultCode, 'faultString': faultString, 'traceback': trace}) + else: + results.append([result]) + + return results + + def list_api(self,opts): + funcs = [] + for name,func in self.funcs.items(): + #the keys in self.funcs determine the name of the method as seen over xmlrpc + #func.__name__ might differ (e.g. for dotted method names) + args = self._getFuncArgs(func) + funcs.append({'name': name, + 'doc': func.__doc__, + 'args': args}) + return funcs + + def ping(self,opts): + """Simple test to see if the XML-RPC is up and active.""" + return "pong" + + def _getFuncArgs(self, func): + args = [] + for x in range(0, func.func_code.co_argcount): + if x == 0 and func.func_code.co_varnames[x] == "self": + continue + # opts is a name we tack on internally. Don't publish it. + if func.func_code.co_varnames[x] == "opts": + continue + if func.func_defaults and func.func_code.co_argcount - x <= len(func.func_defaults): + args.append((func.func_code.co_varnames[x], func.func_defaults[x - func.func_code.co_argcount + len(func.func_defaults)])) + else: + args.append(func.func_code.co_varnames[x]) + return args + + def system_listMethods(self, opts): + return self.funcs.keys() + + def system_methodSignature(self, method, opts): + #it is not possible to autogenerate this data + return 'signatures not supported' + + def system_methodHelp(self, method, opts): + func = self.funcs.get(method) + if func is None: + return "" + arglist = [] + for arg in self._getFuncArgs(func): + if isinstance(arg,str): + arglist.append(arg) + else: + arglist.append('%s=%s' % (arg[0], arg[1])) + ret = '%s(%s)' % (method, ", ".join(arglist)) + if func.__doc__: + ret += "\ndescription: %s" % func.__doc__ + return ret + + def handle_request(self,req): + """Handle a single XML-RPC request""" + + # XMLRPC uses POST only. Reject anything else + if req.method != 'POST': + req.allow_methods(['POST'],1) + raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED + + # The LDAP connection pool is not thread-safe. Avoid problems and + # force the forked model for now. + if apache.mpm_query(apache.AP_MPMQ_IS_THREADED): + response = dumps(Fault(3, "Apache must use the forked model")) + else: + response = self._marshaled_dispatch(req.read(), req) + + req.content_type = "text/xml" + req.set_content_length(len(response)) + req.write(response) + + +# +# mod_python handler +# + +def handler(req, profiling=False): + if profiling: + import profile, pstats, StringIO, tempfile + global _profiling_req + _profiling_req = req + temp = tempfile.NamedTemporaryFile() + profile.run("import ipxmlrpc; ipaxmlrpc.handler(ipaxmlrpc._profiling_req, False)", temp.name) + stats = pstats.Stats(temp.name) + strstream = StringIO.StringIO() + sys.stdout = strstream + stats.sort_stats("time") + stats.print_stats() + req.write("<pre>" + strstream.getvalue() + "</pre>") + _profiling_req = None + else: + opts = req.get_options() + try: + f = funcs.IPAServer() + h = ModXMLRPCRequestHandler() + h.register_function(f.version) + h.register_function(f.get_aci_entry) + h.register_function(f.get_entry_by_dn) + h.register_function(f.get_entry_by_cn) + h.register_function(f.update_entry) + h.register_function(f.get_user_by_uid) + h.register_function(f.get_user_by_principal) + h.register_function(f.get_user_by_email) + h.register_function(f.get_users_by_manager) + h.register_function(f.add_user) + h.register_function(f.get_custom_fields) + h.register_function(f.set_custom_fields) + h.register_function(f.get_all_users) + h.register_function(f.find_users) + h.register_function(f.update_user) + h.register_function(f.delete_user) + h.register_function(f.mark_user_active) + h.register_function(f.mark_user_inactive) + h.register_function(f.mark_group_active) + h.register_function(f.mark_group_inactive) + h.register_function(f.modifyPassword) + h.register_function(f.get_groups_by_member) + h.register_function(f.add_group) + h.register_function(f.find_groups) + h.register_function(f.add_member_to_group) + h.register_function(f.add_members_to_group) + h.register_function(f.remove_member_from_group) + h.register_function(f.remove_members_from_group) + h.register_function(f.add_user_to_group) + h.register_function(f.add_users_to_group) + h.register_function(f.add_group_to_group) + h.register_function(f.remove_user_from_group) + h.register_function(f.remove_users_from_group) + h.register_function(f.add_groups_to_user) + h.register_function(f.remove_groups_from_user) + h.register_function(f.update_group) + h.register_function(f.delete_group) + h.register_function(f.attrs_to_labels) + h.register_function(f.get_all_attrs) + h.register_function(f.group_members) + h.register_function(f.get_ipa_config) + h.register_function(f.update_ipa_config) + h.register_function(f.get_password_policy) + h.register_function(f.update_password_policy) + h.register_function(f.add_service_principal) + h.register_function(f.delete_service_principal) + h.register_function(f.find_service_principal) + h.register_function(f.get_radius_client_by_ip_addr) + h.register_function(f.add_radius_client) + h.register_function(f.update_radius_client) + h.register_function(f.delete_radius_client) + h.register_function(f.find_radius_clients) + h.register_function(f.get_radius_profile_by_uid) + h.register_function(f.add_radius_profile) + h.register_function(f.update_radius_profile) + h.register_function(f.delete_radius_profile) + h.register_function(f.find_radius_profiles) + h.handle_request(req) + finally: + pass + return apache.OK |