Skip to content

Commit 62b5faa

Browse files
committed
Initial checkin for new xmlrpc frontend.
1 parent ae887a4 commit 62b5faa

File tree

3 files changed

+330
-0
lines changed

3 files changed

+330
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#! /usr/bin/env python
2+
#
3+
# Copyright (C) 2007 Stefan Seefeld
4+
# All rights reserved.
5+
# Licensed to the public under the terms of the GNU LGPL (>= 2),
6+
# see the file COPYING for details.
7+
#
8+
import getopt, os, sys, socket
9+
from roundup.xmlrpc import RoundupServer, RoundupRequestHandler
10+
from roundup.instance import TrackerError
11+
from SimpleXMLRPCServer import SimpleXMLRPCServer
12+
13+
def usage():
14+
print """
15+
16+
Options:
17+
-i instance home -- specify the issue tracker "home directory" to administer
18+
-V -- be verbose when importing
19+
-p, --port <port> -- port to listen on
20+
21+
"""
22+
23+
def run():
24+
25+
try:
26+
opts, args = getopt.getopt(sys.argv[1:],
27+
'e:i:p:V', ['encoding=', 'port='])
28+
except getopt.GetoptError, e:
29+
usage()
30+
return 1
31+
32+
verbose = False
33+
tracker = ''
34+
port = 8000
35+
encoding = None
36+
37+
for opt, arg in opts:
38+
if opt == '-V':
39+
verbose = True
40+
elif opt == '-i':
41+
tracker = arg
42+
elif opt in ['-p', '--port']:
43+
port = int(arg)
44+
elif opt in ['-e', '--encoding']:
45+
encoding = encoding
46+
47+
if sys.version_info[0:2] < (2,5):
48+
if encoding:
49+
print 'encodings not supported with python < 2.5'
50+
sys.exit(-1)
51+
server = SimpleXMLRPCServer(('', port), RoundupRequestHandler)
52+
else:
53+
server = SimpleXMLRPCServer(('', port), RoundupRequestHandler,
54+
allow_none=True, encoding=encoding)
55+
if not os.path.exists(tracker):
56+
print 'Instance home does not exist.'
57+
sys.exit(-1)
58+
try:
59+
object = RoundupServer(tracker, verbose)
60+
except TrackerError:
61+
print 'Instance home does not exist.'
62+
sys.exit(-1)
63+
64+
server.register_instance(object)
65+
66+
# Go into the main listener loop
67+
print 'Roundup XMLRPC server started on %s:%d' \
68+
% (socket.gethostname(), port)
69+
try:
70+
server.serve_forever()
71+
except KeyboardInterrupt:
72+
print 'Keyboard Interrupt: exiting'
73+
74+
if __name__ == '__main__':
75+
run()

roundup/xmlrpc.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
#
2+
# Copyright (C) 2007 Stefan Seefeld
3+
# All rights reserved.
4+
# Licensed to the public under the terms of the GNU LGPL (>= 2),
5+
# see the file COPYING for details.
6+
#
7+
8+
9+
import base64
10+
import roundup.instance
11+
from roundup import hyperdb
12+
from roundup.cgi.exceptions import *
13+
from roundup.admin import UsageError
14+
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
15+
16+
class RoundupRequestHandler(SimpleXMLRPCRequestHandler):
17+
"""A SimpleXMLRPCRequestHandler with support for basic
18+
HTTP Authentication."""
19+
20+
def do_POST(self):
21+
"""Extract username and password from authorization header."""
22+
23+
# Try to extract username and password from HTTP Authentication.
24+
self.username = None
25+
self.password = None
26+
authorization = self.headers.get('authorization', ' ')
27+
scheme, challenge = authorization.split(' ', 1)
28+
29+
if scheme.lower() == 'basic':
30+
decoded = base64.decodestring(challenge)
31+
self.username, self.password = decoded.split(':')
32+
33+
SimpleXMLRPCRequestHandler.do_POST(self)
34+
35+
def _dispatch(self, method, params):
36+
"""Inject username and password into function arguments."""
37+
38+
# Add username and password to function arguments
39+
params = [self.username, self.password] + list(params)
40+
return self.server._dispatch(method, params)
41+
42+
43+
class RoundupRequest:
44+
"""Little helper class to handle common per-request tasks such
45+
as authentication and login."""
46+
47+
def __init__(self, tracker, username, password):
48+
"""Open the database for the given tracker, using the given
49+
username and password."""
50+
51+
self.tracker = tracker
52+
self.db = self.tracker.open('admin')
53+
try:
54+
userid = self.db.user.lookup(username)
55+
except KeyError: # No such user
56+
raise Unauthorised, 'Invalid user.'
57+
stored = self.db.user.get(userid, 'password')
58+
if stored != password: # Wrong password
59+
raise Unauthorised, 'Invalid user.'
60+
self.db.setCurrentUser(username)
61+
62+
def __del__(self):
63+
"""Close the database, after committing any changes, if needed."""
64+
65+
if getattr(self, 'db'):
66+
try:
67+
if self.db.transactions:
68+
self.db.commit()
69+
finally:
70+
self.db.close()
71+
72+
73+
def get_class(self, classname):
74+
"""Return the class for the given classname."""
75+
76+
try:
77+
return self.db.getclass(classname)
78+
except KeyError:
79+
raise UsageError, 'no such class "%s"'%classname
80+
81+
def props_from_args(self, cl, args):
82+
"""Construct a list of properties from the given arguments,
83+
and return them after validation."""
84+
85+
props = {}
86+
for arg in args:
87+
if arg.find('=') == -1:
88+
raise UsageError, 'argument "%s" not propname=value'%arg
89+
l = arg.split('=')
90+
if len(l) < 2:
91+
raise UsageError, 'argument "%s" not propname=value'%arg
92+
key, value = l[0], '='.join(l[1:])
93+
if value:
94+
try:
95+
props[key] = hyperdb.rawToHyperdb(self.db, cl, None,
96+
key, value)
97+
except hyperdb.HyperdbValueError, message:
98+
raise UsageError, message
99+
else:
100+
props[key] = None
101+
102+
return props
103+
104+
105+
#The server object
106+
class RoundupServer:
107+
"""The RoundupServer provides the interface accessible through
108+
the Python XMLRPC mapping. All methods take an additional username
109+
and password argument so each request can be authenticated."""
110+
111+
def __init__(self, tracker, verbose = False):
112+
self.tracker = roundup.instance.open(tracker)
113+
self.verbose = verbose
114+
115+
def list(self, username, password, classname, propname = None):
116+
117+
r = RoundupRequest(self.tracker, username, password)
118+
cl = r.get_class(classname)
119+
if not propname:
120+
propname = cl.labelprop()
121+
return [cl.get(id, propname) for id in cl.list()]
122+
123+
def display(self, username, password, designator, *properties):
124+
125+
r = RoundupRequest(self.tracker, username, password)
126+
classname, nodeid = hyperdb.splitDesignator(designator)
127+
cl = r.get_class(classname)
128+
props = properties and list(properties) or cl.properties.keys()
129+
props.sort()
130+
return dict([(property, cl.get(nodeid, property))
131+
for property in props])
132+
133+
def create(self, username, password, classname, *args):
134+
135+
r = RoundupRequest(self.tracker, username, password)
136+
cl = r.get_class(classname)
137+
138+
# convert types
139+
props = r.props_from_args(cl, args)
140+
141+
# check for the key property
142+
key = cl.getkey()
143+
if key and not props.has_key(key):
144+
raise UsageError, 'you must provide the "%s" property.'%key
145+
146+
# do the actual create
147+
try:
148+
return cl.create(**props)
149+
except (TypeError, IndexError, ValueError), message:
150+
raise UsageError, message
151+
152+
def set(self, username, password, designator, *args):
153+
154+
r = RoundupRequest(self.tracker, username, password)
155+
classname, itemid = hyperdb.splitDesignator(designator)
156+
cl = r.get_class(classname)
157+
158+
# convert types
159+
props = r.props_from_args(cl, args)
160+
try:
161+
cl.set(itemid, **props)
162+
except (TypeError, IndexError, ValueError), message:
163+
raise UsageError, message
164+
165+

test/test_xmlrpc.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#
2+
# Copyright (C) 2007 Stefan Seefeld
3+
# All rights reserved.
4+
# Licensed to the public under the terms of the GNU LGPL (>= 2),
5+
# see the file COPYING for details.
6+
#
7+
8+
import unittest, os, shutil, errno, sys, difflib, cgi, re
9+
10+
from roundup.cgi.exceptions import *
11+
from roundup import init, instance, password, hyperdb, date
12+
from roundup.xmlrpc import RoundupServer
13+
14+
import db_test_base
15+
16+
NEEDS_INSTANCE = 1
17+
18+
class TestCaseBase(unittest.TestCase):
19+
20+
def setUp(self):
21+
22+
self.dirname = '_test_xmlrpc'
23+
# set up and open a tracker
24+
self.instance = db_test_base.setupTracker(self.dirname)
25+
26+
# open the database
27+
self.db = self.instance.open('admin')
28+
self.db.user.create(username='joe', password=password.Password('random'),
29+
address='[email protected]',
30+
realname='Joe Random', roles='User')
31+
32+
self.db.commit()
33+
self.db.close()
34+
35+
self.server = RoundupServer(self.dirname)
36+
37+
38+
def tearDown(self):
39+
40+
try:
41+
shutil.rmtree(self.dirname)
42+
except OSError, error:
43+
if error.errno not in (errno.ENOENT, errno.ESRCH): raise
44+
45+
class AccessTestCase(TestCaseBase):
46+
47+
def test(self):
48+
49+
# Retrieve all three users.
50+
results = self.server.list('joe', 'random', 'user', 'id')
51+
self.assertEqual(len(results), 3)
52+
# Obtain data for 'joe'.
53+
userid = 'user' + results[-1]
54+
results = self.server.display('joe', 'random', userid)
55+
self.assertEqual(results['username'], 'joe')
56+
self.assertEqual(results['realname'], 'Joe Random')
57+
# Reset joe's 'realname'.
58+
results = self.server.set('joe', 'random', userid, 'realname=Joe Doe')
59+
results = self.server.display('joe', 'random', userid, 'realname')
60+
self.assertEqual(results['realname'], 'Joe Doe')
61+
# Create test
62+
results = self.server.create('joe', 'random', 'issue', 'title=foo')
63+
issueid = 'issue' + results
64+
results = self.server.display('joe', 'random', issueid, 'title')
65+
self.assertEqual(results['title'], 'foo')
66+
67+
class AuthenticationTestCase(TestCaseBase):
68+
69+
def test(self):
70+
71+
# Unknown user (caught in XMLRPC frontend).
72+
self.assertRaises(Unauthorised, self.server.list,
73+
'nobody', 'nobody', 'user', 'id')
74+
# Wrong permissions (caught by roundup security module).
75+
results = self.server.list('joe', 'random', 'user', 'id')
76+
userid = 'user' + results[0] # admin
77+
# FIXME: why doesn't the following raise an exception ?
78+
# self.assertRaises(Unauthorised, self.server.set,
79+
# 'joe', 'random', userid, 'realname=someone')
80+
81+
82+
def test_suite():
83+
suite = unittest.TestSuite()
84+
suite.addTest(unittest.makeSuite(AccessTestCase))
85+
suite.addTest(unittest.makeSuite(AuthenticationTestCase))
86+
return suite
87+
88+
if __name__ == '__main__':
89+
runner = unittest.TextTestRunner()
90+
unittest.main(testRunner=runner)

0 commit comments

Comments
 (0)