Merge lp:~deryck/launchpad/reauth-for-email-363916 into lp:launchpad

Proposed by Deryck Hodge on 2012-08-07
Status: Merged
Approved by: Deryck Hodge on 2012-08-07
Approved revision: no longer in the source branch.
Merged at revision: 15763
Proposed branch: lp:~deryck/launchpad/reauth-for-email-363916
Merge into: lp:launchpad
Prerequisite: lp:~deryck/launchpad/support-pape-max-auth-age
Diff against target: 660 lines (+198/-93)
6 files modified
lib/lp/registry/browser/person.py (+9/-1)
lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt (+6/-0)
lib/lp/registry/stories/mailinglists/subscriptions.txt (+102/-88)
lib/lp/services/webapp/login.py (+17/-4)
lib/lp/services/webapp/tests/test_session.py (+42/-0)
lib/lp/testing/pages.py (+22/-0)
To merge this branch: bzr merge lp:~deryck/launchpad/reauth-for-email-363916
Reviewer Review Type Date Requested Status
Richard Harding (community) 2012-08-07 Approve on 2012-08-07
Review via email: mp+118612@code.launchpad.net

Commit Message

Require users to re-authenticate when changing email address info.

Description of the Change

This branch ensures that a user has to re-authenticate when editing email info. This builds on the work I did in the pre-req branch to add support for PAPE extension's max_auth_age option when logging in.

The work here is really to add a check on the freshness of a login. If the login is less than 2 minutes old, we allow the user to edit his or her email settings. If not, we signal for a fresh login. To do this, the lp session is updated to store the logintime, and then a helper method is added to check the logintime in the session. There is a test added for this. The only other new work is to use this isFreshLogin helper in the view and redirect too +login if a fresher login is required. All the rest is updating doctests to ensure we have a fresh login to avoid the isFreshLogin, and there is a new setupBrowser function to help do this in doctests.

In later branches, I'll apply this for ssh and gpg and will factor out the code in lp.registry.browser.person to a function to ensure_fresh_login. But I thought it was nice to get this reviewed as is, to see how it fits together better.

The work does add about 100 lines of code, but I have preceeding branches that refactored tests to prepare for this work. And in total, I'm still about -200 LOC, even after this branch lands.

There is a bit of lint still in the long subscriptions doctest, but none that I added. I didn't, however, fix the existing lint because the diff would have grown significantly for very minor lint changes (mostly too long lines in the documentation parts of the test).

To post a comment you must log in.
Richard Harding (rharding) wrote :

Should the setupBrowserFreshLogin provide the ability to setup based on email/etc to skip some test boilerplate? It could use kwargs to allow passing an email or name for the getByXXX

#515
Can we add the else just to jump out the if condition returning a value

#517
Should the timing be a constant in the module for easy finding/change/use?

review: Approve
Deryck Hodge (deryck) wrote :

Thanks for the review, Rick!

The test helper is following the pattern of setupBrowserForUser, which takes a real user. Not sure I want to get into all that refactoring. By refactoring, I mean that I don't like those kind of 1/2 refactors where there are now 2 ways to do something. Anyway, it's a good suggestion, but I'll wait on doing it for now.

As for line #515, I think that's just a style preference and we don't have a rule about being explicit there, I don't think. I prefer the return by itself, rather than inside an else statement. Less code and all, for the same result.

For the timing question, I wondered even if it should be a config value, but wasn't sure. So I'll keep thinking on that, but roll on forward for now. If I changed it, I'll do it in one of the follow on branches.

Thanks, again, for the review!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/person.py'
2--- lib/lp/registry/browser/person.py 2012-08-07 02:31:56 +0000
3+++ lib/lp/registry/browser/person.py 2012-08-07 18:27:30 +0000
4@@ -257,7 +257,10 @@
5 ILaunchBag,
6 IOpenLaunchBag,
7 )
8-from lp.services.webapp.login import logoutPerson
9+from lp.services.webapp.login import (
10+ isFreshLogin,
11+ logoutPerson,
12+ )
13 from lp.services.webapp.menu import get_current_view
14 from lp.services.webapp.publisher import LaunchpadView
15 from lp.services.worlddata.interfaces.country import ICountry
16@@ -2810,6 +2813,11 @@
17 label = 'Change your e-mail settings'
18
19 def initialize(self):
20+ if not isFreshLogin(self.request):
21+ reauth_query = '+login?reauth=1'
22+ base_url = canonical_url(self.context, view_name='+editemails')
23+ login_url = '%s/%s' % (base_url, reauth_query)
24+ self.request.response.redirect(login_url)
25 if self.context.is_team:
26 # +editemails is not available on teams.
27 name = self.request['PATH_INFO'].split('/')[-1]
28
29=== modified file 'lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt'
30--- lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt 2012-06-29 14:34:11 +0000
31+++ lib/lp/registry/stories/gpg-coc/xx-gpg-coc.txt 2012-08-07 18:27:30 +0000
32@@ -141,7 +141,13 @@
33 added as an email address:
34
35 >>> from lp.testing.pages import strip_label
36+ >>> from lp.registry.interfaces.person import IPersonSet
37+ >>> from lp.testing.pages import setupBrowserFreshLogin
38
39+ >>> login(ANONYMOUS)
40+ >>> person = getUtility(IPersonSet).getByEmail('test@canonical.com')
41+ >>> logout()
42+ >>> browser = setupBrowserFreshLogin(person)
43 >>> browser.open("http://launchpad.dev/~name12/+editemails")
44 >>> control = browser.getControl(name='field.UNVALIDATED_SELECTED')
45 >>> [strip_label(label) for label in sorted(control.displayOptions)]
46
47=== modified file 'lib/lp/registry/stories/mailinglists/subscriptions.txt'
48--- lib/lp/registry/stories/mailinglists/subscriptions.txt 2012-01-15 11:06:57 +0000
49+++ lib/lp/registry/stories/mailinglists/subscriptions.txt 2012-08-07 18:27:30 +0000
50@@ -60,8 +60,15 @@
51 >>> admin_browser.getControl('New member').value = 'no-team-memberships'
52 >>> admin_browser.getControl('Add Member').click()
53
54- >>> no_team_browser = setupBrowser(
55- ... auth="Basic no-team-memberships@test.com:test")
56+ >>> from lp.testing.pages import setupBrowserFreshLogin
57+ >>> from zope.component import getUtility
58+ >>> from lp.registry.interfaces.person import IPersonSet
59+ >>> login(ANONYMOUS)
60+ >>> person = getUtility(IPersonSet).getByEmail(
61+ ... 'no-team-memberships@test.com')
62+ >>> logout()
63+ >>> no_team_browser = setupBrowserFreshLogin(person)
64+
65 >>> no_team_browser.open('http://launchpad.dev/people/+me/+editemails')
66 >>> rosetta_admins = no_team_browser.getControl(
67 ... name='field.subscription.rosetta-admins')
68@@ -77,15 +84,19 @@
69 he's a member of. Mailing lists show up in this list regardless of whether
70 it's currently the team contact method.
71
72- >>> browser.open('http://launchpad.dev/~carlos')
73- >>> browser.getLink(url="+editemails").click()
74+ >>> login(ANONYMOUS)
75+ >>> carlos = getUtility(IPersonSet).getByName('carlos')
76+ >>> logout()
77+ >>> carlos_browser = setupBrowserFreshLogin(carlos)
78+ >>> carlos_browser.open('http://launchpad.dev/~carlos')
79+ >>> carlos_browser.getLink(url="+editemails").click()
80
81 >>> from lp.services.helpers import backslashreplace
82- >>> print backslashreplace(browser.title)
83+ >>> print backslashreplace(carlos_browser.title)
84 Change your e-mail settings...
85
86- >>> admins = browser.getControl(name='field.subscription.admins')
87- >>> rosetta_admins = browser.getControl(
88+ >>> admins = carlos_browser.getControl(name='field.subscription.admins')
89+ >>> rosetta_admins = carlos_browser.getControl(
90 ... name='field.subscription.rosetta-admins')
91
92 >>> admins.displayOptions
93@@ -100,7 +111,7 @@
94 However, testing-spanish-team's list doesn't show up because its creation has
95 not been completed (specifically, Mailman hasn't constructed it yet).
96
97- >>> browser.getControl(name='field.subscription.testing-spanish-team')
98+ >>> carlos_browser.getControl(name='field.subscription.testing-spanish-team')
99 Traceback (most recent call last):
100 ...
101 LookupError: name 'field.subscription.testing-spanish-team'
102@@ -110,16 +121,16 @@
103 him to update his subscription. So this is not the same as subscribing
104 explicitly with whatever is his preferred email address.
105
106- >>> admins = browser.getControl(name='field.subscription.admins')
107+ >>> admins = carlos_browser.getControl(name='field.subscription.admins')
108 >>> admins.value = ['Preferred address']
109- >>> browser.getControl('Update Subscriptions').click()
110+ >>> carlos_browser.getControl('Update Subscriptions').click()
111
112- >>> for msg in get_feedback_messages(browser.contents):
113+ >>> for msg in get_feedback_messages(carlos_browser.contents):
114 ... print msg
115 Subscriptions updated.
116
117- >>> admins = browser.getControl(name='field.subscription.admins')
118- >>> rosetta_admins = browser.getControl(
119+ >>> admins = carlos_browser.getControl(name='field.subscription.admins')
120+ >>> rosetta_admins = carlos_browser.getControl(
121 ... name='field.subscription.rosetta-admins')
122 >>> print admins.value
123 ['Preferred address']
124@@ -131,10 +142,10 @@
125
126 >>> admins.value = ['carlos@canonical.com']
127 >>> rosetta_admins.value = ['carlos@test.com']
128- >>> browser.getControl('Update Subscriptions').click()
129+ >>> carlos_browser.getControl('Update Subscriptions').click()
130
131- >>> admins = browser.getControl(name='field.subscription.admins')
132- >>> rosetta_admins = browser.getControl(
133+ >>> admins = carlos_browser.getControl(name='field.subscription.admins')
134+ >>> rosetta_admins = carlos_browser.getControl(
135 ... name='field.subscription.rosetta-admins')
136 >>> print admins.value
137 ['carlos@canonical.com']
138@@ -146,10 +157,10 @@
139
140 >>> admins.value = ['Preferred address']
141 >>> rosetta_admins.value = ['carlos@canonical.com']
142- >>> browser.getControl('Update Subscriptions').click()
143+ >>> carlos_browser.getControl('Update Subscriptions').click()
144
145- >>> admins = browser.getControl(name='field.subscription.admins')
146- >>> rosetta_admins = browser.getControl(
147+ >>> admins = carlos_browser.getControl(name='field.subscription.admins')
148+ >>> rosetta_admins = carlos_browser.getControl(
149 ... name='field.subscription.rosetta-admins')
150 >>> print admins.value
151 ['Preferred address']
152@@ -159,15 +170,15 @@
153 Finally, he can unsubscribe from any mailing list by setting the subscription
154 menu item to "Don't subscribe".
155
156- >>> admins = browser.getControl(name='field.subscription.admins')
157- >>> rosetta_admins = browser.getControl(
158+ >>> admins = carlos_browser.getControl(name='field.subscription.admins')
159+ >>> rosetta_admins = carlos_browser.getControl(
160 ... name='field.subscription.rosetta-admins')
161 >>> admins.value = ["Don't subscribe"]
162 >>> rosetta_admins.value = ["Don't subscribe"]
163- >>> browser.getControl('Update Subscriptions').click()
164+ >>> carlos_browser.getControl('Update Subscriptions').click()
165
166- >>> admins = browser.getControl(name='field.subscription.admins')
167- >>> rosetta_admins = browser.getControl(
168+ >>> admins = carlos_browser.getControl(name='field.subscription.admins')
169+ >>> rosetta_admins = carlos_browser.getControl(
170 ... name='field.subscription.rosetta-admins')
171 >>> print admins.value
172 ["Don't subscribe"]
173@@ -186,12 +197,12 @@
174 to. We will use Carlos, as he is an administrator for the Rosetta
175 Admins team, and he should know if the list is available.
176
177- >>> browser.open('http://launchpad.dev/~carlos')
178- >>> browser.getLink(url="+editemails").click()
179- >>> print backslashreplace(browser.title)
180+ >>> carlos_browser.open('http://launchpad.dev/~carlos')
181+ >>> carlos_browser.getLink(url="+editemails").click()
182+ >>> print backslashreplace(carlos_browser.title)
183 Change your e-mail settings...
184
185- >>> rosetta_admins = browser.getControl(
186+ >>> rosetta_admins = carlos_browser.getControl(
187 ... name='field.subscription.rosetta-admins')
188 >>> rosetta_admins.displayOptions
189 ['Preferred address', "Don't subscribe",
190@@ -220,12 +231,16 @@
191 the list. The list does not show up on his Subscription Management
192 screen.
193
194- >>> browser.open('http://launchpad.dev/~jdub')
195- >>> browser.getLink(url="+editemails").click()
196- >>> print browser.title
197+ >>> login(ANONYMOUS)
198+ >>> jdub = getUtility(IPersonSet).getByName('jdub')
199+ >>> logout()
200+ >>> jdub_browser = setupBrowserFreshLogin(jdub)
201+ >>> jdub_browser.open('http://launchpad.dev/~jdub')
202+ >>> jdub_browser.getLink(url="+editemails").click()
203+ >>> print jdub_browser.title
204 Change your e-mail settings...
205
206- >>> browser.getControl(
207+ >>> jdub_browser.getControl(
208 ... name='field.subscription.rosetta-admins')
209 Traceback (most recent call last):
210 ...
211@@ -244,12 +259,12 @@
212
213 His mailing list subscription is now available to be managed.
214
215- >>> browser.open('http://launchpad.dev/~jdub')
216- >>> browser.getLink(url="+editemails").click()
217- >>> print browser.title
218+ >>> jdub_browser.open('http://launchpad.dev/~jdub')
219+ >>> jdub_browser.getLink(url="+editemails").click()
220+ >>> print jdub_browser.title
221 Change your e-mail settings...
222
223- >>> rosetta_team = browser.getControl(
224+ >>> rosetta_team = jdub_browser.getControl(
225 ... name='field.subscription.rosetta-admins')
226
227 >>> rosetta_team.displayOptions
228@@ -309,60 +324,60 @@
229 Carlos can see the subscribe link on the admin team's Overview
230 page, because he is not subscribed to the team mailing list.
231
232- >>> browser = setupBrowser(auth='Basic carlos@canonical.com:test')
233- >>> browser.open('http://launchpad.dev/~admins')
234- >>> browser.getLink('Subscribe to mailing list').click()
235- >>> print browser.url
236+ >>> carlos_browser.open('http://launchpad.dev/~admins')
237+ >>> carlos_browser.getLink('Subscribe to mailing list').click()
238+ >>> print carlos_browser.url
239 http://launchpad.dev/~carlos/+editemails
240
241 The unsubscribe link is visible for the rosetta admins team, which
242 has an active mailing list.
243
244 # Subscribe to the list using the normal technique.
245- >>> browser.open('http://launchpad.dev/~carlos')
246- >>> browser.getLink(url="+editemails").click()
247- >>> rosetta_admins = browser.getControl(
248+ >>> carlos_browser.open('http://launchpad.dev/~carlos')
249+ >>> carlos_browser.getLink(url="+editemails").click()
250+ >>> rosetta_admins = carlos_browser.getControl(
251 ... name='field.subscription.rosetta-admins')
252 >>> rosetta_admins.value = ['Preferred address']
253- >>> browser.getControl('Update Subscriptions').click()
254+ >>> carlos_browser.getControl('Update Subscriptions').click()
255 >>> print rosetta_admins.value
256 ['Preferred address']
257- >>> for tag in find_tags_by_class(browser.contents, 'informational'):
258+ >>> for tag in find_tags_by_class(
259+ ... carlos_browser.contents, 'informational'):
260 ... print tag.renderContents()
261 Subscriptions updated.
262
263- >>> browser.open('http://launchpad.dev/~rosetta-admins')
264- >>> browser.getControl('Unsubscribe')
265+ >>> carlos_browser.open('http://launchpad.dev/~rosetta-admins')
266+ >>> carlos_browser.getControl('Unsubscribe')
267 <SubmitControl name='unsubscribe' type='submit'>
268
269 Clicking the link will unsubscribe you from the list immediately.
270
271- >>> browser.getControl('Unsubscribe').click()
272- >>> print get_feedback_messages(browser.contents)
273+ >>> carlos_browser.getControl('Unsubscribe').click()
274+ >>> print get_feedback_messages(carlos_browser.contents)
275 [u'You have been unsubscribed from the team mailing list.']
276
277- >>> browser.open('http://launchpad.dev/~rosetta-admins')
278+ >>> carlos_browser.open('http://launchpad.dev/~rosetta-admins')
279 >>> print extract_text(
280- ... find_tag_by_id(browser.contents, 'mailing-lists'))
281+ ... find_tag_by_id(carlos_browser.contents, 'mailing-lists'))
282 Mailing list...
283 Subscribe to mailing list...
284
285 The Ubuntu translators team, which does not have any lists configured,
286 does not show either link.
287
288- >>> browser.open('http://launchpad.dev/~ubuntu-translators')
289+ >>> carlos_browser.open('http://launchpad.dev/~ubuntu-translators')
290 >>> print extract_text(
291- ... find_portlet(browser.contents, 'Mailing list'))
292+ ... find_portlet(carlos_browser.contents, 'Mailing list'))
293 Mailing list
294 This team does not use Launchpad to host a mailing list.
295 Create a mailing list
296
297- >>> browser.getLink('Subscribe')
298+ >>> carlos_browser.getLink('Subscribe')
299 Traceback (most recent call last):
300 ...
301 LinkNotFoundError
302
303- >>> browser.getLink('Unsubscribe')
304+ >>> carlos_browser.getLink('Unsubscribe')
305 Traceback (most recent call last):
306 ...
307 LinkNotFoundError
308@@ -375,9 +390,9 @@
309 mailing list subscribers, if there is an active mailing list. The
310 rosetta admins team has such a list and carlos is the owner.
311
312- >>> browser.open('http://launchpad.dev/~rosetta-admins')
313+ >>> carlos_browser.open('http://launchpad.dev/~rosetta-admins')
314 >>> print extract_text(
315- ... find_portlet(browser.contents, 'Mailing list'))
316+ ... find_portlet(carlos_browser.contents, 'Mailing list'))
317 Mailing list
318 rosetta-admins@lists.launchpad.dev
319 Policy: You must be a team member to subscribe to the team mailing list.
320@@ -389,11 +404,11 @@
321 (Jeff Waugh has asked to subscribe but he's not considered a subscriber
322 because his membership on Rosetta Admins hasn't been approved)
323
324- >>> browser.getLink('View subscribers').click()
325- >>> print browser.title
326+ >>> carlos_browser.getLink('View subscribers').click()
327+ >>> print carlos_browser.title
328 Mailing list subscribers for the Rosetta Administrators team...
329
330- >>> print extract_text(find_tag_by_id(browser.contents, 'subscribers'))
331+ >>> print extract_text(find_tag_by_id(carlos_browser.contents, 'subscribers'))
332 Nobody has subscribed to this team's mailing list yet.
333
334 If it had subscribers, though, they'd be shown on that page, in a batched
335@@ -407,8 +422,6 @@
336 ... """)
337 >>> login('foo.bar@canonical.com')
338
339- >>> from lp.registry.interfaces.person import IPersonSet
340- >>> from zope.component import getUtility
341 >>> person_set = getUtility(IPersonSet)
342 >>> jdub = person_set.getByName('jdub')
343 >>> mark = person_set.getByName('mark')
344@@ -420,14 +433,14 @@
345 >>> ignored = rosetta_admins.addMember(jordi, reviewer=mark)
346 >>> rosetta_admins.mailing_list.subscribe(jordi)
347 >>> logout()
348- >>> browser.reload()
349- >>> print extract_text(find_tag_by_id(browser.contents, 'subscribers'))
350+ >>> carlos_browser.reload()
351+ >>> print extract_text(find_tag_by_id(carlos_browser.contents, 'subscribers'))
352 The following people are subscribed...
353 Guilherme Salgado
354 1 of 2 results...
355
356- >>> browser.getLink('Next').click()
357- >>> print extract_text(find_tag_by_id(browser.contents, 'subscribers'))
358+ >>> carlos_browser.getLink('Next').click()
359+ >>> print extract_text(find_tag_by_id(carlos_browser.contents, 'subscribers'))
360 The following people are subscribed...
361 Jordi Mallach
362 2 of 2 results...
363@@ -452,15 +465,14 @@
364 Launchpad may automatically subscribe a person to a team's mailing
365 list based on a setting in the person's Email preferences page.
366
367- >>> browser = setupBrowser(auth='Basic carlos@canonical.com:test')
368- >>> browser.open('http://launchpad.dev/~carlos')
369- >>> browser.getLink(url="+editemails").click()
370- >>> print backslashreplace(browser.title)
371+ >>> carlos_browser.open('http://launchpad.dev/~carlos')
372+ >>> carlos_browser.getLink(url="+editemails").click()
373+ >>> print backslashreplace(carlos_browser.title)
374 Change your e-mail settings...
375
376 Carlos's default setting, 'Ask me when I join a team', is still in place.
377
378- >>> print_radio_button_field(browser.contents,
379+ >>> print_radio_button_field(carlos_browser.contents,
380 ... 'mailing_list_auto_subscribe_policy')
381 ( ) Never subscribe to mailing lists
382 (*) Ask me when I join a team
383@@ -471,32 +483,32 @@
384
385 # A convenient helper for setting and submitting a new
386 # auto-subscribe policy.
387- >>> def set_autosubscribe_policy_and_submit(newvalue):
388- ... control = browser.getControl(
389+ >>> def set_autosubscribe_policy_and_submit(newvalue, current_browser):
390+ ... control = current_browser.getControl(
391 ... name='field.mailing_list_auto_subscribe_policy')
392 ... control.value = [newvalue]
393- ... browser.getControl('Update Policy').click()
394- ... print_radio_button_field(browser.contents,
395+ ... current_browser.getControl('Update Policy').click()
396+ ... print_radio_button_field(current_browser.contents,
397 ... 'mailing_list_auto_subscribe_policy')
398
399- >>> original_value = browser.getControl(
400+ >>> original_value = carlos_browser.getControl(
401 ... name='field.mailing_list_auto_subscribe_policy').value.pop()
402
403- >>> set_autosubscribe_policy_and_submit('ALWAYS')
404+ >>> set_autosubscribe_policy_and_submit('ALWAYS', carlos_browser)
405 ( ) Never subscribe to mailing lists
406 ( ) Ask me when I join a team
407 (*) Always subscribe me to mailing lists
408
409 # We only need to check this once.
410- >>> get_feedback_messages(browser.contents)
411+ >>> get_feedback_messages(carlos_browser.contents)
412 [u'Your auto-subscription policy has been updated.']
413
414- >>> set_autosubscribe_policy_and_submit('NEVER')
415+ >>> set_autosubscribe_policy_and_submit('NEVER', carlos_browser)
416 (*) Never subscribe to mailing lists
417 ( ) Ask me when I join a team
418 ( ) Always subscribe me to mailing lists
419
420- >>> set_autosubscribe_policy_and_submit('ON_REGISTRATION')
421+ >>> set_autosubscribe_policy_and_submit('ON_REGISTRATION', carlos_browser)
422 ( ) Never subscribe to mailing lists
423 (*) Ask me when I join a team
424 ( ) Always subscribe me to mailing lists
425@@ -505,7 +517,7 @@
426
427 # Restores the original value while performing the test.
428 >>> assert original_value == 'ON_REGISTRATION'
429- >>> set_autosubscribe_policy_and_submit(original_value)
430+ >>> set_autosubscribe_policy_and_submit(original_value, carlos_browser)
431 ( ) Never subscribe to mailing lists
432 (*) Ask me when I join a team
433 ( ) Always subscribe me to mailing lists
434@@ -516,7 +528,7 @@
435 behavior.
436
437 >>> print extract_text(
438- ... find_tag_by_id(browser.contents, 'notification-info'))
439+ ... find_tag_by_id(carlos_browser.contents, 'notification-info'))
440 When a team you are a member of creates a new mailing list, you will
441 receive an email notification offering you the opportunity to join the new
442 mailing list. Launchpad can also automatically subscribe you to a team's
443@@ -528,9 +540,11 @@
444 team. Users who have chosen the 'On registration' or 'Always'
445 subscription settings will see the box checked by default.
446
447- >>> browser = setupBrowser(
448- ... auth='Basic james.blackwell@ubuntulinux.com:test')
449-
450+ >>> login(ANONYMOUS)
451+ >>> james = getUtility(IPersonSet).getByEmail(
452+ ... 'james.blackwell@ubuntulinux.com')
453+ >>> logout()
454+ >>> browser = setupBrowserFreshLogin(james)
455 >>> browser.open('http://launchpad.dev/~jblack')
456 >>> browser.getLink(url="+editemails").click()
457 >>> print_radio_button_field(browser.contents,
458@@ -550,7 +564,7 @@
459 # Change James' setting
460 >>> browser.open('http://launchpad.dev/~jblack')
461 >>> browser.getLink(url="+editemails").click()
462- >>> set_autosubscribe_policy_and_submit('ALWAYS')
463+ >>> set_autosubscribe_policy_and_submit('ALWAYS', browser)
464 ( ) Never subscribe to mailing lists
465 ( ) Ask me when I join a team
466 (*) Always subscribe me to mailing lists
467@@ -566,7 +580,7 @@
468 # Change James' setting
469 >>> browser.open('http://launchpad.dev/~jblack')
470 >>> browser.getLink(url="+editemails").click()
471- >>> set_autosubscribe_policy_and_submit('NEVER')
472+ >>> set_autosubscribe_policy_and_submit('NEVER', browser)
473 (*) Never subscribe to mailing lists
474 ( ) Ask me when I join a team
475 ( ) Always subscribe me to mailing lists
476@@ -579,7 +593,7 @@
477 # Restore James' setting.
478 >>> browser.open('http://launchpad.dev/~jblack')
479 >>> browser.getLink(url="+editemails").click()
480- >>> set_autosubscribe_policy_and_submit('ON_REGISTRATION')
481+ >>> set_autosubscribe_policy_and_submit('ON_REGISTRATION', browser)
482 ( ) Never subscribe to mailing lists
483 (*) Ask me when I join a team
484 ( ) Always subscribe me to mailing lists
485
486=== modified file 'lib/lp/services/webapp/login.py'
487--- lib/lp/services/webapp/login.py 2012-08-07 18:27:27 +0000
488+++ lib/lp/services/webapp/login.py 2012-08-07 18:27:30 +0000
489@@ -338,13 +338,13 @@
490 finally:
491 timeline_action.finish()
492
493- def login(self, person):
494+ def login(self, person, when=None):
495 loginsource = getUtility(IPlacelessLoginSource)
496 # We don't have a logged in principal, so we must remove the security
497 # proxy of the account's preferred email.
498 email = removeSecurityProxy(person.preferredemail).email
499 logInPrincipal(
500- self.request, loginsource.getPrincipalByLogin(email), email)
501+ self.request, loginsource.getPrincipalByLogin(email), email, when)
502
503 @cachedproperty
504 def sreg_response(self):
505@@ -475,7 +475,18 @@
506 template = ViewPageTemplateFile("templates/login-already.pt")
507
508
509-def logInPrincipal(request, principal, email):
510+def isFreshLogin(request):
511+ """Return True if the principal login happened in the last 120 seconds."""
512+ session = ISession(request)
513+ authdata = session['launchpad.authenticateduser']
514+ logintime = authdata.get('logintime', None)
515+ if logintime is not None:
516+ now = datetime.utcnow()
517+ return logintime > now - timedelta(seconds=120)
518+ return False
519+
520+
521+def logInPrincipal(request, principal, email, when=None):
522 """Log the principal in. Password validation must be done in callsites."""
523 # Force a fresh session, per Bug #828638. Any changes to any
524 # existing session made this request will be lost, but that should
525@@ -488,8 +499,10 @@
526 authdata = session['launchpad.authenticateduser']
527 assert principal.id is not None, 'principal.id is None!'
528 request.setPrincipal(principal)
529+ if when is None:
530+ when = datetime.utcnow()
531 authdata['accountid'] = principal.id
532- authdata['logintime'] = datetime.utcnow()
533+ authdata['logintime'] = when
534 authdata['login'] = email
535 notify(CookieAuthLoggedInEvent(request, email))
536
537
538=== modified file 'lib/lp/services/webapp/tests/test_session.py'
539--- lib/lp/services/webapp/tests/test_session.py 2012-03-22 23:21:24 +0000
540+++ lib/lp/services/webapp/tests/test_session.py 2012-08-07 18:27:30 +0000
541@@ -1,14 +1,25 @@
542 # Copyright 2009-2012 Canonical Ltd. This software is licensed under the
543 # GNU Affero General Public License version 3 (see the file LICENSE).
544
545+import datetime
546+
547 from testtools import TestCase
548 from testtools.matchers import Contains
549
550+from lp.services.webapp.login import (
551+ isFreshLogin,
552+ OpenIDCallbackView,
553+ )
554 from lp.services.webapp.servers import LaunchpadTestRequest
555 from lp.services.webapp.session import (
556 get_cookie_domain,
557 LaunchpadCookieClientIdManager,
558 )
559+from lp.testing import (
560+ person_logged_in,
561+ TestCaseWithFactory,
562+ )
563+from lp.testing.layers import DatabaseFunctionalLayer
564
565
566 class GetCookieDomainTestCase(TestCase):
567@@ -55,3 +66,34 @@
568 self.assertThat(
569 dict(request.response.getHeaders())['Set-Cookie'],
570 Contains('; httponly;'))
571+
572+
573+class TestSessionRelatedFunctions(TestCaseWithFactory):
574+
575+ layer = DatabaseFunctionalLayer
576+
577+ def setupLoggedInRequest(self, user, request, when=None):
578+ """Test helper to login a user for a request."""
579+ with person_logged_in(user):
580+ view = OpenIDCallbackView(user, request)
581+ view.login(user, when)
582+
583+ def test_isFreshLogin_returns_false_for_anonymous(self):
584+ """isFreshLogin should return False for anonymous views."""
585+ request = LaunchpadTestRequest()
586+ self.assertFalse(isFreshLogin(request))
587+
588+ def test_isFreshLogin_returns_true(self):
589+ """isFreshLogin should return True with a fresh logged in user."""
590+ user = self.factory.makePerson()
591+ request = LaunchpadTestRequest()
592+ self.setupLoggedInRequest(user, request)
593+ self.assertTrue(isFreshLogin(request))
594+
595+ def test_isFreshLogin_returns_false(self):
596+ """isFreshLogin should be False for users logged in over 2 minutes."""
597+ user = self.factory.makePerson()
598+ request = LaunchpadTestRequest()
599+ when = datetime.datetime.utcnow() - datetime.timedelta(seconds=180)
600+ self.setupLoggedInRequest(user, request, when)
601+ self.assertFalse(isFreshLogin(request))
602
603=== modified file 'lib/lp/testing/pages.py'
604--- lib/lp/testing/pages.py 2012-06-06 16:04:34 +0000
605+++ lib/lp/testing/pages.py 2012-08-07 18:27:30 +0000
606@@ -5,6 +5,7 @@
607
608 __metaclass__ = type
609
610+from datetime import datetime
611 import doctest
612 import os
613 import pdb
614@@ -37,18 +38,21 @@
615 SimpleCookie,
616 )
617 from zope.component import getUtility
618+from zope.session.interfaces import ISession
619 from zope.security.proxy import removeSecurityProxy
620 from zope.testbrowser.testing import Browser
621
622 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
623 from lp.registry.errors import NameAlreadyTaken
624 from lp.registry.interfaces.teammembership import TeamMembershipStatus
625+from lp.services.config import config
626 from lp.services.oauth.interfaces import (
627 IOAuthConsumerSet,
628 OAUTH_REALM,
629 )
630 from lp.services.webapp import canonical_url
631 from lp.services.webapp.interfaces import OAuthPermission
632+from lp.services.webapp.servers import LaunchpadTestRequest
633 from lp.services.webapp.url import urlsplit
634 from lp.testing import (
635 ANONYMOUS,
636@@ -684,6 +688,24 @@
637 return setupBrowser(auth="Basic %s:test" % str(email))
638
639
640+def setupBrowserFreshLogin(user):
641+ """Create a test browser with a recently logged in user.
642+
643+ The request is not shared by the browser, so we create
644+ a session of the test request and set a cookie to reference
645+ the session in the test browser.
646+ """
647+ request = LaunchpadTestRequest()
648+ session = ISession(request)
649+ authdata = session['launchpad.authenticateduser']
650+ authdata['logintime'] = datetime.utcnow()
651+ namespace = config.launchpad_session.cookie
652+ cookie = '%s=%s' % (namespace, session.client_id)
653+ browser = setupBrowserForUser(user)
654+ browser.addHeader('Cookie', cookie)
655+ return browser
656+
657+
658 def safe_canonical_url(*args, **kwargs):
659 """Generate a bytestring URL for an object"""
660 return str(canonical_url(*args, **kwargs))