From: stefan Date: Wed, 25 Feb 2009 18:17:39 +0000 (+0000) Subject: * Refactor XMLRPC interface. X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=de73b5d7268c3411c585861e63c8d9309a7638ea;p=roundup.git * Refactor XMLRPC interface. * Make it accessible through web-server. git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/roundup/trunk@4171 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py index 7d534ca..0ea02f1 100644 --- a/roundup/cgi/client.py +++ b/roundup/cgi/client.py @@ -16,6 +16,7 @@ from roundup.cgi.exceptions import * from roundup.cgi.form_parser import FormParser from roundup.mailer import Mailer, MessageSendError from roundup.cgi import accept_language +from roundup import xmlrpc def initialiseSecurity(security): '''Create some Permissions and Roles on the security object @@ -354,10 +355,41 @@ class Client: """ Wrap the real main in a try/finally so we always close off the db. """ try: - self.inner_main() + if self.env.get('CONTENT_TYPE') == 'text/xml': + self.handle_xmlrpc() + else: + self.inner_main() finally: if hasattr(self, 'db'): self.db.close() + + + def handle_xmlrpc(self): + + # Pull the raw XML out of the form. The "value" attribute + # will be the raw content of the POST request. + assert self.form.file + input = self.form.value + # So that the rest of Roundup can query the form in the + # usual way, we create an empty list of fields. + self.form.list = [] + + # Set the charset and language, since other parts of + # Roundup may depend upon that. + self.determine_charset() + self.determine_language() + # Open the database as the correct user. + self.determine_user() + + # Call the appropriate XML-RPC method. + handler = xmlrpc.RoundupDispatcher(self.db, self.userid, self.translator, + allow_none=True) + output = handler.dispatch(input) + self.db.commit() + + self.setHeader("Content-Type", "text/xml") + self.setHeader("Content-Length", str(len(output))) + self.write(output) def inner_main(self): """Process a request. diff --git a/roundup/scripts/roundup_xmlrpc_server.py b/roundup/scripts/roundup_xmlrpc_server.py index 020cc38..44c626f 100644 --- a/roundup/scripts/roundup_xmlrpc_server.py +++ b/roundup/scripts/roundup_xmlrpc_server.py @@ -5,20 +5,113 @@ # For license terms see the file COPYING.txt. # -import getopt, os, sys, socket -from roundup.xmlrpc import RoundupServer, RoundupRequestHandler +import base64, getopt, os, sys, socket, urllib +from roundup.xmlrpc import translate +from roundup.xmlrpc import RoundupInstance +import roundup.instance from roundup.instance import TrackerError +from roundup.cgi.exceptions import Unauthorised from SimpleXMLRPCServer import SimpleXMLRPCServer +from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler + + +class RequestHandler(SimpleXMLRPCRequestHandler): + """A SimpleXMLRPCRequestHandler with support for basic + HTTP Authentication.""" + + TRACKER_HOMES = {} + TRACKERS = {} + + def is_rpc_path_valid(self): + path = self.path.split('/') + name = urllib.unquote(path[1]).lower() + return name in self.TRACKER_HOMES + + def get_tracker(self, name): + """Return a tracker instance for given tracker name.""" + + if name in self.TRACKERS: + return self.TRACKERS[name] + + if name not in self.TRACKER_HOMES: + raise Exception('No such tracker "%s"'%name) + tracker_home = self.TRACKER_HOMES[name] + tracker = roundup.instance.open(tracker_home) + self.TRACKERS[name] = tracker + return tracker + + + def authenticate(self, tracker): + + + # Try to extract username and password from HTTP Authentication. + username, password = None, None + authorization = self.headers.get('authorization', ' ') + scheme, challenge = authorization.split(' ', 1) + + if scheme.lower() == 'basic': + decoded = base64.decodestring(challenge) + username, password = decoded.split(':') + if not username: + username = 'anonymous' + db = tracker.open('admin') + try: + userid = db.user.lookup(username) + except KeyError: # No such user + db.close() + raise Unauthorised, 'Invalid user' + stored = db.user.get(userid, 'password') + if stored != password: + # Wrong password + db.close() + raise Unauthorised, 'Invalid user' + db.setCurrentUser(username) + return db + + + def do_POST(self): + """Extract username and password from authorization header.""" + + db = None + try: + path = self.path.split('/') + tracker_name = urllib.unquote(path[1]).lower() + tracker = self.get_tracker(tracker_name) + db = self.authenticate(tracker) + + instance = RoundupInstance(db, None) + self.server.register_instance(instance) + SimpleXMLRPCRequestHandler.do_POST(self) + except Unauthorised, message: + self.send_error(403, '%s (%s)'%(self.path, message)) + except: + if db: + db.close() + exc, val, tb = sys.exc_info() + print exc, val, tb + raise + if db: + db.close() + + +class Server(SimpleXMLRPCServer): + + def _dispatch(self, method, params): + + retn = SimpleXMLRPCServer._dispatch(self, method, params) + retn = translate(retn) + return retn + def usage(): - print """ + print """Usage: %s: [options] [name=tracker home]+ Options: -i instance home -- specify the issue tracker "home directory" to administer -V -- be verbose when importing -p, --port -- port to listen on -""" +"""%sys.argv[0] def run(): @@ -30,38 +123,42 @@ def run(): return 1 verbose = False - tracker = '' port = 8000 encoding = None for opt, arg in opts: if opt == '-V': verbose = True - elif opt == '-i': - tracker = arg elif opt in ['-p', '--port']: port = int(arg) elif opt in ['-e', '--encoding']: encoding = encoding - if sys.version_info[0:2] < (2,5): - if encoding: - print 'encodings not supported with python < 2.5' - sys.exit(-1) - server = SimpleXMLRPCServer(('', port), RoundupRequestHandler) - else: - server = SimpleXMLRPCServer(('', port), RoundupRequestHandler, - allow_none=True, encoding=encoding) - if not os.path.exists(tracker): - print 'Instance home does not exist.' - sys.exit(-1) - try: - object = RoundupServer(tracker, verbose) - except TrackerError: - print 'Instance home does not exist.' - sys.exit(-1) + tracker_homes = {} + for arg in args: + try: + name, home = arg.split('=', 1) + # Validate the argument + tracker = roundup.instance.open(home) + except ValueError: + print 'Instances must be name=home' + sys.exit(-1) + except TrackerError: + print 'Tracker home does not exist.' + sys.exit(-1) + + tracker_homes[name] = home + + RequestHandler.TRACKER_HOMES=tracker_homes - server.register_instance(object) + if sys.version_info[0:2] < (2,5): + if encoding: + print 'encodings not supported with python < 2.5' + sys.exit(-1) + server = Server(('', port), RequestHandler) + else: + server = Server(('', port), RequestHandler, + allow_none=True, encoding=encoding) # Go into the main listener loop print 'Roundup XMLRPC server started on %s:%d' \ diff --git a/roundup/xmlrpc.py b/roundup/xmlrpc.py index c3eaccd..4555287 100644 --- a/roundup/xmlrpc.py +++ b/roundup/xmlrpc.py @@ -4,195 +4,144 @@ # For license terms see the file COPYING.txt. # -import base64 -import roundup.instance from roundup import hyperdb from roundup.cgi.exceptions import * -from roundup.admin import UsageError -from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler - -class RoundupRequestHandler(SimpleXMLRPCRequestHandler): - """A SimpleXMLRPCRequestHandler with support for basic - HTTP Authentication.""" - - def do_POST(self): - """Extract username and password from authorization header.""" - - # Try to extract username and password from HTTP Authentication. - self.username = None - self.password = None - authorization = self.headers.get('authorization', ' ') - scheme, challenge = authorization.split(' ', 1) - - if scheme.lower() == 'basic': - decoded = base64.decodestring(challenge) - self.username, self.password = decoded.split(':') - - SimpleXMLRPCRequestHandler.do_POST(self) +from roundup.exceptions import UsageError +from roundup.date import Date, Range, Interval +from SimpleXMLRPCServer import * + +def translate(value): + """Translate value to becomes valid for XMLRPC transmission.""" + + if isinstance(value, (Date, Range, Interval)): + return repr(value) + elif type(value) is list: + return [translate(v) for v in value] + elif type(value) is tuple: + return tuple([translate(v) for v in value]) + elif type(value) is dict: + return dict([[translate(k), translate(value[k])] for k in value]) + else: + return value + + +def props_from_args(db, cl, args, itemid=None): + """Construct a list of properties from the given arguments, + and return them after validation.""" + + props = {} + for arg in args: + if arg.find('=') == -1: + raise UsageError, 'argument "%s" not propname=value'%arg + l = arg.split('=') + if len(l) < 2: + raise UsageError, 'argument "%s" not propname=value'%arg + key, value = l[0], '='.join(l[1:]) + if value: + try: + props[key] = hyperdb.rawToHyperdb(db, cl, itemid, + key, value) + except hyperdb.HyperdbValueError, message: + raise UsageError, message + else: + props[key] = None + + return props + +class RoundupInstance: + """The RoundupInstance provides the interface accessible through + the Python XMLRPC mapping.""" + + def __init__(self, db, translator): + + self.db = db + self.userid = db.getuid() + self.translator = translator + + def list(self, classname, propname=None): + cl = self.db.getclass(classname) + if not propname: + propname = cl.labelprop() + result = [cl.get(itemid, propname) + for itemid in cl.list() + if self.db.security.hasPermission('View', self.db.getuid(), + classname, propname, itemid) + ] + return result - def _dispatch(self, method, params): - """Inject username and password into function arguments.""" + def filter(self, classname, search_matches, filterspec, + sort=[], group=[]): + cl = self.db.getclass(classname) + result = cl.filter(search_matches, filterspec, sort=sort, group=group) + return result - # Add username and password to function arguments - params = [self.username, self.password] + list(params) - return self.server._dispatch(method, params) + def display(self, designator, *properties): + classname, itemid = hyperdb.splitDesignator(designator) + cl = self.db.getclass(classname) + props = properties and list(properties) or cl.properties.keys() + props.sort() + for p in props: + if not self.db.security.hasPermission('View', self.db.getuid(), + classname, p, itemid): + raise Unauthorised('Permission to view %s of %s denied'% + (p, designator)) + result = [(prop, cl.get(itemid, prop)) for prop in props] + return dict(result) + def create(self, classname, *args): + if not self.db.security.hasPermission('Create', self.db.getuid(), classname): + raise Unauthorised('Permission to create %s denied'%classname) -class RoundupRequest: - """Little helper class to handle common per-request tasks such - as authentication and login.""" + cl = self.db.getclass(classname) - def __init__(self, tracker, username, password): - """Open the database for the given tracker, using the given - username and password.""" + # convert types + props = props_from_args(self.db, cl, args) - self.tracker = tracker - self.db = self.tracker.open('admin') - try: - self.userid = self.db.user.lookup(username) - except KeyError: # No such user - self.db.close() - raise Unauthorised, 'Invalid user' - stored = self.db.user.get(self.userid, 'password') - if stored != password: - # Wrong password - self.db.close() - raise Unauthorised, 'Invalid user' - self.db.setCurrentUser(username) - - def close(self): - """Close the database, after committing any changes, if needed.""" + # check for the key property + key = cl.getkey() + if key and not props.has_key(key): + raise UsageError, 'you must provide the "%s" property.'%key + # do the actual create try: - self.db.commit() - finally: - self.db.close() - - def get_class(self, classname): - """Return the class for the given classname.""" - - try: - return self.db.getclass(classname) - except KeyError: - raise UsageError, 'no such class "%s"'%classname - - def props_from_args(self, cl, args, itemid=None): - """Construct a list of properties from the given arguments, - and return them after validation.""" - - props = {} - for arg in args: - if arg.find('=') == -1: - raise UsageError, 'argument "%s" not propname=value'%arg - l = arg.split('=') - if len(l) < 2: - raise UsageError, 'argument "%s" not propname=value'%arg - key, value = l[0], '='.join(l[1:]) - if value: - try: - props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid, - key, value) - except hyperdb.HyperdbValueError, message: - raise UsageError, message - else: - props[key] = None - - return props - - -#The server object -class RoundupServer: - """The RoundupServer provides the interface accessible through - the Python XMLRPC mapping. All methods take an additional username - and password argument so each request can be authenticated.""" - - def __init__(self, tracker, verbose = False): - self.tracker = roundup.instance.open(tracker) - self.verbose = verbose - - def list(self, username, password, classname, propname=None): - r = RoundupRequest(self.tracker, username, password) - try: - cl = r.get_class(classname) - if not propname: - propname = cl.labelprop() - result = [cl.get(itemid, propname) - for itemid in cl.list() - if r.db.security.hasPermission('View', r.userid, - classname, propname, itemid) - ] - finally: - r.close() + result = cl.create(**props) + except (TypeError, IndexError, ValueError), message: + raise UsageError, message return result - def filter(self, username, password, classname, search_matches, filterspec, - sort=[], group=[]): - r = RoundupRequest(self.tracker, username, password) - try: - cl = r.get_class(classname) - result = cl.filter(search_matches, filterspec, sort=sort, group=group) - finally: - r.close() - return result + def set(self, designator, *args): - def display(self, username, password, designator, *properties): - r = RoundupRequest(self.tracker, username, password) + classname, itemid = hyperdb.splitDesignator(designator) + cl = self.db.getclass(classname) + props = props_from_args(self.db, cl, args, itemid) # convert types + for p in props.iterkeys(): + if not self.db.security.hasPermission('Edit', self.db.getuid(), + classname, p, itemid): + raise Unauthorised('Permission to edit %s of %s denied'% + (p, designator)) try: - classname, itemid = hyperdb.splitDesignator(designator) - cl = r.get_class(classname) - props = properties and list(properties) or cl.properties.keys() - props.sort() - for p in props: - if not r.db.security.hasPermission('View', r.userid, - classname, p, itemid): - raise Unauthorised('Permission to view %s of %s denied'% - (p, designator)) - result = [(prop, cl.get(itemid, prop)) for prop in props] - finally: - r.close() - return dict(result) + return cl.set(itemid, **props) + except (TypeError, IndexError, ValueError), message: + raise UsageError, message - def create(self, username, password, classname, *args): - r = RoundupRequest(self.tracker, username, password) - try: - if not r.db.security.hasPermission('Create', r.userid, classname): - raise Unauthorised('Permission to create %s denied'%classname) - cl = r.get_class(classname) +class RoundupDispatcher(SimpleXMLRPCDispatcher): + """RoundupDispatcher bridges from cgi.client to RoundupInstance. + It expects user authentication to be done.""" - # convert types - props = r.props_from_args(cl, args) + def __init__(self, db, userid, translator, + allow_none=False, encoding=None): - # check for the key property - key = cl.getkey() - if key and not props.has_key(key): - raise UsageError, 'you must provide the "%s" property.'%key + SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding) + self.register_instance(RoundupInstance(db, userid, translator)) + - # do the actual create - try: - result = cl.create(**props) - except (TypeError, IndexError, ValueError), message: - raise UsageError, message - finally: - r.close() - return result + def dispatch(self, input): + return self._marshaled_dispatch(input) - def set(self, username, password, designator, *args): - r = RoundupRequest(self.tracker, username, password) - try: - classname, itemid = hyperdb.splitDesignator(designator) - cl = r.get_class(classname) - props = r.props_from_args(cl, args, itemid) # convert types - for p in props.iterkeys (): - if not r.db.security.hasPermission('Edit', r.userid, - classname, p, itemid): - raise Unauthorised('Permission to edit %s of %s denied'% - (p, designator)) - try: - return cl.set(itemid, **props) - except (TypeError, IndexError, ValueError), message: - raise UsageError, message - finally: - r.close() + def _dispatch(self, method, params): -# vim: set et sts=4 sw=4 : + retn = SimpleXMLRPCDispatcher._dispatch(self, method, params) + retn = translate(retn) + return retn +