Merge lp:~salgado/launchpad/bug-415977-2 into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Approved by: Gary Poster
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~salgado/launchpad/bug-415977-2
Merge into: lp:launchpad
Diff against target: 150 lines
3 files modified
database/schema/security.cfg (+1/-0)
lib/canonical/launchpad/webapp/login.py (+16/-7)
lib/lp/registry/stories/foaf/xx-resetpassword-of-sso-account.txt (+83/-0)
To merge this branch: bzr merge lp:~salgado/launchpad/bug-415977-2
Reviewer Review Type Date Requested Status
Gary Poster (community) Approve
Review via email: mp+14187@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Guilherme Salgado (salgado) wrote :

= Summary =

As reported on bug 415977, personless accounts can't use the
+forgottenpassword form on Launchpad because it doesn't know how to deal
with these accounts.

== Proposed fix ==

In the mailing list we discussed getting rid of LP's +forgottenpassword
and change all links to point to the same page on SSO, but that's not
possible because that page doesn't exist on SSO anymore -- they use the
login form with some javascript and a regular link to request a password
reset and we're not doing that in LP.

For that reason we had to resort to the other alternative, which is to
change the +forgottenpassword page to generate AuthTokens when the email
address is associated with a personless account.

== Pre-implementation notes ==

== Implementation details ==

== Tests ==

./bin/test -vvt resetpassword-of-sso-account.txt

== Demo and Q/A ==

= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  database/schema/security.cfg
  lib/canonical/launchpad/webapp/login.py
  lib/lp/registry/stories/foaf/xx-resetpassword-of-sso-account.txt

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

Very nicely done. Reviewing this patch introduced me to a number of valuable pieces of our testing infrastructure. :-)

Thank you!

Gary

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2009-10-29 05:50:08 +0000
3+++ database/schema/security.cfg 2009-10-29 19:50:24 +0000
4@@ -98,6 +98,7 @@
5 type=user
6 public.account = SELECT, INSERT, UPDATE, DELETE
7 public.accountpassword = SELECT, INSERT, UPDATE, DELETE
8+public.authtoken = SELECT, INSERT, UPDATE
9 public.emailaddress = SELECT, INSERT, UPDATE, DELETE
10 public.language = SELECT
11 public.openidrpconfig = SELECT
12
13=== modified file 'lib/canonical/launchpad/webapp/login.py'
14--- lib/canonical/launchpad/webapp/login.py 2009-10-16 16:13:00 +0000
15+++ lib/canonical/launchpad/webapp/login.py 2009-10-29 19:50:24 +0000
16@@ -24,7 +24,8 @@
17 from canonical.config import config
18 from canonical.launchpad import _
19 from canonical.launchpad.interfaces.account import AccountStatus
20-from canonical.launchpad.interfaces.authtoken import LoginTokenType
21+from canonical.launchpad.interfaces.authtoken import (
22+ IAuthTokenSet, LoginTokenType)
23 from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
24 from canonical.launchpad.interfaces.logintoken import ILoginTokenSet
25 from lp.registry.interfaces.person import (
26@@ -563,22 +564,30 @@
27 return
28
29 email = request.form.get("email").strip()
30- person = getUtility(IPersonSet).getByEmail(email)
31- if person is None:
32+ email_address = getUtility(IEmailAddressSet).getByEmail(email)
33+ if email_address is None:
34 self.errortext = ("Your account details have not been found. "
35 "Please check your subscription email "
36 "address and try again.")
37 return
38
39- if person.isTeam():
40+ person = email_address.person
41+ if person is not None and person.isTeam():
42 self.errortext = ("The email address <strong>%s</strong> "
43 "belongs to a team, and teams cannot log in to "
44 "Launchpad." % email)
45 return
46
47- logintokenset = getUtility(ILoginTokenSet)
48- token = logintokenset.new(
49- person, email, email, LoginTokenType.PASSWORDRECOVERY)
50+ if person is None:
51+ account = email_address.account
52+ redirection_url = urlappend(
53+ self.request.getApplicationURL(), '+login')
54+ token = getUtility(IAuthTokenSet).new(
55+ account, email, email, LoginTokenType.PASSWORDRECOVERY,
56+ redirection_url=redirection_url)
57+ else:
58+ token = getUtility(ILoginTokenSet).new(
59+ person, email, email, LoginTokenType.PASSWORDRECOVERY)
60 token.sendPasswordResetEmail()
61 self.submitted = True
62 return
63
64=== added file 'lib/lp/registry/stories/foaf/xx-resetpassword-of-sso-account.txt'
65--- lib/lp/registry/stories/foaf/xx-resetpassword-of-sso-account.txt 1970-01-01 00:00:00 +0000
66+++ lib/lp/registry/stories/foaf/xx-resetpassword-of-sso-account.txt 2009-10-29 19:50:24 +0000
67@@ -0,0 +1,83 @@
68+= Resetting the password of SSO-only accounts =
69+
70+Every once in a while people come to register Launchpad accounts using email
71+addresses that belong to SSO-only accounts. In these cases we explain to them
72+that they should use the SSO credentials to log into Launchpad, but it's
73+possible that they've forgotten their password so they'll try to use the
74+'Forgotten your password?' link to reset it.
75+
76+Since their account is SSO-only (i.e. it has no Person record associated
77+with), we can't use Launchpad's workflow for resetting the password -- it
78+expects a Person entry to be associated with the given email addres. Because
79+of that we'll allow the user to initiate the password-reset workflow on
80+Launchpad but we'll force the use of SSO's password-reset workflow to complete
81+it.
82+
83+ # Create a personless account to demostrate what we want.
84+ >>> login(ANONYMOUS)
85+ >>> account = factory.makeAccount('Test', email='test@example.com')
86+ >>> logout()
87+
88+Trying to register will give an error, telling the user to use the SSO
89+credentials to login.
90+
91+ >>> from lp.testing.registration import set_captcha_answer
92+ >>> browser.open('http://launchpad.dev/+login')
93+ >>> browser.getControl(name='loginpage_email', index=1).value = (
94+ ... 'test@example.com')
95+ >>> set_captcha_answer(browser, prefix='loginpage_')
96+ >>> browser.getControl('Register').click()
97+ >>> print_feedback_messages(browser.contents)
98+ The email address test@example.com is already registered in the Launchpad
99+ Login Service... Please use the same email and password to log into
100+ Launchpad.
101+
102+But the user has forgotten their credentials, so they'll have to reset the
103+password.
104+
105+ >>> browser.getLink('Forgotten your password?').click()
106+ >>> browser.url
107+ 'http://launchpad.dev/+forgottenpassword'
108+ >>> browser.getControl(name='email').value = 'test@example.com'
109+ >>> set_captcha_answer(browser)
110+ >>> browser.getControl('Request Reset').click()
111+ >>> print extract_text(find_main_content(browser.contents))
112+ Need a new Launchpad password?
113+ We have sent you an email with instructions to reset your password.
114+
115+ # Get the link from the email.
116+ >>> from lp.services.mail import stub
117+ >>> from canonical.launchpad.ftests.logintoken import (
118+ ... get_token_url_from_email)
119+ >>> len(stub.test_emails)
120+ 1
121+ >>> from_addr, to_addrs, raw_msg = stub.test_emails.pop()
122+ >>> link = get_token_url_from_email(raw_msg)
123+ >>> to_addrs
124+ ['test@example.com']
125+
126+Following the link sent by email will take the user to the SSO site to
127+complete the password reset.
128+
129+ >>> browser.open(link)
130+ >>> browser.url
131+ 'http://openid.launchpad.dev/token/.../+resetpassword'
132+ >>> browser.getControl(name='field.email').value = 'test@example.com'
133+ >>> browser.getControl(name='field.password').value = 'test'
134+ >>> browser.getControl(name='field.password_dupe').value = 'test'
135+ >>> browser.getControl('Continue').click()
136+ >>> print_feedback_messages(browser.contents)
137+ Your password has been reset successfully.
138+
139+Since the password reset was done on SSO, the user won't be logged into
140+Launchpad, so we redirect the user back to Launchpad's +login page for them to
141+login.
142+
143+ >>> browser.url
144+ 'http://launchpad.dev/+login'
145+ >>> browser.getControl(
146+ ... 'E-mail address:', index=0).value = 'test@example.com'
147+ >>> browser.getControl('Password').value = 'test'
148+ >>> browser.getControl('Log In').click()
149+ >>> print extract_text(find_tag_by_id(browser.contents, 'logincontrol'))
150+ Test...