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 | 190 | return False | 190 | return False |
6 | 191 | return self.preferredemail is not None | 191 | return self.preferredemail is not None |
7 | 192 | 192 | ||
9 | 193 | def createPerson(self, rationale, name=None, comment=None): | 193 | def createPerson(self, rationale, name=None, comment=None, |
10 | 194 | registrant=None): | ||
11 | 194 | """See `IAccount`.""" | 195 | """See `IAccount`.""" |
12 | 195 | # Need a local import because of circular dependencies. | 196 | # Need a local import because of circular dependencies. |
13 | 196 | from lp.registry.model.person import ( | 197 | from lp.registry.model.person import ( |
14 | @@ -204,7 +205,8 @@ | |||
15 | 204 | name = generate_nick(self.preferredemail.email) | 205 | name = generate_nick(self.preferredemail.email) |
16 | 205 | person = PersonSet()._newPerson( | 206 | person = PersonSet()._newPerson( |
17 | 206 | name, self.displayname, hide_email_addresses=True, | 207 | name, self.displayname, hide_email_addresses=True, |
19 | 207 | rationale=rationale, account=self, comment=comment) | 208 | rationale=rationale, account=self, comment=comment, |
20 | 209 | registrant=registrant) | ||
21 | 208 | 210 | ||
22 | 209 | # Update all associated email addresses to point at the new person. | 211 | # Update all associated email addresses to point at the new person. |
23 | 210 | result = IMasterStore(EmailAddress).find( | 212 | result = IMasterStore(EmailAddress).find( |
24 | 211 | 213 | ||
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 | 8 | 8 | ||
30 | 9 | __all__ = [ | 9 | __all__ = [ |
31 | 10 | 'AccountStatus', | 10 | 'AccountStatus', |
32 | 11 | 'AccountSuspendedError', | ||
33 | 11 | 'AccountCreationRationale', | 12 | 'AccountCreationRationale', |
34 | 12 | 'IAccount', | 13 | 'IAccount', |
35 | 13 | 'IAccountPrivate', | 14 | 'IAccountPrivate', |
36 | @@ -27,6 +28,10 @@ | |||
37 | 27 | from lazr.restful.fields import CollectionField, Reference | 28 | from lazr.restful.fields import CollectionField, Reference |
38 | 28 | 29 | ||
39 | 29 | 30 | ||
40 | 31 | class AccountSuspendedError(Exception): | ||
41 | 32 | """The account being accessed has been suspended.""" | ||
42 | 33 | |||
43 | 34 | |||
44 | 30 | class AccountStatus(DBEnumeratedType): | 35 | class AccountStatus(DBEnumeratedType): |
45 | 31 | """The status of an account.""" | 36 | """The status of an account.""" |
46 | 32 | 37 | ||
47 | @@ -179,6 +184,13 @@ | |||
48 | 179 | commented on. | 184 | commented on. |
49 | 180 | """) | 185 | """) |
50 | 181 | 186 | ||
51 | 187 | SOFTWARE_CENTER_PURCHASE = DBItem(16, """ | ||
52 | 188 | Created by purchasing commercial software through Software Center. | ||
53 | 189 | |||
54 | 190 | A purchase of commercial software (ie. subscriptions to a private | ||
55 | 191 | and commercial archive) was made via Software Center. | ||
56 | 192 | """) | ||
57 | 193 | |||
58 | 182 | 194 | ||
59 | 183 | class IAccountPublic(Interface): | 195 | class IAccountPublic(Interface): |
60 | 184 | """Public information on an `IAccount`.""" | 196 | """Public information on an `IAccount`.""" |
61 | @@ -261,7 +273,7 @@ | |||
62 | 261 | password = PasswordField( | 273 | password = PasswordField( |
63 | 262 | title=_("Password."), readonly=False, required=True) | 274 | title=_("Password."), readonly=False, required=True) |
64 | 263 | 275 | ||
66 | 264 | def createPerson(self, rationale, name=None, comment=None): | 276 | def createPerson(rationale, name=None, comment=None, registrant=None): |
67 | 265 | """Create and return a new `IPerson` associated with this account. | 277 | """Create and return a new `IPerson` associated with this account. |
68 | 266 | 278 | ||
69 | 267 | :param rationale: A member of `AccountCreationRationale`. | 279 | :param rationale: A member of `AccountCreationRationale`. |
70 | @@ -269,6 +281,7 @@ | |||
71 | 269 | using an automatically generated one. | 281 | using an automatically generated one. |
72 | 270 | :param comment: Populate `IPerson.creation_comment`. See | 282 | :param comment: Populate `IPerson.creation_comment`. See |
73 | 271 | `IPerson`. | 283 | `IPerson`. |
74 | 284 | :param registrant: Populate `IPerson.registrant`. | ||
75 | 272 | """ | 285 | """ |
76 | 273 | 286 | ||
77 | 274 | 287 | ||
78 | 275 | 288 | ||
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 | 284 | class ViewAccount(EditAccountBySelfOrAdmin): | 284 | class ViewAccount(EditAccountBySelfOrAdmin): |
84 | 285 | permission = 'launchpad.View' | 285 | permission = 'launchpad.View' |
85 | 286 | 286 | ||
86 | 287 | def checkAuthenticated(self, roles): | ||
87 | 288 | """Extend view permission to software-center-agent.""" | ||
88 | 289 | if super(ViewAccount, self).checkAuthenticated(roles): | ||
89 | 290 | return True | ||
90 | 291 | else: | ||
91 | 292 | agent = getUtility(ILaunchpadCelebrities).software_center_agent | ||
92 | 293 | return agent == roles.person | ||
93 | 294 | |||
94 | 287 | 295 | ||
95 | 288 | class SpecialAccount(EditAccountBySelfOrAdmin): | 296 | class SpecialAccount(EditAccountBySelfOrAdmin): |
96 | 289 | permission = 'launchpad.Special' | 297 | permission = 'launchpad.Special' |
97 | @@ -590,6 +598,18 @@ | |||
98 | 590 | usedfor = IPersonSet | 598 | usedfor = IPersonSet |
99 | 591 | 599 | ||
100 | 592 | 600 | ||
101 | 601 | class EditPersonSet(AdminByAdminsTeam): | ||
102 | 602 | permission = 'launchpad.Edit' | ||
103 | 603 | usedfor = IPersonSet | ||
104 | 604 | |||
105 | 605 | def checkAuthenticated(self, user): | ||
106 | 606 | if super(EditPersonSet, self).checkAuthenticated(user): | ||
107 | 607 | return True | ||
108 | 608 | else: | ||
109 | 609 | agent = getUtility(ILaunchpadCelebrities).software_center_agent | ||
110 | 610 | return user.person == agent | ||
111 | 611 | |||
112 | 612 | |||
113 | 593 | class EditTeamByTeamOwnerOrLaunchpadAdmins(AuthorizationBase): | 613 | class EditTeamByTeamOwnerOrLaunchpadAdmins(AuthorizationBase): |
114 | 594 | permission = 'launchpad.Owner' | 614 | permission = 'launchpad.Owner' |
115 | 595 | usedfor = ITeam | 615 | usedfor = ITeam |
116 | 596 | 616 | ||
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 | 32 | from canonical.cachedproperty import cachedproperty | 32 | from canonical.cachedproperty import cachedproperty |
122 | 33 | from canonical.config import config | 33 | from canonical.config import config |
123 | 34 | from canonical.launchpad import _ | 34 | from canonical.launchpad import _ |
126 | 35 | from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet | 35 | from canonical.launchpad.interfaces.account import AccountSuspendedError |
125 | 36 | from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet | ||
127 | 37 | from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore | 36 | from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore |
129 | 38 | from lp.registry.interfaces.person import IPerson, PersonCreationRationale | 37 | from lp.registry.interfaces.person import IPersonSet, PersonCreationRationale |
130 | 39 | from canonical.launchpad.readonly import is_read_only | 38 | from canonical.launchpad.readonly import is_read_only |
131 | 40 | from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy | 39 | from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy |
132 | 41 | from canonical.launchpad.webapp.error import SystemErrorView | 40 | from canonical.launchpad.webapp.error import SystemErrorView |
133 | @@ -199,7 +198,7 @@ | |||
134 | 199 | return_to = "%s?%s" % (return_to, starting_url) | 198 | return_to = "%s?%s" % (return_to, starting_url) |
135 | 200 | form_html = self.openid_request.htmlMarkup(trust_root, return_to) | 199 | form_html = self.openid_request.htmlMarkup(trust_root, return_to) |
136 | 201 | 200 | ||
138 | 202 | # The consumer.begin() call above will insert rows into the | 201 | # The consumer.begin() call above will insert rows into the |
139 | 203 | # OpenIDAssociations table, but since this will be a GET request, the | 202 | # OpenIDAssociations table, but since this will be a GET request, the |
140 | 204 | # transaction would be rolled back, so we need an explicit commit | 203 | # transaction would be rolled back, so we need an explicit commit |
141 | 205 | # here. | 204 | # here. |
142 | @@ -278,23 +277,14 @@ | |||
143 | 278 | "No email address or full name found in sreg response") | 277 | "No email address or full name found in sreg response") |
144 | 279 | return email_address, full_name | 278 | return email_address, full_name |
145 | 280 | 279 | ||
146 | 281 | def _createAccount(self, openid_identifier, email_address, full_name): | ||
147 | 282 | account, email = getUtility(IAccountSet).createAccountAndEmail( | ||
148 | 283 | email_address, PersonCreationRationale.OWNER_CREATED_LAUNCHPAD, | ||
149 | 284 | full_name, password=None, openid_identifier=openid_identifier) | ||
150 | 285 | return account | ||
151 | 286 | |||
152 | 287 | def processPositiveAssertion(self): | 280 | def processPositiveAssertion(self): |
153 | 288 | """Process an OpenID response containing a positive assertion. | 281 | """Process an OpenID response containing a positive assertion. |
154 | 289 | 282 | ||
160 | 290 | We'll get the account with the given OpenID identifier (creating one | 283 | We'll get the person and account with the given OpenID |
161 | 291 | if it doesn't already exist) and act according to the account's state: | 284 | identifier (creating one if necessary), and then login using |
162 | 292 | - If the account is suspended, we stop and render an error page; | 285 | that account. |
158 | 293 | - If the account is deactivated, we reactivate it and proceed; | ||
159 | 294 | - If the account is active, we just proceed. | ||
163 | 295 | 286 | ||
166 | 296 | After that we ensure there's an IPerson associated with the account | 287 | If the account is suspended, we stop and render an error page. |
165 | 297 | and login using that account. | ||
167 | 298 | 288 | ||
168 | 299 | We also update the 'last_write' key in the session if we've done any | 289 | We also update the 'last_write' key in the session if we've done any |
169 | 300 | DB writes, to ensure subsequent requests use the master DB and see | 290 | DB writes, to ensure subsequent requests use the master DB and see |
170 | @@ -302,56 +292,23 @@ | |||
171 | 302 | """ | 292 | """ |
172 | 303 | identifier = self.openid_response.identity_url.split('/')[-1] | 293 | identifier = self.openid_response.identity_url.split('/')[-1] |
173 | 304 | should_update_last_write = False | 294 | should_update_last_write = False |
174 | 305 | email_set = getUtility(IEmailAddressSet) | ||
175 | 306 | # Force the use of the master database to make sure a lagged slave | 295 | # Force the use of the master database to make sure a lagged slave |
176 | 307 | # doesn't fool us into creating a Person/Account when one already | 296 | # doesn't fool us into creating a Person/Account when one already |
177 | 308 | # exists. | 297 | # exists. |
178 | 298 | person_set = getUtility(IPersonSet) | ||
179 | 299 | email_address, full_name = self._getEmailAddressAndFullName() | ||
180 | 300 | try: | ||
181 | 301 | person, db_updated = person_set.getOrCreateByOpenIDIdentifier( | ||
182 | 302 | identifier, email_address, full_name, | ||
183 | 303 | comment='when logging in to Launchpad.', | ||
184 | 304 | creation_rationale=( | ||
185 | 305 | PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)) | ||
186 | 306 | should_update_last_write = db_updated | ||
187 | 307 | except AccountSuspendedError: | ||
188 | 308 | return self.suspended_account_template() | ||
189 | 309 | |||
190 | 309 | with MasterDatabasePolicy(): | 310 | with MasterDatabasePolicy(): |
236 | 310 | try: | 311 | self.login(person.account) |
192 | 311 | account = getUtility(IAccountSet).getByOpenIDIdentifier( | ||
193 | 312 | identifier) | ||
194 | 313 | except LookupError: | ||
195 | 314 | # The two lines below are duplicated a few more lines down, | ||
196 | 315 | # but to avoid this duplication we'd have to refactor most of | ||
197 | 316 | # our tests to provide an SREG response, which would be rather | ||
198 | 317 | # painful. | ||
199 | 318 | email_address, full_name = self._getEmailAddressAndFullName() | ||
200 | 319 | email = email_set.getByEmail(email_address) | ||
201 | 320 | if email is None: | ||
202 | 321 | # We got an OpenID response containing a positive | ||
203 | 322 | # assertion, but we don't have an account for the | ||
204 | 323 | # identifier or for the email address. We'll create one. | ||
205 | 324 | account = self._createAccount( | ||
206 | 325 | identifier, email_address, full_name) | ||
207 | 326 | else: | ||
208 | 327 | account = email.account | ||
209 | 328 | assert account is not None, ( | ||
210 | 329 | "This email address should have an associated " | ||
211 | 330 | "account.") | ||
212 | 331 | removeSecurityProxy(account).openid_identifier = ( | ||
213 | 332 | identifier) | ||
214 | 333 | should_update_last_write = True | ||
215 | 334 | |||
216 | 335 | if account.status == AccountStatus.SUSPENDED: | ||
217 | 336 | return self.suspended_account_template() | ||
218 | 337 | elif account.status in [AccountStatus.DEACTIVATED, | ||
219 | 338 | AccountStatus.NOACCOUNT]: | ||
220 | 339 | comment = 'Reactivated by the user' | ||
221 | 340 | password = '' # Needed just to please reactivate() below. | ||
222 | 341 | email_address, dummy = self._getEmailAddressAndFullName() | ||
223 | 342 | email = email_set.getByEmail(email_address) | ||
224 | 343 | if email is None: | ||
225 | 344 | email = email_set.new(email_address, account=account) | ||
226 | 345 | removeSecurityProxy(account).reactivate( | ||
227 | 346 | comment, password, removeSecurityProxy(email)) | ||
228 | 347 | else: | ||
229 | 348 | # Account is active, so nothing to do. | ||
230 | 349 | pass | ||
231 | 350 | if IPerson(account, None) is None: | ||
232 | 351 | removeSecurityProxy(account).createPerson( | ||
233 | 352 | PersonCreationRationale.OWNER_CREATED_LAUNCHPAD) | ||
234 | 353 | should_update_last_write = True | ||
235 | 354 | self.login(account) | ||
237 | 355 | 312 | ||
238 | 356 | if should_update_last_write: | 313 | if should_update_last_write: |
239 | 357 | # This is a GET request but we changed the database, so update | 314 | # This is a GET request but we changed the database, so update |
240 | 358 | 315 | ||
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 | 111 | view_class=StubbedOpenIDCallbackView): | 111 | view_class=StubbedOpenIDCallbackView): |
246 | 112 | openid_response = FakeOpenIDResponse( | 112 | openid_response = FakeOpenIDResponse( |
247 | 113 | ITestOpenIDPersistentIdentity(account).openid_identity_url, | 113 | ITestOpenIDPersistentIdentity(account).openid_identity_url, |
249 | 114 | status=response_status, message=response_msg) | 114 | status=response_status, message=response_msg, |
250 | 115 | email='non-existent@example.com', full_name='Foo User') | ||
251 | 115 | return self._createAndRenderView( | 116 | return self._createAndRenderView( |
252 | 116 | openid_response, view_class=view_class) | 117 | openid_response, view_class=view_class) |
253 | 117 | 118 | ||
254 | @@ -140,7 +141,8 @@ | |||
255 | 140 | # In the common case we just login and redirect to the URL specified | 141 | # In the common case we just login and redirect to the URL specified |
256 | 141 | # in the 'starting_url' query arg. | 142 | # in the 'starting_url' query arg. |
257 | 142 | person = self.factory.makePerson() | 143 | person = self.factory.makePerson() |
259 | 143 | view, html = self._createViewWithResponse(person.account) | 144 | with SRegResponse_fromSuccessResponse_stubbed(): |
260 | 145 | view, html = self._createViewWithResponse(person.account) | ||
261 | 144 | self.assertTrue(view.login_called) | 146 | self.assertTrue(view.login_called) |
262 | 145 | response = view.request.response | 147 | response = view.request.response |
263 | 146 | self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus()) | 148 | self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus()) |
264 | @@ -157,7 +159,8 @@ | |||
265 | 157 | # create one. | 159 | # create one. |
266 | 158 | account = self.factory.makeAccount('Test account') | 160 | account = self.factory.makeAccount('Test account') |
267 | 159 | self.assertIs(None, IPerson(account, None)) | 161 | self.assertIs(None, IPerson(account, None)) |
269 | 160 | view, html = self._createViewWithResponse(account) | 162 | with SRegResponse_fromSuccessResponse_stubbed(): |
270 | 163 | view, html = self._createViewWithResponse(account) | ||
271 | 161 | self.assertIsNot(None, IPerson(account, None)) | 164 | self.assertIsNot(None, IPerson(account, None)) |
272 | 162 | self.assertTrue(view.login_called) | 165 | self.assertTrue(view.login_called) |
273 | 163 | response = view.request.response | 166 | response = view.request.response |
274 | @@ -167,7 +170,7 @@ | |||
275 | 167 | 170 | ||
276 | 168 | # We also update the last_write flag in the session, to make sure | 171 | # We also update the last_write flag in the session, to make sure |
277 | 169 | # further requests use the master DB and thus see the newly created | 172 | # further requests use the master DB and thus see the newly created |
279 | 170 | # stuff. | 173 | # stuff. |
280 | 171 | self.assertLastWriteIsSet(view.request) | 174 | self.assertLastWriteIsSet(view.request) |
281 | 172 | 175 | ||
282 | 173 | def test_unseen_identity(self): | 176 | def test_unseen_identity(self): |
283 | @@ -196,7 +199,7 @@ | |||
284 | 196 | 199 | ||
285 | 197 | # We also update the last_write flag in the session, to make sure | 200 | # We also update the last_write flag in the session, to make sure |
286 | 198 | # further requests use the master DB and thus see the newly created | 201 | # further requests use the master DB and thus see the newly created |
288 | 199 | # stuff. | 202 | # stuff. |
289 | 200 | self.assertLastWriteIsSet(view.request) | 203 | self.assertLastWriteIsSet(view.request) |
290 | 201 | 204 | ||
291 | 202 | def test_unseen_identity_with_registered_email(self): | 205 | def test_unseen_identity_with_registered_email(self): |
292 | @@ -230,7 +233,7 @@ | |||
293 | 230 | 233 | ||
294 | 231 | # We also update the last_write flag in the session, to make sure | 234 | # We also update the last_write flag in the session, to make sure |
295 | 232 | # further requests use the master DB and thus see the newly created | 235 | # further requests use the master DB and thus see the newly created |
297 | 233 | # stuff. | 236 | # stuff. |
298 | 234 | self.assertLastWriteIsSet(view.request) | 237 | self.assertLastWriteIsSet(view.request) |
299 | 235 | 238 | ||
300 | 236 | def test_deactivated_account(self): | 239 | def test_deactivated_account(self): |
301 | @@ -256,7 +259,7 @@ | |||
302 | 256 | self.assertEquals(email, account.preferredemail.email) | 259 | self.assertEquals(email, account.preferredemail.email) |
303 | 257 | # We also update the last_write flag in the session, to make sure | 260 | # We also update the last_write flag in the session, to make sure |
304 | 258 | # further requests use the master DB and thus see the newly created | 261 | # further requests use the master DB and thus see the newly created |
306 | 259 | # stuff. | 262 | # stuff. |
307 | 260 | self.assertLastWriteIsSet(view.request) | 263 | self.assertLastWriteIsSet(view.request) |
308 | 261 | 264 | ||
309 | 262 | def test_never_used_account(self): | 265 | def test_never_used_account(self): |
310 | @@ -278,7 +281,7 @@ | |||
311 | 278 | self.assertEquals(email, account.preferredemail.email) | 281 | self.assertEquals(email, account.preferredemail.email) |
312 | 279 | # We also update the last_write flag in the session, to make sure | 282 | # We also update the last_write flag in the session, to make sure |
313 | 280 | # further requests use the master DB and thus see the newly created | 283 | # further requests use the master DB and thus see the newly created |
315 | 281 | # stuff. | 284 | # stuff. |
316 | 282 | self.assertLastWriteIsSet(view.request) | 285 | self.assertLastWriteIsSet(view.request) |
317 | 283 | 286 | ||
318 | 284 | def test_suspended_account(self): | 287 | def test_suspended_account(self): |
319 | @@ -286,7 +289,8 @@ | |||
320 | 286 | # login, but we must not allow that. | 289 | # login, but we must not allow that. |
321 | 287 | account = self.factory.makeAccount( | 290 | account = self.factory.makeAccount( |
322 | 288 | 'Test account', status=AccountStatus.SUSPENDED) | 291 | 'Test account', status=AccountStatus.SUSPENDED) |
324 | 289 | view, html = self._createViewWithResponse(account) | 292 | with SRegResponse_fromSuccessResponse_stubbed(): |
325 | 293 | view, html = self._createViewWithResponse(account) | ||
326 | 290 | self.assertFalse(view.login_called) | 294 | self.assertFalse(view.login_called) |
327 | 291 | main_content = extract_text(find_main_content(html)) | 295 | main_content = extract_text(find_main_content(html)) |
328 | 292 | self.assertIn('This account has been suspended', main_content) | 296 | self.assertIn('This account has been suspended', main_content) |
329 | 293 | 297 | ||
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 | 297 | commented on. | 297 | commented on. |
335 | 298 | """) | 298 | """) |
336 | 299 | 299 | ||
337 | 300 | SOFTWARE_CENTER_PURCHASE = DBItem(16, """ | ||
338 | 301 | Created by purchasing commercial software through Software Center. | ||
339 | 302 | |||
340 | 303 | A purchase of commercial software (ie. subscriptions to a private | ||
341 | 304 | and commercial archive) was made via Software Center. | ||
342 | 305 | """) | ||
343 | 306 | |||
344 | 300 | 307 | ||
345 | 301 | class TeamMembershipRenewalPolicy(DBEnumeratedType): | 308 | class TeamMembershipRenewalPolicy(DBEnumeratedType): |
346 | 302 | """TeamMembership Renewal Policy. | 309 | """TeamMembership Renewal Policy. |
347 | @@ -1754,6 +1761,33 @@ | |||
348 | 1754 | on the displayname or other arguments. | 1761 | on the displayname or other arguments. |
349 | 1755 | """ | 1762 | """ |
350 | 1756 | 1763 | ||
351 | 1764 | def getOrCreateByOpenIDIdentifier(openid_identifier, email, | ||
352 | 1765 | full_name, creation_rationale, comment): | ||
353 | 1766 | """Get or create a person for a given OpenID identifier. | ||
354 | 1767 | |||
355 | 1768 | This is used when users login. We get the account with the given | ||
356 | 1769 | OpenID identifier (creating one if it doesn't already exist) and | ||
357 | 1770 | act according to the account's state: | ||
358 | 1771 | - If the account is suspended, we stop and raise an error. | ||
359 | 1772 | - If the account is deactivated, we reactivate it and proceed; | ||
360 | 1773 | - If the account is active, we just proceed. | ||
361 | 1774 | |||
362 | 1775 | If there is no existing Launchpad person for the account, we | ||
363 | 1776 | create it. | ||
364 | 1777 | |||
365 | 1778 | :param openid_identifier: representing the authenticated user. | ||
366 | 1779 | :param email_address: the email address of the user. | ||
367 | 1780 | :param full_name: the full name of the user. | ||
368 | 1781 | :param creation_rationale: When an account or person needs to | ||
369 | 1782 | be created, this indicates why it was created. | ||
370 | 1783 | :param comment: If the account is reactivated or person created, | ||
371 | 1784 | this comment indicates why. | ||
372 | 1785 | :return: a tuple of `IPerson` and a boolean indicating whether the | ||
373 | 1786 | database was updated. | ||
374 | 1787 | :raises AccountSuspendedError: if the account associated with the | ||
375 | 1788 | identifier has been suspended. | ||
376 | 1789 | """ | ||
377 | 1790 | |||
378 | 1757 | @call_with(teamowner=REQUEST_USER) | 1791 | @call_with(teamowner=REQUEST_USER) |
379 | 1758 | @rename_parameters_as( | 1792 | @rename_parameters_as( |
380 | 1759 | displayname='display_name', teamdescription='team_description', | 1793 | displayname='display_name', teamdescription='team_description', |
381 | 1760 | 1794 | ||
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 | 61 | from canonical.lazr.utils import get_current_browser_request, safe_hasattr | 61 | from canonical.lazr.utils import get_current_browser_request, safe_hasattr |
387 | 62 | 62 | ||
388 | 63 | from canonical.launchpad.database.account import Account, AccountPassword | 63 | from canonical.launchpad.database.account import Account, AccountPassword |
389 | 64 | from canonical.launchpad.interfaces.account import AccountSuspendedError | ||
390 | 64 | from lp.bugs.model.bugtarget import HasBugsBase | 65 | from lp.bugs.model.bugtarget import HasBugsBase |
391 | 65 | from canonical.launchpad.database.stormsugar import StartsWith | 66 | from canonical.launchpad.database.stormsugar import StartsWith |
392 | 66 | from lp.registry.model.karma import KarmaCategory | 67 | from lp.registry.model.karma import KarmaCategory |
393 | @@ -152,6 +153,7 @@ | |||
394 | 152 | 153 | ||
395 | 153 | from canonical.launchpad.validators.email import valid_email | 154 | from canonical.launchpad.validators.email import valid_email |
396 | 154 | from canonical.launchpad.validators.name import sanitize_name, valid_name | 155 | from canonical.launchpad.validators.name import sanitize_name, valid_name |
397 | 156 | from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy | ||
398 | 155 | from lp.registry.interfaces.person import validate_public_person | 157 | from lp.registry.interfaces.person import validate_public_person |
399 | 156 | 158 | ||
400 | 157 | 159 | ||
401 | @@ -2429,6 +2431,59 @@ | |||
402 | 2429 | key=lambda obj: (obj.karma, obj.displayname, obj.id), | 2431 | key=lambda obj: (obj.karma, obj.displayname, obj.id), |
403 | 2430 | reverse=True) | 2432 | reverse=True) |
404 | 2431 | 2433 | ||
405 | 2434 | def getOrCreateByOpenIDIdentifier( | ||
406 | 2435 | self, openid_identifier, email_address, full_name, | ||
407 | 2436 | creation_rationale, comment): | ||
408 | 2437 | """See `IPersonSet`.""" | ||
409 | 2438 | assert email_address is not None and full_name is not None, ( | ||
410 | 2439 | "Both email address and full name are required to " | ||
411 | 2440 | "create an account.") | ||
412 | 2441 | db_updated = False | ||
413 | 2442 | with MasterDatabasePolicy(): | ||
414 | 2443 | account_set = getUtility(IAccountSet) | ||
415 | 2444 | email_set = getUtility(IEmailAddressSet) | ||
416 | 2445 | email = email_set.getByEmail(email_address) | ||
417 | 2446 | try: | ||
418 | 2447 | account = account_set.getByOpenIDIdentifier( | ||
419 | 2448 | openid_identifier) | ||
420 | 2449 | except LookupError: | ||
421 | 2450 | if email is None: | ||
422 | 2451 | # There is no account associated with the identifier | ||
423 | 2452 | # nor an email associated with the email address. | ||
424 | 2453 | # We'll create one. | ||
425 | 2454 | account, email = account_set.createAccountAndEmail( | ||
426 | 2455 | email_address, creation_rationale, full_name, | ||
427 | 2456 | password=None, | ||
428 | 2457 | openid_identifier=openid_identifier) | ||
429 | 2458 | else: | ||
430 | 2459 | account = email.account | ||
431 | 2460 | assert account is not None, ( | ||
432 | 2461 | "This email address should have an associated " | ||
433 | 2462 | "account.") | ||
434 | 2463 | removeSecurityProxy(account).openid_identifier = ( | ||
435 | 2464 | openid_identifier) | ||
436 | 2465 | db_updated = True | ||
437 | 2466 | |||
438 | 2467 | if account.status == AccountStatus.SUSPENDED: | ||
439 | 2468 | raise AccountSuspendedError( | ||
440 | 2469 | "The account matching the identifier is suspended.") | ||
441 | 2470 | elif account.status in [AccountStatus.DEACTIVATED, | ||
442 | 2471 | AccountStatus.NOACCOUNT]: | ||
443 | 2472 | password = '' # Needed just to please reactivate() below. | ||
444 | 2473 | if email is None: | ||
445 | 2474 | email = email_set.new(email_address, account=account) | ||
446 | 2475 | removeSecurityProxy(account).reactivate( | ||
447 | 2476 | comment, password, removeSecurityProxy(email)) | ||
448 | 2477 | else: | ||
449 | 2478 | # Account is active, so nothing to do. | ||
450 | 2479 | pass | ||
451 | 2480 | if IPerson(account, None) is None: | ||
452 | 2481 | removeSecurityProxy(account).createPerson( | ||
453 | 2482 | creation_rationale, comment=comment) | ||
454 | 2483 | db_updated = True | ||
455 | 2484 | |||
456 | 2485 | return IPerson(account), db_updated | ||
457 | 2486 | |||
458 | 2432 | def newTeam(self, teamowner, name, displayname, teamdescription=None, | 2487 | def newTeam(self, teamowner, name, displayname, teamdescription=None, |
459 | 2433 | subscriptionpolicy=TeamSubscriptionPolicy.MODERATED, | 2488 | subscriptionpolicy=TeamSubscriptionPolicy.MODERATED, |
460 | 2434 | defaultmembershipperiod=None, defaultrenewalperiod=None): | 2489 | defaultmembershipperiod=None, defaultrenewalperiod=None): |
461 | 2435 | 2490 | ||
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 | 16 | MailingListAutoSubscribePolicy) | 16 | MailingListAutoSubscribePolicy) |
467 | 17 | from lp.registry.interfaces.person import ( | 17 | from lp.registry.interfaces.person import ( |
468 | 18 | PersonCreationRationale, IPersonSet) | 18 | PersonCreationRationale, IPersonSet) |
470 | 19 | from lp.testing import TestCaseWithFactory, login_person, logout | 19 | from lp.testing import ( |
471 | 20 | TestCaseWithFactory, login_person, logout) | ||
472 | 20 | 21 | ||
473 | 21 | from canonical.database.sqlbase import cursor | 22 | from canonical.database.sqlbase import cursor |
474 | 23 | from canonical.launchpad.interfaces.account import ( | ||
475 | 24 | AccountStatus, AccountSuspendedError) | ||
476 | 22 | from canonical.launchpad.testing.databasehelpers import ( | 25 | from canonical.launchpad.testing.databasehelpers import ( |
477 | 23 | remove_all_sample_data_branches) | 26 | remove_all_sample_data_branches) |
478 | 24 | from canonical.testing import DatabaseFunctionalLayer | 27 | from canonical.testing import DatabaseFunctionalLayer |
479 | @@ -146,5 +149,93 @@ | |||
480 | 146 | self.assertEqual(1, self.cur.rowcount) | 149 | self.assertEqual(1, self.cur.rowcount) |
481 | 147 | 150 | ||
482 | 148 | 151 | ||
483 | 152 | class TestPersonSetGetOrCreateByOpenIDIdentifier(TestCaseWithFactory): | ||
484 | 153 | |||
485 | 154 | layer = DatabaseFunctionalLayer | ||
486 | 155 | |||
487 | 156 | def setUp(self): | ||
488 | 157 | super(TestPersonSetGetOrCreateByOpenIDIdentifier, self).setUp() | ||
489 | 158 | self.person_set = getUtility(IPersonSet) | ||
490 | 159 | |||
491 | 160 | def callGetOrCreate(self, identifier, email='a@b.com'): | ||
492 | 161 | return self.person_set.getOrCreateByOpenIDIdentifier( | ||
493 | 162 | identifier, email, "Joe Bloggs", | ||
494 | 163 | PersonCreationRationale.SOFTWARE_CENTER_PURCHASE, | ||
495 | 164 | "when purchasing an application via Software Center.") | ||
496 | 165 | |||
497 | 166 | def test_existing_person(self): | ||
498 | 167 | person = self.factory.makePerson() | ||
499 | 168 | openid_ident = removeSecurityProxy(person.account).openid_identifier | ||
500 | 169 | person_set = getUtility(IPersonSet) | ||
501 | 170 | |||
502 | 171 | result, db_updated = self.callGetOrCreate(openid_ident) | ||
503 | 172 | |||
504 | 173 | self.assertEqual(person, result) | ||
505 | 174 | self.assertFalse(db_updated) | ||
506 | 175 | |||
507 | 176 | def test_existing_account_no_person(self): | ||
508 | 177 | # A person is created with the correct rationale. | ||
509 | 178 | account = self.factory.makeAccount('purchaser') | ||
510 | 179 | openid_ident = removeSecurityProxy(account).openid_identifier | ||
511 | 180 | |||
512 | 181 | person, db_updated = self.callGetOrCreate(openid_ident) | ||
513 | 182 | |||
514 | 183 | self.assertEqual(account, person.account) | ||
515 | 184 | # The person is created with the correct rationale, creation | ||
516 | 185 | # comment, registrant, appropriate display name. | ||
517 | 186 | self.assertEqual( | ||
518 | 187 | "when purchasing an application via Software Center.", | ||
519 | 188 | person.creation_comment) | ||
520 | 189 | self.assertEqual( | ||
521 | 190 | PersonCreationRationale.SOFTWARE_CENTER_PURCHASE, | ||
522 | 191 | person.creation_rationale) | ||
523 | 192 | self.assertTrue(db_updated) | ||
524 | 193 | |||
525 | 194 | def test_existing_deactivated_account(self): | ||
526 | 195 | # An existing deactivated account will be reactivated. | ||
527 | 196 | account = self.factory.makeAccount('purchaser', | ||
528 | 197 | status=AccountStatus.DEACTIVATED) | ||
529 | 198 | openid_ident = removeSecurityProxy(account).openid_identifier | ||
530 | 199 | |||
531 | 200 | person, db_updated = self.callGetOrCreate(openid_ident) | ||
532 | 201 | self.assertEqual(AccountStatus.ACTIVE, person.account.status) | ||
533 | 202 | self.assertTrue(db_updated) | ||
534 | 203 | self.assertEqual( | ||
535 | 204 | "when purchasing an application via Software Center.", | ||
536 | 205 | removeSecurityProxy(person.account).status_comment) | ||
537 | 206 | |||
538 | 207 | def test_existing_suspended_account(self): | ||
539 | 208 | # An existing suspended account will raise an exception. | ||
540 | 209 | account = self.factory.makeAccount('purchaser', | ||
541 | 210 | status=AccountStatus.SUSPENDED) | ||
542 | 211 | openid_ident = removeSecurityProxy(account).openid_identifier | ||
543 | 212 | |||
544 | 213 | self.assertRaises( | ||
545 | 214 | AccountSuspendedError, self.callGetOrCreate, openid_ident) | ||
546 | 215 | |||
547 | 216 | def test_no_account_or_email(self): | ||
548 | 217 | # An identifier can be used to create an account (it is assumed | ||
549 | 218 | # to be already authenticated with SSO). | ||
550 | 219 | person, db_updated = self.callGetOrCreate('openid-identifier') | ||
551 | 220 | |||
552 | 221 | self.assertEqual( | ||
553 | 222 | "openid-identifier", | ||
554 | 223 | removeSecurityProxy(person.account).openid_identifier) | ||
555 | 224 | self.assertTrue(db_updated) | ||
556 | 225 | |||
557 | 226 | def test_no_matching_account_existing_email(self): | ||
558 | 227 | # The openid_identity of the account matching the email will | ||
559 | 228 | # updated. | ||
560 | 229 | other_account = self.factory.makeAccount('test', email='a@b.com') | ||
561 | 230 | |||
562 | 231 | person, db_updated = self.callGetOrCreate( | ||
563 | 232 | 'other-openid-identifier', 'a@b.com') | ||
564 | 233 | |||
565 | 234 | self.assertEqual(other_account, person.account) | ||
566 | 235 | self.assertEqual( | ||
567 | 236 | 'other-openid-identifier', | ||
568 | 237 | removeSecurityProxy(person.account).openid_identifier) | ||
569 | 238 | |||
570 | 239 | |||
571 | 149 | def test_suite(): | 240 | def test_suite(): |
572 | 150 | return TestLoader().loadTestsFromName(__name__) | 241 | return TestLoader().loadTestsFromName(__name__) |
573 | 151 | 242 | ||
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 | 198 | else: | 198 | else: |
579 | 199 | response = self.openid_request.answer(True) | 199 | response = self.openid_request.answer(True) |
580 | 200 | 200 | ||
582 | 201 | sreg_fields = dict(nickname=IPerson(self.account).name) | 201 | sreg_fields = dict( |
583 | 202 | nickname=IPerson(self.account).name, | ||
584 | 203 | email=self.account.preferredemail.email, | ||
585 | 204 | fullname=self.account.displayname) | ||
586 | 202 | sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request) | 205 | sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request) |
587 | 203 | sreg_response = SRegResponse.extractResponse( | 206 | sreg_response = SRegResponse.extractResponse( |
588 | 204 | sreg_request, sreg_fields) | 207 | sreg_request, sreg_fields) |
589 | @@ -224,7 +227,7 @@ | |||
590 | 224 | This class implements an OpenID endpoint using the python-openid | 227 | This class implements an OpenID endpoint using the python-openid |
591 | 225 | library. In addition to the normal modes of operation, it also | 228 | library. In addition to the normal modes of operation, it also |
592 | 226 | implements the OpenID 2.0 identifier select mode. | 229 | implements the OpenID 2.0 identifier select mode. |
594 | 227 | 230 | ||
595 | 228 | Note that the checkid_immediate mode is not supported. | 231 | Note that the checkid_immediate mode is not supported. |
596 | 229 | """ | 232 | """ |
597 | 230 | 233 |
Your general approach looks sane to me. I might be able to do a proper review today or tomorrow if you'd like.