Merge lp:~salgado/launchpad/lp-as-openid-rp into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Approved by: Gary Poster
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~salgado/launchpad/lp-as-openid-rp
Merge into: lp:launchpad
Diff against target: 1163 lines (+939/-0)
23 files modified
BRANCH.TODO (+53/-0)
configs/development/launchpad-lazr.conf (+4/-0)
lib/canonical/config/schema-lazr.conf (+8/-0)
lib/canonical/launchpad/browser/launchpad.py (+2/-0)
lib/canonical/launchpad/configure.zcml (+1/-0)
lib/canonical/launchpad/layers.py (+4/-0)
lib/canonical/launchpad/systemhomes.py (+6/-0)
lib/canonical/launchpad/webapp/servers.py (+16/-0)
lib/lp/testopenid/adapters/openid.py (+32/-0)
lib/lp/testopenid/browser/configure.zcml (+82/-0)
lib/lp/testopenid/browser/server.py (+285/-0)
lib/lp/testopenid/configure.zcml (+28/-0)
lib/lp/testopenid/interfaces/server.py (+29/-0)
lib/lp/testopenid/stories/basics.txt (+163/-0)
lib/lp/testopenid/stories/logging-in.txt (+64/-0)
lib/lp/testopenid/stories/tests.py (+22/-0)
lib/lp/testopenid/templates/application-index.pt (+5/-0)
lib/lp/testopenid/templates/application-xrds.pt (+14/-0)
lib/lp/testopenid/templates/auth.pt (+18/-0)
lib/lp/testopenid/templates/persistentidentity-index.pt (+14/-0)
lib/lp/testopenid/templates/persistentidentity-xrds.pt (+14/-0)
lib/lp/testopenid/testing/helpers.py (+74/-0)
utilities/rocketfuel-setup (+1/-0)
To merge this branch: bzr merge lp:~salgado/launchpad/lp-as-openid-rp
Reviewer Review Type Date Requested Status
Gary Poster (community) code Approve
Canonical Launchpad Engineering Pending
Review via email: mp+17842@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :

This branch adds a new vhost (testopenid) which serves an OpenID
provider that is only enabled when devmode is on. The primary use of
this will be to allow we (developers) to log into launchpad.dev once it
becomes an OpenID RP.

This OpenID provider always requires the user to log in and doesn't ask
users whether or not they authorize the provider to send user details to
the site that's requesting it. The login is always required in order to
make it easier to authenticate as a different user, and the lack of an
authorization step is because it wouldn't make any sense here.

In order to try out the testopenid functionality, we need an openid
consumer, which is included (together with this branch) in
lp:~salgado/launchpad/lp-as-openid-rp-loom. To test it you just need to
go to launchpad.dev/+login-openid

BRANCH.TODO has a bunch of random notes and TODO items for myself; it
will be erased before I land this branch or else a test will fail.

Revision history for this message
Gary Poster (gary) wrote :

Thank you, Guilherme, this is a great step forward.

I didn't get too far on this today. Hopefully I'll have some time tomorrow: I'll resume on line 336, and actually try the server out on my system, per your instructions.

I only have one trivial comment so far. In lib/lp/testopenid/browser/configure.zcml I had a harder time reading it than I needed to because you switched back and forth from relative and absolute syntaxes. Here's an example:

279 + <browser:page
280 + for="..interfaces.server.ITestOpenIDApplication"
281 + class="lp.testopenid.browser.server.TestOpenIDView"
282 + permission="zope.Public"
283 + name="+openid"
284 + />
285 + <browser:page
286 + for="..interfaces.server.ITestOpenIDApplication"
287 + class=".server.TestOpenIDIndexView"
288 + permission="zope.Public"
289 + name="+index"
290 + />

Unless I misread it, the "server" module in lines 281 and 287 are the same, which is not as obvious as it could be. I don't care whether you choose relative or absolute, but sticking with one, particularly for the same module within the same zcml file, would help readability. It seems you mostly use relative, so I'd be inclined to standardize on that.

(I also hate browser:page directives because of the class mixin magic they do, but I won't choose you as the target for that rant. What you have done has precedence in Launchpad and is fine.)

Revision history for this message
Gary Poster (gary) wrote :

merge-conditional

As I said before, this is a great branch. I just a have a few comments. I've given them all to you on IRC, but I'll summarize here.

- I had trouble getting the loom branch to work. We eventually had success with lp:~salgado/launchpad/lp-as-openid-rp-for-ec2 after I manually updated my /etc/hosts (I had not run your revised rocketfuel-setup) and you handled the fact that I had pycurl installed in my system Python.

- lib/lp/testopenid/browser/server.py : PersistentIdentityView should be in __all__

- OpenIDMixin class in lib/lp/testopenid/browser/server.py, the openid_parameters property: I'd like a comment as to why ascii is OK (spec or the fact that this is just a test implementation).

- The docstrings are so nice in lib/lp/testopenid/browser/server.py that when I don't see them I miss them: getSession, renderOpenIDResponse, TestOpenIDLoginView, OpenIDMixin. Consider that a nice-to-have, not a must-have, but it is already so close.

- I'd prefer to be super-explicit in the first para of lib/lp/testopenid/stories/basics.txt : add a sentence specifying that "It is only started for development and testing." If you disagree that's fine though.

- For new doctest files we are supposed to be using ReST.

Thank you again!

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'BRANCH.TODO'
2--- BRANCH.TODO 2010-02-09 03:16:27 +0000
3+++ BRANCH.TODO 2010-02-17 15:49:22 +0000
4@@ -2,3 +2,56 @@
5 # landing. There is a test to ensure it is empty in trunk. If there is
6 # stuff still here when you are ready to land, the items should probably
7 # be converted to bugs so they can be scheduled.
8+
9+* Do we want to change c-i-p to use a separate cookie so that logging into
10+login.lp.net doesn't cause you to end up logged into lp.net or should we just
11+wait for ISD to roll a rebranded version of login.u.c on login.lp.net, which
12+will probably use separate cookies.
13+ - Apart from not being trivial to change the cookie name for a given vhost
14+ we also need to worry about making sure the new cookie (which is supposed
15+ to be valid only for login.lp.net) is not valid for all other vhosts, which
16+ would normally happen thanks to LaunchpadCookieClientIdManager.
17+
18+* We still rely heavily on the Account table and expect all users to have an
19+Account. We need to fix that if we're going to copy the Account table into the
20+main replication set and stop using the one from the auth set. If we fix
21+that, though, the Account table won't be necessary so there'll be no point in
22+copying it. *I'm not sure this is feasible.*
23+
24+* As discussed with Francis, it's not worth porting the team-restricted login
25+stuff to the new system, so we'll drop it.
26+
27+* Functionality needed in the callback view:
28+ - reactivate accounts when credentials belong to a deactivated profile
29+ - create Person entries when the credentials don't exist in our db
30+ - forbid to log suspended accounts in. btw, I guess we're going to copy the
31+ AccountStatus table to the main db.
32+
33+ # To test all this I'll just have to monkey patch
34+ # OpenIDCallbackView.openid_response to return my hand-crafted
35+ # openid response.
36+
37+* Some tests use anon_browser.open() on protected pages so that they get
38+reidrected to +login and can show how the protected page works when the user
39+is not logged in, including the preserved query string. If we want to keep
40+doing that in these tests, we'll need an OpenID provider accessible to the
41+test suite. One option would be to include a crippled version of the one in
42+c-i-p in our tree, to be used only in tests.
43+(lib/canonical/launchpad/pagetests/oauth/authorize-token.txt is one of the
44+tests that rely on +login)
45+
46+* Does not work for /people/+me, because when the OpenID provider sends the
47+user back to /people/+me/+openid-callback, there's a redirect to
48+/~person/+openid-callback, which causes the openid dance to fail: *
49+<openid.consumer.consumer.FailureResponse id=None message="return_to does not
50+match return URL. Expected 'https://launchpad.dev/%7Ename16/+openid-callback',
51+got
52+u'https://launchpad.dev/people/+me/+openid-callback?janrain_nonce=2009-12-23T14%3A47%3A47ZsgdbOJ'">
53+ I think this is a general problem with OpenID and login.lp.net because when
54+you log into login.lp.net and is sent to the callback page (in lp.net), that
55+page will see you as logged in, regardless of the openid response, because the
56+cookie is shared, and that causes /people/+me/+openid-callback to redirect to
57+/~name16/+openid-callback, which causes the error above.
58+ One way around this is to always use /+openid-callback as the return_to URL,
59+and include a lp_redirect_to URL in the query string, where LP will send the
60+user to, once the openid dance is completed.
61
62=== modified file 'configs/development/launchpad-lazr.conf'
63--- configs/development/launchpad-lazr.conf 2010-01-22 04:01:17 +0000
64+++ configs/development/launchpad-lazr.conf 2010-02-17 15:49:22 +0000
65@@ -124,6 +124,7 @@
66 public_host: keyserver.launchpad.dev
67
68 [launchpad]
69+enable_test_openid_provider: True
70 code_domain: code.launchpad.dev
71 default_batch_size: 5
72 max_attachment_size: 2097152
73@@ -277,6 +278,9 @@
74 [vhost.openid]
75 hostname: openid.launchpad.dev
76
77+[vhost.testopenid]
78+hostname: testopenid.dev
79+
80 [vhost.ubuntu_openid]
81 hostname: ubuntu-openid.launchpad.dev
82
83
84=== modified file 'lib/canonical/config/schema-lazr.conf'
85--- lib/canonical/config/schema-lazr.conf 2010-01-22 04:01:17 +0000
86+++ lib/canonical/config/schema-lazr.conf 2010-02-17 15:49:22 +0000
87@@ -884,6 +884,11 @@
88 storm_cache: generational
89 storm_cache_size: 10000
90
91+# Whether or not to enable a test OpenID provider on the testopenid vhost.
92+# It's a test provider, not meant to be enabled on production.
93+# datatype: boolean
94+enable_test_openid_provider: False
95+
96 # Assume the slave database is lagged if it takes more than this many
97 # milliseconds to calculate this information from the Slony-I tables.
98 # datatype: integer
99@@ -1839,6 +1844,9 @@
100 [vhost.openid]
101
102
103+[vhost.testopenid]
104+
105+
106 [vhost.ubuntu_openid]
107
108
109
110=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
111--- lib/canonical/launchpad/browser/launchpad.py 2010-02-02 17:12:29 +0000
112+++ lib/canonical/launchpad/browser/launchpad.py 2010-02-17 15:49:22 +0000
113@@ -96,6 +96,7 @@
114 ITranslationGroupSet)
115 from lp.translations.interfaces.translationimportqueue import (
116 ITranslationImportQueue)
117+from lp.testopenid.interfaces.server import ITestOpenIDApplication
118
119 from canonical.launchpad.webapp import (
120 LaunchpadFormView, LaunchpadView, Link, Navigation,
121@@ -565,6 +566,7 @@
122 'token': ILoginTokenSet,
123 '+groups': ITranslationGroupSet,
124 'translations': IRosettaApplication,
125+ 'testopenid': ITestOpenIDApplication,
126 'questions': IQuestionSet,
127 '+rpconfig': IOpenIDRPConfigSet,
128 # These three have been renamed, and no redirects done, as the old
129
130=== modified file 'lib/canonical/launchpad/configure.zcml'
131--- lib/canonical/launchpad/configure.zcml 2010-02-02 17:12:29 +0000
132+++ lib/canonical/launchpad/configure.zcml 2010-02-17 15:49:22 +0000
133@@ -26,6 +26,7 @@
134 <include package="lp.code" />
135 <include package="lp.soyuz" />
136 <include package="lp.translations" />
137+ <include package="lp.testopenid" />
138 <include package="lp.blueprints" />
139 <include package="lp.services.comments" />
140
141
142=== modified file 'lib/canonical/launchpad/layers.py'
143--- lib/canonical/launchpad/layers.py 2009-10-21 18:33:11 +0000
144+++ lib/canonical/launchpad/layers.py 2010-02-17 15:49:22 +0000
145@@ -57,6 +57,10 @@
146 """
147
148
149+class TestOpenIDLayer(LaunchpadLayer):
150+ """The `TestOpenIDLayer` layer."""
151+
152+
153 class PageTestLayer(LaunchpadLayer):
154 """The `PageTestLayer` layer. (need to register a 404 view for this and
155 for the debug page too. and make the debugview a base class in the
156
157=== modified file 'lib/canonical/launchpad/systemhomes.py'
158--- lib/canonical/launchpad/systemhomes.py 2010-02-10 23:14:56 +0000
159+++ lib/canonical/launchpad/systemhomes.py 2010-02-17 15:49:22 +0000
160@@ -12,6 +12,7 @@
161 'MaloneApplication',
162 'PrivateMaloneApplication',
163 'RosettaApplication',
164+ 'TestOpenIDApplication',
165 ]
166
167 __metaclass__ = type
168@@ -31,6 +32,7 @@
169 IMailingListApplication, IMaloneApplication,
170 IPrivateMaloneApplication, IProductSet, IRosettaApplication,
171 IWebServiceApplication)
172+from lp.testopenid.interfaces.server import ITestOpenIDApplication
173 from lp.translations.interfaces.translationgroup import ITranslationGroupSet
174 from lp.translations.interfaces.translationsoverview import (
175 ITranslationsOverview)
176@@ -382,3 +384,7 @@
177 wadl = super(WebServiceApplication, self).toWADL()
178 self.__class__.cached_wadl = wadl
179 return wadl
180+
181+
182+class TestOpenIDApplication:
183+ implements(ITestOpenIDApplication)
184
185=== modified file 'lib/canonical/launchpad/webapp/servers.py'
186--- lib/canonical/launchpad/webapp/servers.py 2009-12-16 19:59:45 +0000
187+++ lib/canonical/launchpad/webapp/servers.py 2010-02-17 15:49:22 +0000
188@@ -45,6 +45,7 @@
189 from lazr.restful.publisher import (
190 WebServicePublicationMixin, WebServiceRequestTraversal)
191
192+from lp.testopenid.interfaces.server import ITestOpenIDApplication
193 from canonical.launchpad.interfaces.launchpad import (
194 IFeedsApplication, IPrivateApplication, IWebServiceApplication)
195 from canonical.launchpad.interfaces.oauth import (
196@@ -1121,6 +1122,17 @@
197 """Request type for a launchpad feed."""
198 implements(canonical.launchpad.layers.FeedsLayer)
199
200+
201+# ---- testopenid
202+
203+class TestOpenIDBrowserRequest(LaunchpadBrowserRequest):
204+ implements(canonical.launchpad.layers.TestOpenIDLayer)
205+
206+
207+class TestOpenIDBrowserPublication(LaunchpadBrowserPublication):
208+ root_object_interface = ITestOpenIDApplication
209+
210+
211 # ---- web service
212
213 class WebServicePublication(WebServicePublicationMixin,
214@@ -1442,6 +1454,10 @@
215 'xmlrpc', PublicXMLRPCRequest, PublicXMLRPCPublication)
216 ]
217
218+ if config.launchpad.enable_test_openid_provider:
219+ factories.append(VHRP('testopenid', TestOpenIDBrowserRequest,
220+ TestOpenIDBrowserPublication))
221+
222 # We may also have a private XML-RPC server.
223 private_port = None
224 for server in config.servers:
225
226=== added directory 'lib/lp/testopenid'
227=== added file 'lib/lp/testopenid/__init__.py'
228=== added directory 'lib/lp/testopenid/adapters'
229=== added file 'lib/lp/testopenid/adapters/__init__.py'
230=== added file 'lib/lp/testopenid/adapters/openid.py'
231--- lib/lp/testopenid/adapters/openid.py 1970-01-01 00:00:00 +0000
232+++ lib/lp/testopenid/adapters/openid.py 2010-02-17 15:49:22 +0000
233@@ -0,0 +1,32 @@
234+# Copyright 2010 Canonical Ltd. This software is licensed under the
235+# GNU Affero General Public License version 3 (see the file LICENSE).
236+
237+"""TestOpenID adapters and helpers."""
238+
239+__metaclass__ = type
240+
241+__all__ = [
242+ 'TestOpenIDPersistentIdentity',
243+ ]
244+
245+from zope.component import adapts
246+from zope.interface import implements
247+
248+from canonical.launchpad.interfaces.account import IAccount
249+from canonical.launchpad.webapp.vhosts import allvhosts
250+
251+from lp.services.openid.adapters.openid import OpenIDPersistentIdentity
252+from lp.testopenid.interfaces.server import ITestOpenIDPersistentIdentity
253+
254+
255+class TestOpenIDPersistentIdentity(OpenIDPersistentIdentity):
256+ """See `IOpenIDPersistentIdentity`."""
257+
258+ adapts(IAccount)
259+ implements(ITestOpenIDPersistentIdentity)
260+
261+ @property
262+ def openid_identity_url(self):
263+ """See `IOpenIDPersistentIdentity`."""
264+ identity_root_url = allvhosts.configs['testopenid'].rooturl
265+ return identity_root_url + self.openid_identifier.encode('ascii')
266
267=== added directory 'lib/lp/testopenid/browser'
268=== added file 'lib/lp/testopenid/browser/__init__.py'
269=== added file 'lib/lp/testopenid/browser/configure.zcml'
270--- lib/lp/testopenid/browser/configure.zcml 1970-01-01 00:00:00 +0000
271+++ lib/lp/testopenid/browser/configure.zcml 2010-02-17 15:49:22 +0000
272@@ -0,0 +1,82 @@
273+<!-- Copyright 2009 Canonical Ltd. This software is licensed under the
274+ GNU Affero General Public License version 3 (see the file LICENSE).
275+-->
276+
277+<configure
278+ xmlns="http://namespaces.zope.org/zope"
279+ xmlns:browser="http://namespaces.zope.org/browser"
280+ xmlns:i18n="http://namespaces.zope.org/i18n"
281+ i18n_domain="launchpad">
282+
283+ <browser:navigation
284+ module=".server"
285+ classes="TestOpenIDApplicationNavigation"
286+ />
287+
288+ <adapter
289+ provides="canonical.launchpad.webapp.interfaces.ICanonicalUrlData"
290+ for="..interfaces.server.ITestOpenIDApplication"
291+ factory=".server.TestOpenIDRootUrlData"
292+ />
293+
294+ <browser:defaultView
295+ for="..interfaces.server.ITestOpenIDApplication"
296+ name="+index"
297+ />
298+
299+ <browser:page
300+ for="..interfaces.server.ITestOpenIDApplication"
301+ class=".server.TestOpenIDView"
302+ permission="zope.Public"
303+ name="+openid"
304+ />
305+ <browser:page
306+ for="..interfaces.server.ITestOpenIDApplication"
307+ class=".server.TestOpenIDIndexView"
308+ permission="zope.Public"
309+ name="+index"
310+ />
311+ <browser:page
312+ for="..interfaces.server.ITestOpenIDApplication"
313+ class=".server.TestOpenIDLoginView"
314+ permission="zope.Public"
315+ name="+auth"
316+ />
317+
318+ <browser:url
319+ for="..interfaces.server.ITestOpenIDPersistentIdentity"
320+ path_expression="string:${openid_identifier}"
321+ parent_utility="..interfaces.server.ITestOpenIDApplication"
322+ />
323+
324+ <browser:defaultView
325+ for="..interfaces.server.ITestOpenIDPersistentIdentity"
326+ name="+index"
327+ />
328+
329+ <browser:page
330+ for="..interfaces.server.ITestOpenIDPersistentIdentity"
331+ name="+index"
332+ template="../templates/persistentidentity-index.pt"
333+ permission="zope.Public"
334+ class=".server.PersistentIdentityView"
335+ />
336+
337+ <browser:page
338+ name=""
339+ for="..interfaces.server.ITestOpenIDApplication"
340+ class="canonical.launchpad.browser.launchpad.LaunchpadImageFolder"
341+ permission="zope.Public"
342+ layer="canonical.launchpad.layers.TestOpenIDLayer"
343+ />
344+
345+ <!-- A simple view used by the page tests. -->
346+ <browser:page
347+ for="..interfaces.server.ITestOpenIDApplication"
348+ name="+echo"
349+ permission="zope.Public"
350+ class="..testing.helpers.EchoView"
351+ layer="canonical.launchpad.layers.PageTestLayer"
352+ />
353+
354+</configure>
355
356=== added file 'lib/lp/testopenid/browser/server.py'
357--- lib/lp/testopenid/browser/server.py 1970-01-01 00:00:00 +0000
358+++ lib/lp/testopenid/browser/server.py 2010-02-17 15:49:22 +0000
359@@ -0,0 +1,285 @@
360+# Copyright 2010 Canonical Ltd. All rights reserved.
361+
362+"""Test OpenID server."""
363+
364+__metaclass__ = type
365+__all__ = [
366+ 'PersistentIdentityView',
367+ 'TestOpenIDApplicationNavigation',
368+ 'TestOpenIDIndexView'
369+ 'TestOpenIDLoginView',
370+ 'TestOpenIDRootUrlData',
371+ 'TestOpenIDView',
372+ ]
373+
374+from datetime import timedelta
375+
376+from z3c.ptcompat import ViewPageTemplateFile
377+from zope.app.security.interfaces import IUnauthenticatedPrincipal
378+from zope.component import getUtility
379+from zope.interface import implements
380+from zope.security.proxy import isinstance as zisinstance
381+from zope.session.interfaces import ISession
382+
383+from openid.server.server import CheckIDRequest, Server
384+from openid.store.memstore import MemoryStore
385+
386+from canonical.cachedproperty import cachedproperty
387+from canonical.launchpad import _
388+from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet
389+from canonical.launchpad.webapp import (
390+ action, LaunchpadFormView, LaunchpadView)
391+from canonical.launchpad.webapp.interfaces import (
392+ ICanonicalUrlData, IPlacelessLoginSource, UnexpectedFormData)
393+from canonical.launchpad.webapp.login import (
394+ allowUnauthenticatedSession, logInPrincipal, logoutPerson)
395+from canonical.launchpad.webapp.publisher import Navigation, stepthrough
396+from canonical.launchpad.webapp.url import urlappend
397+from canonical.launchpad.webapp.vhosts import allvhosts
398+
399+from lp.services.openid.browser.openiddiscovery import (
400+ XRDSContentNegotiationMixin)
401+from lp.testopenid.interfaces.server import (
402+ ITestOpenIDApplication, ITestOpenIDLoginForm,
403+ ITestOpenIDPersistentIdentity)
404+
405+
406+OPENID_REQUEST_SESSION_KEY = 'testopenid.request'
407+SESSION_PKG_KEY = 'TestOpenID'
408+SERVER_URL = urlappend(allvhosts.configs['testopenid'].rooturl, '+openid')
409+openid_store = MemoryStore()
410+
411+
412+class TestOpenIDRootUrlData:
413+ """`ICanonicalUrlData` for the test OpenID provider."""
414+
415+ implements(ICanonicalUrlData)
416+
417+ path = ''
418+ inside = None
419+ rootsite = 'testopenid'
420+
421+ def __init__(self, context):
422+ self.context = context
423+
424+
425+class TestOpenIDApplicationNavigation(Navigation):
426+ """Navigation for `ITestOpenIDApplication`"""
427+ usedfor = ITestOpenIDApplication
428+
429+ @stepthrough('+id')
430+ def traverse_id(self, name):
431+ """Traverse to persistent OpenID identity URLs."""
432+ try:
433+ account = getUtility(IAccountSet).getByOpenIDIdentifier(name)
434+ except LookupError:
435+ account = None
436+ if account is None or account.status != AccountStatus.ACTIVE:
437+ return None
438+ return ITestOpenIDPersistentIdentity(account)
439+
440+
441+class TestOpenIDXRDSContentNegotiationMixin(XRDSContentNegotiationMixin):
442+ """Custom XRDSContentNegotiationMixin that overrides openid_server_url."""
443+
444+ @property
445+ def openid_server_url(self):
446+ """The OpenID Server endpoint URL for Launchpad."""
447+ return SERVER_URL
448+
449+
450+class TestOpenIDIndexView(
451+ TestOpenIDXRDSContentNegotiationMixin, LaunchpadView):
452+ template = ViewPageTemplateFile("../templates/application-index.pt")
453+ xrds_template = ViewPageTemplateFile("../templates/application-xrds.pt")
454+
455+
456+class OpenIDMixin:
457+ """A mixin with OpenID helper methods."""
458+
459+ openid_request = None
460+
461+ def __init__(self, context, request):
462+ super(OpenIDMixin, self).__init__(context, request)
463+ self.server_url = SERVER_URL
464+ self.openid_server = Server(openid_store, self.server_url)
465+
466+ @property
467+ def user_identity_url(self):
468+ return ITestOpenIDPersistentIdentity(self.account).openid_identity_url
469+
470+ def isIdentityOwner(self):
471+ """Return True if the user can authenticate as the given ID."""
472+ assert self.account is not None, "user should be logged in by now."
473+ return (self.openid_request.idSelect() or
474+ self.openid_request.identity == self.user_identity_url)
475+
476+ @cachedproperty('_openid_parameters')
477+ def openid_parameters(self):
478+ """A dictionary of OpenID query parameters from request."""
479+ query = {}
480+ for key, value in self.request.form.items():
481+ if key.startswith('openid.'):
482+ # All OpenID query args are supposed to be ASCII.
483+ query[key.encode('US-ASCII')] = value.encode('US-ASCII')
484+ return query
485+
486+ def getSession(self):
487+ """Get the session data container that stores the OpenID request."""
488+ if IUnauthenticatedPrincipal.providedBy(self.request.principal):
489+ # A dance to assert that we want to break the rules about no
490+ # unauthenticated sessions. Only after this next line is it
491+ # safe to set session values.
492+ allowUnauthenticatedSession(
493+ self.request, duration=timedelta(minutes=60))
494+ return ISession(self.request)[SESSION_PKG_KEY]
495+
496+ def restoreRequestFromSession(self):
497+ """Get the OpenIDRequest from our session."""
498+ session = self.getSession()
499+ try:
500+ self._openid_parameters = session[OPENID_REQUEST_SESSION_KEY]
501+ except KeyError:
502+ raise UnexpectedFormData("No OpenID request in session")
503+
504+ # Decode the request parameters and create the request object.
505+ self.openid_request = self.openid_server.decodeRequest(
506+ self.openid_parameters)
507+ assert zisinstance(self.openid_request, CheckIDRequest), (
508+ 'Invalid OpenIDRequest in session')
509+
510+ def saveRequestInSession(self):
511+ """Save the OpenIDRequest in our session."""
512+ query = self.openid_parameters
513+ assert query.get('openid.mode') == 'checkid_setup', (
514+ 'Can only serialise checkid_setup OpenID requests')
515+
516+ session = self.getSession()
517+ # If this was meant for use in production we'd have to use a nonce
518+ # as the key when storing the openid request in the session, but as
519+ # it's meant to run only on development instances we can simplify
520+ # things a bit by storing the openid request using a well known key.
521+ session[OPENID_REQUEST_SESSION_KEY] = query
522+
523+ def renderOpenIDResponse(self, openid_response):
524+ """Return a web-suitable response constructed from openid_response."""
525+ webresponse = self.openid_server.encodeResponse(openid_response)
526+ response = self.request.response
527+ response.setStatus(webresponse.code)
528+ for header, value in webresponse.headers.items():
529+ response.setHeader(header, value)
530+ return webresponse.body
531+
532+ def createPositiveResponse(self):
533+ """Create a positive assertion OpenIDResponse.
534+
535+ This method should be called to create the response to
536+ successful checkid requests.
537+
538+ If the trust root for the request is in openid_sreg_trustroots,
539+ then additional user information is included with the
540+ response.
541+ """
542+ assert self.account is not None, (
543+ 'Must be logged in for positive OpenID response')
544+ assert self.openid_request is not None, (
545+ 'No OpenID request to respond to.')
546+
547+ if not self.isIdentityOwner():
548+ return self.createFailedResponse()
549+
550+ if self.openid_request.idSelect():
551+ response = self.openid_request.answer(
552+ True, identity=self.user_identity_url)
553+ else:
554+ response = self.openid_request.answer(True)
555+
556+ return response
557+
558+ def createFailedResponse(self):
559+ """Create a failed assertion OpenIDResponse.
560+
561+ This method should be called to create the response to
562+ unsuccessful checkid requests.
563+ """
564+ assert self.openid_request is not None, (
565+ 'No OpenID request to respond to.')
566+ response = self.openid_request.answer(False, self.server_url)
567+ return response
568+
569+
570+class TestOpenIDView(OpenIDMixin, LaunchpadView):
571+ """An OpenID Provider endpoint for Launchpad.
572+
573+ This class implements an OpenID endpoint using the python-openid
574+ library. In addition to the normal modes of operation, it also
575+ implements the OpenID 2.0 identifier select mode.
576+
577+ Note that the checkid_immediate mode is not supported.
578+ """
579+
580+ def render(self):
581+ """Handle all OpenID requests and form submissions."""
582+ # NB: Will be None if there are no parameters in the request.
583+ self.openid_request = self.openid_server.decodeRequest(
584+ self.openid_parameters)
585+
586+ if self.openid_request.mode == 'checkid_setup':
587+ referer = self.request.get("HTTP_REFERER")
588+ if referer:
589+ self.request.response.setCookie("openid_referer", referer)
590+
591+ # Log the user out and present the login page so that they can
592+ # authenticate as somebody else if they want.
593+ logoutPerson(self.request)
594+ return self.showLoginPage()
595+ elif self.openid_request.mode == 'checkid_immediate':
596+ raise UnexpectedFormData(
597+ 'We do not handle checkid_immediate requests.')
598+ else:
599+ return self.renderOpenIDResponse(
600+ self.openid_server.handleRequest(self.openid_request))
601+
602+ def showLoginPage(self):
603+ """Render the login dialog."""
604+ self.saveRequestInSession()
605+ return TestOpenIDLoginView(self.context, self.request)()
606+
607+
608+class TestOpenIDLoginView(OpenIDMixin, LaunchpadFormView):
609+ """A view for users to log into the OpenID provider."""
610+
611+ page_title = "Login"
612+ schema = ITestOpenIDLoginForm
613+ action_url = '+auth'
614+ template = ViewPageTemplateFile("../templates/auth.pt")
615+
616+ def initialize(self):
617+ self.restoreRequestFromSession()
618+ super(TestOpenIDLoginView, self).initialize()
619+
620+ def validate(self, data):
621+ """Check that the email address and password are valid for login."""
622+ loginsource = getUtility(IPlacelessLoginSource)
623+ principal = loginsource.getPrincipalByLogin(data['email'])
624+ if principal is None or not principal.validate(data['password']):
625+ self.addError(
626+ _("Incorrect password for the provided email address."))
627+
628+ @action('Continue', name='continue')
629+ def continue_action(self, action, data):
630+ email = data['email']
631+ principal = getUtility(IPlacelessLoginSource).getPrincipalByLogin(
632+ email)
633+ logInPrincipal(self.request, principal, email)
634+ # Update the attribute holding the cached user.
635+ self._account = principal.account
636+ return self.renderOpenIDResponse(self.createPositiveResponse())
637+
638+
639+class PersistentIdentityView(
640+ TestOpenIDXRDSContentNegotiationMixin, LaunchpadView):
641+ """Render the OpenID identity page."""
642+
643+ xrds_template = ViewPageTemplateFile(
644+ "../templates/persistentidentity-xrds.pt")
645
646=== added file 'lib/lp/testopenid/configure.zcml'
647--- lib/lp/testopenid/configure.zcml 1970-01-01 00:00:00 +0000
648+++ lib/lp/testopenid/configure.zcml 2010-02-17 15:49:22 +0000
649@@ -0,0 +1,28 @@
650+<!-- Copyright 2009 Canonical Ltd. This software is licensed under the
651+ GNU Affero General Public License version 3 (see the file LICENSE).
652+-->
653+
654+<configure
655+ xmlns="http://namespaces.zope.org/zope"
656+ xmlns:browser="http://namespaces.zope.org/browser"
657+ xmlns:i18n="http://namespaces.zope.org/i18n"
658+ i18n_domain="launchpad">
659+
660+ <securedutility
661+ class="canonical.launchpad.systemhomes.TestOpenIDApplication"
662+ provides="lp.testopenid.interfaces.server.ITestOpenIDApplication">
663+ <allow interface="lp.testopenid.interfaces.server.ITestOpenIDApplication"/>
664+ </securedutility>
665+
666+ <class class=".adapters.openid.TestOpenIDPersistentIdentity">
667+ <allow interface=".interfaces.server.ITestOpenIDPersistentIdentity" />
668+ </class>
669+
670+ <adapter
671+ factory=".adapters.openid.TestOpenIDPersistentIdentity"
672+ provides=".interfaces.server.ITestOpenIDPersistentIdentity" />
673+ />
674+
675+ <include package=".browser"/>
676+
677+</configure>
678
679=== added directory 'lib/lp/testopenid/interfaces'
680=== added file 'lib/lp/testopenid/interfaces/__init__.py'
681=== added file 'lib/lp/testopenid/interfaces/server.py'
682--- lib/lp/testopenid/interfaces/server.py 1970-01-01 00:00:00 +0000
683+++ lib/lp/testopenid/interfaces/server.py 2010-02-17 15:49:22 +0000
684@@ -0,0 +1,29 @@
685+# Copyright 2010 Canonical Ltd. All rights reserved.
686+
687+__metaclass__ = type
688+__all__ = [
689+ 'ITestOpenIDApplication',
690+ 'ITestOpenIDLoginForm',
691+ 'ITestOpenIDPersistentIdentity',
692+ ]
693+
694+from zope.interface import Interface
695+from zope.schema import TextLine
696+
697+from canonical.launchpad.fields import PasswordField
698+from canonical.launchpad.webapp.interfaces import ILaunchpadApplication
699+
700+from lp.services.openid.interfaces.openid import IOpenIDPersistentIdentity
701+
702+
703+class ITestOpenIDApplication(ILaunchpadApplication):
704+ """Launchpad's testing OpenID application root."""
705+
706+
707+class ITestOpenIDLoginForm(Interface):
708+ email = TextLine(title=u'What is your e-mail address?', required=True)
709+ password = PasswordField(title=u'Password', required=True)
710+
711+
712+class ITestOpenIDPersistentIdentity(IOpenIDPersistentIdentity):
713+ """Marker interface for IOpenIDPersistentIdentity on testopenid."""
714
715=== added directory 'lib/lp/testopenid/stories'
716=== added file 'lib/lp/testopenid/stories/__init__.py'
717=== added file 'lib/lp/testopenid/stories/basics.txt'
718--- lib/lp/testopenid/stories/basics.txt 1970-01-01 00:00:00 +0000
719+++ lib/lp/testopenid/stories/basics.txt 2010-02-17 15:49:22 +0000
720@@ -0,0 +1,163 @@
721+====================
722+Test OpenID provider
723+====================
724+
725+Introduction
726+============
727+
728+Launchpad provides an OpenID provider (under the testopenid.dev
729+vhost) for testing purposes and for developers to be able to log into their
730+development instances. This provider is only available for development and
731+testing.
732+
733+We are going to fake a consumer for these examples. In order to ensure
734+that the consumer is being fed the correct replies, we use a view that
735+renders the parameters in the response in an easily testable format.
736+
737+ >>> anon_browser.open('http://testopenid.dev/+echo?foo=bar')
738+ >>> print anon_browser.contents
739+ Request method: GET
740+ foo:bar
741+
742+
743+associate Mode
744+==============
745+
746+Establish a shared secret between Consumer and Identity Provider.
747+
748+After determining the URL of the OpenID server, the next thing a consumer
749+needs to do is associate with the server and get a shared secret via a
750+POST request.
751+
752+ >>> from urllib import urlencode
753+ >>> anon_browser.open(
754+ ... 'http://testopenid.dev/+openid', data=urlencode({
755+ ... 'openid.mode': 'associate',
756+ ... 'openid.assoc_type': 'HMAC-SHA1'}))
757+ >>> print anon_browser.headers
758+ Status: 200 Ok
759+ ...
760+ Content-Type: text/plain
761+ ...
762+ >>> print anon_browser.contents
763+ assoc_handle:{HMAC-SHA1}{...}{...}
764+ assoc_type:HMAC-SHA1
765+ expires_in:1209...
766+ mac_key:...
767+ <BLANKLINE>
768+
769+Get the association handle, which we will need for later tests.
770+
771+ >>> import re
772+ >>> [assoc_handle] = re.findall('assoc_handle:(.*)', anon_browser.contents)
773+
774+
775+checkid_setup Mode
776+==================
777+
778+When we go to the OpenID setup URL, we are presented with a login
779+form. By entering an email address and password, we are directed back
780+to the consumer, completing the OpenID request:
781+
782+ >>> args = urlencode({
783+ ... 'openid.mode': 'checkid_setup',
784+ ... 'openid.identity': 'http://testopenid.dev/+id/mark_oid',
785+ ... 'openid.assoc_handle': assoc_handle,
786+ ... 'openid.return_to': 'http://testopenid.dev/+echo',
787+ ... })
788+ >>> user_browser.open('http://testopenid.dev/+openid?%s' % args)
789+ >>> print user_browser.url
790+ http://testopenid.dev/+openid?...
791+ >>> print user_browser.title
792+ Login
793+ >>> user_browser.getControl(name='field.email').value = 'mark@example.com'
794+ >>> user_browser.getControl(name='field.password').value = 'test'
795+ >>> user_browser.getControl('Continue').click()
796+
797+ >>> print user_browser.url
798+ http://testopenid.dev/+echo?...
799+ >>> print user_browser.contents
800+ Request method: GET
801+ openid.assoc_handle:...
802+ openid.identity:http://testopenid.dev/+id/mark_oid
803+ openid.mode:id_res...
804+ openid.op_endpoint:http://testopenid.dev/+openid
805+ openid.response_nonce:...
806+ openid.return_to:http://testopenid.dev/+echo
807+ openid.sig:...
808+ openid.signed:...
809+ <BLANKLINE>
810+
811+We will record the signature from this response to use in the next test:
812+
813+ >>> [sig] = re.findall('sig:(.*)', user_browser.contents)
814+
815+
816+check_authentication Mode
817+=========================
818+
819+Ask an Identity Provider if a message is valid. For dumb, stateless
820+Consumers or when verifying an invalidate_handle response.
821+
822+If an association handle is stateful (genereted using the associate Mode),
823+check_authentication will fail.
824+
825+ >>> args = urlencode({
826+ ... 'openid.mode': 'check_authentication',
827+ ... 'openid.assoc_handle': assoc_handle,
828+ ... 'openid.sig': sig,
829+ ... 'openid.signed': 'return_to,mode,identity',
830+ ... 'openid.identity':
831+ ... 'http://testopenid.dev/+id/mark_oid',
832+ ... 'openid.return_to': 'http://testopenid.dev/+echo',
833+ ... })
834+ >>> user_browser.open('http://testopenid.dev/+openid?%s' % args)
835+ >>> print user_browser.contents
836+ is_valid:false
837+ <BLANKLINE>
838+
839+If we are a dumb consumer though, we must invoke the check_authentication
840+mode, passing back the association handle, signature and values of all
841+fields that were signed.
842+
843+ >>> args = urlencode({
844+ ... 'openid.mode': 'checkid_setup',
845+ ... 'openid.identity':
846+ ... 'http://testopenid.dev/+id/mark_oid',
847+ ... 'openid.return_to': 'http://testopenid.dev/+echo',
848+ ... })
849+ >>> user_browser.open('http://testopenid.dev/+openid?%s' % args)
850+ >>> user_browser.getControl(name='field.email').value = 'mark@example.com'
851+ >>> user_browser.getControl(name='field.password').value = 'test'
852+ >>> user_browser.getControl('Continue').click()
853+ >>> print user_browser.contents
854+ Request method: GET
855+ openid.assoc_handle:...
856+ openid.identity:http://testopenid.dev/+id/mark_oid
857+ openid.mode:id_res
858+ openid.op_endpoint:http://testopenid.dev/+openid
859+ openid.response_nonce:...
860+ openid.return_to:http://testopenid.dev/+echo
861+ openid.sig:...
862+ openid.signed:...
863+ <BLANKLINE>
864+
865+ >>> fields = dict(line.split(':', 1)
866+ ... for line in user_browser.contents.splitlines()[1:]
867+ ... if line.startswith('openid.'))
868+ >>> signed = ['openid.' + name
869+ ... for name in fields['openid.signed'].split(',')]
870+ >>> message = dict((key, value) for (key, value) in fields.items()
871+ ... if key in signed)
872+ >>> message.update({
873+ ... 'openid.mode': 'check_authentication',
874+ ... 'openid.assoc_handle': fields['openid.assoc_handle'],
875+ ... 'openid.sig': fields['openid.sig'],
876+ ... 'openid.signed': fields['openid.signed'],
877+ ... })
878+
879+ >>> args = urlencode(message)
880+ >>> user_browser.open('http://testopenid.dev/+openid', args)
881+ >>> print user_browser.contents
882+ is_valid:true
883+ <BLANKLINE>
884
885=== added file 'lib/lp/testopenid/stories/logging-in.txt'
886--- lib/lp/testopenid/stories/logging-in.txt 1970-01-01 00:00:00 +0000
887+++ lib/lp/testopenid/stories/logging-in.txt 2010-02-17 15:49:22 +0000
888@@ -0,0 +1,64 @@
889+========================================
890+Logging in using the TestOpenID provider
891+========================================
892+
893+A user with an existing account may log into Launchpad using the OpenID
894+provider available on testopenid.dev.
895+
896+First we will set up the helper view that lets us test the final
897+portion of the authentication process:
898+
899+ >>> from openid.consumer.consumer import Consumer
900+ >>> from openid.fetchers import setDefaultFetcher
901+ >>> from openid.store.memstore import MemoryStore
902+ >>> from lp.testopenid.testing.helpers import (
903+ ... complete_from_browser, make_identifier_select_endpoint,
904+ ... PublisherFetcher)
905+ >>> setDefaultFetcher(PublisherFetcher())
906+
907+The authentication process is started by the relying party issuing a
908+checkid_setup request, sending the user to Launchpad:
909+
910+ >>> openid_store = MemoryStore()
911+ >>> consumer = Consumer(session={}, store=openid_store)
912+
913+ >>> request = consumer.beginWithoutDiscovery(
914+ ... make_identifier_select_endpoint())
915+ >>> browser.open(request.redirectURL(
916+ ... 'http://testopenid.dev/',
917+ ... 'http://testopenid.dev/+echo'))
918+
919+At this point, the user is presented with a login form:
920+
921+ >>> print browser.title
922+ Login
923+
924+As the user already has an account, they can enter their email address and
925+password. If the password does not match the given email address, an error is
926+shown:
927+
928+ >>> browser.getControl(name='field.email').value = 'mark@example.com'
929+ >>> browser.getControl(name='field.password').value = 'not the password'
930+ >>> browser.getControl('Continue').click()
931+ >>> print browser.title
932+ Login
933+ >>> for tag in find_tags_by_class(browser.contents, 'error'):
934+ ... print extract_text(tag)
935+ There is 1 error.
936+ Incorrect password for the provided email address.
937+
938+If the email address and password match, the user is logged in and returned to
939+the relying party, with the user's identity URL:
940+
941+ >>> browser.getControl(name='field.password').value = 'test'
942+ >>> browser.getControl('Continue').click()
943+ >>> print browser.url
944+ http://testopenid.dev/+echo?...
945+ >>> info = complete_from_browser(consumer, browser)
946+ >>> print info.status
947+ success
948+ >>> print info.endpoint.claimed_id
949+ http://testopenid.dev/+id/mark_oid
950+
951+ # Clean up the changes we did to the openid module.
952+ >>> setDefaultFetcher(None)
953
954=== added file 'lib/lp/testopenid/stories/tests.py'
955--- lib/lp/testopenid/stories/tests.py 1970-01-01 00:00:00 +0000
956+++ lib/lp/testopenid/stories/tests.py 2010-02-17 15:49:22 +0000
957@@ -0,0 +1,22 @@
958+# Copyright 2004-2008 Canonical Ltd. All rights reserved.
959+
960+import os
961+import unittest
962+
963+from canonical.launchpad.testing.pages import PageTestSuite
964+
965+
966+here = os.path.dirname(os.path.realpath(__file__))
967+
968+
969+def test_suite():
970+ stories = sorted(
971+ dir for dir in os.listdir(here)
972+ if not dir.startswith('.') and os.path.isdir(os.path.join(here, dir)))
973+
974+ suite = unittest.TestSuite()
975+ suite.addTest(PageTestSuite('.'))
976+ for storydir in stories:
977+ suite.addTest(PageTestSuite(storydir))
978+
979+ return suite
980
981=== added directory 'lib/lp/testopenid/templates'
982=== added file 'lib/lp/testopenid/templates/application-index.pt'
983--- lib/lp/testopenid/templates/application-index.pt 1970-01-01 00:00:00 +0000
984+++ lib/lp/testopenid/templates/application-index.pt 2010-02-17 15:49:22 +0000
985@@ -0,0 +1,5 @@
986+<html>
987+ <body>
988+ <h1>Test OpenID provider for launchpad.dev</h1>
989+ </body>
990+</html>
991
992=== added file 'lib/lp/testopenid/templates/application-xrds.pt'
993--- lib/lp/testopenid/templates/application-xrds.pt 1970-01-01 00:00:00 +0000
994+++ lib/lp/testopenid/templates/application-xrds.pt 2010-02-17 15:49:22 +0000
995@@ -0,0 +1,14 @@
996+<?xml version="1.0"?>
997+<xrds:XRDS
998+ xmlns="xri://$xrd*($v*2.0)"
999+ xmlns:xrds="xri://$xrds"
1000+ xmlns:tal="http://xml.zope.org/namespaces/tal">
1001+ <XRD>
1002+ <Service priority="0">
1003+ <Type>http://specs.openid.net/auth/2.0/server</Type>
1004+ <URI tal:content="view/openid_server_url">
1005+ https://login.launchpad.net/+openid
1006+ </URI>
1007+ </Service>
1008+ </XRD>
1009+</xrds:XRDS>
1010
1011=== added file 'lib/lp/testopenid/templates/auth.pt'
1012--- lib/lp/testopenid/templates/auth.pt 1970-01-01 00:00:00 +0000
1013+++ lib/lp/testopenid/templates/auth.pt 2010-02-17 15:49:22 +0000
1014@@ -0,0 +1,18 @@
1015+<html
1016+ xmlns="http://www.w3.org/1999/xhtml"
1017+ xmlns:tal="http://xml.zope.org/namespaces/tal"
1018+ xmlns:metal="http://xml.zope.org/namespaces/metal"
1019+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1020+ xml:lang="en"
1021+ lang="en"
1022+ dir="ltr"
1023+ metal:use-macro="view/macro:page/locationless"
1024+ i18n:domain="launchpad"
1025+>
1026+ <body>
1027+ <div metal:fill-slot="main">
1028+ <div metal:use-macro="context/@@launchpad_form/form" />
1029+ </div>
1030+ </body>
1031+</html>
1032+
1033
1034=== added file 'lib/lp/testopenid/templates/persistentidentity-index.pt'
1035--- lib/lp/testopenid/templates/persistentidentity-index.pt 1970-01-01 00:00:00 +0000
1036+++ lib/lp/testopenid/templates/persistentidentity-index.pt 2010-02-17 15:49:22 +0000
1037@@ -0,0 +1,14 @@
1038+<html xmlns="http://www.w3.org/1999/xhtml"
1039+ xmlns:tal="http://xml.zope.org/namespaces/tal">
1040+ <head>
1041+ <link rel="openid.server"
1042+ tal:attributes="href view/openid_server_url" />
1043+ </head>
1044+ <body>
1045+ <h1>
1046+ OpenID Identity URL for
1047+ <tal:user content="context/account/displayname">Display Name</tal:user>
1048+ </h1>
1049+ </body>
1050+</html>
1051+
1052
1053=== added file 'lib/lp/testopenid/templates/persistentidentity-xrds.pt'
1054--- lib/lp/testopenid/templates/persistentidentity-xrds.pt 1970-01-01 00:00:00 +0000
1055+++ lib/lp/testopenid/templates/persistentidentity-xrds.pt 2010-02-17 15:49:22 +0000
1056@@ -0,0 +1,14 @@
1057+<?xml version="1.0"?>
1058+<xrds:XRDS
1059+ xmlns="xri://$xrd*($v*2.0)"
1060+ xmlns:xrds="xri://$xrds"
1061+ xmlns:tal="http://xml.zope.org/namespaces/tal">
1062+ <XRD>
1063+ <Service priority="0">
1064+ <Type>http://specs.openid.net/auth/2.0/signon</Type>
1065+ <URI priority="0" tal:content="view/openid_server_url">
1066+ https://testopenid.dev/+openid
1067+ </URI>
1068+ </Service>
1069+ </XRD>
1070+</xrds:XRDS>
1071
1072=== added directory 'lib/lp/testopenid/testing'
1073=== added file 'lib/lp/testopenid/testing/__init__.py'
1074=== added file 'lib/lp/testopenid/testing/helpers.py'
1075--- lib/lp/testopenid/testing/helpers.py 1970-01-01 00:00:00 +0000
1076+++ lib/lp/testopenid/testing/helpers.py 2010-02-17 15:49:22 +0000
1077@@ -0,0 +1,74 @@
1078+# Copyright 2010 Canonical Ltd. All rights reserved.
1079+
1080+"""Helpers for TestOpenID page tests."""
1081+
1082+__metaclass__ = type
1083+__all__ = [
1084+ 'complete_from_browser',
1085+ 'EchoView',
1086+ 'make_identifier_select_endpoint',
1087+ 'PublisherFetcher',
1088+ ]
1089+
1090+from StringIO import StringIO
1091+import urllib2
1092+
1093+from zope.testbrowser.testing import PublisherHTTPHandler
1094+
1095+from openid import fetchers
1096+from openid.consumer.discover import (
1097+ OpenIDServiceEndpoint, OPENID_IDP_2_0_TYPE)
1098+
1099+from canonical.launchpad.webapp import LaunchpadView
1100+
1101+from lp.testopenid.browser.server import SERVER_URL
1102+
1103+
1104+class EchoView(LaunchpadView):
1105+ """A view which just echoes its form arguments in the response."""
1106+
1107+ def render(self):
1108+ out = StringIO()
1109+ print >> out, 'Request method: %s' % self.request.method
1110+ keys = sorted(self.request.form.keys())
1111+ for key in keys:
1112+ print >> out, '%s:%s' % (key, self.request.form[key])
1113+ return out.getvalue()
1114+
1115+
1116+class PublisherFetcher(fetchers.Urllib2Fetcher):
1117+ """An `HTTPFetcher` that passes requests on to the Zope publisher."""
1118+ def __init__(self):
1119+ super(PublisherFetcher, self).__init__()
1120+ self.opener = urllib2.build_opener(PublisherHTTPHandler)
1121+
1122+ def urlopen(self, request):
1123+ request.add_header('X-zope-handle-errors', True)
1124+ return self.opener.open(request)
1125+
1126+
1127+def complete_from_browser(consumer, browser):
1128+ """Complete OpenID request based on output of +echo.
1129+
1130+ :param consumer: an OpenID `Consumer` instance.
1131+ :param browser: a Zope testbrowser `Browser` instance.
1132+
1133+ This function parses the body of the +echo view into a set of query
1134+ arguments representing the OpenID response.
1135+ """
1136+ assert browser.contents.startswith('Request method'), (
1137+ "Browser contents does not look like it came from +echo")
1138+ # Skip the first line.
1139+ query = dict(line.split(':', 1)
1140+ for line in browser.contents.splitlines()[1:])
1141+
1142+ response = consumer.complete(query, browser.url)
1143+ return response
1144+
1145+
1146+def make_identifier_select_endpoint():
1147+ """Create an endpoint for use in OpenID identifier select mode."""
1148+ endpoint = OpenIDServiceEndpoint()
1149+ endpoint.server_url = SERVER_URL
1150+ endpoint.type_uris = [OPENID_IDP_2_0_TYPE]
1151+ return endpoint
1152
1153=== modified file 'utilities/rocketfuel-setup'
1154--- utilities/rocketfuel-setup 2009-12-13 01:38:59 +0000
1155+++ utilities/rocketfuel-setup 2010-02-17 15:49:22 +0000
1156@@ -136,6 +136,7 @@
1157 shipit.edubuntu.dev
1158 shipit.kubuntu.dev
1159 shipit.ubuntu.dev
1160+ testopenid.dev
1161 translations.launchpad.dev
1162 xmlrpc-private.launchpad.dev
1163 xmlrpc.launchpad.dev