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
=== modified file 'BRANCH.TODO'
--- BRANCH.TODO 2010-02-09 03:16:27 +0000
+++ BRANCH.TODO 2010-02-17 15:49:22 +0000
@@ -2,3 +2,56 @@
2# landing. There is a test to ensure it is empty in trunk. If there is2# landing. There is a test to ensure it is empty in trunk. If there is
3# stuff still here when you are ready to land, the items should probably3# stuff still here when you are ready to land, the items should probably
4# be converted to bugs so they can be scheduled.4# be converted to bugs so they can be scheduled.
5
6* Do we want to change c-i-p to use a separate cookie so that logging into
7login.lp.net doesn't cause you to end up logged into lp.net or should we just
8wait for ISD to roll a rebranded version of login.u.c on login.lp.net, which
9will probably use separate cookies.
10 - Apart from not being trivial to change the cookie name for a given vhost
11 we also need to worry about making sure the new cookie (which is supposed
12 to be valid only for login.lp.net) is not valid for all other vhosts, which
13 would normally happen thanks to LaunchpadCookieClientIdManager.
14
15* We still rely heavily on the Account table and expect all users to have an
16Account. We need to fix that if we're going to copy the Account table into the
17main replication set and stop using the one from the auth set. If we fix
18that, though, the Account table won't be necessary so there'll be no point in
19copying it. *I'm not sure this is feasible.*
20
21* As discussed with Francis, it's not worth porting the team-restricted login
22stuff to the new system, so we'll drop it.
23
24* Functionality needed in the callback view:
25 - reactivate accounts when credentials belong to a deactivated profile
26 - create Person entries when the credentials don't exist in our db
27 - forbid to log suspended accounts in. btw, I guess we're going to copy the
28 AccountStatus table to the main db.
29
30 # To test all this I'll just have to monkey patch
31 # OpenIDCallbackView.openid_response to return my hand-crafted
32 # openid response.
33
34* Some tests use anon_browser.open() on protected pages so that they get
35reidrected to +login and can show how the protected page works when the user
36is not logged in, including the preserved query string. If we want to keep
37doing that in these tests, we'll need an OpenID provider accessible to the
38test suite. One option would be to include a crippled version of the one in
39c-i-p in our tree, to be used only in tests.
40(lib/canonical/launchpad/pagetests/oauth/authorize-token.txt is one of the
41tests that rely on +login)
42
43* Does not work for /people/+me, because when the OpenID provider sends the
44user back to /people/+me/+openid-callback, there's a redirect to
45/~person/+openid-callback, which causes the openid dance to fail: *
46<openid.consumer.consumer.FailureResponse id=None message="return_to does not
47match return URL. Expected 'https://launchpad.dev/%7Ename16/+openid-callback',
48got
49u'https://launchpad.dev/people/+me/+openid-callback?janrain_nonce=2009-12-23T14%3A47%3A47ZsgdbOJ'">
50 I think this is a general problem with OpenID and login.lp.net because when
51you log into login.lp.net and is sent to the callback page (in lp.net), that
52page will see you as logged in, regardless of the openid response, because the
53cookie is shared, and that causes /people/+me/+openid-callback to redirect to
54/~name16/+openid-callback, which causes the error above.
55 One way around this is to always use /+openid-callback as the return_to URL,
56and include a lp_redirect_to URL in the query string, where LP will send the
57user to, once the openid dance is completed.
558
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf 2010-01-22 04:01:17 +0000
+++ configs/development/launchpad-lazr.conf 2010-02-17 15:49:22 +0000
@@ -124,6 +124,7 @@
124public_host: keyserver.launchpad.dev124public_host: keyserver.launchpad.dev
125125
126[launchpad]126[launchpad]
127enable_test_openid_provider: True
127code_domain: code.launchpad.dev128code_domain: code.launchpad.dev
128default_batch_size: 5129default_batch_size: 5
129max_attachment_size: 2097152130max_attachment_size: 2097152
@@ -277,6 +278,9 @@
277[vhost.openid]278[vhost.openid]
278hostname: openid.launchpad.dev279hostname: openid.launchpad.dev
279280
281[vhost.testopenid]
282hostname: testopenid.dev
283
280[vhost.ubuntu_openid]284[vhost.ubuntu_openid]
281hostname: ubuntu-openid.launchpad.dev285hostname: ubuntu-openid.launchpad.dev
282286
283287
=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf 2010-01-22 04:01:17 +0000
+++ lib/canonical/config/schema-lazr.conf 2010-02-17 15:49:22 +0000
@@ -884,6 +884,11 @@
884storm_cache: generational884storm_cache: generational
885storm_cache_size: 10000885storm_cache_size: 10000
886886
887# Whether or not to enable a test OpenID provider on the testopenid vhost.
888# It's a test provider, not meant to be enabled on production.
889# datatype: boolean
890enable_test_openid_provider: False
891
887# Assume the slave database is lagged if it takes more than this many892# Assume the slave database is lagged if it takes more than this many
888# milliseconds to calculate this information from the Slony-I tables.893# milliseconds to calculate this information from the Slony-I tables.
889# datatype: integer894# datatype: integer
@@ -1839,6 +1844,9 @@
1839[vhost.openid]1844[vhost.openid]
18401845
18411846
1847[vhost.testopenid]
1848
1849
1842[vhost.ubuntu_openid]1850[vhost.ubuntu_openid]
18431851
18441852
18451853
=== modified file 'lib/canonical/launchpad/browser/launchpad.py'
--- lib/canonical/launchpad/browser/launchpad.py 2010-02-02 17:12:29 +0000
+++ lib/canonical/launchpad/browser/launchpad.py 2010-02-17 15:49:22 +0000
@@ -96,6 +96,7 @@
96 ITranslationGroupSet)96 ITranslationGroupSet)
97from lp.translations.interfaces.translationimportqueue import (97from lp.translations.interfaces.translationimportqueue import (
98 ITranslationImportQueue)98 ITranslationImportQueue)
99from lp.testopenid.interfaces.server import ITestOpenIDApplication
99100
100from canonical.launchpad.webapp import (101from canonical.launchpad.webapp import (
101 LaunchpadFormView, LaunchpadView, Link, Navigation,102 LaunchpadFormView, LaunchpadView, Link, Navigation,
@@ -565,6 +566,7 @@
565 'token': ILoginTokenSet,566 'token': ILoginTokenSet,
566 '+groups': ITranslationGroupSet,567 '+groups': ITranslationGroupSet,
567 'translations': IRosettaApplication,568 'translations': IRosettaApplication,
569 'testopenid': ITestOpenIDApplication,
568 'questions': IQuestionSet,570 'questions': IQuestionSet,
569 '+rpconfig': IOpenIDRPConfigSet,571 '+rpconfig': IOpenIDRPConfigSet,
570 # These three have been renamed, and no redirects done, as the old572 # These three have been renamed, and no redirects done, as the old
571573
=== modified file 'lib/canonical/launchpad/configure.zcml'
--- lib/canonical/launchpad/configure.zcml 2010-02-02 17:12:29 +0000
+++ lib/canonical/launchpad/configure.zcml 2010-02-17 15:49:22 +0000
@@ -26,6 +26,7 @@
26 <include package="lp.code" />26 <include package="lp.code" />
27 <include package="lp.soyuz" />27 <include package="lp.soyuz" />
28 <include package="lp.translations" />28 <include package="lp.translations" />
29 <include package="lp.testopenid" />
29 <include package="lp.blueprints" />30 <include package="lp.blueprints" />
30 <include package="lp.services.comments" />31 <include package="lp.services.comments" />
3132
3233
=== modified file 'lib/canonical/launchpad/layers.py'
--- lib/canonical/launchpad/layers.py 2009-10-21 18:33:11 +0000
+++ lib/canonical/launchpad/layers.py 2010-02-17 15:49:22 +0000
@@ -57,6 +57,10 @@
57 """57 """
5858
5959
60class TestOpenIDLayer(LaunchpadLayer):
61 """The `TestOpenIDLayer` layer."""
62
63
60class PageTestLayer(LaunchpadLayer):64class PageTestLayer(LaunchpadLayer):
61 """The `PageTestLayer` layer. (need to register a 404 view for this and65 """The `PageTestLayer` layer. (need to register a 404 view for this and
62 for the debug page too. and make the debugview a base class in the66 for the debug page too. and make the debugview a base class in the
6367
=== modified file 'lib/canonical/launchpad/systemhomes.py'
--- lib/canonical/launchpad/systemhomes.py 2010-02-10 23:14:56 +0000
+++ lib/canonical/launchpad/systemhomes.py 2010-02-17 15:49:22 +0000
@@ -12,6 +12,7 @@
12 'MaloneApplication',12 'MaloneApplication',
13 'PrivateMaloneApplication',13 'PrivateMaloneApplication',
14 'RosettaApplication',14 'RosettaApplication',
15 'TestOpenIDApplication',
15 ]16 ]
1617
17__metaclass__ = type18__metaclass__ = type
@@ -31,6 +32,7 @@
31 IMailingListApplication, IMaloneApplication,32 IMailingListApplication, IMaloneApplication,
32 IPrivateMaloneApplication, IProductSet, IRosettaApplication,33 IPrivateMaloneApplication, IProductSet, IRosettaApplication,
33 IWebServiceApplication)34 IWebServiceApplication)
35from lp.testopenid.interfaces.server import ITestOpenIDApplication
34from lp.translations.interfaces.translationgroup import ITranslationGroupSet36from lp.translations.interfaces.translationgroup import ITranslationGroupSet
35from lp.translations.interfaces.translationsoverview import (37from lp.translations.interfaces.translationsoverview import (
36 ITranslationsOverview)38 ITranslationsOverview)
@@ -382,3 +384,7 @@
382 wadl = super(WebServiceApplication, self).toWADL()384 wadl = super(WebServiceApplication, self).toWADL()
383 self.__class__.cached_wadl = wadl385 self.__class__.cached_wadl = wadl
384 return wadl386 return wadl
387
388
389class TestOpenIDApplication:
390 implements(ITestOpenIDApplication)
385391
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2009-12-16 19:59:45 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2010-02-17 15:49:22 +0000
@@ -45,6 +45,7 @@
45from lazr.restful.publisher import (45from lazr.restful.publisher import (
46 WebServicePublicationMixin, WebServiceRequestTraversal)46 WebServicePublicationMixin, WebServiceRequestTraversal)
4747
48from lp.testopenid.interfaces.server import ITestOpenIDApplication
48from canonical.launchpad.interfaces.launchpad import (49from canonical.launchpad.interfaces.launchpad import (
49 IFeedsApplication, IPrivateApplication, IWebServiceApplication)50 IFeedsApplication, IPrivateApplication, IWebServiceApplication)
50from canonical.launchpad.interfaces.oauth import (51from canonical.launchpad.interfaces.oauth import (
@@ -1121,6 +1122,17 @@
1121 """Request type for a launchpad feed."""1122 """Request type for a launchpad feed."""
1122 implements(canonical.launchpad.layers.FeedsLayer)1123 implements(canonical.launchpad.layers.FeedsLayer)
11231124
1125
1126# ---- testopenid
1127
1128class TestOpenIDBrowserRequest(LaunchpadBrowserRequest):
1129 implements(canonical.launchpad.layers.TestOpenIDLayer)
1130
1131
1132class TestOpenIDBrowserPublication(LaunchpadBrowserPublication):
1133 root_object_interface = ITestOpenIDApplication
1134
1135
1124# ---- web service1136# ---- web service
11251137
1126class WebServicePublication(WebServicePublicationMixin,1138class WebServicePublication(WebServicePublicationMixin,
@@ -1442,6 +1454,10 @@
1442 'xmlrpc', PublicXMLRPCRequest, PublicXMLRPCPublication)1454 'xmlrpc', PublicXMLRPCRequest, PublicXMLRPCPublication)
1443 ]1455 ]
14441456
1457 if config.launchpad.enable_test_openid_provider:
1458 factories.append(VHRP('testopenid', TestOpenIDBrowserRequest,
1459 TestOpenIDBrowserPublication))
1460
1445 # We may also have a private XML-RPC server.1461 # We may also have a private XML-RPC server.
1446 private_port = None1462 private_port = None
1447 for server in config.servers:1463 for server in config.servers:
14481464
=== added directory 'lib/lp/testopenid'
=== added file 'lib/lp/testopenid/__init__.py'
=== added directory 'lib/lp/testopenid/adapters'
=== added file 'lib/lp/testopenid/adapters/__init__.py'
=== added file 'lib/lp/testopenid/adapters/openid.py'
--- lib/lp/testopenid/adapters/openid.py 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/adapters/openid.py 2010-02-17 15:49:22 +0000
@@ -0,0 +1,32 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""TestOpenID adapters and helpers."""
5
6__metaclass__ = type
7
8__all__ = [
9 'TestOpenIDPersistentIdentity',
10 ]
11
12from zope.component import adapts
13from zope.interface import implements
14
15from canonical.launchpad.interfaces.account import IAccount
16from canonical.launchpad.webapp.vhosts import allvhosts
17
18from lp.services.openid.adapters.openid import OpenIDPersistentIdentity
19from lp.testopenid.interfaces.server import ITestOpenIDPersistentIdentity
20
21
22class TestOpenIDPersistentIdentity(OpenIDPersistentIdentity):
23 """See `IOpenIDPersistentIdentity`."""
24
25 adapts(IAccount)
26 implements(ITestOpenIDPersistentIdentity)
27
28 @property
29 def openid_identity_url(self):
30 """See `IOpenIDPersistentIdentity`."""
31 identity_root_url = allvhosts.configs['testopenid'].rooturl
32 return identity_root_url + self.openid_identifier.encode('ascii')
033
=== added directory 'lib/lp/testopenid/browser'
=== added file 'lib/lp/testopenid/browser/__init__.py'
=== added file 'lib/lp/testopenid/browser/configure.zcml'
--- lib/lp/testopenid/browser/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/browser/configure.zcml 2010-02-17 15:49:22 +0000
@@ -0,0 +1,82 @@
1<!-- Copyright 2009 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->
4
5<configure
6 xmlns="http://namespaces.zope.org/zope"
7 xmlns:browser="http://namespaces.zope.org/browser"
8 xmlns:i18n="http://namespaces.zope.org/i18n"
9 i18n_domain="launchpad">
10
11 <browser:navigation
12 module=".server"
13 classes="TestOpenIDApplicationNavigation"
14 />
15
16 <adapter
17 provides="canonical.launchpad.webapp.interfaces.ICanonicalUrlData"
18 for="..interfaces.server.ITestOpenIDApplication"
19 factory=".server.TestOpenIDRootUrlData"
20 />
21
22 <browser:defaultView
23 for="..interfaces.server.ITestOpenIDApplication"
24 name="+index"
25 />
26
27 <browser:page
28 for="..interfaces.server.ITestOpenIDApplication"
29 class=".server.TestOpenIDView"
30 permission="zope.Public"
31 name="+openid"
32 />
33 <browser:page
34 for="..interfaces.server.ITestOpenIDApplication"
35 class=".server.TestOpenIDIndexView"
36 permission="zope.Public"
37 name="+index"
38 />
39 <browser:page
40 for="..interfaces.server.ITestOpenIDApplication"
41 class=".server.TestOpenIDLoginView"
42 permission="zope.Public"
43 name="+auth"
44 />
45
46 <browser:url
47 for="..interfaces.server.ITestOpenIDPersistentIdentity"
48 path_expression="string:${openid_identifier}"
49 parent_utility="..interfaces.server.ITestOpenIDApplication"
50 />
51
52 <browser:defaultView
53 for="..interfaces.server.ITestOpenIDPersistentIdentity"
54 name="+index"
55 />
56
57 <browser:page
58 for="..interfaces.server.ITestOpenIDPersistentIdentity"
59 name="+index"
60 template="../templates/persistentidentity-index.pt"
61 permission="zope.Public"
62 class=".server.PersistentIdentityView"
63 />
64
65 <browser:page
66 name=""
67 for="..interfaces.server.ITestOpenIDApplication"
68 class="canonical.launchpad.browser.launchpad.LaunchpadImageFolder"
69 permission="zope.Public"
70 layer="canonical.launchpad.layers.TestOpenIDLayer"
71 />
72
73 <!-- A simple view used by the page tests. -->
74 <browser:page
75 for="..interfaces.server.ITestOpenIDApplication"
76 name="+echo"
77 permission="zope.Public"
78 class="..testing.helpers.EchoView"
79 layer="canonical.launchpad.layers.PageTestLayer"
80 />
81
82</configure>
083
=== added file 'lib/lp/testopenid/browser/server.py'
--- lib/lp/testopenid/browser/server.py 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/browser/server.py 2010-02-17 15:49:22 +0000
@@ -0,0 +1,285 @@
1# Copyright 2010 Canonical Ltd. All rights reserved.
2
3"""Test OpenID server."""
4
5__metaclass__ = type
6__all__ = [
7 'PersistentIdentityView',
8 'TestOpenIDApplicationNavigation',
9 'TestOpenIDIndexView'
10 'TestOpenIDLoginView',
11 'TestOpenIDRootUrlData',
12 'TestOpenIDView',
13 ]
14
15from datetime import timedelta
16
17from z3c.ptcompat import ViewPageTemplateFile
18from zope.app.security.interfaces import IUnauthenticatedPrincipal
19from zope.component import getUtility
20from zope.interface import implements
21from zope.security.proxy import isinstance as zisinstance
22from zope.session.interfaces import ISession
23
24from openid.server.server import CheckIDRequest, Server
25from openid.store.memstore import MemoryStore
26
27from canonical.cachedproperty import cachedproperty
28from canonical.launchpad import _
29from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet
30from canonical.launchpad.webapp import (
31 action, LaunchpadFormView, LaunchpadView)
32from canonical.launchpad.webapp.interfaces import (
33 ICanonicalUrlData, IPlacelessLoginSource, UnexpectedFormData)
34from canonical.launchpad.webapp.login import (
35 allowUnauthenticatedSession, logInPrincipal, logoutPerson)
36from canonical.launchpad.webapp.publisher import Navigation, stepthrough
37from canonical.launchpad.webapp.url import urlappend
38from canonical.launchpad.webapp.vhosts import allvhosts
39
40from lp.services.openid.browser.openiddiscovery import (
41 XRDSContentNegotiationMixin)
42from lp.testopenid.interfaces.server import (
43 ITestOpenIDApplication, ITestOpenIDLoginForm,
44 ITestOpenIDPersistentIdentity)
45
46
47OPENID_REQUEST_SESSION_KEY = 'testopenid.request'
48SESSION_PKG_KEY = 'TestOpenID'
49SERVER_URL = urlappend(allvhosts.configs['testopenid'].rooturl, '+openid')
50openid_store = MemoryStore()
51
52
53class TestOpenIDRootUrlData:
54 """`ICanonicalUrlData` for the test OpenID provider."""
55
56 implements(ICanonicalUrlData)
57
58 path = ''
59 inside = None
60 rootsite = 'testopenid'
61
62 def __init__(self, context):
63 self.context = context
64
65
66class TestOpenIDApplicationNavigation(Navigation):
67 """Navigation for `ITestOpenIDApplication`"""
68 usedfor = ITestOpenIDApplication
69
70 @stepthrough('+id')
71 def traverse_id(self, name):
72 """Traverse to persistent OpenID identity URLs."""
73 try:
74 account = getUtility(IAccountSet).getByOpenIDIdentifier(name)
75 except LookupError:
76 account = None
77 if account is None or account.status != AccountStatus.ACTIVE:
78 return None
79 return ITestOpenIDPersistentIdentity(account)
80
81
82class TestOpenIDXRDSContentNegotiationMixin(XRDSContentNegotiationMixin):
83 """Custom XRDSContentNegotiationMixin that overrides openid_server_url."""
84
85 @property
86 def openid_server_url(self):
87 """The OpenID Server endpoint URL for Launchpad."""
88 return SERVER_URL
89
90
91class TestOpenIDIndexView(
92 TestOpenIDXRDSContentNegotiationMixin, LaunchpadView):
93 template = ViewPageTemplateFile("../templates/application-index.pt")
94 xrds_template = ViewPageTemplateFile("../templates/application-xrds.pt")
95
96
97class OpenIDMixin:
98 """A mixin with OpenID helper methods."""
99
100 openid_request = None
101
102 def __init__(self, context, request):
103 super(OpenIDMixin, self).__init__(context, request)
104 self.server_url = SERVER_URL
105 self.openid_server = Server(openid_store, self.server_url)
106
107 @property
108 def user_identity_url(self):
109 return ITestOpenIDPersistentIdentity(self.account).openid_identity_url
110
111 def isIdentityOwner(self):
112 """Return True if the user can authenticate as the given ID."""
113 assert self.account is not None, "user should be logged in by now."
114 return (self.openid_request.idSelect() or
115 self.openid_request.identity == self.user_identity_url)
116
117 @cachedproperty('_openid_parameters')
118 def openid_parameters(self):
119 """A dictionary of OpenID query parameters from request."""
120 query = {}
121 for key, value in self.request.form.items():
122 if key.startswith('openid.'):
123 # All OpenID query args are supposed to be ASCII.
124 query[key.encode('US-ASCII')] = value.encode('US-ASCII')
125 return query
126
127 def getSession(self):
128 """Get the session data container that stores the OpenID request."""
129 if IUnauthenticatedPrincipal.providedBy(self.request.principal):
130 # A dance to assert that we want to break the rules about no
131 # unauthenticated sessions. Only after this next line is it
132 # safe to set session values.
133 allowUnauthenticatedSession(
134 self.request, duration=timedelta(minutes=60))
135 return ISession(self.request)[SESSION_PKG_KEY]
136
137 def restoreRequestFromSession(self):
138 """Get the OpenIDRequest from our session."""
139 session = self.getSession()
140 try:
141 self._openid_parameters = session[OPENID_REQUEST_SESSION_KEY]
142 except KeyError:
143 raise UnexpectedFormData("No OpenID request in session")
144
145 # Decode the request parameters and create the request object.
146 self.openid_request = self.openid_server.decodeRequest(
147 self.openid_parameters)
148 assert zisinstance(self.openid_request, CheckIDRequest), (
149 'Invalid OpenIDRequest in session')
150
151 def saveRequestInSession(self):
152 """Save the OpenIDRequest in our session."""
153 query = self.openid_parameters
154 assert query.get('openid.mode') == 'checkid_setup', (
155 'Can only serialise checkid_setup OpenID requests')
156
157 session = self.getSession()
158 # If this was meant for use in production we'd have to use a nonce
159 # as the key when storing the openid request in the session, but as
160 # it's meant to run only on development instances we can simplify
161 # things a bit by storing the openid request using a well known key.
162 session[OPENID_REQUEST_SESSION_KEY] = query
163
164 def renderOpenIDResponse(self, openid_response):
165 """Return a web-suitable response constructed from openid_response."""
166 webresponse = self.openid_server.encodeResponse(openid_response)
167 response = self.request.response
168 response.setStatus(webresponse.code)
169 for header, value in webresponse.headers.items():
170 response.setHeader(header, value)
171 return webresponse.body
172
173 def createPositiveResponse(self):
174 """Create a positive assertion OpenIDResponse.
175
176 This method should be called to create the response to
177 successful checkid requests.
178
179 If the trust root for the request is in openid_sreg_trustroots,
180 then additional user information is included with the
181 response.
182 """
183 assert self.account is not None, (
184 'Must be logged in for positive OpenID response')
185 assert self.openid_request is not None, (
186 'No OpenID request to respond to.')
187
188 if not self.isIdentityOwner():
189 return self.createFailedResponse()
190
191 if self.openid_request.idSelect():
192 response = self.openid_request.answer(
193 True, identity=self.user_identity_url)
194 else:
195 response = self.openid_request.answer(True)
196
197 return response
198
199 def createFailedResponse(self):
200 """Create a failed assertion OpenIDResponse.
201
202 This method should be called to create the response to
203 unsuccessful checkid requests.
204 """
205 assert self.openid_request is not None, (
206 'No OpenID request to respond to.')
207 response = self.openid_request.answer(False, self.server_url)
208 return response
209
210
211class TestOpenIDView(OpenIDMixin, LaunchpadView):
212 """An OpenID Provider endpoint for Launchpad.
213
214 This class implements an OpenID endpoint using the python-openid
215 library. In addition to the normal modes of operation, it also
216 implements the OpenID 2.0 identifier select mode.
217
218 Note that the checkid_immediate mode is not supported.
219 """
220
221 def render(self):
222 """Handle all OpenID requests and form submissions."""
223 # NB: Will be None if there are no parameters in the request.
224 self.openid_request = self.openid_server.decodeRequest(
225 self.openid_parameters)
226
227 if self.openid_request.mode == 'checkid_setup':
228 referer = self.request.get("HTTP_REFERER")
229 if referer:
230 self.request.response.setCookie("openid_referer", referer)
231
232 # Log the user out and present the login page so that they can
233 # authenticate as somebody else if they want.
234 logoutPerson(self.request)
235 return self.showLoginPage()
236 elif self.openid_request.mode == 'checkid_immediate':
237 raise UnexpectedFormData(
238 'We do not handle checkid_immediate requests.')
239 else:
240 return self.renderOpenIDResponse(
241 self.openid_server.handleRequest(self.openid_request))
242
243 def showLoginPage(self):
244 """Render the login dialog."""
245 self.saveRequestInSession()
246 return TestOpenIDLoginView(self.context, self.request)()
247
248
249class TestOpenIDLoginView(OpenIDMixin, LaunchpadFormView):
250 """A view for users to log into the OpenID provider."""
251
252 page_title = "Login"
253 schema = ITestOpenIDLoginForm
254 action_url = '+auth'
255 template = ViewPageTemplateFile("../templates/auth.pt")
256
257 def initialize(self):
258 self.restoreRequestFromSession()
259 super(TestOpenIDLoginView, self).initialize()
260
261 def validate(self, data):
262 """Check that the email address and password are valid for login."""
263 loginsource = getUtility(IPlacelessLoginSource)
264 principal = loginsource.getPrincipalByLogin(data['email'])
265 if principal is None or not principal.validate(data['password']):
266 self.addError(
267 _("Incorrect password for the provided email address."))
268
269 @action('Continue', name='continue')
270 def continue_action(self, action, data):
271 email = data['email']
272 principal = getUtility(IPlacelessLoginSource).getPrincipalByLogin(
273 email)
274 logInPrincipal(self.request, principal, email)
275 # Update the attribute holding the cached user.
276 self._account = principal.account
277 return self.renderOpenIDResponse(self.createPositiveResponse())
278
279
280class PersistentIdentityView(
281 TestOpenIDXRDSContentNegotiationMixin, LaunchpadView):
282 """Render the OpenID identity page."""
283
284 xrds_template = ViewPageTemplateFile(
285 "../templates/persistentidentity-xrds.pt")
0286
=== added file 'lib/lp/testopenid/configure.zcml'
--- lib/lp/testopenid/configure.zcml 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/configure.zcml 2010-02-17 15:49:22 +0000
@@ -0,0 +1,28 @@
1<!-- Copyright 2009 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->
4
5<configure
6 xmlns="http://namespaces.zope.org/zope"
7 xmlns:browser="http://namespaces.zope.org/browser"
8 xmlns:i18n="http://namespaces.zope.org/i18n"
9 i18n_domain="launchpad">
10
11 <securedutility
12 class="canonical.launchpad.systemhomes.TestOpenIDApplication"
13 provides="lp.testopenid.interfaces.server.ITestOpenIDApplication">
14 <allow interface="lp.testopenid.interfaces.server.ITestOpenIDApplication"/>
15 </securedutility>
16
17 <class class=".adapters.openid.TestOpenIDPersistentIdentity">
18 <allow interface=".interfaces.server.ITestOpenIDPersistentIdentity" />
19 </class>
20
21 <adapter
22 factory=".adapters.openid.TestOpenIDPersistentIdentity"
23 provides=".interfaces.server.ITestOpenIDPersistentIdentity" />
24 />
25
26 <include package=".browser"/>
27
28</configure>
029
=== added directory 'lib/lp/testopenid/interfaces'
=== added file 'lib/lp/testopenid/interfaces/__init__.py'
=== added file 'lib/lp/testopenid/interfaces/server.py'
--- lib/lp/testopenid/interfaces/server.py 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/interfaces/server.py 2010-02-17 15:49:22 +0000
@@ -0,0 +1,29 @@
1# Copyright 2010 Canonical Ltd. All rights reserved.
2
3__metaclass__ = type
4__all__ = [
5 'ITestOpenIDApplication',
6 'ITestOpenIDLoginForm',
7 'ITestOpenIDPersistentIdentity',
8 ]
9
10from zope.interface import Interface
11from zope.schema import TextLine
12
13from canonical.launchpad.fields import PasswordField
14from canonical.launchpad.webapp.interfaces import ILaunchpadApplication
15
16from lp.services.openid.interfaces.openid import IOpenIDPersistentIdentity
17
18
19class ITestOpenIDApplication(ILaunchpadApplication):
20 """Launchpad's testing OpenID application root."""
21
22
23class ITestOpenIDLoginForm(Interface):
24 email = TextLine(title=u'What is your e-mail address?', required=True)
25 password = PasswordField(title=u'Password', required=True)
26
27
28class ITestOpenIDPersistentIdentity(IOpenIDPersistentIdentity):
29 """Marker interface for IOpenIDPersistentIdentity on testopenid."""
030
=== added directory 'lib/lp/testopenid/stories'
=== added file 'lib/lp/testopenid/stories/__init__.py'
=== added file 'lib/lp/testopenid/stories/basics.txt'
--- lib/lp/testopenid/stories/basics.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/stories/basics.txt 2010-02-17 15:49:22 +0000
@@ -0,0 +1,163 @@
1====================
2Test OpenID provider
3====================
4
5Introduction
6============
7
8Launchpad provides an OpenID provider (under the testopenid.dev
9vhost) for testing purposes and for developers to be able to log into their
10development instances. This provider is only available for development and
11testing.
12
13We are going to fake a consumer for these examples. In order to ensure
14that the consumer is being fed the correct replies, we use a view that
15renders the parameters in the response in an easily testable format.
16
17 >>> anon_browser.open('http://testopenid.dev/+echo?foo=bar')
18 >>> print anon_browser.contents
19 Request method: GET
20 foo:bar
21
22
23associate Mode
24==============
25
26Establish a shared secret between Consumer and Identity Provider.
27
28After determining the URL of the OpenID server, the next thing a consumer
29needs to do is associate with the server and get a shared secret via a
30POST request.
31
32 >>> from urllib import urlencode
33 >>> anon_browser.open(
34 ... 'http://testopenid.dev/+openid', data=urlencode({
35 ... 'openid.mode': 'associate',
36 ... 'openid.assoc_type': 'HMAC-SHA1'}))
37 >>> print anon_browser.headers
38 Status: 200 Ok
39 ...
40 Content-Type: text/plain
41 ...
42 >>> print anon_browser.contents
43 assoc_handle:{HMAC-SHA1}{...}{...}
44 assoc_type:HMAC-SHA1
45 expires_in:1209...
46 mac_key:...
47 <BLANKLINE>
48
49Get the association handle, which we will need for later tests.
50
51 >>> import re
52 >>> [assoc_handle] = re.findall('assoc_handle:(.*)', anon_browser.contents)
53
54
55checkid_setup Mode
56==================
57
58When we go to the OpenID setup URL, we are presented with a login
59form. By entering an email address and password, we are directed back
60to the consumer, completing the OpenID request:
61
62 >>> args = urlencode({
63 ... 'openid.mode': 'checkid_setup',
64 ... 'openid.identity': 'http://testopenid.dev/+id/mark_oid',
65 ... 'openid.assoc_handle': assoc_handle,
66 ... 'openid.return_to': 'http://testopenid.dev/+echo',
67 ... })
68 >>> user_browser.open('http://testopenid.dev/+openid?%s' % args)
69 >>> print user_browser.url
70 http://testopenid.dev/+openid?...
71 >>> print user_browser.title
72 Login
73 >>> user_browser.getControl(name='field.email').value = 'mark@example.com'
74 >>> user_browser.getControl(name='field.password').value = 'test'
75 >>> user_browser.getControl('Continue').click()
76
77 >>> print user_browser.url
78 http://testopenid.dev/+echo?...
79 >>> print user_browser.contents
80 Request method: GET
81 openid.assoc_handle:...
82 openid.identity:http://testopenid.dev/+id/mark_oid
83 openid.mode:id_res...
84 openid.op_endpoint:http://testopenid.dev/+openid
85 openid.response_nonce:...
86 openid.return_to:http://testopenid.dev/+echo
87 openid.sig:...
88 openid.signed:...
89 <BLANKLINE>
90
91We will record the signature from this response to use in the next test:
92
93 >>> [sig] = re.findall('sig:(.*)', user_browser.contents)
94
95
96check_authentication Mode
97=========================
98
99Ask an Identity Provider if a message is valid. For dumb, stateless
100Consumers or when verifying an invalidate_handle response.
101
102If an association handle is stateful (genereted using the associate Mode),
103check_authentication will fail.
104
105 >>> args = urlencode({
106 ... 'openid.mode': 'check_authentication',
107 ... 'openid.assoc_handle': assoc_handle,
108 ... 'openid.sig': sig,
109 ... 'openid.signed': 'return_to,mode,identity',
110 ... 'openid.identity':
111 ... 'http://testopenid.dev/+id/mark_oid',
112 ... 'openid.return_to': 'http://testopenid.dev/+echo',
113 ... })
114 >>> user_browser.open('http://testopenid.dev/+openid?%s' % args)
115 >>> print user_browser.contents
116 is_valid:false
117 <BLANKLINE>
118
119If we are a dumb consumer though, we must invoke the check_authentication
120mode, passing back the association handle, signature and values of all
121fields that were signed.
122
123 >>> args = urlencode({
124 ... 'openid.mode': 'checkid_setup',
125 ... 'openid.identity':
126 ... 'http://testopenid.dev/+id/mark_oid',
127 ... 'openid.return_to': 'http://testopenid.dev/+echo',
128 ... })
129 >>> user_browser.open('http://testopenid.dev/+openid?%s' % args)
130 >>> user_browser.getControl(name='field.email').value = 'mark@example.com'
131 >>> user_browser.getControl(name='field.password').value = 'test'
132 >>> user_browser.getControl('Continue').click()
133 >>> print user_browser.contents
134 Request method: GET
135 openid.assoc_handle:...
136 openid.identity:http://testopenid.dev/+id/mark_oid
137 openid.mode:id_res
138 openid.op_endpoint:http://testopenid.dev/+openid
139 openid.response_nonce:...
140 openid.return_to:http://testopenid.dev/+echo
141 openid.sig:...
142 openid.signed:...
143 <BLANKLINE>
144
145 >>> fields = dict(line.split(':', 1)
146 ... for line in user_browser.contents.splitlines()[1:]
147 ... if line.startswith('openid.'))
148 >>> signed = ['openid.' + name
149 ... for name in fields['openid.signed'].split(',')]
150 >>> message = dict((key, value) for (key, value) in fields.items()
151 ... if key in signed)
152 >>> message.update({
153 ... 'openid.mode': 'check_authentication',
154 ... 'openid.assoc_handle': fields['openid.assoc_handle'],
155 ... 'openid.sig': fields['openid.sig'],
156 ... 'openid.signed': fields['openid.signed'],
157 ... })
158
159 >>> args = urlencode(message)
160 >>> user_browser.open('http://testopenid.dev/+openid', args)
161 >>> print user_browser.contents
162 is_valid:true
163 <BLANKLINE>
0164
=== added file 'lib/lp/testopenid/stories/logging-in.txt'
--- lib/lp/testopenid/stories/logging-in.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/stories/logging-in.txt 2010-02-17 15:49:22 +0000
@@ -0,0 +1,64 @@
1========================================
2Logging in using the TestOpenID provider
3========================================
4
5A user with an existing account may log into Launchpad using the OpenID
6provider available on testopenid.dev.
7
8First we will set up the helper view that lets us test the final
9portion of the authentication process:
10
11 >>> from openid.consumer.consumer import Consumer
12 >>> from openid.fetchers import setDefaultFetcher
13 >>> from openid.store.memstore import MemoryStore
14 >>> from lp.testopenid.testing.helpers import (
15 ... complete_from_browser, make_identifier_select_endpoint,
16 ... PublisherFetcher)
17 >>> setDefaultFetcher(PublisherFetcher())
18
19The authentication process is started by the relying party issuing a
20checkid_setup request, sending the user to Launchpad:
21
22 >>> openid_store = MemoryStore()
23 >>> consumer = Consumer(session={}, store=openid_store)
24
25 >>> request = consumer.beginWithoutDiscovery(
26 ... make_identifier_select_endpoint())
27 >>> browser.open(request.redirectURL(
28 ... 'http://testopenid.dev/',
29 ... 'http://testopenid.dev/+echo'))
30
31At this point, the user is presented with a login form:
32
33 >>> print browser.title
34 Login
35
36As the user already has an account, they can enter their email address and
37password. If the password does not match the given email address, an error is
38shown:
39
40 >>> browser.getControl(name='field.email').value = 'mark@example.com'
41 >>> browser.getControl(name='field.password').value = 'not the password'
42 >>> browser.getControl('Continue').click()
43 >>> print browser.title
44 Login
45 >>> for tag in find_tags_by_class(browser.contents, 'error'):
46 ... print extract_text(tag)
47 There is 1 error.
48 Incorrect password for the provided email address.
49
50If the email address and password match, the user is logged in and returned to
51the relying party, with the user's identity URL:
52
53 >>> browser.getControl(name='field.password').value = 'test'
54 >>> browser.getControl('Continue').click()
55 >>> print browser.url
56 http://testopenid.dev/+echo?...
57 >>> info = complete_from_browser(consumer, browser)
58 >>> print info.status
59 success
60 >>> print info.endpoint.claimed_id
61 http://testopenid.dev/+id/mark_oid
62
63 # Clean up the changes we did to the openid module.
64 >>> setDefaultFetcher(None)
065
=== added file 'lib/lp/testopenid/stories/tests.py'
--- lib/lp/testopenid/stories/tests.py 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/stories/tests.py 2010-02-17 15:49:22 +0000
@@ -0,0 +1,22 @@
1# Copyright 2004-2008 Canonical Ltd. All rights reserved.
2
3import os
4import unittest
5
6from canonical.launchpad.testing.pages import PageTestSuite
7
8
9here = os.path.dirname(os.path.realpath(__file__))
10
11
12def test_suite():
13 stories = sorted(
14 dir for dir in os.listdir(here)
15 if not dir.startswith('.') and os.path.isdir(os.path.join(here, dir)))
16
17 suite = unittest.TestSuite()
18 suite.addTest(PageTestSuite('.'))
19 for storydir in stories:
20 suite.addTest(PageTestSuite(storydir))
21
22 return suite
023
=== added directory 'lib/lp/testopenid/templates'
=== added file 'lib/lp/testopenid/templates/application-index.pt'
--- lib/lp/testopenid/templates/application-index.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/templates/application-index.pt 2010-02-17 15:49:22 +0000
@@ -0,0 +1,5 @@
1<html>
2 <body>
3 <h1>Test OpenID provider for launchpad.dev</h1>
4 </body>
5</html>
06
=== added file 'lib/lp/testopenid/templates/application-xrds.pt'
--- lib/lp/testopenid/templates/application-xrds.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/templates/application-xrds.pt 2010-02-17 15:49:22 +0000
@@ -0,0 +1,14 @@
1<?xml version="1.0"?>
2<xrds:XRDS
3 xmlns="xri://$xrd*($v*2.0)"
4 xmlns:xrds="xri://$xrds"
5 xmlns:tal="http://xml.zope.org/namespaces/tal">
6 <XRD>
7 <Service priority="0">
8 <Type>http://specs.openid.net/auth/2.0/server</Type>
9 <URI tal:content="view/openid_server_url">
10 https://login.launchpad.net/+openid
11 </URI>
12 </Service>
13 </XRD>
14</xrds:XRDS>
015
=== added file 'lib/lp/testopenid/templates/auth.pt'
--- lib/lp/testopenid/templates/auth.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/templates/auth.pt 2010-02-17 15:49:22 +0000
@@ -0,0 +1,18 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 xml:lang="en"
7 lang="en"
8 dir="ltr"
9 metal:use-macro="view/macro:page/locationless"
10 i18n:domain="launchpad"
11>
12 <body>
13 <div metal:fill-slot="main">
14 <div metal:use-macro="context/@@launchpad_form/form" />
15 </div>
16 </body>
17</html>
18
019
=== added file 'lib/lp/testopenid/templates/persistentidentity-index.pt'
--- lib/lp/testopenid/templates/persistentidentity-index.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/templates/persistentidentity-index.pt 2010-02-17 15:49:22 +0000
@@ -0,0 +1,14 @@
1<html xmlns="http://www.w3.org/1999/xhtml"
2 xmlns:tal="http://xml.zope.org/namespaces/tal">
3 <head>
4 <link rel="openid.server"
5 tal:attributes="href view/openid_server_url" />
6 </head>
7 <body>
8 <h1>
9 OpenID Identity URL for
10 <tal:user content="context/account/displayname">Display Name</tal:user>
11 </h1>
12 </body>
13</html>
14
015
=== added file 'lib/lp/testopenid/templates/persistentidentity-xrds.pt'
--- lib/lp/testopenid/templates/persistentidentity-xrds.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/templates/persistentidentity-xrds.pt 2010-02-17 15:49:22 +0000
@@ -0,0 +1,14 @@
1<?xml version="1.0"?>
2<xrds:XRDS
3 xmlns="xri://$xrd*($v*2.0)"
4 xmlns:xrds="xri://$xrds"
5 xmlns:tal="http://xml.zope.org/namespaces/tal">
6 <XRD>
7 <Service priority="0">
8 <Type>http://specs.openid.net/auth/2.0/signon</Type>
9 <URI priority="0" tal:content="view/openid_server_url">
10 https://testopenid.dev/+openid
11 </URI>
12 </Service>
13 </XRD>
14</xrds:XRDS>
015
=== added directory 'lib/lp/testopenid/testing'
=== added file 'lib/lp/testopenid/testing/__init__.py'
=== added file 'lib/lp/testopenid/testing/helpers.py'
--- lib/lp/testopenid/testing/helpers.py 1970-01-01 00:00:00 +0000
+++ lib/lp/testopenid/testing/helpers.py 2010-02-17 15:49:22 +0000
@@ -0,0 +1,74 @@
1# Copyright 2010 Canonical Ltd. All rights reserved.
2
3"""Helpers for TestOpenID page tests."""
4
5__metaclass__ = type
6__all__ = [
7 'complete_from_browser',
8 'EchoView',
9 'make_identifier_select_endpoint',
10 'PublisherFetcher',
11 ]
12
13from StringIO import StringIO
14import urllib2
15
16from zope.testbrowser.testing import PublisherHTTPHandler
17
18from openid import fetchers
19from openid.consumer.discover import (
20 OpenIDServiceEndpoint, OPENID_IDP_2_0_TYPE)
21
22from canonical.launchpad.webapp import LaunchpadView
23
24from lp.testopenid.browser.server import SERVER_URL
25
26
27class EchoView(LaunchpadView):
28 """A view which just echoes its form arguments in the response."""
29
30 def render(self):
31 out = StringIO()
32 print >> out, 'Request method: %s' % self.request.method
33 keys = sorted(self.request.form.keys())
34 for key in keys:
35 print >> out, '%s:%s' % (key, self.request.form[key])
36 return out.getvalue()
37
38
39class PublisherFetcher(fetchers.Urllib2Fetcher):
40 """An `HTTPFetcher` that passes requests on to the Zope publisher."""
41 def __init__(self):
42 super(PublisherFetcher, self).__init__()
43 self.opener = urllib2.build_opener(PublisherHTTPHandler)
44
45 def urlopen(self, request):
46 request.add_header('X-zope-handle-errors', True)
47 return self.opener.open(request)
48
49
50def complete_from_browser(consumer, browser):
51 """Complete OpenID request based on output of +echo.
52
53 :param consumer: an OpenID `Consumer` instance.
54 :param browser: a Zope testbrowser `Browser` instance.
55
56 This function parses the body of the +echo view into a set of query
57 arguments representing the OpenID response.
58 """
59 assert browser.contents.startswith('Request method'), (
60 "Browser contents does not look like it came from +echo")
61 # Skip the first line.
62 query = dict(line.split(':', 1)
63 for line in browser.contents.splitlines()[1:])
64
65 response = consumer.complete(query, browser.url)
66 return response
67
68
69def make_identifier_select_endpoint():
70 """Create an endpoint for use in OpenID identifier select mode."""
71 endpoint = OpenIDServiceEndpoint()
72 endpoint.server_url = SERVER_URL
73 endpoint.type_uris = [OPENID_IDP_2_0_TYPE]
74 return endpoint
075
=== modified file 'utilities/rocketfuel-setup'
--- utilities/rocketfuel-setup 2009-12-13 01:38:59 +0000
+++ utilities/rocketfuel-setup 2010-02-17 15:49:22 +0000
@@ -136,6 +136,7 @@
136 shipit.edubuntu.dev136 shipit.edubuntu.dev
137 shipit.kubuntu.dev137 shipit.kubuntu.dev
138 shipit.ubuntu.dev138 shipit.ubuntu.dev
139 testopenid.dev
139 translations.launchpad.dev140 translations.launchpad.dev
140 xmlrpc-private.launchpad.dev141 xmlrpc-private.launchpad.dev
141 xmlrpc.launchpad.dev142 xmlrpc.launchpad.dev