Merge lp:~salgado/launchpad/openid-consumer into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Merged at revision: not available
Proposed branch: lp:~salgado/launchpad/openid-consumer
Merge into: lp:launchpad
Prerequisite: lp:~salgado/launchpad/lp-as-openid-rp
Diff against target: 1660 lines (+742/-331)
38 files modified
BRANCH.TODO (+15/-45)
configs/development/launchpad-lazr.conf (+1/-0)
configs/testrunner-appserver/launchpad-lazr.conf (+3/-0)
lib/canonical/config/schema-lazr.conf (+4/-0)
lib/canonical/launchpad/database/baseopenidstore.py (+1/-1)
lib/canonical/launchpad/database/tests/test_baseopenidstore.py (+5/-0)
lib/canonical/launchpad/doc/webapp-publication.txt (+5/-0)
lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt (+0/-9)
lib/canonical/launchpad/pagetests/oauth/authorize-token.txt (+11/-19)
lib/canonical/launchpad/templates/login-already.pt (+18/-0)
lib/canonical/launchpad/templates/login-error.pt (+17/-0)
lib/canonical/launchpad/templates/login-suspended-account.pt (+20/-0)
lib/canonical/launchpad/testing/browser.py (+11/-3)
lib/canonical/launchpad/testing/cookie.py (+0/-67)
lib/canonical/launchpad/webapp/login.py (+188/-8)
lib/canonical/launchpad/webapp/session.py (+5/-7)
lib/canonical/launchpad/webapp/tests/cookie-authentication.txt (+29/-18)
lib/canonical/launchpad/webapp/tests/login.txt (+47/-0)
lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt (+30/-25)
lib/canonical/launchpad/webapp/tests/test_cookie_authentication.py (+24/-0)
lib/canonical/launchpad/webapp/tests/test_new_login.py (+188/-0)
lib/canonical/launchpad/webapp/tests/test_no_anonymous_session_cookies.py (+24/-0)
lib/canonical/launchpad/windmill/testing/lpuser.py (+24/-26)
lib/canonical/launchpad/windmill/testing/widgets.py (+2/-4)
lib/canonical/launchpad/zcml/launchpad.zcml (+31/-5)
lib/canonical/testing/layers.py (+1/-0)
lib/devscripts/ec2test/instance.py (+1/-1)
lib/lp/app/stories/basics/xx-beta-testers-redirection.txt (+0/-23)
lib/lp/app/stories/launchpad-root/site-search.txt (+0/-9)
lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt (+5/-4)
lib/lp/bugs/stories/upstream-bugprivacy/10-file-private-upstream-bug.txt (+5/-8)
lib/lp/bugs/windmill/tests/test_bug_tags_entry.py (+1/-1)
lib/lp/bugs/windmill/tests/test_filebug_dupe_finder.py (+1/-1)
lib/lp/code/stories/branches/xx-register-a-branch.txt (+4/-10)
lib/lp/registry/stories/person/xx-deactivate-account.txt (+7/-24)
lib/lp/scripts/utilities/importfascist.py (+1/-0)
lib/lp/translations/stories/standalone/xx-person-editlanguages.txt (+13/-11)
lib/lp/translations/windmill/tests/test_documentation_links.py (+0/-2)
To merge this branch: bzr merge lp:~salgado/launchpad/openid-consumer
Reviewer Review Type Date Requested Status
Gary Poster (community) code Approve
Review via email: mp+19496@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :

This branch changes the +login page to use OpenID to authenticate, also adding a callback view for OpenID providers to send the users to, when the authentication is completed.

Some existing tests were changed to use AppServerLayer so that they can do
OpenID login, so they now use launchpad.dev:8085

There's a bug fix in baseopenidstore.py, with an ammended test in
test_baseopenidstore.py

Some tests relied on the +login page but they didn't really need to, so
instead of changing them to use the AppServerLayer (which is a lot more
expensive), I've changed them to use HTTP basic auth.

lib/canonical/launchpad/testing/cookie.py is no longer needed as
testbrowser.Browser now provides a .cookies

BasicLoginPage is now enabled for development and the test runner (including
the AppServerLayer, which uses a different config) so that Windmill tests can
use it to login -- using OpenID on windmill doesn't work because it can't
cleanly switch between domains (e.g. launchpad.dev <-> testopenid.dev).

The existing +login page was renamed to +login-old and it'll be removed
(already is, in fact) in a later branch.

Revision history for this message
Gary Poster (gary) wrote :
Download full text (3.7 KiB)

merge-conditional

Thank you for this excellent branch. I can tell it was a lot of hard work--the mechanize testing code alone must have been "fun" to come up with.

Thanks also for the many clean-ups in this branch, ranging from the removal of the now-unnecessary cookie test code to the salt fix.

I had several comments that we talked about on IRC. Here is a summary of what we discussed.

- You removed the test of "locationless pages" and the fact that they do not offer to redirect. I wondered if the +login page were in fact the only locationless page. You reported that you had done a search, and I did a spot check (+graphics) and concluded that it was fine.

- I had a concern about the +basiclogin login registration: it is hidden. If I were to try to find out where +basiclogin appeared so I could change something about it then I would look in zcml, and not find it. We have no other "do this arbitrary thing now" kind of mechanism. Moreover, it is handled by a package import which is never lovely. I suggested that you make this registration an event handler for the server starting (which Zope broadcasts). The subscription could be registered in zcml with a zcml comment duplicating what the standard registration would be. You agreed and showed me http://paste.ubuntu.com/378472/ .

- Typo: s/where/were/ within "# once that's done they must be sent back to the URL they where when"

- Move imports of transaction and removeSecurityProxy out of functions/methods to top of file (e.g., see OpenIDLogin.render and other code in the same file).

- Typo: s/\(will/will/ within "# Commit because the consumer.complete() call above (will create"

- I asked why we had an explicit transaction commit in OpenIDCallbackView.render. You explained that it was "because the requests will use GET, so the transaction gets rolled back at the end of the request." We agreed that it would be a good idea to make that clear in the comment.

- I noted that I don't like properties that write when you get them, so I don't like OpenIDCallbackView.openid_response as a property. I'd prefer a method that has a name that gives more of a hint that there might be a database write--or, perhaps simpler, you could calculate the response in the __init__ and actually put it on the view class as a normal attribute. Then you could add a comment that this writes to the database in the call in the __init__, the code is even smaller, and it is clear that you will need that value for all code paths (which you do afaict). You agreed, which I think means that you were going to pursue the second option.

- In in no-anonymous-session-cookies.txt, I suggested changing """We want to check whether the browser has a session cookie set, not whether the server has sent a "set-cookie" header, so we have to dig into the underlying mechanize browser (which is done transparently by browser.cookies, though).""" to """Note that we are checking whether the browser has a session cookie set, not whether the server has sent a "set-cookie" header.""" You agreed.

- I noted that I saw four instances of the same test code to log in (e.g., ``browser.getControl(name='field.email').value = ...``). We a...

Read more...

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-18 10:55:29 +0000
3+++ BRANCH.TODO 2010-02-18 10:55:33 +0000
4@@ -3,14 +3,20 @@
5 # stuff still here when you are ready to land, the items should probably
6 # be converted to bugs so they can be scheduled.
7
8-* Do we want to change c-i-p to use a separate cookie so that logging into
9-login.lp.net doesn't cause you to end up logged into lp.net or should we just
10-wait for ISD to roll a rebranded version of login.u.c on login.lp.net, which
11-will probably use separate cookies.
12- - Apart from not being trivial to change the cookie name for a given vhost
13- we also need to worry about making sure the new cookie (which is supposed
14- to be valid only for login.lp.net) is not valid for all other vhosts, which
15- would normally happen thanks to LaunchpadCookieClientIdManager.
16+1. Email Matt describing what I, as a LP user, would need to know about the
17+change to use OpenID.
18+2. Ping Tom when the config change to make the login servers use a different
19+cookie is ready to land.
20+
21+* Need a migration plan, for existing NEWACCOUNT/RESETPASSWORD login
22+tokens that end up used only after we roll out.
23+ * This is specially important if we take into account that we plan to
24+ roll this out together with the main/auth DBs split, meaning it won't
25+ be possible for people to create new accounts and/or reset passwords in
26+ Launchpad itself.
27+
28+* The LoginToken pages for creating new accounts and reset passwords can be
29+removed.
30
31 * We still rely heavily on the Account table and expect all users to have an
32 Account. We need to fix that if we're going to copy the Account table into the
33@@ -18,40 +24,4 @@
34 that, though, the Account table won't be necessary so there'll be no point in
35 copying it. *I'm not sure this is feasible.*
36
37-* As discussed with Francis, it's not worth porting the team-restricted login
38-stuff to the new system, so we'll drop it.
39-
40-* Functionality needed in the callback view:
41- - reactivate accounts when credentials belong to a deactivated profile
42- - create Person entries when the credentials don't exist in our db
43- - forbid to log suspended accounts in. btw, I guess we're going to copy the
44- AccountStatus table to the main db.
45-
46- # To test all this I'll just have to monkey patch
47- # OpenIDCallbackView.openid_response to return my hand-crafted
48- # openid response.
49-
50-* Some tests use anon_browser.open() on protected pages so that they get
51-reidrected to +login and can show how the protected page works when the user
52-is not logged in, including the preserved query string. If we want to keep
53-doing that in these tests, we'll need an OpenID provider accessible to the
54-test suite. One option would be to include a crippled version of the one in
55-c-i-p in our tree, to be used only in tests.
56-(lib/canonical/launchpad/pagetests/oauth/authorize-token.txt is one of the
57-tests that rely on +login)
58-
59-* Does not work for /people/+me, because when the OpenID provider sends the
60-user back to /people/+me/+openid-callback, there's a redirect to
61-/~person/+openid-callback, which causes the openid dance to fail: *
62-<openid.consumer.consumer.FailureResponse id=None message="return_to does not
63-match return URL. Expected 'https://launchpad.dev/%7Ename16/+openid-callback',
64-got
65-u'https://launchpad.dev/people/+me/+openid-callback?janrain_nonce=2009-12-23T14%3A47%3A47ZsgdbOJ'">
66- I think this is a general problem with OpenID and login.lp.net because when
67-you log into login.lp.net and is sent to the callback page (in lp.net), that
68-page will see you as logged in, regardless of the openid response, because the
69-cookie is shared, and that causes /people/+me/+openid-callback to redirect to
70-/~name16/+openid-callback, which causes the error above.
71- One way around this is to always use /+openid-callback as the return_to URL,
72-and include a lp_redirect_to URL in the query string, where LP will send the
73-user to, once the openid dance is completed.
74+* We will need to copy the status from Account back to Person.
75
76=== modified file 'configs/development/launchpad-lazr.conf'
77--- configs/development/launchpad-lazr.conf 2010-02-18 10:55:29 +0000
78+++ configs/development/launchpad-lazr.conf 2010-02-18 10:55:33 +0000
79@@ -125,6 +125,7 @@
80
81 [launchpad]
82 enable_test_openid_provider: True
83+openid_provider_vhost: testopenid
84 code_domain: code.launchpad.dev
85 default_batch_size: 5
86 max_attachment_size: 2097152
87
88=== modified file 'configs/testrunner-appserver/launchpad-lazr.conf'
89--- configs/testrunner-appserver/launchpad-lazr.conf 2009-04-29 19:10:17 +0000
90+++ configs/testrunner-appserver/launchpad-lazr.conf 2010-02-18 10:55:33 +0000
91@@ -43,6 +43,9 @@
92 [vhost.openid]
93 rooturl: http://openid.launchpad.dev:8085/
94
95+[vhost.testopenid]
96+rooturl: http://testopenid.dev:8085/
97+
98 [vhost.shipitubuntu]
99 rooturl: http://shipit.ubuntu.dev:8085/
100
101
102=== modified file 'lib/canonical/config/schema-lazr.conf'
103--- lib/canonical/config/schema-lazr.conf 2010-02-18 10:55:29 +0000
104+++ lib/canonical/config/schema-lazr.conf 2010-02-18 10:55:33 +0000
105@@ -898,6 +898,10 @@
106 # datatype: boolean
107 launch: True
108
109+# The vhost used as the OpenID provider to log into Launchpad.
110+# datatype: string
111+openid_provider_vhost: openid
112+
113 # If true, the main template will be styled so that it is
114 # obvious to the end user that they are using a demo system
115 # and that any changes they make will be lost at some point.
116
117=== modified file 'lib/canonical/launchpad/database/baseopenidstore.py'
118--- lib/canonical/launchpad/database/baseopenidstore.py 2009-06-25 05:30:52 +0000
119+++ lib/canonical/launchpad/database/baseopenidstore.py 2010-02-18 10:55:33 +0000
120@@ -130,7 +130,7 @@
121 return False
122
123 server_url = server_url.decode('UTF-8')
124- salt = server_url.decode('ASCII')
125+ salt = salt.decode('ASCII')
126
127 store = IMasterStore(self.Nonce)
128 old_nonce = store.get(self.Nonce, (server_url, timestamp, salt))
129
130=== modified file 'lib/canonical/launchpad/database/tests/test_baseopenidstore.py'
131--- lib/canonical/launchpad/database/tests/test_baseopenidstore.py 2009-06-25 05:30:52 +0000
132+++ lib/canonical/launchpad/database/tests/test_baseopenidstore.py 2010-02-18 10:55:33 +0000
133@@ -115,6 +115,11 @@
134 # The nonce can only be used once.
135 self.assertEqual(
136 self.store.useNonce('server-url', timestamp, 'salt'), True)
137+ storm_store = IMasterStore(self.store.Nonce)
138+ nonce = storm_store.get(
139+ self.store.Nonce, (u'server-url', timestamp, u'salt'))
140+ self.assertIsNot(None, nonce)
141+
142 self.assertEqual(
143 self.store.useNonce('server-url', timestamp, 'salt'), False)
144 self.assertEqual(
145
146=== modified file 'lib/canonical/launchpad/doc/webapp-publication.txt'
147--- lib/canonical/launchpad/doc/webapp-publication.txt 2009-10-23 16:17:40 +0000
148+++ lib/canonical/launchpad/doc/webapp-publication.txt 2010-02-18 10:55:33 +0000
149@@ -77,6 +77,10 @@
150 rooturl: http://shipit.ubuntu.dev/
151 althosts:
152 ----
153+ testopenid @ testopenid.dev
154+ rooturl: http://testopenid.dev/
155+ althosts:
156+ ----
157 translations @ translations.launchpad.dev
158 rooturl: http://translations.launchpad.dev/
159 althosts:
160@@ -112,6 +116,7 @@
161 shipit.edubuntu.dev
162 shipit.kubuntu.dev
163 shipit.ubuntu.dev
164+ testopenid.dev
165 translations.launchpad.dev
166 ubuntu-openid.launchpad.dev
167 xmlrpc-private.launchpad.dev
168
169=== modified file 'lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt'
170--- lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt 2009-09-18 23:48:15 +0000
171+++ lib/canonical/launchpad/pagetests/basics/demo-and-lpnet.txt 2010-02-18 10:55:33 +0000
172@@ -103,15 +103,6 @@
173 ... 'a', onclick="setBetaRedirect(false)"))
174 Disable edge redirect.
175
176-The disable-redirect link will not appear for locationless pages, such
177-as the login page, or any other view that does not inherit from
178-LaunchpadView and therefore does not provide isBetaUser().
179-
180- >>> beta_browser.open('http://launchpad.dev/+login')
181- >>> print extract_text(find_tags_by_class(
182- ... beta_browser.contents, 'sitemessage')[0])
183- This is a beta site.
184-
185 The disable-redirect link will not appear in the site_message when
186 browsed by non-beta users.
187
188
189=== modified file 'lib/canonical/launchpad/pagetests/oauth/authorize-token.txt'
190--- lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2009-09-09 20:08:36 +0000
191+++ lib/canonical/launchpad/pagetests/oauth/authorize-token.txt 2010-02-18 10:55:33 +0000
192@@ -19,29 +19,21 @@
193 The oauth_token parameter, on the other hand, is required in the
194 Launchpad implementation.
195
196-The +authorize-token page is restricted to logged in users, so users
197-will first be asked to log in.
198+The +authorize-token page is restricted to logged in users, so users will
199+first be asked to log in. (We won't show the actual login process because
200+it involves OpenID, which would complicate this test quite a bit.)
201
202- # Set handleErrors to True so that the Unauthorized exception is handled
203- # by the publisher and we get redirected to the +login page.
204- >>> browser.handleErrors = True
205 >>> from urllib import urlencode
206 >>> params = dict(
207 ... oauth_token=token.key, oauth_callback='http://launchpad.dev/bzr')
208- >>> browser.open(
209- ... "http://launchpad.dev/+authorize-token?%s" % urlencode(params))
210- >>> browser.url
211- 'http://.../+authorize-token/+login?oauth_token=...&oauth_callback=...'
212-
213-And then the user is redirected back to the +authorize-token page.
214-Note how the query parameters are kept.
215-
216- >>> browser.getControl('E-mail address:', index=0).value = (
217- ... 'no-priv@canonical.com')
218- >>> browser.getControl('Password:').value = 'test'
219- >>> browser.getControl(name='loginpage_submit_login').click()
220- >>> browser.url
221- 'http://.../+authorize-token?oauth_token=...&oauth_callback=...'
222+ >>> url = "http://launchpad.dev/+authorize-token?%s" % urlencode(params)
223+ >>> browser.open(url)
224+ Traceback (most recent call last):
225+ ...
226+ Unauthorized:...
227+
228+ >>> browser = setupBrowser(auth='Basic no-priv@canonical.com:test')
229+ >>> browser.open(url)
230 >>> browser.title
231 'Authorize application to access Launchpad on your behalf'
232
233
234=== added file 'lib/canonical/launchpad/templates/login-already.pt'
235--- lib/canonical/launchpad/templates/login-already.pt 1970-01-01 00:00:00 +0000
236+++ lib/canonical/launchpad/templates/login-already.pt 2010-02-18 10:55:33 +0000
237@@ -0,0 +1,18 @@
238+<tal:root
239+ xmlns:tal="http://xml.zope.org/namespaces/tal"
240+ xmlns:metal="http://xml.zope.org/namespaces/metal"
241+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
242+ omit-tag="">
243+<html metal:use-macro="view/macro:page/locationless">
244+ <body>
245+ <div class="top-portlet" metal:fill-slot="main">
246+ <h1>You are already logged in</h1>
247+ <p>
248+ You are already logged in as
249+ <strong tal:content="request/lp:person/displayname">name</strong>.
250+ If this is not you, please <a href="/+logout">log out now</a>.
251+ </p>
252+ </div>
253+ </body>
254+</html>
255+</tal:root>
256
257=== added file 'lib/canonical/launchpad/templates/login-error.pt'
258--- lib/canonical/launchpad/templates/login-error.pt 1970-01-01 00:00:00 +0000
259+++ lib/canonical/launchpad/templates/login-error.pt 2010-02-18 10:55:33 +0000
260@@ -0,0 +1,17 @@
261+<html
262+ xmlns="http://www.w3.org/1999/xhtml"
263+ xmlns:tal="http://xml.zope.org/namespaces/tal"
264+ xmlns:metal="http://xml.zope.org/namespaces/metal"
265+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
266+ metal:use-macro="view/macro:page/locationless"
267+ i18n:domain="launchpad">
268+
269+ <body>
270+ <div class="top-portlet" metal:fill-slot="main">
271+
272+ <h1>Your login was unsuccessful</h1>
273+ <p class="error" tal:content="view/login_error" />
274+
275+ </div>
276+ </body>
277+</html>
278
279=== added file 'lib/canonical/launchpad/templates/login-suspended-account.pt'
280--- lib/canonical/launchpad/templates/login-suspended-account.pt 1970-01-01 00:00:00 +0000
281+++ lib/canonical/launchpad/templates/login-suspended-account.pt 2010-02-18 10:55:33 +0000
282@@ -0,0 +1,20 @@
283+<html
284+ xmlns="http://www.w3.org/1999/xhtml"
285+ xmlns:tal="http://xml.zope.org/namespaces/tal"
286+ xmlns:metal="http://xml.zope.org/namespaces/metal"
287+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
288+ metal:use-macro="view/macro:page/locationless"
289+ i18n:domain="launchpad">
290+
291+ <body>
292+ <div class="top-portlet" metal:fill-slot="main">
293+
294+ <h1>This account has been suspended</h1>
295+ <p class="error">
296+ Contact a <a href="mailto:feedback@launchpad.net?subject=SUSPENDED%20account"
297+ >Launchpad admin</a> about this issue.
298+ </p>
299+
300+ </div>
301+ </body>
302+</html>
303
304=== modified file 'lib/canonical/launchpad/testing/browser.py'
305--- lib/canonical/launchpad/testing/browser.py 2009-06-25 05:30:52 +0000
306+++ lib/canonical/launchpad/testing/browser.py 2010-02-18 10:55:33 +0000
307@@ -30,6 +30,9 @@
308
309 from zope.testbrowser.browser import Browser as _Browser
310
311+from canonical.launchpad.testing.pages import (
312+ extract_text, find_main_content, find_tag_by_id, get_feedback_messages)
313+
314
315 class SocketClosingOnErrorHandler(urllib2.BaseHandler):
316 """A handler that ensures that the socket gets closed on errors.
317@@ -56,10 +59,10 @@
318
319
320 class Browser(_Browser):
321- """A browser subsclass that knows about basic auth."""
322+ """A browser subclass that knows about basic auth."""
323
324- def __init__(self, auth=None):
325- super(Browser, self).__init__()
326+ def __init__(self, auth=None, mech_browser=None):
327+ super(Browser, self).__init__(mech_browser=mech_browser)
328 # We have to add the error handler to the mechanize browser underlying
329 # the Zope browser, because it's the former that's actually doing all
330 # the work.
331@@ -95,6 +98,11 @@
332 def setUp(test):
333 """Set up appserver tests."""
334 test.globs['Browser'] = Browser
335+ test.globs['browser'] = Browser()
336+ test.globs['find_tag_by_id'] = find_tag_by_id
337+ test.globs['find_main_content'] = find_main_content
338+ test.globs['get_feedback_messages'] = get_feedback_messages
339+ test.globs['extract_text'] = extract_text
340
341
342 def tearDown(test):
343
344=== removed file 'lib/canonical/launchpad/testing/cookie.py'
345--- lib/canonical/launchpad/testing/cookie.py 2009-06-25 05:30:52 +0000
346+++ lib/canonical/launchpad/testing/cookie.py 1970-01-01 00:00:00 +0000
347@@ -1,67 +0,0 @@
348-# Copyright 2009 Canonical Ltd. This software is licensed under the
349-# GNU Affero General Public License version 3 (see the file LICENSE).
350-
351-"""Helper to analyze cookies in a zope.testbrowser browser.
352-
353-This API and idea has been accepted into zope.testbrowser, upstream, so this
354-code will be removed from here once that change has landed and we are able to
355-use it. https://bugs.edge.launchpad.net/zope3/+bug/286842
356-"""
357-
358-__metaclass__ = type
359-__all__ = [
360- 'Cookies',
361- ]
362-
363-from UserDict import DictMixin
364-import datetime
365-import pytz
366-
367-class Cookies(DictMixin):
368- """Cookies for testbrowser. Currently does not implement setting.
369- """
370-
371- def __init__(self, testbrowser):
372- self.testbrowser = testbrowser
373-
374- @property
375- def _jar(self):
376- for handler in self.testbrowser.mech_browser.handlers:
377- if getattr(handler, 'cookiejar', None) is not None:
378- return handler.cookiejar
379- raise RuntimeError('no cookiejar found')
380-
381- def _get(self, key):
382- for ck in self._jar:
383- if ck.name == key:
384- return ck
385-
386- def __getitem__(self, key):
387- ck = self._get(key)
388- if ck is None:
389- raise KeyError(key)
390- return ck.value
391-
392- def keys(self):
393- return [ck.name for ck in self._jar]
394-
395- def __contains__(self, key):
396- return self._get(key) is not None
397-
398- def getInfo(self, key):
399- ck = self._get(key)
400- if ck is None:
401- raise KeyError(key)
402- res = {'value': ck.value,
403- 'port': ck.port,
404- 'domain': ck.domain,
405- 'path': ck.path,
406- 'secure': ck.secure,
407- 'expires': None}
408- if ck.expires is not None:
409- res['expires'] = datetime.datetime.fromtimestamp(
410- ck.expires, pytz.UTC)
411- return res
412-
413- def __len__(self):
414- return len(self._jar)
415
416=== modified file 'lib/canonical/launchpad/webapp/login.py'
417--- lib/canonical/launchpad/webapp/login.py 2010-02-09 00:17:40 +0000
418+++ lib/canonical/launchpad/webapp/login.py 2010-02-18 10:55:33 +0000
419@@ -14,21 +14,31 @@
420
421 from BeautifulSoup import UnicodeDammit
422
423-from zope.component import getUtility
424+from openid.consumer.consumer import CANCEL, Consumer, FAILURE, SUCCESS
425+from openid.fetchers import setDefaultFetcher, Urllib2Fetcher
426+
427+import transaction
428+
429+from zope.app.security.interfaces import IUnauthenticatedPrincipal
430+from zope.component import getUtility, getSiteManager
431+from zope.event import notify
432+from zope.interface import Interface
433+from zope.publisher.browser import BrowserPage
434+from zope.publisher.interfaces.http import IHTTPApplicationRequest
435+from zope.security.proxy import removeSecurityProxy
436 from zope.session.interfaces import ISession, IClientIdManager
437-from zope.event import notify
438-from zope.app.security.interfaces import IUnauthenticatedPrincipal
439
440 from z3c.ptcompat import ViewPageTemplateFile
441
442 from canonical.cachedproperty import cachedproperty
443 from canonical.config import config
444 from canonical.launchpad import _
445-from canonical.launchpad.interfaces.account import AccountStatus
446+from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet
447 from canonical.launchpad.interfaces.authtoken import (
448 IAuthTokenSet, LoginTokenType)
449 from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
450 from canonical.launchpad.interfaces.logintoken import ILoginTokenSet
451+from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore
452 from lp.registry.interfaces.person import (
453 IPerson, IPersonSet, PersonCreationRationale)
454 from canonical.launchpad.interfaces.validation import valid_password
455@@ -36,10 +46,12 @@
456 from canonical.launchpad.validators.email import valid_email
457 from canonical.launchpad.webapp.error import SystemErrorView
458 from canonical.launchpad.webapp.interfaces import (
459- CookieAuthLoggedInEvent, ILaunchpadPrincipal, IPlacelessAuthUtility,
460- IPlacelessLoginSource, LoggedOutEvent)
461+ CookieAuthLoggedInEvent, ILaunchpadApplication, ILaunchpadPrincipal,
462+ IPlacelessAuthUtility, IPlacelessLoginSource, LoggedOutEvent)
463 from canonical.launchpad.webapp.metazcml import ILaunchpadPermission
464+from canonical.launchpad.webapp.publisher import LaunchpadView
465 from canonical.launchpad.webapp.url import urlappend
466+from canonical.launchpad.webapp.vhosts import allvhosts
467
468
469 class UnauthorizedView(SystemErrorView):
470@@ -119,14 +131,14 @@
471 return urlappend(current_url, '+login' + query_string)
472
473
474-class BasicLoginPage:
475+class BasicLoginPage(BrowserPage):
476
477 def isSameHost(self, url):
478 """Returns True if the url appears to be from the same host as we are.
479 """
480 return url.startswith(self.request.getApplicationURL())
481
482- def login(self):
483+ def __call__(self):
484 if IUnauthenticatedPrincipal.providedBy(self.request.principal):
485 self.request.principal.__parent__.unauthorized(
486 self.request.principal.id, self.request)
487@@ -139,6 +151,19 @@
488 return ''
489
490
491+def register_basiclogin(event):
492+ # The +basiclogin page should only be enabled for development and tests,
493+ # but we can't rely on config.devmode because it's turned off for
494+ # AppServerLayer tests, so we (ab)use the config switch for the test
495+ # OpenID provider, which has similar requirements.
496+ if config.launchpad.enable_test_openid_provider:
497+ getSiteManager().registerAdapter(
498+ BasicLoginPage,
499+ required=(ILaunchpadApplication, IHTTPApplicationRequest),
500+ provided=Interface,
501+ name='+basiclogin')
502+
503+
504 class RestrictedLoginInfo:
505 """On a team-restricted launchpad server, show who may access the server.
506
507@@ -193,6 +218,160 @@
508 return '%d + %d =' % (op1, op2)
509
510
511+# The Python OpenID package uses pycurl by default, but pycurl chokes on
512+# self-signed certificates (like the ones we use when developing), so we
513+# change the default to urllib2 here. That's also a good thing because it
514+# ensures we test the same thing that we run on production.
515+setDefaultFetcher(Urllib2Fetcher())
516+
517+
518+class OpenIDLogin(LaunchpadView):
519+ """A view which initiates the OpenID handshake with our provider."""
520+ _openid_session_ns = 'OPENID'
521+
522+ def _getConsumer(self):
523+ session = ISession(self.request)[self._openid_session_ns]
524+ openid_store = getUtility(IOpenIDConsumerStore)
525+ return Consumer(session, openid_store)
526+
527+ def render(self):
528+ if self.account is not None:
529+ return AlreadyLoggedInView(self.context, self.request)()
530+
531+ # Allow unauthenticated users to have sessions for the OpenID
532+ # handshake to work.
533+ allowUnauthenticatedSession(self.request)
534+ consumer = self._getConsumer()
535+ openid_vhost = config.launchpad.openid_provider_vhost
536+ self.openid_request = consumer.begin(
537+ allvhosts.configs[openid_vhost].rooturl)
538+
539+ trust_root = self.request.getApplicationURL()
540+ assert not self.openid_request.shouldSendRedirect(), (
541+ "Our fixed OpenID server should not need us to redirect.")
542+ # Once the user authenticates with the OpenID provider they will be
543+ # sent to the /+openid-callback page, where we log them in, but
544+ # once that's done they must be sent back to the URL they were when
545+ # they started the login process (i.e. the current URL without the
546+ # '+login' bit). To do that we encode that URL as a query arg in the
547+ # return_to URL passed to the OpenID Provider
548+ starting_url = urllib.urlencode([('starting_url', self.starting_url)])
549+ return_to = urlappend(
550+ self.request.getApplicationURL(), '+openid-callback')
551+ return_to = "%s?%s" % (return_to, starting_url)
552+ form_html = self.openid_request.htmlMarkup(trust_root, return_to)
553+
554+ # The consumer.begin() call above will insert rows into the
555+ # OpenIDAssociations table, but since this will be a GET request, the
556+ # transaction would be rolled back, so we need an explicit commit
557+ # here.
558+ transaction.commit()
559+
560+ return form_html
561+
562+ @property
563+ def starting_url(self):
564+ starting_url = self.request.getURL(1)
565+ query_string = "&".join([arg for arg in self.form_args])
566+ if query_string:
567+ starting_url += "?%s" % query_string
568+ return starting_url
569+
570+ @property
571+ def form_args(self):
572+ """Iterate over form args, yielding 'key=value' strings for them.
573+
574+ Exclude things such as 'loggingout' and starting with 'openid.', which
575+ we don't want.
576+ """
577+ for name, value in self.request.form.items():
578+ if name == 'loggingout' or name.startswith('openid.'):
579+ continue
580+ if isinstance(value, list):
581+ value_list = value
582+ else:
583+ value_list = [value]
584+ for value_list_item in value_list:
585+ # Thanks to apport (https://launchpad.net/bugs/61171), we need
586+ # to do this here.
587+ value_list_item = UnicodeDammit(value_list_item).markup
588+ yield "%s=%s" % (name, value_list_item)
589+
590+
591+class OpenIDCallbackView(OpenIDLogin):
592+ """The OpenID callback page for logging into Launchpad.
593+
594+ This is the page the OpenID provider will send the user's browser to,
595+ after the user has authenticated on the provider.
596+ """
597+
598+ suspended_account_template = ViewPageTemplateFile(
599+ '../templates/login-suspended-account.pt')
600+
601+ def initialize(self):
602+ self.openid_response = self._getConsumer().complete(
603+ self.request.form, self.request.getURL())
604+
605+ def login(self, account):
606+ loginsource = getUtility(IPlacelessLoginSource)
607+ # We don't have a logged in principal, so we must remove the security
608+ # proxy of the account's preferred email.
609+ email = removeSecurityProxy(account.preferredemail).email
610+ logInPrincipal(
611+ self.request, loginsource.getPrincipalByLogin(email), email)
612+
613+ def render(self):
614+ if self.openid_response.status == SUCCESS:
615+ account = getUtility(IAccountSet).getByOpenIDIdentifier(
616+ self.openid_response.identity_url.split('/')[-1])
617+ if account.status == AccountStatus.SUSPENDED:
618+ return self.suspended_account_template()
619+ if IPerson(account, None) is None:
620+ removeSecurityProxy(account).createPerson(
621+ PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
622+ self.login(account)
623+ target = self.request.form.get('starting_url')
624+ if target is None:
625+ target = self.getApplicationURL()
626+ self.request.response.redirect(target, temporary_if_possible=True)
627+ # No need to return anything as we redirect above.
628+ retval = None
629+ else:
630+ retval = OpenIDLoginErrorView(
631+ self.context, self.request, self.openid_response)()
632+
633+ # The consumer.complete() call above will create entries in
634+ # OpenIDConsumerNonce to prevent replay attacks, but since this will
635+ # be a GET request, the transaction would be rolled back, so we need
636+ # an explicit commit here.
637+ transaction.commit()
638+
639+ return retval
640+
641+
642+class OpenIDLoginErrorView(LaunchpadView):
643+
644+ page_title = 'Error logging in'
645+ template = ViewPageTemplateFile("../templates/login-error.pt")
646+
647+ def __init__(self, context, request, openid_response):
648+ super(OpenIDLoginErrorView, self).__init__(context, request)
649+ assert self.account is None, (
650+ "Don't try to render this page when the user is logged in.")
651+ if openid_response.status == CANCEL:
652+ self.login_error = "User cancelled"
653+ elif openid_response.status == FAILURE:
654+ self.login_error = openid_response.message
655+ else:
656+ self.login_error = "Unknown error: %s" % openid_response
657+
658+
659+class AlreadyLoggedInView(LaunchpadView):
660+
661+ page_title = 'Already logged in'
662+ template = ViewPageTemplateFile("../templates/login-already.pt")
663+
664+
665 class LoginOrRegister(CaptchaMixin):
666 """Merges the former CookieLoginPage and JoinLaunchpadView classes
667 to allow the two forms to appear on a single page.
668@@ -438,6 +617,7 @@
669 L.append(html % (name, cgi.escape(value, quote=True)))
670 return '\n'.join(L)
671
672+
673 def logInPrincipal(request, principal, email):
674 """Log the principal in. Password validation must be done in callsites."""
675 session = ISession(request)
676
677=== modified file 'lib/canonical/launchpad/webapp/session.py'
678--- lib/canonical/launchpad/webapp/session.py 2009-06-25 05:30:52 +0000
679+++ lib/canonical/launchpad/webapp/session.py 2010-02-18 10:55:33 +0000
680@@ -11,8 +11,9 @@
681
682 from storm.zope.interfaces import IZStorm
683
684+from lazr.uri import URI
685+
686 from canonical.config import config
687-from canonical.launchpad.webapp.url import urlparse
688
689
690 SECONDS = 1
691@@ -99,22 +100,19 @@
692 We also log the referrer url on creation of a new
693 requestid so we can track where first time users arrive from.
694 """
695- # XXX: SteveAlexander, 2007-04-01.
696- # This is on the codepath where anon users get a session cookie
697- # set unnecessarily.
698 CookieClientIdManager.setRequestId(self, request, id)
699
700 cookie = request.response.getCookie(self.namespace)
701- protocol, request_domain = urlparse(request.getURL())[:2]
702+ uri = URI(request.getURL())
703
704 # Set secure flag on cookie.
705- if protocol != 'http':
706+ if uri.scheme != 'http':
707 cookie['secure'] = True
708 else:
709 cookie['secure'] = False
710
711 # Set domain attribute on cookie if vhosting requires it.
712- cookie_domain = get_cookie_domain(request_domain)
713+ cookie_domain = get_cookie_domain(uri.host)
714 if cookie_domain is not None:
715 cookie['domain'] = cookie_domain
716
717
718=== renamed file 'lib/canonical/launchpad/pagetests/standalone/xx-cookie-authentication.txt' => 'lib/canonical/launchpad/webapp/tests/cookie-authentication.txt'
719--- lib/canonical/launchpad/pagetests/standalone/xx-cookie-authentication.txt 2008-10-11 00:37:08 +0000
720+++ lib/canonical/launchpad/webapp/tests/cookie-authentication.txt 2010-02-18 10:55:33 +0000
721@@ -2,45 +2,56 @@
722 on http instead of https, it cannot read the secure cookie on https,
723 so it cannot tell that it will end up overwriting the existing cookie.
724
725-
726- >>> from canonical.launchpad.testing.cookie import Cookies
727- >>> cookies = Cookies(browser)
728- >>> browser.open('http://feeds.launchpad.dev/announcements.atom')
729+ >>> browser.open('http://feeds.launchpad.dev:8085/announcements.atom')
730 >>> browser.url
731- 'http://feeds.launchpad.dev/announcements.atom'
732- >>> len(cookies)
733+ 'http://feeds.launchpad.dev:8085/announcements.atom'
734+ >>> len(browser.cookies)
735 0
736
737 Our cookies need to have their domain attribute set to ensure that they
738 are sent to other vhosts in the same domain.
739
740- >>> browser.open('http://blueprints.launchpad.dev/+login')
741- >>> browser.getControl('E-mail', index=0).value = 'foo.bar@canonical.com'
742- >>> browser.getControl('Password').value = 'test'
743- >>> browser.getControl('Log In').click()
744+ >>> browser.open('http://blueprints.launchpad.dev:8085/+login')
745+
746+ # On a browser with JS support, this page would've been automatically
747+ # submitted (thanks to the onload handler), but testbrowser doesn't support
748+ # JS, so we have to submit the form manually.
749+ >>> print browser.contents
750+ <html>...<body onload="document.forms[0].submit();"...
751+ >>> browser.getControl('Continue').click()
752+
753+ >>> from canonical.launchpad.webapp.tests.test_new_login import (
754+ ... fill_login_form_and_submit)
755+ >>> fill_login_form_and_submit(browser, 'foo.bar@canonical.com', 'test')
756 >>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
757 Foo Bar...
758- >>> len(cookies)
759+
760+ # Open a page again so that we see the cookie for a launchpad.dev request
761+ # and not a testopenid.dev request (as above).
762+ >>> browser.open('http://blueprints.launchpad.dev:8085')
763+ >>> len(browser.cookies)
764 1
765- >>> session_cookie_name = cookies.keys()[0]
766- >>> cookies.getInfo(session_cookie_name)['domain']
767+ >>> browser.cookies.keys()
768+ ['launchpad_tests']
769+ >>> session_cookie_name = browser.cookies.keys()[0]
770+ >>> browser.cookies.getinfo(session_cookie_name)['domain']
771 '.launchpad.dev'
772
773 If we visit another vhost in the domain, we remain logged in.
774
775- >>> browser.open('http://launchpad.dev/')
776+ >>> browser.open('http://launchpad.dev:8085/')
777 >>> browser.url
778- 'http://launchpad.dev/'
779+ 'http://launchpad.dev:8085/'
780 >>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
781 Foo Bar...
782- >>> cookies.getInfo(session_cookie_name)['domain']
783+ >>> browser.cookies.getinfo(session_cookie_name)['domain']
784 '.launchpad.dev'
785
786 Even if the browser passes in a cookie, the feeds vhost should not set one.
787
788- >>> browser.open('http://feeds.launchpad.dev/announcements.atom')
789+ >>> browser.open('http://feeds.launchpad.dev:8085/announcements.atom')
790 >>> browser.url
791- 'http://feeds.launchpad.dev/announcements.atom'
792+ 'http://feeds.launchpad.dev:8085/announcements.atom'
793 >>> print browser.headers.get('Set-Cookie')
794 None
795
796
797=== added file 'lib/canonical/launchpad/webapp/tests/login.txt'
798--- lib/canonical/launchpad/webapp/tests/login.txt 1970-01-01 00:00:00 +0000
799+++ lib/canonical/launchpad/webapp/tests/login.txt 2010-02-18 10:55:33 +0000
800@@ -0,0 +1,47 @@
801+======================
802+Logging into Launchpad
803+======================
804+
805+Launchpad is an OpenID Relying Party that uses the Login Service as its fixed
806+OpenID Provider. Because of that, when a user clicks the 'Log in / Register'
807+link, they'll be sent to the OP to authenticate.
808+
809+ # Set handleErrors to True so that the Unauthorized exception is handled
810+ # by the publisher and we get redirected to the +login page.
811+ >>> browser = Browser()
812+ >>> browser.handleErrors = True
813+ >>> browser.open(
814+ ... 'http://launchpad.dev:8085/people/?name=foo&searchfor=all')
815+ >>> browser.getLink('Log in / Register').click()
816+
817+ # On a browser with JS support, this page would've been automatically
818+ # submitted (thanks to the onload handler), but testbrowser doesn't support
819+ # JS, so we have to submit the form manually.
820+ >>> print browser.contents
821+ <html>...<body onload="document.forms[0].submit();"...
822+ >>> browser.getControl('Continue').click()
823+
824+The OpenID Provider will ask us to authenticate.
825+
826+ >>> print browser.title
827+ Login
828+ >>> from canonical.launchpad.webapp.tests.test_new_login import (
829+ ... fill_login_form_and_submit)
830+ >>> fill_login_form_and_submit(browser, 'test@canonical.com', 'test')
831+
832+Once authenticated, we're redirected back to the page where we started, with
833+the query args preserved.
834+
835+ >>> url = browser.url
836+ >>> print url
837+ http://launchpad.dev:8085/people?...
838+ >>> import re
839+ >>> print sorted(re.sub('.*\?', '', url).split('&'))
840+ ['name=foo', 'searchfor=all']
841+
842+If we load the +login page while already logged in, it will say we're already
843+logged in and ask us to log out if we're somebody else.
844+
845+ >>> browser.open('http://launchpad.dev:8085/+login')
846+ >>> print extract_text(find_main_content(browser.contents))
847+ You are already logged in...
848
849=== renamed file 'lib/canonical/launchpad/pagetests/standalone/xx-no-anonymous-session-cookies.txt' => 'lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt'
850--- lib/canonical/launchpad/pagetests/standalone/xx-no-anonymous-session-cookies.txt 2008-10-11 00:37:08 +0000
851+++ lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt 2010-02-18 10:55:33 +0000
852@@ -1,20 +1,12 @@
853 We will verify that we do not put session cookies in anonymous requests. This
854 is important for cacheing anonymous requests in front of Zope, such as with
855-Squid.
856-
857-Because testbrowser does not have easy access to cookies, we'll start with a
858-helper class. We want to check whether the browser has a session cookie set,
859-not whether the server has sent a "set-cookie" header, so we have to dig into
860-the underlying mechanize browser.
861-
862- >>> from canonical.launchpad.testing.cookie import Cookies
863- >>> cookies = Cookies(browser)
864-
865-Now we'll actually begin the demonstration. When we go to launchpad as an
866-anonymous user, the browser has no cookies.
867-
868- >>> browser.open('http://launchpad.dev')
869- >>> len(cookies)
870+Squid. Note that we are checking whether the browser has a session cookie
871+set, not whether the server has sent a "set-cookie" header.
872+
873+When we go to launchpad as an anonymous user, the browser has no cookies.
874+
875+ >>> browser.open('http://launchpad.dev:8085')
876+ >>> len(browser.cookies)
877 0
878
879 Now let's log in and show that the session cookie is set.
880@@ -24,20 +16,33 @@
881 >>> now = datetime.datetime.now(pytz.UTC).replace(microsecond=0)
882 >>> year_from_now = now + datetime.timedelta(days=365)
883 >>> year_plus_from_now = year_from_now + datetime.timedelta(minutes=1)
884- >>> browser.open('http://launchpad.dev/+login')
885- >>> browser.getControl('E-mail', index=0).value = 'foo.bar@canonical.com'
886- >>> browser.getControl('Password').value = 'test'
887- >>> browser.getControl('Log In').click()
888+ >>> browser.open('http://launchpad.dev:8085/+login')
889+
890+ # On a browser with JS support, this page would've been automatically
891+ # submitted (thanks to the onload handler), but testbrowser doesn't support
892+ # JS, so we have to submit the form manually.
893+ >>> print browser.contents
894+ <html>...<body onload="document.forms[0].submit();"...
895+ >>> browser.getControl('Continue').click()
896+
897+ >>> from canonical.launchpad.webapp.tests.test_new_login import (
898+ ... fill_login_form_and_submit)
899+ >>> fill_login_form_and_submit(browser, 'foo.bar@canonical.com', 'test')
900 >>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
901 Foo Bar...
902- >>> len(cookies)
903+
904+ # Open a page again so that we see the cookie for a launchpad.dev request
905+ # and not a testopenid.dev request (as above).
906+ >>> browser.open('http://launchpad.dev:8085')
907+
908+ >>> len(browser.cookies)
909 1
910- >>> cookies.keys()
911+ >>> browser.cookies.keys()
912 ['launchpad_tests']
913- >>> expires = cookies.getInfo('launchpad_tests')['expires']
914+ >>> expires = browser.cookies.getinfo('launchpad_tests')['expires']
915 >>> year_from_now <= expires < year_plus_from_now
916 True
917- >>> cookies.getInfo('launchpad_tests')['domain']
918+ >>> browser.cookies.getinfo('launchpad_tests')['domain']
919 '.launchpad.dev'
920
921 The cookie will be set to expire in ten minutes when you log out. The ten
922@@ -46,9 +51,9 @@
923 time for browsers with bad system clocks.
924
925 >>> browser.getControl('Log Out').click()
926- >>> len(cookies)
927+ >>> len(browser.cookies)
928 1
929- >>> expires = cookies.getInfo('launchpad_tests')['expires']
930+ >>> expires = browser.cookies.getinfo('launchpad_tests')['expires']
931 >>> ten_minutes_from_now = now + datetime.timedelta(minutes=10)
932 >>> eleven_minutes_from_now = now + datetime.timedelta(minutes=11)
933 >>> ten_minutes_from_now <= expires < eleven_minutes_from_now
934
935=== added file 'lib/canonical/launchpad/webapp/tests/test_cookie_authentication.py'
936--- lib/canonical/launchpad/webapp/tests/test_cookie_authentication.py 1970-01-01 00:00:00 +0000
937+++ lib/canonical/launchpad/webapp/tests/test_cookie_authentication.py 2010-02-18 10:55:33 +0000
938@@ -0,0 +1,24 @@
939+# Copyright 2010 Canonical Ltd. All rights reserved.
940+
941+"""Test harness for running the cookie-authentication.txt tests."""
942+
943+__metaclass__ = type
944+
945+__all__ = []
946+
947+import unittest
948+
949+from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite
950+from canonical.launchpad.testing.browser import setUp, tearDown
951+from canonical.testing.layers import AppServerLayer
952+
953+
954+def test_suite():
955+ suite = unittest.TestSuite()
956+ # We run this test on the AppServerLayer because it needs the cookie login
957+ # page (+login), which cannot be used through the normal testbrowser that
958+ # goes straight to zope's publication instead of making HTTP requests.
959+ suite.addTest(LayeredDocFileSuite(
960+ 'cookie-authentication.txt', setUp=setUp, tearDown=tearDown,
961+ layer=AppServerLayer))
962+ return suite
963
964=== added file 'lib/canonical/launchpad/webapp/tests/test_new_login.py'
965--- lib/canonical/launchpad/webapp/tests/test_new_login.py 1970-01-01 00:00:00 +0000
966+++ lib/canonical/launchpad/webapp/tests/test_new_login.py 2010-02-18 10:55:33 +0000
967@@ -0,0 +1,188 @@
968+# Copyright 2009 Canonical Ltd. All rights reserved.
969+
970+"""Test harness for running the new-login.txt tests."""
971+
972+__metaclass__ = type
973+
974+__all__ = []
975+
976+import httplib
977+import unittest
978+
979+import mechanize
980+
981+from openid.consumer.consumer import FAILURE, SUCCESS
982+
983+from canonical.launchpad.interfaces.account import AccountStatus
984+from canonical.launchpad.testing.pages import (
985+ extract_text, find_main_content, find_tag_by_id, find_tags_by_class)
986+from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite
987+from canonical.launchpad.testing.browser import Browser, setUp, tearDown
988+from canonical.launchpad.webapp.login import OpenIDCallbackView
989+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
990+from canonical.testing.layers import AppServerLayer, DatabaseFunctionalLayer
991+
992+from lp.registry.interfaces.person import IPerson
993+from lp.testopenid.interfaces.server import ITestOpenIDPersistentIdentity
994+from lp.testing import TestCaseWithFactory
995+
996+
997+class FakeOpenIDResponse:
998+
999+ def __init__(self, identity_url, status=SUCCESS, message=''):
1000+ self.message = message
1001+ self.status = status
1002+ self.identity_url = identity_url
1003+
1004+
1005+class StubbedOpenIDCallbackView(OpenIDCallbackView):
1006+ login_called = False
1007+
1008+ def login(self, account):
1009+ self.login_called = True
1010+
1011+
1012+class TestOpenIDCallbackView(TestCaseWithFactory):
1013+ layer = DatabaseFunctionalLayer
1014+
1015+ def _createView(self, account, response_status=SUCCESS, response_msg=''):
1016+ request = LaunchpadTestRequest(
1017+ form={'starting_url': 'http://launchpad.dev/after-login'},
1018+ environ={'PATH_INFO': '/'})
1019+ view = StubbedOpenIDCallbackView(object(), request)
1020+ view.initialize()
1021+ view.openid_response = FakeOpenIDResponse(
1022+ ITestOpenIDPersistentIdentity(account).openid_identity_url,
1023+ status=response_status, message=response_msg)
1024+ return view
1025+
1026+ def test_full_fledged_account(self):
1027+ # In the common case we just login and redirect to the URL specified
1028+ # in the 'starting_url' query arg.
1029+ person = self.factory.makePerson()
1030+ view = self._createView(person.account)
1031+ view.render()
1032+ self.assertTrue(view.login_called)
1033+ response = view.request.response
1034+ self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())
1035+ self.assertEquals(view.request.form['starting_url'],
1036+ response.getHeader('Location'))
1037+
1038+ def test_personless_account(self):
1039+ # When there is no Person record associated with the account, we
1040+ # create one.
1041+ account = self.factory.makeAccount('Test account')
1042+ self.assertIs(None, IPerson(account, None))
1043+ view = self._createView(account)
1044+ view.render()
1045+ self.assertIsNot(None, IPerson(account, None))
1046+ self.assertTrue(view.login_called)
1047+ response = view.request.response
1048+ self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())
1049+ self.assertEquals(view.request.form['starting_url'],
1050+ response.getHeader('Location'))
1051+
1052+ def test_deactivated_account(self):
1053+ # The user has the account's password and is trying to login, so we'll
1054+ # just re-activate their account.
1055+ account = self.factory.makeAccount(
1056+ 'Test account', status=AccountStatus.DEACTIVATED)
1057+ self.assertIs(None, IPerson(account, None))
1058+ view = self._createView(account)
1059+ view.render()
1060+ self.assertIsNot(None, IPerson(account, None))
1061+ self.assertTrue(view.login_called)
1062+ response = view.request.response
1063+ self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())
1064+ self.assertEquals(view.request.form['starting_url'],
1065+ response.getHeader('Location'))
1066+
1067+ def test_suspended_account(self):
1068+ # There's a chance that our OpenID Provider lets a suspended account
1069+ # login, but we must not allow that.
1070+ account = self.factory.makeAccount(
1071+ 'Test account', status=AccountStatus.SUSPENDED)
1072+ view = self._createView(account)
1073+ html = view.render()
1074+ self.assertFalse(view.login_called)
1075+ main_content = extract_text(find_main_content(html))
1076+ self.assertIn('This account has been suspended', main_content)
1077+
1078+ def test_negative_openid_assertion(self):
1079+ # The OpenID provider responded with a negative assertion, so the
1080+ # login error page is shown.
1081+ account = self.factory.makeAccount('Test account')
1082+ view = self._createView(
1083+ account, response_status=FAILURE,
1084+ response_msg='Server denied check_authentication')
1085+ html = view.render()
1086+ self.assertFalse(view.login_called)
1087+ main_content = extract_text(find_main_content(html))
1088+ self.assertIn('Your login was unsuccessful', main_content)
1089+
1090+
1091+urls_redirected_to = []
1092+
1093+
1094+class MyHTTPRedirectHandler(mechanize.HTTPRedirectHandler):
1095+ """Custom HTTPRedirectHandler which stores the URLs redirected to."""
1096+
1097+ def redirect_request(self, newurl, req, fp, code, msg, headers):
1098+ urls_redirected_to.append(newurl)
1099+ return mechanize.HTTPRedirectHandler.redirect_request(
1100+ self, newurl, req, fp, code, msg, headers)
1101+
1102+
1103+class MyMechanizeBrowser(mechanize.Browser):
1104+ """Custom Browser which uses MyHTTPRedirectHandler to handle redirects."""
1105+ handler_classes = mechanize.Browser.handler_classes.copy()
1106+ handler_classes['_redirect'] = MyHTTPRedirectHandler
1107+
1108+
1109+class TestOpenIDReplayAttack(TestCaseWithFactory):
1110+ layer = AppServerLayer
1111+
1112+ def test_replay_attacks_do_not_succeed(self):
1113+ browser = Browser(mech_browser=MyMechanizeBrowser())
1114+ browser.open('http://launchpad.dev:8085/+login')
1115+ # On a JS-enabled browser this page would've been auto-submitted
1116+ # (thanks to the onload handler), but here we have to do it manually.
1117+ self.assertIn('body onload', browser.contents)
1118+ browser.getControl('Continue').click()
1119+
1120+ self.assertEquals('Login', browser.title)
1121+ fill_login_form_and_submit(browser, 'test@canonical.com', 'test')
1122+ login_status = extract_text(
1123+ find_tag_by_id(browser.contents, 'logincontrol'))
1124+ self.assertIn('Sample Person', login_status)
1125+
1126+ # Now we look up (in urls_redirected_to) the +openid-callback URL that
1127+ # was used to complete the authentication and open it on a different
1128+ # browser with a fresh set of cookies.
1129+ replay_browser = Browser()
1130+ [callback_url] = [
1131+ url for url in urls_redirected_to if '+openid-callback' in url]
1132+ self.assertIsNot(None, callback_url)
1133+ replay_browser.open(callback_url)
1134+ login_status = extract_text(
1135+ find_tag_by_id(replay_browser.contents, 'logincontrol'))
1136+ self.assertEquals('Log in / Register', login_status)
1137+ error_msg = find_tags_by_class(replay_browser.contents, 'error')[0]
1138+ self.assertEquals('Nonce already used or out of range',
1139+ extract_text(error_msg))
1140+
1141+
1142+def fill_login_form_and_submit(browser, email_address, password):
1143+ assert browser.getControl(name='field.email') is not None, (
1144+ "We don't seem to be looking at a login form.")
1145+ browser.getControl(name='field.email').value = email_address
1146+ browser.getControl(name='field.password').value = password
1147+ browser.getControl('Continue').click()
1148+
1149+
1150+def test_suite():
1151+ suite = unittest.TestSuite()
1152+ suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
1153+ suite.addTest(LayeredDocFileSuite(
1154+ 'login.txt', setUp=setUp, tearDown=tearDown, layer=AppServerLayer))
1155+ return suite
1156
1157=== added file 'lib/canonical/launchpad/webapp/tests/test_no_anonymous_session_cookies.py'
1158--- lib/canonical/launchpad/webapp/tests/test_no_anonymous_session_cookies.py 1970-01-01 00:00:00 +0000
1159+++ lib/canonical/launchpad/webapp/tests/test_no_anonymous_session_cookies.py 2010-02-18 10:55:33 +0000
1160@@ -0,0 +1,24 @@
1161+# Copyright 2010 Canonical Ltd. All rights reserved.
1162+
1163+"""Test harness for running the no-anonymous-session-cookies.txt tests."""
1164+
1165+__metaclass__ = type
1166+
1167+__all__ = []
1168+
1169+import unittest
1170+
1171+from canonical.launchpad.testing.systemdocs import LayeredDocFileSuite
1172+from canonical.launchpad.testing.browser import setUp, tearDown
1173+from canonical.testing.layers import AppServerLayer
1174+
1175+
1176+def test_suite():
1177+ suite = unittest.TestSuite()
1178+ # We run this test on the AppServerLayer because it needs the cookie login
1179+ # page (+login), which cannot be used through the normal testbrowser that
1180+ # goes straight to zope's publication instead of making HTTP requests.
1181+ suite.addTest(LayeredDocFileSuite(
1182+ 'no-anonymous-session-cookies.txt', setUp=setUp, tearDown=tearDown,
1183+ layer=AppServerLayer))
1184+ return suite
1185
1186=== modified file 'lib/canonical/launchpad/windmill/testing/lpuser.py'
1187--- lib/canonical/launchpad/windmill/testing/lpuser.py 2010-02-02 11:36:08 +0000
1188+++ lib/canonical/launchpad/windmill/testing/lpuser.py 2010-02-18 10:55:33 +0000
1189@@ -6,6 +6,8 @@
1190 __metaclass__ = type
1191 __all__ = []
1192
1193+import windmill
1194+
1195 from canonical.launchpad.windmill.testing import constants
1196
1197
1198@@ -20,31 +22,22 @@
1199 def ensure_login(self, client):
1200 """Ensure that this user is logged on the page under windmill."""
1201 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
1202- result = client.asserts.assertNode(
1203- name=u'loginpage_submit_login', assertion=False)
1204- already_on_login_page = result['result']
1205- if not already_on_login_page:
1206- result = client.asserts.assertNode(
1207- link=u'Log in / Register', assertion=False)
1208- if not result['result']:
1209- # User is probably logged in.
1210- # Check under which name they are logged in.
1211- result = client.commands.execJS(
1212- code="""lookupNode({xpath: '//div[@id="logincontrol"]//a'}).text""")
1213- if (result['result'] is not None and
1214- result['result'].strip() == self.display_name):
1215- # We are logged as that user.
1216- return
1217- client.click(name="logout")
1218- client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
1219- client.waits.forElement(
1220- link=u'Log in / Register', timeout=constants.FOR_ELEMENT)
1221- client.click(link=u'Log in / Register')
1222+ lookup_user = (
1223+ """lookupNode({xpath: '//div[@id="logincontrol"]//a'}).text""")
1224+ result = client.commands.execJS(code=lookup_user)
1225+ if (result['result'] is not None and
1226+ result['result'].strip() == self.display_name):
1227+ # We are logged in as that user already.
1228+ return
1229+
1230+ current_url = client.commands.execJS(
1231+ code='windmill.testWin().location;')['result']['href']
1232+ base_url = windmill.settings['TEST_URL']
1233+ basic_auth_url = base_url.replace('http://', 'http://%s:%s@')
1234+ basic_auth_url = basic_auth_url + '+basiclogin'
1235+ client.open(url=basic_auth_url % (self.email, self.password))
1236 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
1237- client.waits.forElement(timeout=constants.FOR_ELEMENT, id=u'email')
1238- client.type(text=self.email, id=u'email')
1239- client.type(text=self.password, id=u'password')
1240- client.click(name=u'loginpage_submit_login')
1241+ client.open(url=current_url)
1242 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
1243
1244
1245@@ -58,8 +51,13 @@
1246 link=u'Log in / Register', assertion=False)
1247 if result['result']:
1248 return
1249- client.waits.forElement(name="logout", timeout=constants.FOR_ELEMENT)
1250- client.click(name="logout")
1251+
1252+ # Open a page with invalid HTTP Basic Auth credentials just to
1253+ # invalidate the ones previously used.
1254+ current_url = client.commands.execJS(
1255+ code='windmill.testWin().location;')['result']['href']
1256+ current_url = current_url.replace('http://', 'http://foo:foo@')
1257+ client.open(url=current_url)
1258 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
1259
1260
1261
1262=== modified file 'lib/canonical/launchpad/windmill/testing/widgets.py'
1263--- lib/canonical/launchpad/windmill/testing/widgets.py 2009-12-11 19:54:04 +0000
1264+++ lib/canonical/launchpad/windmill/testing/widgets.py 2010-02-18 10:55:33 +0000
1265@@ -60,10 +60,9 @@
1266 * reloads and verifies that the new value sticked.
1267 """
1268 client = WindmillTestClient(self.suite)
1269+ self.user.ensure_login(client)
1270 client.open(url=self.url)
1271
1272- self.user.ensure_login(client)
1273-
1274 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
1275 widget_base = u"//%s[@id='%s']" % (self.widget_tag, self.widget_id)
1276 client.waits.forElement(
1277@@ -298,12 +297,11 @@
1278
1279 def __call__(self):
1280 client = WindmillTestClient(self.suite)
1281+ self.user.ensure_login(client)
1282
1283 # Load page.
1284 client.open(url=self.url)
1285
1286- self.user.ensure_login(client)
1287-
1288 # Click on "Choose" link to show picker for the given field.
1289 client.waits.forElement(
1290 id=self.choose_link_id, timeout=constants.PAGE_LOAD)
1291
1292=== modified file 'lib/canonical/launchpad/zcml/launchpad.zcml'
1293--- lib/canonical/launchpad/zcml/launchpad.zcml 2010-01-12 22:07:13 +0000
1294+++ lib/canonical/launchpad/zcml/launchpad.zcml 2010-02-18 10:55:33 +0000
1295@@ -175,14 +175,40 @@
1296 permission="zope.Public"
1297 />
1298
1299- <!-- Login and logout pages, and login status. -->
1300+
1301+ <!-- The +basiclogin view is registered using Python code so that we can do
1302+ it only for development and tests. Below is what its declaration would
1303+ look like, and it's here so that someone grepping zcml files for its
1304+ name will find it.
1305 <browser:page
1306 for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication"
1307 class="canonical.launchpad.webapp.login.BasicLoginPage"
1308 name="+basiclogin"
1309- permission="zope.Public"
1310- attribute="login"
1311- layer="canonical.launchpad.layers.DebugLayer"
1312+ permission="zope.Public" />
1313+ -->
1314+
1315+ <class class="canonical.launchpad.webapp.login.BasicLoginPage">
1316+ <allow attributes="__call__" />
1317+ <allow interface="zope.publisher.interfaces.browser.IBrowserPublisher" />
1318+ </class>
1319+
1320+ <subscriber
1321+ for="zope.processlifetime.ProcessStarting"
1322+ handler="canonical.launchpad.webapp.login.register_basiclogin"
1323+ />
1324+
1325+ <!-- OpenID RP views -->
1326+ <browser:page
1327+ for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication"
1328+ class="canonical.launchpad.webapp.login.OpenIDLogin"
1329+ permission="zope.Public"
1330+ name="+login"
1331+ />
1332+ <browser:page
1333+ for="canonical.launchpad.webapp.interfaces.ILaunchpadApplication"
1334+ class="canonical.launchpad.webapp.login.OpenIDCallbackView"
1335+ permission="zope.Public"
1336+ name="+openid-callback"
1337 />
1338
1339 <browser:page
1340@@ -190,7 +216,7 @@
1341 template="../templates/launchpad-login.pt"
1342 class="canonical.launchpad.webapp.login.LoginOrRegister"
1343 permission="zope.Public"
1344- name="+login"
1345+ name="+login-old"
1346 />
1347
1348 <browser:page
1349
1350=== modified file 'lib/canonical/testing/layers.py'
1351--- lib/canonical/testing/layers.py 2010-02-05 12:16:34 +0000
1352+++ lib/canonical/testing/layers.py 2010-02-18 10:55:33 +0000
1353@@ -1880,6 +1880,7 @@
1354 'rooturl: http://blueprints.launchpad.dev:8085/'),
1355 ('vhost.bugs', 'rooturl: http://bugs.launchpad.dev:8085/'),
1356 ('vhost.code', 'rooturl: http://code.launchpad.dev:8085/'),
1357+ ('vhost.testopenid', 'rooturl: http://testopenid.dev:8085/'),
1358 ('vhost.translations',
1359 'rooturl: http://translations.launchpad.dev:8085/'))
1360 for site in sites:
1361
1362=== modified file 'lib/devscripts/ec2test/instance.py'
1363--- lib/devscripts/ec2test/instance.py 2010-02-09 14:20:54 +0000
1364+++ lib/devscripts/ec2test/instance.py 2010-02-18 10:55:33 +0000
1365@@ -105,7 +105,7 @@
1366 shipit.edubuntu.dev
1367 shipit.kubuntu.dev
1368 shipit.ubuntu.dev
1369- testopenid.launchpad.dev
1370+ testopenid.dev
1371 translations.launchpad.dev
1372 xmlrpc-private.launchpad.dev
1373 xmlrpc.launchpad.dev
1374
1375=== modified file 'lib/lp/app/stories/basics/xx-beta-testers-redirection.txt'
1376--- lib/lp/app/stories/basics/xx-beta-testers-redirection.txt 2010-01-08 20:57:08 +0000
1377+++ lib/lp/app/stories/basics/xx-beta-testers-redirection.txt 2010-02-18 10:55:33 +0000
1378@@ -157,29 +157,6 @@
1379 >>> print beta_browser.url
1380 http://launchpad.dev/ubuntu
1381
1382-If a beta tester comes to Launchpad but is not yet logged in, they
1383-will be redirected once they do log in. First we'll go to the Ubuntu
1384-product page and try to log in. We get redirected back to the Ubuntu
1385-page after authenticating successfully:
1386-
1387- >>> browser.mech_browser.set_handle_redirect(False)
1388- >>> browser.open('http://launchpad.dev/ubuntu')
1389- >>> browser.getLink('Log in / Register').click()
1390- >>> print browser.url
1391- http://launchpad.dev/ubuntu/+login
1392- >>> browser.getControl('E-mail address',
1393- ... index=0).value = 'beta-admin@launchpad.net'
1394- >>> browser.getControl('Password').value = 'test'
1395- >>> check(browser.getControl('Log In').click)
1396- HTTP Error 303: See Other
1397- Location: http://launchpad.dev/ubuntu
1398-
1399-The Ubuntu product page then redirects to the beta site:
1400-
1401- >>> check(browser.open, 'http://launchpad.dev/ubuntu')
1402- HTTP Error 303: See Other
1403- Location: http://beta.launchpad.dev/ubuntu
1404-
1405
1406 == Shortcut redirection for bugs ==
1407
1408
1409=== modified file 'lib/lp/app/stories/launchpad-root/site-search.txt'
1410--- lib/lp/app/stories/launchpad-root/site-search.txt 2009-11-22 15:51:50 +0000
1411+++ lib/lp/app/stories/launchpad-root/site-search.txt 2010-02-18 10:55:33 +0000
1412@@ -39,15 +39,6 @@
1413 ...
1414 LookupError
1415
1416-And on the login and registration pages, the global form is also omitted to
1417-reduce confusion.
1418-
1419- >>> anon_browser.open('http://launchpad.dev/+login')
1420- >>> global_search_form = anon_browser.getForm('globalsearch')
1421- Traceback (most recent call last):
1422- ...
1423- LookupError
1424-
1425 If by chance someone ends up at /+search with no search parameters, they get
1426 an explanation of the search function.
1427
1428
1429=== modified file 'lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt'
1430--- lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt 2009-10-22 13:02:12 +0000
1431+++ lib/lp/bugs/stories/bugtracker/xx-bugtracker-remote-bug.txt 2010-02-18 10:55:33 +0000
1432@@ -92,13 +92,14 @@
1433 Bug #2: Blackhole Trash folder
1434
1435 For the case where the private bug is the only one watching the given
1436-remote bug, we don't perform the redirect ahead of time:
1437+remote bug, we don't perform the redirect ahead of time (i.e. before the
1438+user logs in):
1439
1440- >>> anon_browser.handleErrors = True
1441 >>> anon_browser.open(
1442 ... 'http://bugs.launchpad.dev/bugs/bugtrackers/mozilla.org/2000')
1443- >>> print_feedback_messages(anon_browser.contents)
1444- To continue, you must log in to Launchpad.
1445+ Traceback (most recent call last):
1446+ ...
1447+ Unauthorized:...
1448
1449 Set the bug back to public:
1450
1451
1452=== modified file 'lib/lp/bugs/stories/upstream-bugprivacy/10-file-private-upstream-bug.txt'
1453--- lib/lp/bugs/stories/upstream-bugprivacy/10-file-private-upstream-bug.txt 2009-08-14 18:02:29 +0000
1454+++ lib/lp/bugs/stories/upstream-bugprivacy/10-file-private-upstream-bug.txt 2010-02-18 10:55:33 +0000
1455@@ -109,21 +109,18 @@
1456 redirects the anonymous user to the login page.
1457
1458 >>> browser = setupBrowser()
1459- >>> browser.handleErrors = True
1460 >>> browser.open(
1461 ... "http://launchpad.dev/firefox/+bug/%s/+editstatus" % bug_id)
1462-
1463-XXX: Brad Bollenbach, 2005-09-13: This redirect is going to the wrong
1464-URL. See https://launchpad.net/malone/bugs/2265.
1465-
1466- >>> print browser.url
1467- http://launchpad.dev/firefox/+bug/.../+editstatus/+login
1468+ Traceback (most recent call last):
1469+ ...
1470+ Unauthorized:...
1471
1472 The no-privs user cannot access bug #10, because it's filed on a private bug on
1473 which the no-privs is not an explicit subscriber.
1474
1475 >>> browser = setupBrowser(auth="Basic no-priv@canonical.com:test")
1476- >>> browser.open("http://launchpad.dev/firefox/+bug/%s/+editstatus" % bug_id)
1477+ >>> browser.open(
1478+ ... "http://launchpad.dev/firefox/+bug/%s/+editstatus" % bug_id)
1479 Traceback (most recent call last):
1480 ...
1481 Unauthorized:...
1482
1483=== modified file 'lib/lp/bugs/windmill/tests/test_bug_tags_entry.py'
1484--- lib/lp/bugs/windmill/tests/test_bug_tags_entry.py 2010-02-01 18:37:00 +0000
1485+++ lib/lp/bugs/windmill/tests/test_bug_tags_entry.py 2010-02-18 10:55:33 +0000
1486@@ -70,7 +70,7 @@
1487 client.click(id=u'edit-tags-trigger')
1488 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
1489 client.asserts.assertJS(
1490- js=u'window.location.href.indexOf("+login") > 0')
1491+ js=u'window.location.href.indexOf("+openid") > 0')
1492
1493
1494 def test_suite():
1495
1496=== modified file 'lib/lp/bugs/windmill/tests/test_filebug_dupe_finder.py'
1497--- lib/lp/bugs/windmill/tests/test_filebug_dupe_finder.py 2010-02-02 11:26:31 +0000
1498+++ lib/lp/bugs/windmill/tests/test_filebug_dupe_finder.py 2010-02-18 10:55:33 +0000
1499@@ -39,11 +39,11 @@
1500 more information if they wish.
1501 """
1502 client = self.client
1503+ lpuser.SAMPLE_PERSON.ensure_login(client)
1504
1505 # Go to the +filebug page for Firefox
1506 client.open(url=FILEBUG_URL)
1507 client.waits.forPageLoad(timeout=constants.PAGE_LOAD)
1508- lpuser.SAMPLE_PERSON.ensure_login(client)
1509
1510 # Ensure the "search" field has finished loading, then enter a simple
1511 # search and hit search.
1512
1513=== modified file 'lib/lp/code/stories/branches/xx-register-a-branch.txt'
1514--- lib/lp/code/stories/branches/xx-register-a-branch.txt 2009-09-18 15:24:30 +0000
1515+++ lib/lp/code/stories/branches/xx-register-a-branch.txt 2010-02-18 10:55:33 +0000
1516@@ -10,16 +10,10 @@
1517 Register a branch...
1518
1519 Clicking the link as an anonymous user should take you to a login page. Once
1520-you've logged in, you should be redirected to the registration page.
1521+you've logged in, you will be redirected to the registration page.
1522
1523- >>> anon_browser.handleErrors = True
1524 >>> anon_browser.open('http://code.launchpad.dev/')
1525 >>> anon_browser.getLink('Register a branch').click()
1526- >>> print anon_browser.title
1527- Log in or register with Launchpad
1528- >>> anon_browser.getControl('E-mail address', index=0).value = (
1529- ... 'test@canonical.com')
1530- >>> anon_browser.getControl('Password').value = 'test'
1531- >>> anon_browser.getControl('Log In').click()
1532- >>> print anon_browser.title
1533- Register a branch...
1534+ Traceback (most recent call last):
1535+ ...
1536+ Unauthorized:...
1537
1538=== modified file 'lib/lp/registry/stories/person/xx-deactivate-account.txt'
1539--- lib/lp/registry/stories/person/xx-deactivate-account.txt 2009-09-16 17:56:17 +0000
1540+++ lib/lp/registry/stories/person/xx-deactivate-account.txt 2010-02-18 10:55:33 +0000
1541@@ -4,17 +4,11 @@
1542 accounts, so they stop receiving emails from Launchpad and make it impossible
1543 to use their accounts to log in.
1544
1545- >>> browser.open('http://launchpad.dev/+login')
1546- >>> browser.getControl('E-mail', index=0).value = 'test@canonical.com'
1547- >>> browser.getControl('Password').value = 'test'
1548- >>> browser.getControl('Log In').click()
1549- >>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
1550- Sample Person...
1551-
1552 Deactivating a user's account will un-assign all their bug tasks. To
1553 demonstrate this, we'll assign a bug to the user that we're going to
1554 deactivate.
1555
1556+ >>> browser = setupBrowser(auth='Basic test@canonical.com:test')
1557 >>> edit_bug_url = ("http://bugs.launchpad.dev/debian/sarge/+source/"
1558 ... "mozilla-firefox/+bug/3/+editstatus")
1559 >>> browser.open(edit_bug_url)
1560@@ -67,16 +61,13 @@
1561 ... print msg
1562 Your account has been deactivated.
1563
1564-Sample Person can't log into Launchpad anymore.
1565+And now the Launchpad page for Sample Person person will clearly say he
1566+does not use Launchpad.
1567
1568- >>> browser.open('http://launchpad.dev/+login')
1569- >>> browser.getControl('E-mail', index=0).value = 'test@canonical.com'
1570- >>> browser.getControl('Password').value = 'test'
1571- >>> browser.getControl('Log In').click()
1572- >>> for msg in get_feedback_messages(browser.contents):
1573- ... print msg
1574- The email address belongs to a deactivated account.
1575- Use the "Forgotten your password" link to reactivate it.
1576+ >>> browser.open('http://launchpad.dev/~name12-deactivatedaccount')
1577+ >>> print extract_text(
1578+ ... find_tag_by_id(browser.contents, 'not-lp-user-or-team'))
1579+ Sample Person does not use Launchpad.
1580
1581 The bugs that were assigned to Sample Person will no longer have an
1582 assignee.
1583@@ -89,14 +80,6 @@
1584 Bug #3
1585 ...Assigned to unknown...
1586
1587-And now the Launchpad page for that person will clearly say (s)he does not use
1588-Launchpad.
1589-
1590- >>> browser.open('http://launchpad.dev/~name12-deactivatedaccount')
1591- >>> print extract_text(
1592- ... find_tag_by_id(browser.contents, 'not-lp-user-or-team'))
1593- Sample Person does not use Launchpad.
1594-
1595 Although teams have NOACCOUNT as their account_status, they are teams and so
1596 it makes no sense to say they don't use Launchpad.
1597
1598
1599=== modified file 'lib/lp/scripts/utilities/importfascist.py'
1600--- lib/lp/scripts/utilities/importfascist.py 2010-02-04 03:07:25 +0000
1601+++ lib/lp/scripts/utilities/importfascist.py 2010-02-18 10:55:33 +0000
1602@@ -59,6 +59,7 @@
1603 valid_imports_not_in_all = {
1604 'cookielib': set(['domain_match']),
1605 'email.Utils': set(['mktime_tz']),
1606+ 'openid.fetchers': set(['Urllib2Fetcher']),
1607 'storm.database': set(['STATE_DISCONNECTED']),
1608 'textwrap': set(['dedent']),
1609 'zope.component': set(
1610
1611=== modified file 'lib/lp/translations/stories/standalone/xx-person-editlanguages.txt'
1612--- lib/lp/translations/stories/standalone/xx-person-editlanguages.txt 2009-11-07 04:30:07 +0000
1613+++ lib/lp/translations/stories/standalone/xx-person-editlanguages.txt 2010-02-18 10:55:33 +0000
1614@@ -124,18 +124,20 @@
1615 with launchpad.AnyPerson as permission. This is the page to which we
1616 direct non-logged in users to edit their preferred languages.
1617
1618- >>> anon_browser.handleErrors = True
1619+The launchpad.AnyPerson permission means that when an anonymous user goes
1620+to that page, they'll be asked to login.
1621+
1622 >>> anon_browser.open('http://launchpad.dev/+editmylanguages')
1623- >>> anon_browser.url
1624- 'http://launchpad.dev/+editmylanguages/+login'
1625-
1626- >>> anon_browser.open('http://launchpad.dev/+editmylanguages/+login')
1627- >>> anon_browser.getControl('E-mail address:', index=0).value = (
1628- ... 'no-priv@canonical.com')
1629- >>> anon_browser.getControl('Password:').value = 'test'
1630- >>> anon_browser.getControl(name='loginpage_submit_login').click()
1631- >>> anon_browser.url
1632- 'http://launchpad.dev/~no-priv/+editlanguages'
1633+ Traceback (most recent call last):
1634+ ...
1635+ Unauthorized: ...
1636+
1637+But a logged in user will be sent straight to their /~user/+editlanguages
1638+page.
1639+
1640+ >>> browser.open('http://launchpad.dev/+editmylanguages')
1641+ >>> browser.url
1642+ 'http://launchpad.dev/~name12/+editlanguages'
1643
1644
1645 == Adding languages to teams ==
1646
1647=== modified file 'lib/lp/translations/windmill/tests/test_documentation_links.py'
1648--- lib/lp/translations/windmill/tests/test_documentation_links.py 2010-02-01 18:37:00 +0000
1649+++ lib/lp/translations/windmill/tests/test_documentation_links.py 2010-02-18 10:55:33 +0000
1650@@ -38,10 +38,8 @@
1651 """
1652 client = self.client
1653
1654- start_url = 'http://translations.launchpad.dev:8085/'
1655 user = lpuser.TRANSLATIONS_ADMIN
1656
1657-
1658 # Create a translation group with documentation to use in the test.
1659 group = self.factory.makeTranslationGroup(
1660 name='testing-group', title='Testing group',