Merge lp:~jameinel/launchpad/py27-xmlrpc-auth-1019292 into lp:launchpad

Proposed by John A Meinel on 2012-06-29
Status: Merged
Approved by: Brad Crittenden on 2012-06-29
Approved revision: no longer in the source branch.
Merged at revision: 15538
Proposed branch: lp:~jameinel/launchpad/py27-xmlrpc-auth-1019292
Merge into: lp:launchpad
Diff against target: 389 lines (+195/-170)
4 files modified
lib/lp/testing/xmlrpc.py (+4/-1)
lib/lp/xmlrpc/tests/test_doc.py (+0/-18)
lib/lp/xmlrpc/tests/test_private_xmlrpc.py (+91/-68)
lib/lp/xmlrpc/tests/test_xmlrpc_selftest.py (+100/-83)
To merge this branch: bzr merge lp:~jameinel/launchpad/py27-xmlrpc-auth-1019292
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code 2012-06-29 Approve on 2012-06-29
Review via email: mp+112792@code.launchpad.net

Commit Message

Fix XMLRPC Authentication for python-2.7

Description of the Change

= Summary =

This fixes another python-2.7 vs 2.6 change in XMLRPC.
Specifically, we have a wrapper that avoids having to set up a real XMLRPC
server and talking to it (XMLRPCTestTransport).
However, it overrode a function that changed in 2.7. This specific fix is
because 'make_connection' in 2.7 now has a side effect of parsing the
extra headers that need to be passed to the server. Without them, you lose
the authentication information.

== Proposed fix ==

In 2.7 make_connection just saves the extra headers as
'self._extra_headers'. So we just follow suite. In 2.6 it just means an
extra attribute that will be ignored.

== LOC Rationale ==

This adds about 30 LoC. However the bulk of this is moving all of the doc
tests over to be unittests. Which should be more maintainable. (It
certainly was easier to debug the failure, since I could step around with
pdb.)

== Tests ==

No new tests added, just moved from doc tests to unit tests.

bin/test -vvv -m lp.xmlrpc.tests

Should run the test suite for this.

== Demo and Q/A ==

This seems to all be test-suite infrastructure, so the QA is 'do the tests
pass, even on python-2.7'.

To post a comment you must log in.
Brad Crittenden (bac) wrote :

Hi John,

Thanks for the fix and going the extra step to convert to a unit test.

Rather than login(ANONYMOUS) I think using a with statement with the 'person_logged_in' context manager is cleaner.

typo: hello() returns Anonymous

Otherwise it looks good.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/testing/xmlrpc.py'
2--- lib/lp/testing/xmlrpc.py 2012-06-29 10:32:56 +0000
3+++ lib/lp/testing/xmlrpc.py 2012-07-02 09:14:27 +0000
4@@ -103,5 +103,8 @@
5
6 def make_connection(self, host):
7 """Return our custom HTTPCaller HTTPConnection."""
8- host, extra_headers, x509 = self.get_host_info(host)
9+ # In Python2.7, make_connection caches the extra_headers, which is
10+ # where authorization is stored. In 2.6 it is safe to cache them,
11+ # because it just ignores the extra attribute.
12+ host, self._extra_headers, x509 = self.get_host_info(host)
13 return HTTPCallerHTTPConnection(host)
14
15=== removed directory 'lib/lp/xmlrpc/doc'
16=== removed file 'lib/lp/xmlrpc/tests/test_doc.py'
17--- lib/lp/xmlrpc/tests/test_doc.py 2012-01-01 02:58:52 +0000
18+++ lib/lp/xmlrpc/tests/test_doc.py 1970-01-01 00:00:00 +0000
19@@ -1,18 +0,0 @@
20-# Copyright 2009 Canonical Ltd. This software is licensed under the
21-# GNU Affero General Public License version 3 (see the file LICENSE).
22-
23-"""
24-Run the doctests and pagetests.
25-"""
26-
27-import os
28-
29-from lp.services.testing import build_test_suite
30-from lp.testing.layers import LaunchpadFunctionalLayer
31-
32-
33-here = os.path.dirname(os.path.realpath(__file__))
34-
35-
36-def test_suite():
37- return build_test_suite(here, {}, layer=LaunchpadFunctionalLayer)
38
39=== renamed file 'lib/lp/xmlrpc/doc/private-xmlrpc.txt' => 'lib/lp/xmlrpc/tests/test_private_xmlrpc.py'
40--- lib/lp/xmlrpc/doc/private-xmlrpc.txt 2011-12-23 16:15:14 +0000
41+++ lib/lp/xmlrpc/tests/test_private_xmlrpc.py 2012-07-02 09:14:27 +0000
42@@ -1,68 +1,91 @@
43-Private XML-RPC ports
44-=====================
45-
46-Several internal services require access to Launchpad data, typically over
47-XML-RPC. Because these services are internal and the data they expose is not
48-needed by the outside world -- nor should it be -- Launchpad exposes an
49-internal port for these private XML-RPC end points. These internal-only end
50-points are not available on the public XML-RPC port.
51-
52- >>> public_root = 'http://test@canonical.com:test@xmlrpc.launchpad.dev/'
53- >>> private_root = 'http://xmlrpc-private.launchpad.dev:8087/'
54-
55-For example, the team mailing list feature requires a connection between an
56-internal Mailman server and Launchpad. This end point is not available on the
57-external XML-RPC port.
58-
59- >>> import xmlrpclib
60- >>> from lp.testing.xmlrpc import XMLRPCTestTransport
61- >>> external_api = xmlrpclib.ServerProxy(
62- ... public_root + 'mailinglists/',
63- ... transport=XMLRPCTestTransport())
64- >>> external_api.getPendingActions()
65- Traceback (most recent call last):
66- ...
67- ProtocolError: <ProtocolError for
68- test@canonical.com:test@xmlrpc.launchpad.dev/mailinglists/:
69- 404 404 Not Found>
70-
71-However, the end point is available on the internal port and does not require
72-authentication.
73-
74- >>> internal_api = xmlrpclib.ServerProxy(
75- ... private_root + 'mailinglists/',
76- ... transport=XMLRPCTestTransport())
77- >>> internal_api.getPendingActions()
78- {}
79-
80-The bugs API on the other hand is an external service so it is available on
81-the external port.
82-
83- >>> external_api = xmlrpclib.ServerProxy(
84- ... public_root + 'bugs/',
85- ... transport=XMLRPCTestTransport())
86- >>> external_api.filebug(dict(
87- ... product='firefox', summary='the summary', comment='the comment'))
88- 'http://bugs.launchpad.dev/bugs/16'
89-
90-There is an interal bugs api, too, but that doesn't share the same
91-methods as those exposed publicly.
92-
93- >>> internal_api = xmlrpclib.ServerProxy(
94- ... private_root + 'bugs/',
95- ... transport=XMLRPCTestTransport())
96- >>> internal_api.filebug(dict(
97- ... product='firefox', summary='the summary', comment='the comment'))
98- Traceback (most recent call last):
99- ...
100- ProtocolError: <ProtocolError for xmlrpc-private.launchpad.dev:8087/bugs/:
101- 404 404 Not Found>
102-
103- >>> from zope.component import getUtility
104- >>> from lp.services.verification.interfaces.logintoken import (
105- ... ILoginTokenSet)
106- >>> token_string = internal_api.newBugTrackerToken()
107- >>> token = getUtility(ILoginTokenSet)[token_string]
108- >>> token
109- <LoginToken at ...>
110-
111+# Copyright 2012 Canonical Ltd. This software is licensed under the
112+# GNU Affero General Public License version 3 (see the file LICENSE).
113+
114+"""Private XMLRPC tests.
115+"""
116+
117+import xmlrpclib
118+
119+from zope.component import getUtility
120+
121+from lp.services.verification.interfaces.logintoken import ILoginTokenSet
122+
123+from lp.testing import (
124+ anonymous_logged_in,
125+ person_logged_in,
126+ )
127+from lp.testing.layers import LaunchpadFunctionalLayer
128+from lp.testing.xmlrpc import XMLRPCTestTransport
129+from lp.testing import TestCase
130+
131+
132+class TestPrivateXMLRPC(TestCase):
133+ """Several internal services require access to Launchpad data, typically
134+ over XML-RPC. Because these services are internal and the data they expose
135+ is not needed by the outside world -- nor should it be -- Launchpad exposes
136+ an internal port for these private XML-RPC end points. These internal-only
137+ end points are not available on the public XML-RPC port.
138+ """
139+
140+ layer = LaunchpadFunctionalLayer
141+
142+ public_root = 'http://test@canonical.com:test@xmlrpc.launchpad.dev/'
143+ private_root = 'http://xmlrpc-private.launchpad.dev:8087/'
144+
145+ def get_public_proxy(self, path):
146+ """Get an xmlrpclib.ServerProxy pointing at the public URL"""
147+ return xmlrpclib.ServerProxy(
148+ self.public_root + path,
149+ transport=XMLRPCTestTransport())
150+
151+ def get_private_proxy(self, path):
152+ """Get an xmlrpclib.ServerProxy pointing at the private URL"""
153+ return xmlrpclib.ServerProxy(
154+ self.private_root + path,
155+ transport=XMLRPCTestTransport())
156+
157+ def test_mailing_lists_not_public(self):
158+ """For example, the team mailing list feature requires a connection
159+ between an internal Mailman server and Launchpad. This end point is
160+ not available on the external XML-RPC port.
161+ """
162+ external_api = self.get_public_proxy('mailinglists/')
163+ e = self.assertRaises(xmlrpclib.ProtocolError,
164+ external_api.getPendingActions)
165+ self.assertEqual(404, e.errcode)
166+
167+ def test_mailing_lists_internally_available(self):
168+ """However, the end point is available on the internal port and does
169+ not require authentication.
170+ """
171+ internal_api = self.get_private_proxy('mailinglists/')
172+ self.assertEqual({}, internal_api.getPendingActions())
173+
174+ def test_external_bugs_api(self):
175+ """The bugs API on the other hand is an external service so it is
176+ available on the external port.
177+ """
178+ with anonymous_logged_in():
179+ external_api = self.get_public_proxy('bugs/')
180+ bug_dict = dict(
181+ product='firefox', summary='the summary', comment='the comment')
182+ result = external_api.filebug(bug_dict)
183+ self.assertEqual('http://bugs.launchpad.dev/bugs/16', result)
184+
185+ def test_internal_bugs_api(self):
186+ """There is an interal bugs api, too, but that doesn't share the same
187+ methods as those exposed publicly.
188+ """
189+ internal_api = self.get_private_proxy('bugs/')
190+ bug_dict = dict(
191+ product='firefox', summary='the summary', comment='the comment')
192+ e = self.assertRaises(xmlrpclib.ProtocolError,
193+ internal_api.filebug, bug_dict)
194+ self.assertEqual(404, e.errcode)
195+
196+ def test_get_utility(self):
197+ with anonymous_logged_in():
198+ internal_api = self.get_private_proxy('bugs/')
199+ token_string = internal_api.newBugTrackerToken()
200+ token = getUtility(ILoginTokenSet)[token_string]
201+ self.assertEqual('LoginToken', token.__class__.__name__)
202
203=== renamed file 'lib/lp/xmlrpc/doc/xmlrpc-selftest.txt' => 'lib/lp/xmlrpc/tests/test_xmlrpc_selftest.py'
204--- lib/lp/xmlrpc/doc/xmlrpc-selftest.txt 2011-12-24 17:49:30 +0000
205+++ lib/lp/xmlrpc/tests/test_xmlrpc_selftest.py 2012-07-02 09:14:27 +0000
206@@ -1,83 +1,100 @@
207-= XMLRPC self-test API =
208-
209-The Launchpad root object has a simple XMLRPC API to show that XMLRPC works.
210-
211- >>> from lp.xmlrpc.application import SelfTest, ISelfTest
212- >>> from lp.services.webapp.testing import verifyObject
213- >>> selftestview = SelfTest('somecontext', 'somerequest')
214- >>> verifyObject(ISelfTest, selftestview)
215- True
216- >>> selftestview.concatenate('foo', 'bar')
217- u'foo bar'
218- >>> selftestview.make_fault()
219- <Fault 666: 'Yoghurt and spanners.'>
220-
221-We can test our XMLRPC APIs using xmlrpclib, using a custom Transport
222-which talks with the publisher directly.
223-
224- >>> import xmlrpclib
225- >>> from lp.testing.xmlrpc import XMLRPCTestTransport
226- >>> selftest = xmlrpclib.ServerProxy(
227- ... 'http://xmlrpc.launchpad.dev/', transport=XMLRPCTestTransport())
228- >>> selftest.concatenate('foo', 'bar')
229- 'foo bar'
230- >>> selftest.make_fault()
231- Traceback (most recent call last):
232- ...
233- Fault: <Fault 666: 'Yoghurt and spanners.'>
234-
235-
236-== Unexpected Exceptions ==
237-
238-Sometimes an XML-RPC method will be buggy, and raise an exception
239-other than xmlrpclib.Fault. We have such a method on the self test
240-view:
241-
242- >>> selftestview.raise_exception()
243- Traceback (most recent call last):
244- ...
245- RuntimeError: selftest exception
246-
247-
248-As with normal browser requests, we don't want to expose these error
249-messages to the user since they could contain confidential
250-information. Such exceptions get converted to a fault listing the
251-OOPS ID (assuming one was generated):
252-
253- >>> selftest.raise_exception()
254- Traceback (most recent call last):
255- ...
256- Fault: <Fault -1: 'OOPS-...'>
257-
258-
259-== Authentication ==
260-
261- >>> selftest.hello()
262- 'Hello Anonymous.'
263-
264-The last call returned 'Anonymous', since we didn't provided a username
265-and a password. If we do that, hello() will print the name of the
266-logged in user:
267-
268- >>> selftest = xmlrpclib.ServerProxy(
269- ... 'http://test@canonical.com:test@xmlrpc.launchpad.dev/',
270- ... transport=XMLRPCTestTransport())
271- >>> selftest.hello()
272- 'Hello Sample Person.'
273-
274-The interactions in this test, and the interaction in the XMLRPC
275-methods are different, so we still have an anonymous interaction in
276-this test.
277-
278- >>> getUtility(ILaunchBag).user is None
279- True
280-
281-Even if we log in as Foo Bar here, the XMLRPC method will see Sample
282-Person as the logged in user.
283-
284- >>> login('foo.bar@canonical.com')
285- >>> selftest.hello()
286- 'Hello Sample Person.'
287-
288- >>> print getUtility(ILaunchBag).user.displayname
289- Foo Bar
290+# Copyright 2012 Canonical Ltd. This software is licensed under the
291+# GNU Affero General Public License version 3 (see the file LICENSE).
292+
293+"""XMLRPC self-test api.
294+"""
295+
296+import xmlrpclib
297+
298+from zope.component import getUtility
299+
300+from lp.services.webapp.interfaces import ILaunchBag
301+from lp.xmlrpc.application import SelfTest, ISelfTest
302+from lp.services.webapp.testing import verifyObject
303+from lp.testing import (
304+ anonymous_logged_in,
305+ TestCaseWithFactory,
306+ person_logged_in,
307+ )
308+from lp.testing.layers import LaunchpadFunctionalLayer
309+from lp.testing.xmlrpc import XMLRPCTestTransport
310+
311+
312+class TestXMLRPCSelfTest(TestCaseWithFactory):
313+
314+ layer = LaunchpadFunctionalLayer
315+
316+ def make_proxy(self):
317+ return xmlrpclib.ServerProxy(
318+ 'http://xmlrpc.launchpad.dev/', transport=XMLRPCTestTransport())
319+
320+ def make_logged_in_proxy(self):
321+ return xmlrpclib.ServerProxy(
322+ 'http://test@canonical.com:test@xmlrpc.launchpad.dev/',
323+ transport=XMLRPCTestTransport())
324+
325+ def test_launchpad_root_object(self):
326+ """The Launchpad root object has a simple XMLRPC API to show that
327+ XMLRPC works.
328+ """
329+ selftestview = SelfTest('somecontext', 'somerequest')
330+ self.assertTrue(verifyObject(ISelfTest, selftestview))
331+ self.assertEqual(u'foo bar', selftestview.concatenate('foo', 'bar'))
332+ fault = selftestview.make_fault()
333+ self.assertEqual("<Fault 666: 'Yoghurt and spanners.'>", str(fault))
334+
335+ def test_custom_transport(self):
336+ """We can test our XMLRPC APIs using xmlrpclib, using a custom
337+ Transport which talks with the publisher directly.
338+ """
339+ selftest = self.make_proxy()
340+ self.assertEqual('foo bar', selftest.concatenate('foo', 'bar'))
341+ fault = self.assertRaises(xmlrpclib.Fault, selftest.make_fault)
342+ self.assertEqual("<Fault 666: 'Yoghurt and spanners.'>", str(fault))
343+
344+ def test_unexpected_exception(self):
345+ """Sometimes an XML-RPC method will be buggy, and raise an exception
346+ other than xmlrpclib.Fault. We have such a method on the self test
347+ view.
348+ """
349+ selftestview = SelfTest('somecontext', 'somerequest')
350+ self.assertRaises(RuntimeError, selftestview.raise_exception)
351+
352+ def test_exception_converted_to_fault(self):
353+ """As with normal browser requests, we don't want to expose these error
354+ messages to the user since they could contain confidential information.
355+ Such exceptions get converted to a fault listing the OOPS ID (assuming
356+ one was generated):
357+ """
358+ selftest = self.make_proxy()
359+ e = self.assertRaises(xmlrpclib.Fault, selftest.raise_exception)
360+ self.assertStartsWith(str(e), "<Fault -1: 'OOPS-")
361+
362+ def test_anonymous_authentication(self):
363+ """hello() returns Anonymous because we haven't logged in."""
364+ selftest = self.make_proxy()
365+ self.assertEqual('Hello Anonymous.', selftest.hello())
366+
367+ def test_user_pass_authentication(self):
368+ """If we provide a username and password, hello() will
369+ include the name of the logged in user.
370+
371+ The interactions in this test, and the interaction in the XMLRPC
372+ methods are different, so we still have an anonymous interaction in
373+ this test.
374+ """
375+ with anonymous_logged_in():
376+ self.assertIs(None, getUtility(ILaunchBag).user)
377+ selftest = self.make_logged_in_proxy()
378+ self.assertEqual('Hello Sample Person.', selftest.hello())
379+
380+ def test_login_differences(self):
381+ """Even if we log in as Foo Bar here, the XMLRPC method will see Sample
382+ Person as the logged in user.
383+ """
384+ person = self.factory.makePerson()
385+ with person_logged_in(person):
386+ selftest = self.make_logged_in_proxy()
387+ self.assertEqual('Hello Sample Person.', selftest.hello())
388+ self.assertEqual(person.displayname,
389+ getUtility(ILaunchBag).user.displayname)