Merge lp:~michael.nelson/launchpad/db-598464-get-or-create-from-identity into lp:launchpad/db-devel
- db-598464-get-or-create-from-identity
- Merge into db-devel
Status: | Merged |
---|---|
Approved by: | Michael Nelson |
Approved revision: | no longer in the source branch. |
Merged at revision: | 9526 |
Proposed branch: | lp:~michael.nelson/launchpad/db-598464-get-or-create-from-identity |
Merge into: | lp:launchpad/db-devel |
Prerequisite: | lp:~stevenk/launchpad/db-software-center-agent-celebrity |
Diff against target: |
596 lines (+257/-78) 9 files modified
lib/canonical/launchpad/database/account.py (+4/-2) lib/canonical/launchpad/interfaces/account.py (+14/-1) lib/canonical/launchpad/security.py (+20/-0) lib/canonical/launchpad/webapp/login.py (+20/-63) lib/canonical/launchpad/webapp/tests/test_login.py (+13/-9) lib/lp/registry/interfaces/person.py (+34/-0) lib/lp/registry/model/person.py (+55/-0) lib/lp/registry/tests/test_personset.py (+92/-1) lib/lp/testopenid/browser/server.py (+5/-2) |
To merge this branch: | bzr merge lp:~michael.nelson/launchpad/db-598464-get-or-create-from-identity |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Abel Deuring (community) | code | Approve | |
Guilherme Salgado | Pending | ||
Review via email: mp+28885@code.launchpad.net |
Commit message
Description of the change
Overview
=======
This branch is part of the fix for bug 598464 - enabling the software center agent to get-or-create an LP person based on an openid identifier.
After a pre- (or mid-)implementation call with Francis, I've reworked the branch to re-use the existing code from webapp.
Details
======
I pulled out the relevant code from processPositive
Based on the comment in the code:
{{{
# The two lines below are duplicated a few more lines down,
# but to avoid this duplication we'd have to refactor most of
# our tests to provide an SREG response, which would be rather
# painful.
}}}
I updated the login_test tests to always provide the SREG response, as initially I was always calling self._getEmailA
Issues
=====
Always calling _getEmailAddres
For this reason, I had to make the changes in r9514 which I'm not particularly happy with, and I'd like to better understand why we need to do this (hence the request Guilherme :) ).
To test:
bin/test -vvm test_personset -m test_login
Guilherme Salgado (salgado) wrote : | # |
Michael Nelson (michael.nelson) wrote : | # |
{{{
13:55 < noodles775> Hi salgado, if you've got a spare few mins, can you take a look at the issue I've identified at: https:/
.nelson/
13:55 < noodles775> Let me know if I'm not making sense on the MP.
13:56 < salgado> noodles775, sure, I have it in my inbox and will get to it shortly
13:56 < noodles775> salgado: thanks.
14:06 < salgado> noodles775, in which case is the sreg extension not included in the response?
14:07 < noodles775> salgado: normal login with existing account. ie. 1) start dev server, 2) click login 3) enter <email address hidden>:test 4) click login. In
this case calling _getEmailAddres
14:08 < noodles775> It's not seen in the current code as _getEmailAddres
14:09 -!- abentley [~<email address hidden>] has quit [Ping timeout: 260 seconds]
14:10 < salgado> noodles775, that's weird because the code that generates the positive openid response is including the sreg extension unconditionally
14:10 * salgado tries to dupe
14:11 < salgado> indeed, duped
14:11 -!- abentley [~<email address hidden>] has joined #launchpad-reviews
14:12 < noodles775> Yeah, I'd assumed the sreg response should always be there, and was surprised too.
14:14 < salgado> noodles775, but this is a bug in the testopenid implementation
14:14 < noodles775> Where should I look?
14:14 < salgado> testopenid == the OpenID provider we use when developing/testing
14:14 < salgado> I'm checking it now
14:14 < salgado> noodles775, lib/lp/
14:15 < noodles775> OK, so we can be confident that the staging/production SSO always include the sreg.
14:15 < noodles775> Thanks.
14:15 < salgado> createPositiveR
14:15 -!- jtv-afk [~jtv@canonical
14:16 < salgado> noodles775, I think I've found the problem
14:17 < salgado> noodles775, OpenIDLogin.
14:17 < salgado> bug testopenid ignores that and only includes the nickname
14:17 -!- rinze is now known as jelmer-lunch
14:17 < salgado> s/bug/but
14:18 < noodles775> Right... I see.
14:18 < noodles775> So should it instead include *only* the requested items (email/fullname) or in addition to the nickname?
14:19 -!- abentley [~<email address hidden>] has quit [Quit: Ex-Chat]
14:19 < salgado> I think it's fine to include the 3 of them, but the nickname shouldn't be used anywhere
14:20 < noodles775> OK, I'll update it... thanks salgado !
14:22 < noodles775> salgado: also, I assume you don't have time to review the actual branch, but if you could have a brief look and make sure you think it's a sane change, that would be great.
14:24 < salgado> noodles775, sure, I can do that
14:25 < noodles775> Ta.
14:27 < salgado> noodles775, out of curiosity, why somebody needs a launchpad account to buy stuff from the software center? wouldn't a Ubuntu SSO account be more app...
Abel Deuring (adeuring) wrote : | # |
(12:04:05) adeuring: noodles775: overall, your code looks good. Just one question: SHouldn't the "with MasterDBPolicy():" be moved from processPositive
(12:06:19) noodles775: adeuring: yes, you're right.
And a minor nitpick:
> @@ -1754,6 +1761,33 @@
> on the displayname or other arguments.
> """
>
> + def getOrCreateByOp
> + full_name, creation_rationale, comment):
> + """Get or create a person for a given OpenID identifier.
> +
> + This is used when users login. We get the account with the given
> + OpenID identifier (creating one if it doesn't already exist) and
> + act according to the account's state:
> + - If the account is suspended, we stop and raise an error.
> + - If the account is deactivated, we reactivate it and proceed;
> + - If the account is active, we just proceed.
> +
> + If there is no existing Launchpad person for the account, we
> + create it.
> +
> + :param openid_identifier: representing the authenticated user.
> + :param email_address: the email address of the user.
> + :param full_name: the full name of the user.
> + :param creataion_
> + be created, this indicates why it was created.
s/creataion_
Preview Diff
1 | === modified file 'lib/canonical/launchpad/database/account.py' |
2 | --- lib/canonical/launchpad/database/account.py 2010-05-07 20:02:33 +0000 |
3 | +++ lib/canonical/launchpad/database/account.py 2010-07-02 10:41:03 +0000 |
4 | @@ -190,7 +190,8 @@ |
5 | return False |
6 | return self.preferredemail is not None |
7 | |
8 | - def createPerson(self, rationale, name=None, comment=None): |
9 | + def createPerson(self, rationale, name=None, comment=None, |
10 | + registrant=None): |
11 | """See `IAccount`.""" |
12 | # Need a local import because of circular dependencies. |
13 | from lp.registry.model.person import ( |
14 | @@ -204,7 +205,8 @@ |
15 | name = generate_nick(self.preferredemail.email) |
16 | person = PersonSet()._newPerson( |
17 | name, self.displayname, hide_email_addresses=True, |
18 | - rationale=rationale, account=self, comment=comment) |
19 | + rationale=rationale, account=self, comment=comment, |
20 | + registrant=registrant) |
21 | |
22 | # Update all associated email addresses to point at the new person. |
23 | result = IMasterStore(EmailAddress).find( |
24 | |
25 | === modified file 'lib/canonical/launchpad/interfaces/account.py' |
26 | --- lib/canonical/launchpad/interfaces/account.py 2009-12-24 11:06:04 +0000 |
27 | +++ lib/canonical/launchpad/interfaces/account.py 2010-07-02 10:41:03 +0000 |
28 | @@ -8,6 +8,7 @@ |
29 | |
30 | __all__ = [ |
31 | 'AccountStatus', |
32 | + 'AccountSuspendedError', |
33 | 'AccountCreationRationale', |
34 | 'IAccount', |
35 | 'IAccountPrivate', |
36 | @@ -27,6 +28,10 @@ |
37 | from lazr.restful.fields import CollectionField, Reference |
38 | |
39 | |
40 | +class AccountSuspendedError(Exception): |
41 | + """The account being accessed has been suspended.""" |
42 | + |
43 | + |
44 | class AccountStatus(DBEnumeratedType): |
45 | """The status of an account.""" |
46 | |
47 | @@ -179,6 +184,13 @@ |
48 | commented on. |
49 | """) |
50 | |
51 | + SOFTWARE_CENTER_PURCHASE = DBItem(16, """ |
52 | + Created by purchasing commercial software through Software Center. |
53 | + |
54 | + A purchase of commercial software (ie. subscriptions to a private |
55 | + and commercial archive) was made via Software Center. |
56 | + """) |
57 | + |
58 | |
59 | class IAccountPublic(Interface): |
60 | """Public information on an `IAccount`.""" |
61 | @@ -261,7 +273,7 @@ |
62 | password = PasswordField( |
63 | title=_("Password."), readonly=False, required=True) |
64 | |
65 | - def createPerson(self, rationale, name=None, comment=None): |
66 | + def createPerson(rationale, name=None, comment=None, registrant=None): |
67 | """Create and return a new `IPerson` associated with this account. |
68 | |
69 | :param rationale: A member of `AccountCreationRationale`. |
70 | @@ -269,6 +281,7 @@ |
71 | using an automatically generated one. |
72 | :param comment: Populate `IPerson.creation_comment`. See |
73 | `IPerson`. |
74 | + :param registrant: Populate `IPerson.registrant`. |
75 | """ |
76 | |
77 | |
78 | |
79 | === modified file 'lib/canonical/launchpad/security.py' |
80 | --- lib/canonical/launchpad/security.py 2010-06-30 13:49:38 +0000 |
81 | +++ lib/canonical/launchpad/security.py 2010-07-02 10:41:03 +0000 |
82 | @@ -284,6 +284,14 @@ |
83 | class ViewAccount(EditAccountBySelfOrAdmin): |
84 | permission = 'launchpad.View' |
85 | |
86 | + def checkAuthenticated(self, roles): |
87 | + """Extend view permission to software-center-agent.""" |
88 | + if super(ViewAccount, self).checkAuthenticated(roles): |
89 | + return True |
90 | + else: |
91 | + agent = getUtility(ILaunchpadCelebrities).software_center_agent |
92 | + return agent == roles.person |
93 | + |
94 | |
95 | class SpecialAccount(EditAccountBySelfOrAdmin): |
96 | permission = 'launchpad.Special' |
97 | @@ -590,6 +598,18 @@ |
98 | usedfor = IPersonSet |
99 | |
100 | |
101 | +class EditPersonSet(AdminByAdminsTeam): |
102 | + permission = 'launchpad.Edit' |
103 | + usedfor = IPersonSet |
104 | + |
105 | + def checkAuthenticated(self, user): |
106 | + if super(EditPersonSet, self).checkAuthenticated(user): |
107 | + return True |
108 | + else: |
109 | + agent = getUtility(ILaunchpadCelebrities).software_center_agent |
110 | + return user.person == agent |
111 | + |
112 | + |
113 | class EditTeamByTeamOwnerOrLaunchpadAdmins(AuthorizationBase): |
114 | permission = 'launchpad.Owner' |
115 | usedfor = ITeam |
116 | |
117 | === modified file 'lib/canonical/launchpad/webapp/login.py' |
118 | --- lib/canonical/launchpad/webapp/login.py 2010-05-15 17:43:59 +0000 |
119 | +++ lib/canonical/launchpad/webapp/login.py 2010-07-02 10:41:03 +0000 |
120 | @@ -32,10 +32,9 @@ |
121 | from canonical.cachedproperty import cachedproperty |
122 | from canonical.config import config |
123 | from canonical.launchpad import _ |
124 | -from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet |
125 | -from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet |
126 | +from canonical.launchpad.interfaces.account import AccountSuspendedError |
127 | from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore |
128 | -from lp.registry.interfaces.person import IPerson, PersonCreationRationale |
129 | +from lp.registry.interfaces.person import IPersonSet, PersonCreationRationale |
130 | from canonical.launchpad.readonly import is_read_only |
131 | from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy |
132 | from canonical.launchpad.webapp.error import SystemErrorView |
133 | @@ -199,7 +198,7 @@ |
134 | return_to = "%s?%s" % (return_to, starting_url) |
135 | form_html = self.openid_request.htmlMarkup(trust_root, return_to) |
136 | |
137 | - # The consumer.begin() call above will insert rows into the |
138 | + # The consumer.begin() call above will insert rows into the |
139 | # OpenIDAssociations table, but since this will be a GET request, the |
140 | # transaction would be rolled back, so we need an explicit commit |
141 | # here. |
142 | @@ -278,23 +277,14 @@ |
143 | "No email address or full name found in sreg response") |
144 | return email_address, full_name |
145 | |
146 | - def _createAccount(self, openid_identifier, email_address, full_name): |
147 | - account, email = getUtility(IAccountSet).createAccountAndEmail( |
148 | - email_address, PersonCreationRationale.OWNER_CREATED_LAUNCHPAD, |
149 | - full_name, password=None, openid_identifier=openid_identifier) |
150 | - return account |
151 | - |
152 | def processPositiveAssertion(self): |
153 | """Process an OpenID response containing a positive assertion. |
154 | |
155 | - We'll get the account with the given OpenID identifier (creating one |
156 | - if it doesn't already exist) and act according to the account's state: |
157 | - - If the account is suspended, we stop and render an error page; |
158 | - - If the account is deactivated, we reactivate it and proceed; |
159 | - - If the account is active, we just proceed. |
160 | + We'll get the person and account with the given OpenID |
161 | + identifier (creating one if necessary), and then login using |
162 | + that account. |
163 | |
164 | - After that we ensure there's an IPerson associated with the account |
165 | - and login using that account. |
166 | + If the account is suspended, we stop and render an error page. |
167 | |
168 | We also update the 'last_write' key in the session if we've done any |
169 | DB writes, to ensure subsequent requests use the master DB and see |
170 | @@ -302,56 +292,23 @@ |
171 | """ |
172 | identifier = self.openid_response.identity_url.split('/')[-1] |
173 | should_update_last_write = False |
174 | - email_set = getUtility(IEmailAddressSet) |
175 | # Force the use of the master database to make sure a lagged slave |
176 | # doesn't fool us into creating a Person/Account when one already |
177 | # exists. |
178 | + person_set = getUtility(IPersonSet) |
179 | + email_address, full_name = self._getEmailAddressAndFullName() |
180 | + try: |
181 | + person, db_updated = person_set.getOrCreateByOpenIDIdentifier( |
182 | + identifier, email_address, full_name, |
183 | + comment='when logging in to Launchpad.', |
184 | + creation_rationale=( |
185 | + PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)) |
186 | + should_update_last_write = db_updated |
187 | + except AccountSuspendedError: |
188 | + return self.suspended_account_template() |
189 | + |
190 | with MasterDatabasePolicy(): |
191 | - try: |
192 | - account = getUtility(IAccountSet).getByOpenIDIdentifier( |
193 | - identifier) |
194 | - except LookupError: |
195 | - # The two lines below are duplicated a few more lines down, |
196 | - # but to avoid this duplication we'd have to refactor most of |
197 | - # our tests to provide an SREG response, which would be rather |
198 | - # painful. |
199 | - email_address, full_name = self._getEmailAddressAndFullName() |
200 | - email = email_set.getByEmail(email_address) |
201 | - if email is None: |
202 | - # We got an OpenID response containing a positive |
203 | - # assertion, but we don't have an account for the |
204 | - # identifier or for the email address. We'll create one. |
205 | - account = self._createAccount( |
206 | - identifier, email_address, full_name) |
207 | - else: |
208 | - account = email.account |
209 | - assert account is not None, ( |
210 | - "This email address should have an associated " |
211 | - "account.") |
212 | - removeSecurityProxy(account).openid_identifier = ( |
213 | - identifier) |
214 | - should_update_last_write = True |
215 | - |
216 | - if account.status == AccountStatus.SUSPENDED: |
217 | - return self.suspended_account_template() |
218 | - elif account.status in [AccountStatus.DEACTIVATED, |
219 | - AccountStatus.NOACCOUNT]: |
220 | - comment = 'Reactivated by the user' |
221 | - password = '' # Needed just to please reactivate() below. |
222 | - email_address, dummy = self._getEmailAddressAndFullName() |
223 | - email = email_set.getByEmail(email_address) |
224 | - if email is None: |
225 | - email = email_set.new(email_address, account=account) |
226 | - removeSecurityProxy(account).reactivate( |
227 | - comment, password, removeSecurityProxy(email)) |
228 | - else: |
229 | - # Account is active, so nothing to do. |
230 | - pass |
231 | - if IPerson(account, None) is None: |
232 | - removeSecurityProxy(account).createPerson( |
233 | - PersonCreationRationale.OWNER_CREATED_LAUNCHPAD) |
234 | - should_update_last_write = True |
235 | - self.login(account) |
236 | + self.login(person.account) |
237 | |
238 | if should_update_last_write: |
239 | # This is a GET request but we changed the database, so update |
240 | |
241 | === modified file 'lib/canonical/launchpad/webapp/tests/test_login.py' |
242 | --- lib/canonical/launchpad/webapp/tests/test_login.py 2010-04-27 15:50:02 +0000 |
243 | +++ lib/canonical/launchpad/webapp/tests/test_login.py 2010-07-02 10:41:03 +0000 |
244 | @@ -111,7 +111,8 @@ |
245 | view_class=StubbedOpenIDCallbackView): |
246 | openid_response = FakeOpenIDResponse( |
247 | ITestOpenIDPersistentIdentity(account).openid_identity_url, |
248 | - status=response_status, message=response_msg) |
249 | + status=response_status, message=response_msg, |
250 | + email='non-existent@example.com', full_name='Foo User') |
251 | return self._createAndRenderView( |
252 | openid_response, view_class=view_class) |
253 | |
254 | @@ -140,7 +141,8 @@ |
255 | # In the common case we just login and redirect to the URL specified |
256 | # in the 'starting_url' query arg. |
257 | person = self.factory.makePerson() |
258 | - view, html = self._createViewWithResponse(person.account) |
259 | + with SRegResponse_fromSuccessResponse_stubbed(): |
260 | + view, html = self._createViewWithResponse(person.account) |
261 | self.assertTrue(view.login_called) |
262 | response = view.request.response |
263 | self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus()) |
264 | @@ -157,7 +159,8 @@ |
265 | # create one. |
266 | account = self.factory.makeAccount('Test account') |
267 | self.assertIs(None, IPerson(account, None)) |
268 | - view, html = self._createViewWithResponse(account) |
269 | + with SRegResponse_fromSuccessResponse_stubbed(): |
270 | + view, html = self._createViewWithResponse(account) |
271 | self.assertIsNot(None, IPerson(account, None)) |
272 | self.assertTrue(view.login_called) |
273 | response = view.request.response |
274 | @@ -167,7 +170,7 @@ |
275 | |
276 | # We also update the last_write flag in the session, to make sure |
277 | # further requests use the master DB and thus see the newly created |
278 | - # stuff. |
279 | + # stuff. |
280 | self.assertLastWriteIsSet(view.request) |
281 | |
282 | def test_unseen_identity(self): |
283 | @@ -196,7 +199,7 @@ |
284 | |
285 | # We also update the last_write flag in the session, to make sure |
286 | # further requests use the master DB and thus see the newly created |
287 | - # stuff. |
288 | + # stuff. |
289 | self.assertLastWriteIsSet(view.request) |
290 | |
291 | def test_unseen_identity_with_registered_email(self): |
292 | @@ -230,7 +233,7 @@ |
293 | |
294 | # We also update the last_write flag in the session, to make sure |
295 | # further requests use the master DB and thus see the newly created |
296 | - # stuff. |
297 | + # stuff. |
298 | self.assertLastWriteIsSet(view.request) |
299 | |
300 | def test_deactivated_account(self): |
301 | @@ -256,7 +259,7 @@ |
302 | self.assertEquals(email, account.preferredemail.email) |
303 | # We also update the last_write flag in the session, to make sure |
304 | # further requests use the master DB and thus see the newly created |
305 | - # stuff. |
306 | + # stuff. |
307 | self.assertLastWriteIsSet(view.request) |
308 | |
309 | def test_never_used_account(self): |
310 | @@ -278,7 +281,7 @@ |
311 | self.assertEquals(email, account.preferredemail.email) |
312 | # We also update the last_write flag in the session, to make sure |
313 | # further requests use the master DB and thus see the newly created |
314 | - # stuff. |
315 | + # stuff. |
316 | self.assertLastWriteIsSet(view.request) |
317 | |
318 | def test_suspended_account(self): |
319 | @@ -286,7 +289,8 @@ |
320 | # login, but we must not allow that. |
321 | account = self.factory.makeAccount( |
322 | 'Test account', status=AccountStatus.SUSPENDED) |
323 | - view, html = self._createViewWithResponse(account) |
324 | + with SRegResponse_fromSuccessResponse_stubbed(): |
325 | + view, html = self._createViewWithResponse(account) |
326 | self.assertFalse(view.login_called) |
327 | main_content = extract_text(find_main_content(html)) |
328 | self.assertIn('This account has been suspended', main_content) |
329 | |
330 | === modified file 'lib/lp/registry/interfaces/person.py' |
331 | --- lib/lp/registry/interfaces/person.py 2010-06-03 14:51:07 +0000 |
332 | +++ lib/lp/registry/interfaces/person.py 2010-07-02 10:41:03 +0000 |
333 | @@ -297,6 +297,13 @@ |
334 | commented on. |
335 | """) |
336 | |
337 | + SOFTWARE_CENTER_PURCHASE = DBItem(16, """ |
338 | + Created by purchasing commercial software through Software Center. |
339 | + |
340 | + A purchase of commercial software (ie. subscriptions to a private |
341 | + and commercial archive) was made via Software Center. |
342 | + """) |
343 | + |
344 | |
345 | class TeamMembershipRenewalPolicy(DBEnumeratedType): |
346 | """TeamMembership Renewal Policy. |
347 | @@ -1754,6 +1761,33 @@ |
348 | on the displayname or other arguments. |
349 | """ |
350 | |
351 | + def getOrCreateByOpenIDIdentifier(openid_identifier, email, |
352 | + full_name, creation_rationale, comment): |
353 | + """Get or create a person for a given OpenID identifier. |
354 | + |
355 | + This is used when users login. We get the account with the given |
356 | + OpenID identifier (creating one if it doesn't already exist) and |
357 | + act according to the account's state: |
358 | + - If the account is suspended, we stop and raise an error. |
359 | + - If the account is deactivated, we reactivate it and proceed; |
360 | + - If the account is active, we just proceed. |
361 | + |
362 | + If there is no existing Launchpad person for the account, we |
363 | + create it. |
364 | + |
365 | + :param openid_identifier: representing the authenticated user. |
366 | + :param email_address: the email address of the user. |
367 | + :param full_name: the full name of the user. |
368 | + :param creation_rationale: When an account or person needs to |
369 | + be created, this indicates why it was created. |
370 | + :param comment: If the account is reactivated or person created, |
371 | + this comment indicates why. |
372 | + :return: a tuple of `IPerson` and a boolean indicating whether the |
373 | + database was updated. |
374 | + :raises AccountSuspendedError: if the account associated with the |
375 | + identifier has been suspended. |
376 | + """ |
377 | + |
378 | @call_with(teamowner=REQUEST_USER) |
379 | @rename_parameters_as( |
380 | displayname='display_name', teamdescription='team_description', |
381 | |
382 | === modified file 'lib/lp/registry/model/person.py' |
383 | --- lib/lp/registry/model/person.py 2010-06-22 11:19:34 +0000 |
384 | +++ lib/lp/registry/model/person.py 2010-07-02 10:41:03 +0000 |
385 | @@ -61,6 +61,7 @@ |
386 | from canonical.lazr.utils import get_current_browser_request, safe_hasattr |
387 | |
388 | from canonical.launchpad.database.account import Account, AccountPassword |
389 | +from canonical.launchpad.interfaces.account import AccountSuspendedError |
390 | from lp.bugs.model.bugtarget import HasBugsBase |
391 | from canonical.launchpad.database.stormsugar import StartsWith |
392 | from lp.registry.model.karma import KarmaCategory |
393 | @@ -152,6 +153,7 @@ |
394 | |
395 | from canonical.launchpad.validators.email import valid_email |
396 | from canonical.launchpad.validators.name import sanitize_name, valid_name |
397 | +from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy |
398 | from lp.registry.interfaces.person import validate_public_person |
399 | |
400 | |
401 | @@ -2429,6 +2431,59 @@ |
402 | key=lambda obj: (obj.karma, obj.displayname, obj.id), |
403 | reverse=True) |
404 | |
405 | + def getOrCreateByOpenIDIdentifier( |
406 | + self, openid_identifier, email_address, full_name, |
407 | + creation_rationale, comment): |
408 | + """See `IPersonSet`.""" |
409 | + assert email_address is not None and full_name is not None, ( |
410 | + "Both email address and full name are required to " |
411 | + "create an account.") |
412 | + db_updated = False |
413 | + with MasterDatabasePolicy(): |
414 | + account_set = getUtility(IAccountSet) |
415 | + email_set = getUtility(IEmailAddressSet) |
416 | + email = email_set.getByEmail(email_address) |
417 | + try: |
418 | + account = account_set.getByOpenIDIdentifier( |
419 | + openid_identifier) |
420 | + except LookupError: |
421 | + if email is None: |
422 | + # There is no account associated with the identifier |
423 | + # nor an email associated with the email address. |
424 | + # We'll create one. |
425 | + account, email = account_set.createAccountAndEmail( |
426 | + email_address, creation_rationale, full_name, |
427 | + password=None, |
428 | + openid_identifier=openid_identifier) |
429 | + else: |
430 | + account = email.account |
431 | + assert account is not None, ( |
432 | + "This email address should have an associated " |
433 | + "account.") |
434 | + removeSecurityProxy(account).openid_identifier = ( |
435 | + openid_identifier) |
436 | + db_updated = True |
437 | + |
438 | + if account.status == AccountStatus.SUSPENDED: |
439 | + raise AccountSuspendedError( |
440 | + "The account matching the identifier is suspended.") |
441 | + elif account.status in [AccountStatus.DEACTIVATED, |
442 | + AccountStatus.NOACCOUNT]: |
443 | + password = '' # Needed just to please reactivate() below. |
444 | + if email is None: |
445 | + email = email_set.new(email_address, account=account) |
446 | + removeSecurityProxy(account).reactivate( |
447 | + comment, password, removeSecurityProxy(email)) |
448 | + else: |
449 | + # Account is active, so nothing to do. |
450 | + pass |
451 | + if IPerson(account, None) is None: |
452 | + removeSecurityProxy(account).createPerson( |
453 | + creation_rationale, comment=comment) |
454 | + db_updated = True |
455 | + |
456 | + return IPerson(account), db_updated |
457 | + |
458 | def newTeam(self, teamowner, name, displayname, teamdescription=None, |
459 | subscriptionpolicy=TeamSubscriptionPolicy.MODERATED, |
460 | defaultmembershipperiod=None, defaultrenewalperiod=None): |
461 | |
462 | === modified file 'lib/lp/registry/tests/test_personset.py' |
463 | --- lib/lp/registry/tests/test_personset.py 2010-03-31 18:51:32 +0000 |
464 | +++ lib/lp/registry/tests/test_personset.py 2010-07-02 10:41:03 +0000 |
465 | @@ -16,9 +16,12 @@ |
466 | MailingListAutoSubscribePolicy) |
467 | from lp.registry.interfaces.person import ( |
468 | PersonCreationRationale, IPersonSet) |
469 | -from lp.testing import TestCaseWithFactory, login_person, logout |
470 | +from lp.testing import ( |
471 | + TestCaseWithFactory, login_person, logout) |
472 | |
473 | from canonical.database.sqlbase import cursor |
474 | +from canonical.launchpad.interfaces.account import ( |
475 | + AccountStatus, AccountSuspendedError) |
476 | from canonical.launchpad.testing.databasehelpers import ( |
477 | remove_all_sample_data_branches) |
478 | from canonical.testing import DatabaseFunctionalLayer |
479 | @@ -146,5 +149,93 @@ |
480 | self.assertEqual(1, self.cur.rowcount) |
481 | |
482 | |
483 | +class TestPersonSetGetOrCreateByOpenIDIdentifier(TestCaseWithFactory): |
484 | + |
485 | + layer = DatabaseFunctionalLayer |
486 | + |
487 | + def setUp(self): |
488 | + super(TestPersonSetGetOrCreateByOpenIDIdentifier, self).setUp() |
489 | + self.person_set = getUtility(IPersonSet) |
490 | + |
491 | + def callGetOrCreate(self, identifier, email='a@b.com'): |
492 | + return self.person_set.getOrCreateByOpenIDIdentifier( |
493 | + identifier, email, "Joe Bloggs", |
494 | + PersonCreationRationale.SOFTWARE_CENTER_PURCHASE, |
495 | + "when purchasing an application via Software Center.") |
496 | + |
497 | + def test_existing_person(self): |
498 | + person = self.factory.makePerson() |
499 | + openid_ident = removeSecurityProxy(person.account).openid_identifier |
500 | + person_set = getUtility(IPersonSet) |
501 | + |
502 | + result, db_updated = self.callGetOrCreate(openid_ident) |
503 | + |
504 | + self.assertEqual(person, result) |
505 | + self.assertFalse(db_updated) |
506 | + |
507 | + def test_existing_account_no_person(self): |
508 | + # A person is created with the correct rationale. |
509 | + account = self.factory.makeAccount('purchaser') |
510 | + openid_ident = removeSecurityProxy(account).openid_identifier |
511 | + |
512 | + person, db_updated = self.callGetOrCreate(openid_ident) |
513 | + |
514 | + self.assertEqual(account, person.account) |
515 | + # The person is created with the correct rationale, creation |
516 | + # comment, registrant, appropriate display name. |
517 | + self.assertEqual( |
518 | + "when purchasing an application via Software Center.", |
519 | + person.creation_comment) |
520 | + self.assertEqual( |
521 | + PersonCreationRationale.SOFTWARE_CENTER_PURCHASE, |
522 | + person.creation_rationale) |
523 | + self.assertTrue(db_updated) |
524 | + |
525 | + def test_existing_deactivated_account(self): |
526 | + # An existing deactivated account will be reactivated. |
527 | + account = self.factory.makeAccount('purchaser', |
528 | + status=AccountStatus.DEACTIVATED) |
529 | + openid_ident = removeSecurityProxy(account).openid_identifier |
530 | + |
531 | + person, db_updated = self.callGetOrCreate(openid_ident) |
532 | + self.assertEqual(AccountStatus.ACTIVE, person.account.status) |
533 | + self.assertTrue(db_updated) |
534 | + self.assertEqual( |
535 | + "when purchasing an application via Software Center.", |
536 | + removeSecurityProxy(person.account).status_comment) |
537 | + |
538 | + def test_existing_suspended_account(self): |
539 | + # An existing suspended account will raise an exception. |
540 | + account = self.factory.makeAccount('purchaser', |
541 | + status=AccountStatus.SUSPENDED) |
542 | + openid_ident = removeSecurityProxy(account).openid_identifier |
543 | + |
544 | + self.assertRaises( |
545 | + AccountSuspendedError, self.callGetOrCreate, openid_ident) |
546 | + |
547 | + def test_no_account_or_email(self): |
548 | + # An identifier can be used to create an account (it is assumed |
549 | + # to be already authenticated with SSO). |
550 | + person, db_updated = self.callGetOrCreate('openid-identifier') |
551 | + |
552 | + self.assertEqual( |
553 | + "openid-identifier", |
554 | + removeSecurityProxy(person.account).openid_identifier) |
555 | + self.assertTrue(db_updated) |
556 | + |
557 | + def test_no_matching_account_existing_email(self): |
558 | + # The openid_identity of the account matching the email will |
559 | + # updated. |
560 | + other_account = self.factory.makeAccount('test', email='a@b.com') |
561 | + |
562 | + person, db_updated = self.callGetOrCreate( |
563 | + 'other-openid-identifier', 'a@b.com') |
564 | + |
565 | + self.assertEqual(other_account, person.account) |
566 | + self.assertEqual( |
567 | + 'other-openid-identifier', |
568 | + removeSecurityProxy(person.account).openid_identifier) |
569 | + |
570 | + |
571 | def test_suite(): |
572 | return TestLoader().loadTestsFromName(__name__) |
573 | |
574 | === modified file 'lib/lp/testopenid/browser/server.py' |
575 | --- lib/lp/testopenid/browser/server.py 2010-05-15 17:43:59 +0000 |
576 | +++ lib/lp/testopenid/browser/server.py 2010-07-02 10:41:03 +0000 |
577 | @@ -198,7 +198,10 @@ |
578 | else: |
579 | response = self.openid_request.answer(True) |
580 | |
581 | - sreg_fields = dict(nickname=IPerson(self.account).name) |
582 | + sreg_fields = dict( |
583 | + nickname=IPerson(self.account).name, |
584 | + email=self.account.preferredemail.email, |
585 | + fullname=self.account.displayname) |
586 | sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request) |
587 | sreg_response = SRegResponse.extractResponse( |
588 | sreg_request, sreg_fields) |
589 | @@ -224,7 +227,7 @@ |
590 | This class implements an OpenID endpoint using the python-openid |
591 | library. In addition to the normal modes of operation, it also |
592 | implements the OpenID 2.0 identifier select mode. |
593 | - |
594 | + |
595 | Note that the checkid_immediate mode is not supported. |
596 | """ |
597 |
Your general approach looks sane to me. I might be able to do a proper review today or tomorrow if you'd like.