Merge lp:~salgado/launchpad/openid-consumer into lp:launchpad
- openid-consumer
- Merge into devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gary Poster (community) | code | Approve | |
Review via email:
|
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Guilherme Salgado (salgado) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
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://
- 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 OpenIDCallbackV
- I noted that I don't like properties that write when you get them, so I don't like OpenIDCallbackV
- In in no-anonymous-
- I noted that I saw four instances of the same test code to log in (e.g., ``browser.
Preview Diff
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', |
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 store.py
test_baseopenid
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.