Merge lp:~salgado/launchpad/bug-556594 into lp:launchpad

Proposed by Guilherme Salgado
Status: Merged
Merged at revision: not available
Proposed branch: lp:~salgado/launchpad/bug-556594
Merge into: lp:launchpad
Diff against target: 228 lines (+94/-35)
5 files modified
lib/canonical/launchpad/database/account.py (+0/-4)
lib/canonical/launchpad/webapp/login.py (+41/-11)
lib/canonical/launchpad/webapp/tests/test_login.py (+44/-2)
lib/lp/registry/doc/person-account.txt (+5/-16)
lib/lp/testing/factory.py (+4/-2)
To merge this branch: bzr merge lp:~salgado/launchpad/bug-556594
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) release-critical Approve
Brad Crittenden (community) code Approve
Review via email: mp+22906@code.launchpad.net

Description of the change

Fix account-reactivation upon logging in

While fixing bug 556594 I realized we weren't actually reactivating
accounts on login, so I had to fix this first.

As part of this I had to fix the makeAccount() factory method to not
create deactivated accounts with preferred email addresses (as that's
not a valid state for them to be in), and also changed
IAccount.reactivate() to not assert that the password is not empty as
that doesn't matter anymore (the password is no longer used for anything
in LP, but it'll be around until we find the time to get rid of the
Account and AccountPassword tables).

= Launchpad lint =

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

Linting changed files:
  lib/canonical/launchpad/database/account.py
  lib/canonical/launchpad/webapp/login.py
  lib/lp/testing/factory.py
  lib/canonical/launchpad/webapp/tests/test_login.py

== Pylint notices ==

lib/canonical/launchpad/database/account.py
    9: [F0401] Unable to import 'zope.component'
    10: [F0401] Unable to import 'zope.security.proxy'
    11: [F0401] Unable to import 'zope.interface'

lib/canonical/launchpad/webapp/login.py
    5: [W0105] String statement has no effect
    21: [F0401] Unable to import 'zope.app.security.interfaces'
    22: [F0401] Unable to import 'zope.component'
    23: [F0401] Unable to import 'zope.event'
    24: [F0401] Unable to import 'zope.interface'
    25: [F0401] Unable to import 'zope.publisher.browser'
    26: [F0401] Unable to import 'zope.publisher.interfaces.http'
    27: [F0401] Unable to import 'zope.security.proxy'
    28: [F0401] Unable to import 'zope.session.interfaces'

lib/lp/testing/factory.py
    33: [F0401] Unable to import 'zope.component'
    34: [F0401] Unable to import 'zope.security.proxy'
    1753: [F0401, LaunchpadObjectFactory.makeRecipe] Unable to import 'bzrlib.plugins.builder.recipe'

lib/canonical/launchpad/webapp/tests/test_login.py
    21: [F0401] Unable to import 'zope.component'
    22: [F0401] Unable to import 'zope.security.management'
    23: [F0401] Unable to import 'zope.security.proxy'
    24: [F0401] Unable to import 'zope.session.interfaces'

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Salgado,

This branch looks good. I'd change one thing:
s/'Reactivated by the user herself'/'Reactivated by the user'/

It conveys the full message without getting into the need for a gender-specific pronoun.

review: Approve (code)
Revision history for this message
Guilherme Salgado (salgado) wrote :

r10632 fixes the actual bug.

When logging in with an mail address that's associated with a different
OpenID identifier, change the identifier associated with the registered
email address to match the one given by the OP. (Fixes bug 556594)

Part of the changes on this branch have already been reviewed
by Brad, so I'm including the incremental diff here: http://paste.ubuntu.com/410236/

Revision history for this message
Brad Crittenden (bac) wrote :

The additional changes look good Salgado. Thanks for the update.

review: Approve (code)
Revision history for this message
Francis J. Lacoste (flacoste) :
review: Approve (release-critical)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/database/account.py'
2--- lib/canonical/launchpad/database/account.py 2010-02-26 02:33:23 +0000
3+++ lib/canonical/launchpad/database/account.py 2010-04-07 12:53:31 +0000
4@@ -144,10 +144,6 @@
5
6 def reactivate(self, comment, password, preferred_email):
7 """See `IAccountSpecialRestricted`."""
8- if password in (None, ''):
9- raise AssertionError(
10- "Account %s cannot be reactivated without a "
11- "password." % self.id)
12 self.activate(comment, password, preferred_email)
13
14 # The password is actually stored in a separate table for security
15
16=== modified file 'lib/canonical/launchpad/webapp/login.py'
17--- lib/canonical/launchpad/webapp/login.py 2010-04-05 17:09:46 +0000
18+++ lib/canonical/launchpad/webapp/login.py 2010-04-07 12:53:31 +0000
19@@ -29,9 +29,11 @@
20
21 from z3c.ptcompat import ViewPageTemplateFile
22
23+from canonical.cachedproperty import cachedproperty
24 from canonical.config import config
25 from canonical.launchpad import _
26 from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet
27+from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
28 from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore
29 from lp.registry.interfaces.person import IPerson, PersonCreationRationale
30 from canonical.launchpad.readonly import is_read_only
31@@ -256,6 +258,10 @@
32 logInPrincipal(
33 self.request, loginsource.getPrincipalByLogin(email), email)
34
35+ @cachedproperty
36+ def sreg_response(self):
37+ return sreg.SRegResponse.fromSuccessResponse(self.openid_response)
38+
39 def _createAccount(self, openid_identifier):
40 # Here we assume the OP sent us the user's email address and
41 # full name in the response. Note we can only do that because
42@@ -264,21 +270,31 @@
43 # asked to. Once we start using other OPs we won't be able to
44 # make this assumption here as they might not include what we
45 # want in the response.
46- sreg_response = sreg.SRegResponse.fromSuccessResponse(
47- self.openid_response)
48- assert sreg_response is not None, (
49+ assert self.sreg_response is not None, (
50 "OP didn't include an sreg extension in the response.")
51- email_address = sreg_response.get('email')
52- full_name = sreg_response.get('fullname')
53+ email_address = self.sreg_response.get('email')
54+ full_name = self.sreg_response.get('fullname')
55 assert email_address is not None and full_name is not None, (
56 "No email address or full name found in sreg response; "
57 "can't create a new account for this identity URL.")
58- account, email = getUtility(IAccountSet).createAccountAndEmail(
59- email_address,
60- PersonCreationRationale.OWNER_CREATED_LAUNCHPAD,
61- full_name,
62- password=None,
63- openid_identifier=openid_identifier)
64+ email = getUtility(IEmailAddressSet).getByEmail(email_address)
65+ if email is not None:
66+ account = email.account
67+ assert account is not None, (
68+ "This email address should have an associated account.")
69+ removeSecurityProxy(account).openid_identifier = openid_identifier
70+ if account.status == AccountStatus.DEACTIVATED:
71+ comment = 'Reactivated by the user'
72+ password = None # Needed just to please reactivate() below.
73+ removeSecurityProxy(account).reactivate(
74+ comment, password, removeSecurityProxy(email))
75+ else:
76+ account, email = getUtility(IAccountSet).createAccountAndEmail(
77+ email_address,
78+ PersonCreationRationale.OWNER_CREATED_LAUNCHPAD,
79+ full_name,
80+ password=None,
81+ openid_identifier=openid_identifier)
82 return account
83
84 def render(self):
85@@ -298,6 +314,20 @@
86
87 if account.status == AccountStatus.SUSPENDED:
88 return self.suspended_account_template()
89+ elif account.status == AccountStatus.DEACTIVATED:
90+ comment = 'Reactivated by the user'
91+ password = '' # Needed just to please reactivate() below.
92+ sreg_email = self.sreg_response.get('email')
93+ email_address = getUtility(IEmailAddressSet).getByEmail(
94+ sreg_email)
95+ if email_address is None:
96+ email_address = getUtility(IEmailAddressSet).new(
97+ sreg_email, account=account)
98+ removeSecurityProxy(account).reactivate(
99+ comment, password, removeSecurityProxy(email_address))
100+ else:
101+ # Account is active, so nothing to do.
102+ pass
103 if IPerson(account, None) is None:
104 removeSecurityProxy(account).createPerson(
105 PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
106
107=== modified file 'lib/canonical/launchpad/webapp/tests/test_login.py'
108--- lib/canonical/launchpad/webapp/tests/test_login.py 2010-03-30 14:57:02 +0000
109+++ lib/canonical/launchpad/webapp/tests/test_login.py 2010-04-07 12:53:31 +0000
110@@ -199,19 +199,61 @@
111 # stuff.
112 self.assertLastWriteIsSet(view.request)
113
114+ def test_unseen_identity_with_registered_email(self):
115+ # When we get a positive assertion about an identity URL we've never
116+ # seen but whose email address is already registered, we just change
117+ # the identity URL that's associated with the existing email address.
118+ identifier = '4w7kmzU'
119+ email = 'test@example.com'
120+ account = self.factory.makeAccount(
121+ 'Test account', email=email, status=AccountStatus.DEACTIVATED)
122+ account_set = getUtility(IAccountSet)
123+ self.assertRaises(
124+ LookupError, account_set.getByOpenIDIdentifier, identifier)
125+ openid_response = FakeOpenIDResponse(
126+ 'http://testopenid.dev/+id/%s' % identifier, status=SUCCESS,
127+ email=email, full_name='Foo User')
128+ with SRegResponse_fromSuccessResponse_stubbed():
129+ view, html = self._createAndRenderView(openid_response)
130+ self.assertTrue(view.login_called)
131+
132+ # The existing account's openid_identifier was updated, the account
133+ # was reactivated and its preferred email was set, but its display
134+ # name was not changed.
135+ self.assertEquals(identifier, account.openid_identifier)
136+ self.assertEquals(AccountStatus.ACTIVE, account.status)
137+ self.assertEquals(
138+ email, removeSecurityProxy(account.preferredemail).email)
139+ person = IPerson(account, None)
140+ self.assertIsNot(None, person)
141+ self.assertEquals('Test account', person.displayname)
142+
143+ # We also update the last_write flag in the session, to make sure
144+ # further requests use the master DB and thus see the newly created
145+ # stuff.
146+ self.assertLastWriteIsSet(view.request)
147+
148 def test_deactivated_account(self):
149 # The user has the account's password and is trying to login, so we'll
150 # just re-activate their account.
151+ email = 'foo@example.com'
152 account = self.factory.makeAccount(
153- 'Test account', status=AccountStatus.DEACTIVATED)
154+ 'Test account', email=email, status=AccountStatus.DEACTIVATED)
155 self.assertIs(None, IPerson(account, None))
156- view, html = self._createViewWithResponse(account)
157+ openid_identifier = removeSecurityProxy(account).openid_identifier
158+ openid_response = FakeOpenIDResponse(
159+ 'http://testopenid.dev/+id/%s' % openid_identifier,
160+ status=SUCCESS, email=email, full_name=account.displayname)
161+ with SRegResponse_fromSuccessResponse_stubbed():
162+ view, html = self._createAndRenderView(openid_response)
163 self.assertIsNot(None, IPerson(account, None))
164 self.assertTrue(view.login_called)
165 response = view.request.response
166 self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())
167 self.assertEquals(view.request.form['starting_url'],
168 response.getHeader('Location'))
169+ self.assertEquals(AccountStatus.ACTIVE, account.status)
170+ self.assertEquals(email, account.preferredemail.email)
171 # We also update the last_write flag in the session, to make sure
172 # further requests use the master DB and thus see the newly created
173 # stuff.
174
175=== modified file 'lib/lp/registry/doc/person-account.txt'
176--- lib/lp/registry/doc/person-account.txt 2010-01-19 14:43:13 +0000
177+++ lib/lp/registry/doc/person-account.txt 2010-04-07 12:53:31 +0000
178@@ -208,27 +208,16 @@
179
180 == Reactivating user accounts ==
181
182-Accounts can be reactivated. A comment is required to log why this was
183-done, and a password is required too. There are two preconditions before
184-this method can be called on an Account: The password and the preferred
185-email address cannot not be None.
186-
187- >>> foobar.account.reactivate(
188- ... 'This will raise an error.', password=None, preferred_email=None)
189- Traceback (most recent call last):
190- ...
191- AssertionError: Account ... cannot be reactivated without a password.
192-
193- >>> foobar.account.reactivate(
194- ... 'This will raise an error.', password='ok', preferred_email=None)
195+Accounts can be reactivated. A comment and a non-None preferred email address
196+are required to reactivate() an account, though.
197+
198+ >>> foobar.account.reactivate(
199+ ... 'This will raise an error.', password='', preferred_email=None)
200 Traceback (most recent call last):
201 ...
202 AssertionError: Account ... cannot be activated without a preferred
203 email address.
204
205-The account can be reactivated after the password and preferred email
206-address are set.
207-
208 >>> foobar.reactivate(
209 ... 'User reactivated the account using reset password.',
210 ... password="ok",
211
212=== modified file 'lib/lp/testing/factory.py'
213--- lib/lp/testing/factory.py 2010-04-05 17:40:35 +0000
214+++ lib/lp/testing/factory.py 2010-04-07 12:53:31 +0000
215@@ -340,9 +340,11 @@
216 removeSecurityProxy(account).status = status
217 if email is None:
218 email = self.getUniqueEmailAddress()
219+ email_status = EmailAddressStatus.PREFERRED
220+ if status != AccountStatus.ACTIVE:
221+ email_status = EmailAddressStatus.NEW
222 email = self.makeEmail(
223- email, person=None, account=account,
224- email_status=EmailAddressStatus.PREFERRED)
225+ email, person=None, account=account, email_status=email_status)
226 if commit:
227 transaction.commit()
228 return account