Code

* Add support for actions to XMLRPC interface.
authorstefan <stefan@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 27 Feb 2009 17:46:47 +0000 (17:46 +0000)
committerstefan <stefan@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 27 Feb 2009 17:46:47 +0000 (17:46 +0000)
* Provide bridge so user actions may be executed
  either via CGI or XMLRPC.
* Adjust XMLRPC tests to recent work.
* Cleanup.

git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/roundup/trunk@4175 57a73879-2fb5-44c3-a270-3262357dd7e2

roundup/actions.py [new file with mode: 0644]
roundup/cgi/actions.py
roundup/cgi/client.py
roundup/cgi/exceptions.py
roundup/exceptions.py
roundup/instance.py
roundup/scripts/roundup_xmlrpc_server.py
roundup/xmlrpc.py
test/test_xmlrpc.py

diff --git a/roundup/actions.py b/roundup/actions.py
new file mode 100644 (file)
index 0000000..0741aed
--- /dev/null
@@ -0,0 +1,68 @@
+#
+# Copyright (C) 2009 Stefan Seefeld
+# All rights reserved.
+# For license terms see the file COPYING.txt.
+#
+
+from roundup.exceptions import *
+from roundup import hyperdb
+from roundup.i18n import _
+
+class Action:
+    def __init__(self, db, translator):
+        self.db = db
+        self.translator = translator
+
+    def handle(self, *args):
+        """Action handler procedure"""
+        raise NotImplementedError
+
+    def execute(self, *args):
+        """Execute the action specified by this object."""
+
+        self.permission(*args)
+        return self.handle(*args)
+
+
+    def permission(self, *args):
+        """Check whether the user has permission to execute this action.
+
+        If not, raise Unauthorised."""
+
+        pass
+
+
+    def gettext(self, msgid):
+        """Return the localized translation of msgid"""
+        return self.translator.gettext(msgid)
+
+
+    _ = gettext
+
+
+class Retire(Action):
+
+    def handle(self, designator):
+
+        classname, itemid = hyperdb.splitDesignator(designator)
+
+        # make sure we don't try to retire admin or anonymous
+        if (classname == 'user' and
+            self.db.user.get(itemid, 'username') in ('admin', 'anonymous')):
+            raise ValueError, self._(
+                'You may not retire the admin or anonymous user')
+
+        # do the retire
+        self.db.getclass(classname).retire(itemid)
+        self.db.commit()
+
+
+    def permission(self, designator):
+
+        classname, itemid = hyperdb.splitDesignator(designator)
+
+        if not self.db.security.hasPermission('Edit', self.db.getuid(),
+                                              classname=classname, itemid=itemid):
+            raise Unauthorised(self._('You do not have permission to '
+                                      '%(action)s the %(classname)s class.')%info)
+            
index e8d69ac251a464e05024c5a4b8cd6d1369a4a5aa..4f5a0b00c9dec3f7d41efccc67844f0efc2f60a5 100755 (executable)
@@ -1,8 +1,7 @@
-#$Id: actions.py,v 1.73 2008-08-18 05:04:01 richard Exp $
-
 import re, cgi, StringIO, urllib, time, random, csv, codecs
 
 from roundup import hyperdb, token, date, password
+from roundup.actions import Action as BaseAction
 from roundup.i18n import _
 import roundup.exceptions
 from roundup.cgi import exceptions, templating
@@ -991,4 +990,46 @@ class ExportCSVAction(Action):
 
         return '\n'
 
+
+class Bridge(BaseAction):
+    """Make roundup.actions.Action executable via CGI request.
+
+    Using this allows users to write actions executable from multiple frontends.
+    CGI Form content is translated into a dictionary, which then is passed as
+    argument to 'handle()'. XMLRPC requests have to pass this dictionary
+    directly.
+    """
+
+    def __init__(self, *args):
+
+        # As this constructor is callable from multiple frontends, each with
+        # different Action interfaces, we have to look at the arguments to
+        # figure out how to complete construction.
+        if (len(args) == 1 and
+            hasattr(args[0], '__class__') and
+            args[0].__class__.__name__ == 'Client'):
+            self.cgi = True
+            self.execute = self.execute_cgi
+            self.client = args[0]
+            self.form = self.client.form
+        else:
+            self.cgi = False
+
+    def execute_cgi(self):
+        args = {}
+        for key in self.form.keys():
+            args[key] = self.form.getvalue(key)
+        self.permission(args)
+        return self.handle(args)
+
+    def permission(self, args):
+        """Raise Unauthorised if the current user is not allowed to execute
+        this action. Users may override this method."""
+        
+        pass
+
+    def handle(self, args):
+        
+        raise NotImplementedError
+
 # vim: set filetype=python sts=4 sw=4 et si :
index 0ea02f13388ea04c5bbde32a1ba79f160730f570..dad5c8ecda0bb3aff80fe1d98dabb6eb4b5d195d 100644 (file)
@@ -382,7 +382,9 @@ class Client:
         self.determine_user()
 
         # Call the appropriate XML-RPC method.
-        handler = xmlrpc.RoundupDispatcher(self.db, self.userid, self.translator,
+        handler = xmlrpc.RoundupDispatcher(self.db,
+                                           self.instance.actions,
+                                           self.translator,
                                            allow_none=True)
         output = handler.dispatch(input)
         self.db.commit()
index b7f33cbd8108da756394d79c9212b284867532cf..c0cb886b4bb77f4326515066bae81376f2593005 100755 (executable)
@@ -1,20 +1,14 @@
-#$Id: exceptions.py,v 1.6 2004-11-18 14:10:27 a1s Exp $
-'''Exceptions for use in Roundup's web interface.
-'''
+"""Exceptions for use in Roundup's web interface.
+"""
 
 __docformat__ = 'restructuredtext'
 
+from roundup.exceptions import LoginError, Unauthorised
 import cgi
 
 class HTTPException(Exception):
     pass
 
-class LoginError(HTTPException):
-    pass
-
-class Unauthorised(HTTPException):
-    pass
-
 class Redirect(HTTPException):
     pass
 
@@ -50,13 +44,13 @@ class SeriousError(Exception):
     escaped.
     """
     def __str__(self):
-        return '''
+        return """
 <html><head><title>Roundup issue tracker: An error has occurred</title>
  <link rel="stylesheet" type="text/css" href="@@file/style.css">
 </head>
 <body class="body" marginwidth="0" marginheight="0">
  <p class="error-message">%s</p>
 </body></html>
-'''%cgi.escape(self.args[0])
+"""%cgi.escape(self.args[0])
 
 # vim: set filetype=python sts=4 sw=4 et si :
index a9ba2cf8ebd78d028ceda744a416598400191464..629516d3a0a6ed42c9c2e3d1671304a6c06fd80d 100644 (file)
@@ -1,11 +1,16 @@
-#$Id: exceptions.py,v 1.1 2004-03-26 00:44:11 richard Exp $
-'''Exceptions for use across all Roundup components.
-'''
+"""Exceptions for use across all Roundup components.
+"""
 
 __docformat__ = 'restructuredtext'
 
+class LoginError(Exception):
+    pass
+
+class Unauthorised(Exception):
+    pass
+
 class Reject(Exception):
-    '''An auditor may raise this exception when the current create or set
+    """An auditor may raise this exception when the current create or set
     operation should be stopped.
 
     It is up to the specific interface invoking the create or set to
@@ -13,7 +18,7 @@ class Reject(Exception):
 
     - mailgw will trap and ignore Reject for file attachments and messages
     - cgi will trap and present the exception in a nice format
-    '''
+    """
     pass
 
 class UsageError(ValueError):
index 9aba2d053aa1138d13bd1bc655d393ca6e3a0a37..127590b2dc8ce72dfa4c6842a51ba7def64480c7 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: instance.py,v 1.37 2006-12-11 23:36:15 richard Exp $
 
-'''Tracker handling (open tracker).
+"""Tracker handling (open tracker).
 
 Backwards compatibility for the old-style "imported" trackers.
-'''
+"""
 __docformat__ = 'restructuredtext'
 
 import os
 import sys
 from roundup import configuration, mailgw
-from roundup import hyperdb, backends
+from roundup import hyperdb, backends, actions
 from roundup.cgi import client, templating
+from roundup.cgi import actions as cgi_actions
 
 class Vars:
     def __init__(self, vars):
@@ -47,6 +47,7 @@ class Tracker:
         self.tracker_home = tracker_home
         self.optimize = optimize
         self.config = configuration.CoreConfig(tracker_home)
+        self.actions = {}
         self.cgi_actions = {}
         self.templating_utils = {}
         self.load_interfaces()
@@ -182,7 +183,20 @@ class Tracker:
         return vars
 
     def registerAction(self, name, action):
-        self.cgi_actions[name] = action
+
+        # The logic here is this:
+        # * if `action` derives from actions.Action,
+        #   it is executable as a generic action.
+        # * if, moreover, it also derives from cgi.actions.Bridge,
+        #   it may in addition be called via CGI
+        # * in all other cases we register it as a CGI action, without
+        #   any check (for backward compatibility).
+        if issubclass(action, actions.Action):
+            self.actions[name] = action
+            if issubclass(action, cgi_actions.Bridge):
+                self.cgi_actions[name] = action
+        else:
+            self.cgi_actions[name] = action
 
     def registerUtil(self, name, function):
         self.templating_utils[name] = function
index 44c626f80c55c19db0367daee285dba8418b7666..b5458e18337503979f7ba5880def45ad1b51b4d4 100644 (file)
@@ -51,7 +51,10 @@ class RequestHandler(SimpleXMLRPCRequestHandler):
 
         if scheme.lower() == 'basic':
             decoded = base64.decodestring(challenge)
-            username, password = decoded.split(':')
+            if ':' in decoded:
+                username, password = decoded.split(':')
+            else:
+                username = decoded
         if not username:
             username = 'anonymous'
         db = tracker.open('admin')
@@ -79,7 +82,7 @@ class RequestHandler(SimpleXMLRPCRequestHandler):
             tracker = self.get_tracker(tracker_name)
             db = self.authenticate(tracker)
 
-            instance = RoundupInstance(db, None)
+            instance = RoundupInstance(db, tracker.actions, None)
             self.server.register_instance(instance)
             SimpleXMLRPCRequestHandler.do_POST(self)
         except Unauthorised, message:
index 45552872576c75a94f200567e1ca2f5d961d6d1b..03ef5a1c107fdc812e3f5a5d6fee9ef8866152ef 100644 (file)
@@ -8,6 +8,7 @@ from roundup import hyperdb
 from roundup.cgi.exceptions import *
 from roundup.exceptions import UsageError
 from roundup.date import Date, Range, Interval
+from roundup import actions
 from SimpleXMLRPCServer import *
 
 def translate(value):
@@ -52,10 +53,10 @@ class RoundupInstance:
     """The RoundupInstance provides the interface accessible through
     the Python XMLRPC mapping."""
 
-    def __init__(self, db, translator):
+    def __init__(self, db, actions, translator):
 
         self.db = db
-        self.userid = db.getuid()
+        self.actions = actions
         self.translator = translator
 
     def list(self, classname, propname=None):
@@ -125,15 +126,30 @@ class RoundupInstance:
             raise UsageError, message
 
 
+    builtin_actions = {'retire': actions.Retire}
+
+    def action(self, name, *args):
+        """"""
+        
+        if name in self.actions:
+            action_type = self.actions[name]
+        elif name in self.builtin_actions:
+            action_type = self.builtin_actions[name]
+        else:
+            raise Exception('action "%s" is not supported %s' % (name, ','.join(self.actions.keys())))
+        action = action_type(self.db, self.translator)
+        return action.execute(*args)
+
+
 class RoundupDispatcher(SimpleXMLRPCDispatcher):
     """RoundupDispatcher bridges from cgi.client to RoundupInstance.
     It expects user authentication to be done."""
 
-    def __init__(self, db, userid, translator,
+    def __init__(self, db, actions, translator,
                  allow_none=False, encoding=None):
 
         SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
-        self.register_instance(RoundupInstance(db, userid, translator))
+        self.register_instance(RoundupInstance(db, actions, translator))
                  
 
     def dispatch(self, input):
index 14e19b4d8dcee215aaa9206c978d95b70ef54f53..5409ea03954b04954240884a8d2a83fea5ce6d62 100644 (file)
@@ -8,7 +8,7 @@ import unittest, os, shutil, errno, sys, difflib, cgi, re
 
 from roundup.cgi.exceptions import *
 from roundup import init, instance, password, hyperdb, date
-from roundup.xmlrpc import RoundupServer
+from roundup.xmlrpc import RoundupInstance
 from roundup.backends import list_backends
 
 import db_test_base
@@ -32,8 +32,8 @@ class TestCase(unittest.TestCase):
 
         self.db.commit()
         self.db.close()
-
-        self.server = RoundupServer(self.dirname)
+        self.db = self.instance.open('joe')
+        self.server = RoundupInstance(self.db, self.instance.actions, None)
 
     def tearDown(self):
         try:
@@ -43,63 +43,74 @@ class TestCase(unittest.TestCase):
 
     def testAccess(self):
         # Retrieve all three users.
-        results = self.server.list('joe', 'random', 'user', 'id')
+        results = self.server.list('user', 'id')
         self.assertEqual(len(results), 3)
 
         # Obtain data for 'joe'.
-        results = self.server.display('joe', 'random', self.joeid)
+        results = self.server.display(self.joeid)
         self.assertEqual(results['username'], 'joe')
         self.assertEqual(results['realname'], 'Joe Random')
 
     def testChange(self):
         # Reset joe's 'realname'.
-        results = self.server.set('joe', 'random', self.joeid,
-            'realname=Joe Doe')
-        results = self.server.display('joe', 'random', self.joeid,
-            'realname')
+        results = self.server.set(self.joeid, 'realname=Joe Doe')
+        results = self.server.display(self.joeid, 'realname')
         self.assertEqual(results['realname'], 'Joe Doe')
 
         # check we can't change admin's details
-        self.assertRaises(Unauthorised, self.server.set, 'joe', 'random',
-            'user1', 'realname=Joe Doe')
+        self.assertRaises(Unauthorised, self.server.set, 'user1', 'realname=Joe Doe')
 
     def testCreate(self):
-        results = self.server.create('joe', 'random', 'issue', 'title=foo')
+        results = self.server.create('issue', 'title=foo')
         issueid = 'issue' + results
-        results = self.server.display('joe', 'random', issueid, 'title')
+        results = self.server.display(issueid, 'title')
         self.assertEqual(results['title'], 'foo')
 
     def testFileCreate(self):
-        results = self.server.create('joe', 'random', 'file', 'content=hello\r\nthere')
+        results = self.server.create('file', 'content=hello\r\nthere')
         fileid = 'file' + results
-        results = self.server.display('joe', 'random', fileid, 'content')
+        results = self.server.display(fileid, 'content')
         self.assertEqual(results['content'], 'hello\r\nthere')
 
-    def testAuthUnknown(self):
-        # Unknown user (caught in XMLRPC frontend).
-        self.assertRaises(Unauthorised, self.server.list,
-            'nobody', 'nobody', 'user', 'id')
+    def testAction(self):
+        # As this action requires special previledges, we temporarily switch
+        # to 'admin'
+        self.db.setCurrentUser('admin')
+        users_before = self.server.list('user')
+        try:
+            tmp = 'user' + self.db.user.create(username='tmp')
+            self.server.action('retire', tmp)
+        finally:
+            self.db.setCurrentUser('joe')
+        users_after = self.server.list('user')
+        self.assertEqual(users_before, users_after)
 
     def testAuthDeniedEdit(self):
         # Wrong permissions (caught by roundup security module).
         self.assertRaises(Unauthorised, self.server.set,
-            'joe', 'random', 'user1', 'realname=someone')
+                          'user1', 'realname=someone')
 
     def testAuthDeniedCreate(self):
         self.assertRaises(Unauthorised, self.server.create,
-            'joe', 'random', 'user', {'username': 'blah'})
+                          'user', {'username': 'blah'})
 
     def testAuthAllowedEdit(self):
+        self.db.setCurrentUser('admin')
         try:
-            self.server.set('admin', 'sekrit', 'user2', 'realname=someone')
+            self.server.set('user2', 'realname=someone')
         except Unauthorised, err:
             self.fail('raised %s'%err)
+        finally:
+            self.db.setCurrentUser('joe')
 
     def testAuthAllowedCreate(self):
+        self.db.setCurrentUser('admin')
         try:
-            self.server.create('admin', 'sekrit', 'user', 'username=blah')
+            self.server.create('user', 'username=blah')
         except Unauthorised, err:
             self.fail('raised %s'%err)
+        finally:
+            self.db.setCurrentUser('joe')
 
 def test_suite():
     suite = unittest.TestSuite()