Merge lp:~salgado/launchpad/bug-568102 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-568102
Merge into: lp:launchpad
Diff against target: 253 lines (+123/-77)
3 files modified
lib/canonical/launchpad/testing/browser.py (+1/-1)
lib/canonical/launchpad/webapp/login.py (+100/-76)
lib/canonical/launchpad/webapp/tests/test_login.py (+22/-0)
To merge this branch: bzr merge lp:~salgado/launchpad/bug-568102
Reviewer Review Type Date Requested Status
Gary Poster (community) Approve
Review via email: mp+24229@code.launchpad.net

Description of the change

This branch contains a fix for bug 568102.

The actual fix is trivial and can be seen on r10785, together with its
test.

However, to make the trivial fix possible I had to refactor a few things
on the view. This refactoring is on r10784.

I think it will be easier to review them separately.

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Very nicely done. Good advice to look at the two revisions separately, thank you.

My only suggestion is to add a comment within processPositiveAssertion for the code that creates an account when there was a positive OpenId assertion but no Account--something that just says what the code path is for. On this order:

# We got an OpenID response containing a positive assertion, but we don't have an account for the identifier. We'll create one.

Thank you!

Gary

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

Went with a slightly modified version of that comment:

         # We got an OpenID response containing a positive
         # assertion, but we don't have an account for the
         # identifier or for the email address. We'll create one.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/testing/browser.py'
2--- lib/canonical/launchpad/testing/browser.py 2010-03-19 19:57:30 +0000
3+++ lib/canonical/launchpad/testing/browser.py 2010-04-27 19:15:40 +0000
4@@ -28,7 +28,7 @@
5 import weakref
6 import transaction
7
8-from zope.testbrowser.browser import Browser as _Browser
9+from zope.testbrowser.browser import Browser as _Browser, fix_exception_name
10
11 from canonical.launchpad.testing.pages import (
12 extract_text, find_main_content, find_tag_by_id, get_feedback_messages)
13
14=== modified file 'lib/canonical/launchpad/webapp/login.py'
15--- lib/canonical/launchpad/webapp/login.py 2010-04-06 20:40:14 +0000
16+++ lib/canonical/launchpad/webapp/login.py 2010-04-27 19:15:40 +0000
17@@ -262,7 +262,7 @@
18 def sreg_response(self):
19 return sreg.SRegResponse.fromSuccessResponse(self.openid_response)
20
21- def _createAccount(self, openid_identifier):
22+ def _getEmailAddressAndFullName(self):
23 # Here we assume the OP sent us the user's email address and
24 # full name in the response. Note we can only do that because
25 # we used a fixed OP (login.launchpad.net) that includes the
26@@ -275,95 +275,119 @@
27 email_address = self.sreg_response.get('email')
28 full_name = self.sreg_response.get('fullname')
29 assert email_address is not None and full_name is not None, (
30- "No email address or full name found in sreg response; "
31- "can't create a new account for this identity URL.")
32- email = getUtility(IEmailAddressSet).getByEmail(email_address)
33- if email is not None:
34- account = email.account
35- assert account is not None, (
36- "This email address should have an associated account.")
37- removeSecurityProxy(account).openid_identifier = openid_identifier
38- if account.status == AccountStatus.DEACTIVATED:
39+ "No email address or full name found in sreg response")
40+ return email_address, full_name
41+
42+ def _createAccount(self, openid_identifier, email_address, full_name):
43+ account, email = getUtility(IAccountSet).createAccountAndEmail(
44+ email_address, PersonCreationRationale.OWNER_CREATED_LAUNCHPAD,
45+ full_name, password=None, openid_identifier=openid_identifier)
46+ return account
47+
48+ def processPositiveAssertion(self):
49+ """Process an OpenID response containing a positive assertion.
50+
51+ We'll get the account with the given OpenID identifier (creating one
52+ if it doesn't already exist) and act according to the account's state:
53+ - If the account is suspended, we stop and render an error page;
54+ - If the account is deactivated, we reactivate it and proceed;
55+ - If the account is active, we just proceed.
56+
57+ After that we ensure there's an IPerson associated with the account
58+ and login using that account.
59+
60+ We also update the 'last_write' key in the session if we've done any
61+ DB writes, to ensure subsequent requests use the master DB and see
62+ the changes we just did.
63+ """
64+ identifier = self.openid_response.identity_url.split('/')[-1]
65+ should_update_last_write = False
66+ email_set = getUtility(IEmailAddressSet)
67+ # Force the use of the master database to make sure a lagged slave
68+ # doesn't fool us into creating a Person/Account when one already
69+ # exists.
70+ with MasterDatabasePolicy():
71+ try:
72+ account = getUtility(IAccountSet).getByOpenIDIdentifier(
73+ identifier)
74+ except LookupError:
75+ # The two lines below are duplicated a few more lines down,
76+ # but to avoid this duplication we'd have to refactor most of
77+ # our tests to provide an SREG response, which would be rather
78+ # painful.
79+ email_address, full_name = self._getEmailAddressAndFullName()
80+ email = email_set.getByEmail(email_address)
81+ if email is None:
82+ # We got an OpenID response containing a positive
83+ # assertion, but we don't have an account for the
84+ # identifier or for the email address. We'll create one.
85+ account = self._createAccount(
86+ identifier, email_address, full_name)
87+ else:
88+ account = email.account
89+ assert account is not None, (
90+ "This email address should have an associated "
91+ "account.")
92+ removeSecurityProxy(account).openid_identifier = (
93+ identifier)
94+ should_update_last_write = True
95+
96+ if account.status == AccountStatus.SUSPENDED:
97+ return self.suspended_account_template()
98+ elif account.status in [AccountStatus.DEACTIVATED,
99+ AccountStatus.NOACCOUNT]:
100 comment = 'Reactivated by the user'
101- password = None # Needed just to please reactivate() below.
102+ password = '' # Needed just to please reactivate() below.
103+ email_address, dummy = self._getEmailAddressAndFullName()
104+ email = email_set.getByEmail(email_address)
105+ if email is None:
106+ email = email_set.new(email_address, account=account)
107 removeSecurityProxy(account).reactivate(
108 comment, password, removeSecurityProxy(email))
109- else:
110- account, email = getUtility(IAccountSet).createAccountAndEmail(
111- email_address,
112- PersonCreationRationale.OWNER_CREATED_LAUNCHPAD,
113- full_name,
114- password=None,
115- openid_identifier=openid_identifier)
116- return account
117+ else:
118+ # Account is active, so nothing to do.
119+ pass
120+ if IPerson(account, None) is None:
121+ removeSecurityProxy(account).createPerson(
122+ PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
123+ should_update_last_write = True
124+ self.login(account)
125+
126+ if should_update_last_write:
127+ # This is a GET request but we changed the database, so update
128+ # session_data['last_write'] to make sure further requests use
129+ # the master DB and thus see the changes we've just made.
130+ session_data = ISession(self.request)['lp.dbpolicy']
131+ session_data['last_write'] = datetime.utcnow()
132+ self._redirect()
133+ # No need to return anything as we redirect above.
134+ return None
135
136 def render(self):
137 if self.openid_response.status == SUCCESS:
138- identifier = self.openid_response.identity_url.split('/')[-1]
139- should_update_last_write = False
140- # Force the use of the master database to make sure a lagged slave
141- # doesn't fool us into creating a Person/Account when one already
142- # exists.
143- with MasterDatabasePolicy():
144- try:
145- account = getUtility(IAccountSet).getByOpenIDIdentifier(
146- identifier)
147- except LookupError:
148- account = self._createAccount(identifier)
149- should_update_last_write = True
150-
151- if account.status == AccountStatus.SUSPENDED:
152- return self.suspended_account_template()
153- elif account.status == AccountStatus.DEACTIVATED:
154- comment = 'Reactivated by the user'
155- password = '' # Needed just to please reactivate() below.
156- sreg_email = self.sreg_response.get('email')
157- email_address = getUtility(IEmailAddressSet).getByEmail(
158- sreg_email)
159- if email_address is None:
160- email_address = getUtility(IEmailAddressSet).new(
161- sreg_email, account=account)
162- removeSecurityProxy(account).reactivate(
163- comment, password, removeSecurityProxy(email_address))
164- else:
165- # Account is active, so nothing to do.
166- pass
167- if IPerson(account, None) is None:
168- removeSecurityProxy(account).createPerson(
169- PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
170- should_update_last_write = True
171- self.login(account)
172-
173- if should_update_last_write:
174- # This is a GET request but we changed the database, so update
175- # session_data['last_write'] to make sure further requests use
176- # the master DB and thus see the changes we've just made.
177- session_data = ISession(self.request)['lp.dbpolicy']
178- session_data['last_write'] = datetime.utcnow()
179+ return self.processPositiveAssertion()
180+
181+ if self.account is not None:
182+ # The authentication failed (or was canceled), but the user is
183+ # already logged in, so we just add a notification message and
184+ # redirect.
185+ self.request.response.addNoticeNotification(
186+ _(u'Your authentication failed but you were already '
187+ 'logged into Launchpad.'))
188 self._redirect()
189 # No need to return anything as we redirect above.
190- retval = None
191+ return None
192 else:
193- if self.account is not None:
194- # The authentication failed (or was canceled), but the user is
195- # already logged in, so we just add a notification message and
196- # redirect.
197- self.request.response.addNoticeNotification(
198- _(u'Your authentication failed but you were already '
199- 'logged into Launchpad.'))
200- self._redirect()
201- # No need to return anything as we redirect above.
202- retval = None
203- else:
204- retval = OpenIDLoginErrorView(
205- self.context, self.request, self.openid_response)()
206+ return OpenIDLoginErrorView(
207+ self.context, self.request, self.openid_response)()
208
209- # The consumer.complete() call above will create entries in
210+ def __call__(self):
211+ retval = super(OpenIDCallbackView, self).__call__()
212+ # The consumer.complete() call in initialize() will create entries in
213 # OpenIDConsumerNonce to prevent replay attacks, but since this will
214 # be a GET request, the transaction would be rolled back, so we need
215 # an explicit commit here.
216 transaction.commit()
217-
218 return retval
219
220 def _redirect(self):
221
222=== modified file 'lib/canonical/launchpad/webapp/tests/test_login.py'
223--- lib/canonical/launchpad/webapp/tests/test_login.py 2010-04-06 20:40:14 +0000
224+++ lib/canonical/launchpad/webapp/tests/test_login.py 2010-04-27 19:15:40 +0000
225@@ -259,6 +259,28 @@
226 # stuff.
227 self.assertLastWriteIsSet(view.request)
228
229+ def test_never_used_account(self):
230+ # The account was created by one of our scripts but was never
231+ # activated, so we just activate it.
232+ email = 'foo@example.com'
233+ account = self.factory.makeAccount(
234+ 'Test account', email=email, status=AccountStatus.NOACCOUNT)
235+ self.assertIs(None, IPerson(account, None))
236+ openid_identifier = removeSecurityProxy(account).openid_identifier
237+ openid_response = FakeOpenIDResponse(
238+ 'http://testopenid.dev/+id/%s' % openid_identifier,
239+ status=SUCCESS, email=email, full_name=account.displayname)
240+ with SRegResponse_fromSuccessResponse_stubbed():
241+ view, html = self._createAndRenderView(openid_response)
242+ self.assertIsNot(None, IPerson(account, None))
243+ self.assertTrue(view.login_called)
244+ self.assertEquals(AccountStatus.ACTIVE, account.status)
245+ self.assertEquals(email, account.preferredemail.email)
246+ # We also update the last_write flag in the session, to make sure
247+ # further requests use the master DB and thus see the newly created
248+ # stuff.
249+ self.assertLastWriteIsSet(view.request)
250+
251 def test_suspended_account(self):
252 # There's a chance that our OpenID Provider lets a suspended account
253 # login, but we must not allow that.