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
=== modified file 'lib/canonical/launchpad/database/account.py'
--- lib/canonical/launchpad/database/account.py 2010-02-26 02:33:23 +0000
+++ lib/canonical/launchpad/database/account.py 2010-04-07 12:53:31 +0000
@@ -144,10 +144,6 @@
144144
145 def reactivate(self, comment, password, preferred_email):145 def reactivate(self, comment, password, preferred_email):
146 """See `IAccountSpecialRestricted`."""146 """See `IAccountSpecialRestricted`."""
147 if password in (None, ''):
148 raise AssertionError(
149 "Account %s cannot be reactivated without a "
150 "password." % self.id)
151 self.activate(comment, password, preferred_email)147 self.activate(comment, password, preferred_email)
152148
153 # The password is actually stored in a separate table for security149 # The password is actually stored in a separate table for security
154150
=== modified file 'lib/canonical/launchpad/webapp/login.py'
--- lib/canonical/launchpad/webapp/login.py 2010-04-05 17:09:46 +0000
+++ lib/canonical/launchpad/webapp/login.py 2010-04-07 12:53:31 +0000
@@ -29,9 +29,11 @@
2929
30from z3c.ptcompat import ViewPageTemplateFile30from z3c.ptcompat import ViewPageTemplateFile
3131
32from canonical.cachedproperty import cachedproperty
32from canonical.config import config33from canonical.config import config
33from canonical.launchpad import _34from canonical.launchpad import _
34from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet35from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet
36from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
35from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore37from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore
36from lp.registry.interfaces.person import IPerson, PersonCreationRationale38from lp.registry.interfaces.person import IPerson, PersonCreationRationale
37from canonical.launchpad.readonly import is_read_only39from canonical.launchpad.readonly import is_read_only
@@ -256,6 +258,10 @@
256 logInPrincipal(258 logInPrincipal(
257 self.request, loginsource.getPrincipalByLogin(email), email)259 self.request, loginsource.getPrincipalByLogin(email), email)
258260
261 @cachedproperty
262 def sreg_response(self):
263 return sreg.SRegResponse.fromSuccessResponse(self.openid_response)
264
259 def _createAccount(self, openid_identifier):265 def _createAccount(self, openid_identifier):
260 # Here we assume the OP sent us the user's email address and266 # Here we assume the OP sent us the user's email address and
261 # full name in the response. Note we can only do that because267 # full name in the response. Note we can only do that because
@@ -264,21 +270,31 @@
264 # asked to. Once we start using other OPs we won't be able to270 # asked to. Once we start using other OPs we won't be able to
265 # make this assumption here as they might not include what we271 # make this assumption here as they might not include what we
266 # want in the response.272 # want in the response.
267 sreg_response = sreg.SRegResponse.fromSuccessResponse(273 assert self.sreg_response is not None, (
268 self.openid_response)
269 assert sreg_response is not None, (
270 "OP didn't include an sreg extension in the response.")274 "OP didn't include an sreg extension in the response.")
271 email_address = sreg_response.get('email')275 email_address = self.sreg_response.get('email')
272 full_name = sreg_response.get('fullname')276 full_name = self.sreg_response.get('fullname')
273 assert email_address is not None and full_name is not None, (277 assert email_address is not None and full_name is not None, (
274 "No email address or full name found in sreg response; "278 "No email address or full name found in sreg response; "
275 "can't create a new account for this identity URL.")279 "can't create a new account for this identity URL.")
276 account, email = getUtility(IAccountSet).createAccountAndEmail(280 email = getUtility(IEmailAddressSet).getByEmail(email_address)
277 email_address,281 if email is not None:
278 PersonCreationRationale.OWNER_CREATED_LAUNCHPAD,282 account = email.account
279 full_name,283 assert account is not None, (
280 password=None,284 "This email address should have an associated account.")
281 openid_identifier=openid_identifier)285 removeSecurityProxy(account).openid_identifier = openid_identifier
286 if account.status == AccountStatus.DEACTIVATED:
287 comment = 'Reactivated by the user'
288 password = None # Needed just to please reactivate() below.
289 removeSecurityProxy(account).reactivate(
290 comment, password, removeSecurityProxy(email))
291 else:
292 account, email = getUtility(IAccountSet).createAccountAndEmail(
293 email_address,
294 PersonCreationRationale.OWNER_CREATED_LAUNCHPAD,
295 full_name,
296 password=None,
297 openid_identifier=openid_identifier)
282 return account298 return account
283299
284 def render(self):300 def render(self):
@@ -298,6 +314,20 @@
298314
299 if account.status == AccountStatus.SUSPENDED:315 if account.status == AccountStatus.SUSPENDED:
300 return self.suspended_account_template()316 return self.suspended_account_template()
317 elif account.status == AccountStatus.DEACTIVATED:
318 comment = 'Reactivated by the user'
319 password = '' # Needed just to please reactivate() below.
320 sreg_email = self.sreg_response.get('email')
321 email_address = getUtility(IEmailAddressSet).getByEmail(
322 sreg_email)
323 if email_address is None:
324 email_address = getUtility(IEmailAddressSet).new(
325 sreg_email, account=account)
326 removeSecurityProxy(account).reactivate(
327 comment, password, removeSecurityProxy(email_address))
328 else:
329 # Account is active, so nothing to do.
330 pass
301 if IPerson(account, None) is None:331 if IPerson(account, None) is None:
302 removeSecurityProxy(account).createPerson(332 removeSecurityProxy(account).createPerson(
303 PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)333 PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
304334
=== modified file 'lib/canonical/launchpad/webapp/tests/test_login.py'
--- lib/canonical/launchpad/webapp/tests/test_login.py 2010-03-30 14:57:02 +0000
+++ lib/canonical/launchpad/webapp/tests/test_login.py 2010-04-07 12:53:31 +0000
@@ -199,19 +199,61 @@
199 # stuff. 199 # stuff.
200 self.assertLastWriteIsSet(view.request)200 self.assertLastWriteIsSet(view.request)
201201
202 def test_unseen_identity_with_registered_email(self):
203 # When we get a positive assertion about an identity URL we've never
204 # seen but whose email address is already registered, we just change
205 # the identity URL that's associated with the existing email address.
206 identifier = '4w7kmzU'
207 email = 'test@example.com'
208 account = self.factory.makeAccount(
209 'Test account', email=email, status=AccountStatus.DEACTIVATED)
210 account_set = getUtility(IAccountSet)
211 self.assertRaises(
212 LookupError, account_set.getByOpenIDIdentifier, identifier)
213 openid_response = FakeOpenIDResponse(
214 'http://testopenid.dev/+id/%s' % identifier, status=SUCCESS,
215 email=email, full_name='Foo User')
216 with SRegResponse_fromSuccessResponse_stubbed():
217 view, html = self._createAndRenderView(openid_response)
218 self.assertTrue(view.login_called)
219
220 # The existing account's openid_identifier was updated, the account
221 # was reactivated and its preferred email was set, but its display
222 # name was not changed.
223 self.assertEquals(identifier, account.openid_identifier)
224 self.assertEquals(AccountStatus.ACTIVE, account.status)
225 self.assertEquals(
226 email, removeSecurityProxy(account.preferredemail).email)
227 person = IPerson(account, None)
228 self.assertIsNot(None, person)
229 self.assertEquals('Test account', person.displayname)
230
231 # We also update the last_write flag in the session, to make sure
232 # further requests use the master DB and thus see the newly created
233 # stuff.
234 self.assertLastWriteIsSet(view.request)
235
202 def test_deactivated_account(self):236 def test_deactivated_account(self):
203 # The user has the account's password and is trying to login, so we'll237 # The user has the account's password and is trying to login, so we'll
204 # just re-activate their account.238 # just re-activate their account.
239 email = 'foo@example.com'
205 account = self.factory.makeAccount(240 account = self.factory.makeAccount(
206 'Test account', status=AccountStatus.DEACTIVATED)241 'Test account', email=email, status=AccountStatus.DEACTIVATED)
207 self.assertIs(None, IPerson(account, None))242 self.assertIs(None, IPerson(account, None))
208 view, html = self._createViewWithResponse(account)243 openid_identifier = removeSecurityProxy(account).openid_identifier
244 openid_response = FakeOpenIDResponse(
245 'http://testopenid.dev/+id/%s' % openid_identifier,
246 status=SUCCESS, email=email, full_name=account.displayname)
247 with SRegResponse_fromSuccessResponse_stubbed():
248 view, html = self._createAndRenderView(openid_response)
209 self.assertIsNot(None, IPerson(account, None))249 self.assertIsNot(None, IPerson(account, None))
210 self.assertTrue(view.login_called)250 self.assertTrue(view.login_called)
211 response = view.request.response251 response = view.request.response
212 self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())252 self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())
213 self.assertEquals(view.request.form['starting_url'],253 self.assertEquals(view.request.form['starting_url'],
214 response.getHeader('Location'))254 response.getHeader('Location'))
255 self.assertEquals(AccountStatus.ACTIVE, account.status)
256 self.assertEquals(email, account.preferredemail.email)
215 # We also update the last_write flag in the session, to make sure257 # We also update the last_write flag in the session, to make sure
216 # further requests use the master DB and thus see the newly created258 # further requests use the master DB and thus see the newly created
217 # stuff. 259 # stuff.
218260
=== modified file 'lib/lp/registry/doc/person-account.txt'
--- lib/lp/registry/doc/person-account.txt 2010-01-19 14:43:13 +0000
+++ lib/lp/registry/doc/person-account.txt 2010-04-07 12:53:31 +0000
@@ -208,27 +208,16 @@
208208
209== Reactivating user accounts ==209== Reactivating user accounts ==
210210
211Accounts can be reactivated. A comment is required to log why this was211Accounts can be reactivated. A comment and a non-None preferred email address
212done, and a password is required too. There are two preconditions before212are required to reactivate() an account, though.
213this method can be called on an Account: The password and the preferred213
214email address cannot not be None.214 >>> foobar.account.reactivate(
215215 ... 'This will raise an error.', password='', preferred_email=None)
216 >>> foobar.account.reactivate(
217 ... 'This will raise an error.', password=None, preferred_email=None)
218 Traceback (most recent call last):
219 ...
220 AssertionError: Account ... cannot be reactivated without a password.
221
222 >>> foobar.account.reactivate(
223 ... 'This will raise an error.', password='ok', preferred_email=None)
224 Traceback (most recent call last):216 Traceback (most recent call last):
225 ...217 ...
226 AssertionError: Account ... cannot be activated without a preferred218 AssertionError: Account ... cannot be activated without a preferred
227 email address.219 email address.
228220
229The account can be reactivated after the password and preferred email
230address are set.
231
232 >>> foobar.reactivate(221 >>> foobar.reactivate(
233 ... 'User reactivated the account using reset password.',222 ... 'User reactivated the account using reset password.',
234 ... password="ok",223 ... password="ok",
235224
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-04-05 17:40:35 +0000
+++ lib/lp/testing/factory.py 2010-04-07 12:53:31 +0000
@@ -340,9 +340,11 @@
340 removeSecurityProxy(account).status = status340 removeSecurityProxy(account).status = status
341 if email is None:341 if email is None:
342 email = self.getUniqueEmailAddress()342 email = self.getUniqueEmailAddress()
343 email_status = EmailAddressStatus.PREFERRED
344 if status != AccountStatus.ACTIVE:
345 email_status = EmailAddressStatus.NEW
343 email = self.makeEmail(346 email = self.makeEmail(
344 email, person=None, account=account,347 email, person=None, account=account, email_status=email_status)
345 email_status=EmailAddressStatus.PREFERRED)
346 if commit:348 if commit:
347 transaction.commit()349 transaction.commit()
348 return account350 return account