summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: c9ee48d)
raw | patch | inline | side by side (parent: c9ee48d)
author | stefan <stefan@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Fri, 27 Feb 2009 17:46:47 +0000 (17:46 +0000) | ||
committer | stefan <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
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] | patch | blob |
roundup/cgi/actions.py | patch | blob | history | |
roundup/cgi/client.py | patch | blob | history | |
roundup/cgi/exceptions.py | patch | blob | history | |
roundup/exceptions.py | patch | blob | history | |
roundup/instance.py | patch | blob | history | |
roundup/scripts/roundup_xmlrpc_server.py | patch | blob | history | |
roundup/xmlrpc.py | patch | blob | history | |
test/test_xmlrpc.py | patch | blob | history |
diff --git a/roundup/actions.py b/roundup/actions.py
--- /dev/null
+++ b/roundup/actions.py
@@ -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)
+
diff --git a/roundup/cgi/actions.py b/roundup/cgi/actions.py
index e8d69ac251a464e05024c5a4b8cd6d1369a4a5aa..4f5a0b00c9dec3f7d41efccc67844f0efc2f60a5 100755 (executable)
--- a/roundup/cgi/actions.py
+++ b/roundup/cgi/actions.py
-#$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
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 :
diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py
index 0ea02f13388ea04c5bbde32a1ba79f160730f570..dad5c8ecda0bb3aff80fe1d98dabb6eb4b5d195d 100644 (file)
--- a/roundup/cgi/client.py
+++ b/roundup/cgi/client.py
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)
-#$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
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 :
diff --git a/roundup/exceptions.py b/roundup/exceptions.py
index a9ba2cf8ebd78d028ceda744a416598400191464..629516d3a0a6ed42c9c2e3d1671304a6c06fd80d 100644 (file)
--- a/roundup/exceptions.py
+++ b/roundup/exceptions.py
-#$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
- 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):
diff --git a/roundup/instance.py b/roundup/instance.py
index 9aba2d053aa1138d13bd1bc655d393ca6e3a0a37..127590b2dc8ce72dfa4c6842a51ba7def64480c7 100644 (file)
--- a/roundup/instance.py
+++ b/roundup/instance.py
# 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):
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()
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)
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')
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:
diff --git a/roundup/xmlrpc.py b/roundup/xmlrpc.py
index 45552872576c75a94f200567e1ca2f5d961d6d1b..03ef5a1c107fdc812e3f5a5d6fee9ef8866152ef 100644 (file)
--- a/roundup/xmlrpc.py
+++ b/roundup/xmlrpc.py
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):
"""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):
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):
diff --git a/test/test_xmlrpc.py b/test/test_xmlrpc.py
index 14e19b4d8dcee215aaa9206c978d95b70ef54f53..5409ea03954b04954240884a8d2a83fea5ce6d62 100644 (file)
--- a/test/test_xmlrpc.py
+++ b/test/test_xmlrpc.py
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
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:
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()