Merge lp:~wgrant/launchpad/sca-api into lp:launchpad
- sca-api
- Merge into devel
Proposed by
William Grant
Status: | Merged |
---|---|
Merged at revision: | 17390 |
Proposed branch: | lp:~wgrant/launchpad/sca-api |
Merge into: | lp:launchpad |
Diff against target: |
475 lines (+290/-13) 6 files modified
lib/lp/registry/browser/tests/test_person_webservice.py (+61/-1) lib/lp/registry/interfaces/person.py (+25/-1) lib/lp/registry/model/person.py (+57/-8) lib/lp/registry/tests/test_personset.py (+138/-1) lib/lp/services/identity/interfaces/account.py (+4/-1) lib/lp/services/identity/interfaces/emailaddress.py (+5/-1) |
To merge this branch: | bzr merge lp:~wgrant/launchpad/sca-api |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Nelson (community) | Approve | ||
Colin Watson (community) | Approve | ||
Review via email: mp+252420@code.launchpad.net |
Commit message
Implement PersonSet.
Description of the change
Implement PersonSet.
It will replace the xmlrpc-private method of the same name with something that's a bit less insecure. SCA will no longer be able to compromise existing active accounts by attaching new OpenID identifiers or email addresses to them. It retains the ability to:
- Look up an existing active account by OpenID identifier
- Activate an unactivated account by OpenID identifier and with the given email address
- Create a new account with the given OpenID identifier and email address
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/registry/browser/tests/test_person_webservice.py' | |||
2 | --- lib/lp/registry/browser/tests/test_person_webservice.py 2015-01-07 00:35:53 +0000 | |||
3 | +++ lib/lp/registry/browser/tests/test_person_webservice.py 2015-03-10 22:08:18 +0000 | |||
4 | @@ -1,8 +1,9 @@ | |||
6 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
7 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
8 | 3 | 3 | ||
9 | 4 | __metaclass__ = type | 4 | __metaclass__ = type |
10 | 5 | 5 | ||
11 | 6 | from storm.store import Store | ||
12 | 6 | from testtools.matchers import Equals | 7 | from testtools.matchers import Equals |
13 | 7 | from zope.component import getUtility | 8 | from zope.component import getUtility |
14 | 8 | from zope.security.management import endInteraction | 9 | from zope.security.management import endInteraction |
15 | @@ -14,6 +15,7 @@ | |||
16 | 14 | ) | 15 | ) |
17 | 15 | from lp.registry.interfaces.teammembership import ITeamMembershipSet | 16 | from lp.registry.interfaces.teammembership import ITeamMembershipSet |
18 | 16 | from lp.services.identity.interfaces.account import AccountStatus | 17 | from lp.services.identity.interfaces.account import AccountStatus |
19 | 18 | from lp.services.openid.model.openididentifier import OpenIdIdentifier | ||
20 | 17 | from lp.services.webapp.interfaces import OAuthPermission | 19 | from lp.services.webapp.interfaces import OAuthPermission |
21 | 18 | from lp.testing import ( | 20 | from lp.testing import ( |
22 | 19 | admin_logged_in, | 21 | admin_logged_in, |
23 | @@ -282,3 +284,61 @@ | |||
24 | 282 | 'identifier=http://login1.dev/%%2Bid/%s' | 284 | 'identifier=http://login1.dev/%%2Bid/%s' |
25 | 283 | % person_openid, | 285 | % person_openid, |
26 | 284 | api_version='devel').jsonBody()['name']) | 286 | api_version='devel').jsonBody()['name']) |
27 | 287 | |||
28 | 288 | def getOrCreateSoftwareCenterCustomer(self, user): | ||
29 | 289 | webservice = webservice_for_person( | ||
30 | 290 | user, permission=OAuthPermission.WRITE_PRIVATE) | ||
31 | 291 | response = webservice.named_post( | ||
32 | 292 | '/people', 'getOrCreateSoftwareCenterCustomer', | ||
33 | 293 | openid_identifier='somebody', | ||
34 | 294 | email_address='somebody@example.com', display_name='Somebody', | ||
35 | 295 | api_version='devel') | ||
36 | 296 | return response | ||
37 | 297 | |||
38 | 298 | def test_getOrCreateSoftwareCenterCustomer(self): | ||
39 | 299 | # Software Center Agent (SCA) can get or create people by OpenID | ||
40 | 300 | # identifier. | ||
41 | 301 | with admin_logged_in(): | ||
42 | 302 | sca = getUtility(IPersonSet).getByName('software-center-agent') | ||
43 | 303 | response = self.getOrCreateSoftwareCenterCustomer(sca) | ||
44 | 304 | self.assertEqual('Somebody', response.jsonBody()['display_name']) | ||
45 | 305 | with admin_logged_in(): | ||
46 | 306 | person = getUtility(IPersonSet).getByEmail('somebody@example.com') | ||
47 | 307 | self.assertEqual('Somebody', person.displayname) | ||
48 | 308 | self.assertEqual( | ||
49 | 309 | ['somebody'], | ||
50 | 310 | [oid.identifier for oid in person.account.openid_identifiers]) | ||
51 | 311 | self.assertEqual( | ||
52 | 312 | 'somebody@example.com', person.preferredemail.email) | ||
53 | 313 | |||
54 | 314 | def test_getOrCreateSoftwareCenterCustomer_is_restricted(self): | ||
55 | 315 | # The method may only be invoked by the ~software-center-agent | ||
56 | 316 | # celebrity user, as it is security-sensitive. | ||
57 | 317 | with admin_logged_in(): | ||
58 | 318 | random = self.factory.makePerson() | ||
59 | 319 | response = self.getOrCreateSoftwareCenterCustomer(random) | ||
60 | 320 | self.assertEqual(401, response.status) | ||
61 | 321 | |||
62 | 322 | def test_getOrCreateSoftwareCenterCustomer_rejects_email_conflicts(self): | ||
63 | 323 | # An unknown OpenID identifier with a known email address causes | ||
64 | 324 | # the request to fail with 409 Conflict, as we'd otherwise end | ||
65 | 325 | # up linking the OpenID identifier to an existing account. | ||
66 | 326 | with admin_logged_in(): | ||
67 | 327 | self.factory.makePerson(email='somebody@example.com') | ||
68 | 328 | sca = getUtility(IPersonSet).getByName('software-center-agent') | ||
69 | 329 | response = self.getOrCreateSoftwareCenterCustomer(sca) | ||
70 | 330 | self.assertEqual(409, response.status) | ||
71 | 331 | |||
72 | 332 | def test_getOrCreateSoftwareCenterCustomer_rejects_suspended(self): | ||
73 | 333 | # Suspended accounts are not returned. | ||
74 | 334 | with admin_logged_in(): | ||
75 | 335 | existing = self.factory.makePerson( | ||
76 | 336 | email='somebody@example.com', | ||
77 | 337 | account_status=AccountStatus.SUSPENDED) | ||
78 | 338 | oid = OpenIdIdentifier() | ||
79 | 339 | oid.account = existing.account | ||
80 | 340 | oid.identifier = u'somebody' | ||
81 | 341 | Store.of(existing).add(oid) | ||
82 | 342 | sca = getUtility(IPersonSet).getByName('software-center-agent') | ||
83 | 343 | response = self.getOrCreateSoftwareCenterCustomer(sca) | ||
84 | 344 | self.assertEqual(400, response.status) | ||
85 | 285 | 345 | ||
86 | === modified file 'lib/lp/registry/interfaces/person.py' | |||
87 | --- lib/lp/registry/interfaces/person.py 2015-02-26 11:34:47 +0000 | |||
88 | +++ lib/lp/registry/interfaces/person.py 2015-03-10 22:08:18 +0000 | |||
89 | @@ -1,4 +1,4 @@ | |||
91 | 1 | # Copyright 2009-2014 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
92 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
93 | 3 | 3 | ||
94 | 4 | """Person interfaces.""" | 4 | """Person interfaces.""" |
95 | @@ -2173,6 +2173,30 @@ | |||
96 | 2173 | identifier has been suspended. | 2173 | identifier has been suspended. |
97 | 2174 | """ | 2174 | """ |
98 | 2175 | 2175 | ||
99 | 2176 | @call_with(user=REQUEST_USER) | ||
100 | 2177 | @operation_parameters( | ||
101 | 2178 | openid_identifier=TextLine( | ||
102 | 2179 | title=_("OpenID identifier suffix"), required=True), | ||
103 | 2180 | email_address=TextLine(title=_("Email address"), required=True), | ||
104 | 2181 | display_name=TextLine(title=_("Display name"), required=True)) | ||
105 | 2182 | @export_write_operation() | ||
106 | 2183 | @operation_for_version("devel") | ||
107 | 2184 | def getOrCreateSoftwareCenterCustomer(user, openid_identifier, | ||
108 | 2185 | email_address, display_name): | ||
109 | 2186 | """Restricted person creation API for Software Center Agent. | ||
110 | 2187 | |||
111 | 2188 | This method can only be called by Software Center Agent. It gets | ||
112 | 2189 | a person by OpenID identifier or creates a new Launchpad person | ||
113 | 2190 | from the OpenID identifier, email address and display name. | ||
114 | 2191 | |||
115 | 2192 | :param user: the `IPerson` performing the operation. Only the | ||
116 | 2193 | software-center-agent celebrity is allowed. | ||
117 | 2194 | :param openid_identifier: OpenID identifier suffix for the user. | ||
118 | 2195 | This is *not* the full URL, just the unique suffix portion. | ||
119 | 2196 | :param email_address: the email address of the user. | ||
120 | 2197 | :param full_name: the full name of the user. | ||
121 | 2198 | """ | ||
122 | 2199 | |||
123 | 2176 | @call_with(teamowner=REQUEST_USER) | 2200 | @call_with(teamowner=REQUEST_USER) |
124 | 2177 | @rename_parameters_as( | 2201 | @rename_parameters_as( |
125 | 2178 | displayname='display_name', teamdescription='team_description', | 2202 | displayname='display_name', teamdescription='team_description', |
126 | 2179 | 2203 | ||
127 | === modified file 'lib/lp/registry/model/person.py' | |||
128 | --- lib/lp/registry/model/person.py 2015-03-04 19:05:47 +0000 | |||
129 | +++ lib/lp/registry/model/person.py 2015-03-10 22:08:18 +0000 | |||
130 | @@ -268,6 +268,7 @@ | |||
131 | 268 | INACTIVE_ACCOUNT_STATUSES, | 268 | INACTIVE_ACCOUNT_STATUSES, |
132 | 269 | ) | 269 | ) |
133 | 270 | from lp.services.identity.interfaces.emailaddress import ( | 270 | from lp.services.identity.interfaces.emailaddress import ( |
134 | 271 | EmailAddressAlreadyTaken, | ||
135 | 271 | EmailAddressStatus, | 272 | EmailAddressStatus, |
136 | 272 | IEmailAddress, | 273 | IEmailAddress, |
137 | 273 | IEmailAddressSet, | 274 | IEmailAddressSet, |
138 | @@ -3317,8 +3318,26 @@ | |||
139 | 3317 | return IPerson(account) | 3318 | return IPerson(account) |
140 | 3318 | 3319 | ||
141 | 3319 | def getOrCreateByOpenIDIdentifier(self, openid_identifier, email_address, | 3320 | def getOrCreateByOpenIDIdentifier(self, openid_identifier, email_address, |
143 | 3320 | full_name, creation_rationale, comment): | 3321 | full_name, creation_rationale, comment, |
144 | 3322 | trust_email=True): | ||
145 | 3321 | """See `IPersonSet`.""" | 3323 | """See `IPersonSet`.""" |
146 | 3324 | # trust_email is an internal flag used by | ||
147 | 3325 | # getOrCreateSoftwareCenterCustomer. We don't want SCA to be | ||
148 | 3326 | # able to use the API to associate arbitrary OpenID identifiers | ||
149 | 3327 | # and email addresses when that could compromise existing | ||
150 | 3328 | # accounts. | ||
151 | 3329 | # | ||
152 | 3330 | # To that end, if trust_email is not set then the given | ||
153 | 3331 | # email address will only be used to create an account. It will | ||
154 | 3332 | # never be used to look up an account, nor will it be added to | ||
155 | 3333 | # an existing account. This causes two additional cases to be | ||
156 | 3334 | # rejected: unknown OpenID identifier but known email address, | ||
157 | 3335 | # and deactivated account. | ||
158 | 3336 | # | ||
159 | 3337 | # Exempting account creation and activation from this rule opens | ||
160 | 3338 | # us to a potential account fixation attack, but the risk is | ||
161 | 3339 | # minimal. | ||
162 | 3340 | |||
163 | 3322 | assert email_address is not None and full_name is not None, ( | 3341 | assert email_address is not None and full_name is not None, ( |
164 | 3323 | "Both email address and full name are required to create an " | 3342 | "Both email address and full name are required to create an " |
165 | 3324 | "account.") | 3343 | "account.") |
166 | @@ -3341,13 +3360,19 @@ | |||
167 | 3341 | # We don't know about the OpenID identifier yet, so try | 3360 | # We don't know about the OpenID identifier yet, so try |
168 | 3342 | # to match a person by email address, or as a last | 3361 | # to match a person by email address, or as a last |
169 | 3343 | # resort create a new one. | 3362 | # resort create a new one. |
173 | 3344 | if email is not None: | 3363 | if email is None: |
171 | 3345 | person = email.person | ||
172 | 3346 | else: | ||
174 | 3347 | person_set = getUtility(IPersonSet) | 3364 | person_set = getUtility(IPersonSet) |
175 | 3348 | person, email = person_set.createPersonAndEmail( | 3365 | person, email = person_set.createPersonAndEmail( |
176 | 3349 | email_address, creation_rationale, comment=comment, | 3366 | email_address, creation_rationale, comment=comment, |
177 | 3350 | displayname=full_name) | 3367 | displayname=full_name) |
178 | 3368 | elif trust_email: | ||
179 | 3369 | person = email.person | ||
180 | 3370 | else: | ||
181 | 3371 | # The email address originated from a source that's | ||
182 | 3372 | # not completely trustworth (eg. SCA), so we can't | ||
183 | 3373 | # use it to link the OpenID identifier to an | ||
184 | 3374 | # existing person. | ||
185 | 3375 | raise EmailAddressAlreadyTaken() | ||
186 | 3351 | 3376 | ||
187 | 3352 | # It's possible that the email address is owned by a | 3377 | # It's possible that the email address is owned by a |
188 | 3353 | # team. Reject the login attempt, and wait for the user | 3378 | # team. Reject the login attempt, and wait for the user |
189 | @@ -3373,10 +3398,21 @@ | |||
190 | 3373 | person = IPerson(identifier.account, None) | 3398 | person = IPerson(identifier.account, None) |
191 | 3374 | assert person is not None, ('Received a personless account.') | 3399 | assert person is not None, ('Received a personless account.') |
192 | 3375 | 3400 | ||
194 | 3376 | if person.account.status == AccountStatus.SUSPENDED: | 3401 | status = person.account.status |
195 | 3402 | if status == AccountStatus.ACTIVE: | ||
196 | 3403 | # Account is active, so nothing to do. | ||
197 | 3404 | pass | ||
198 | 3405 | elif status == AccountStatus.SUSPENDED: | ||
199 | 3377 | raise AccountSuspendedError( | 3406 | raise AccountSuspendedError( |
200 | 3378 | "The account matching the identifier is suspended.") | 3407 | "The account matching the identifier is suspended.") |
202 | 3379 | 3408 | elif not trust_email and status != AccountStatus.NOACCOUNT: | |
203 | 3409 | # If the email address is not completely trustworthy | ||
204 | 3410 | # (ie. it comes from SCA) and the account has already | ||
205 | 3411 | # been used, then we don't want to proceed as we might | ||
206 | 3412 | # end up adding a malicious OpenID identifier to an | ||
207 | 3413 | # existing account. | ||
208 | 3414 | raise NameAlreadyTaken( | ||
209 | 3415 | "The account matching the identifier is inactive.") | ||
210 | 3380 | elif person.account.status in [AccountStatus.DEACTIVATED, | 3416 | elif person.account.status in [AccountStatus.DEACTIVATED, |
211 | 3381 | AccountStatus.NOACCOUNT]: | 3417 | AccountStatus.NOACCOUNT]: |
212 | 3382 | removeSecurityProxy(person.account).reactivate(comment) | 3418 | removeSecurityProxy(person.account).reactivate(comment) |
213 | @@ -3386,11 +3422,24 @@ | |||
214 | 3386 | removeSecurityProxy(person).setPreferredEmail(email) | 3422 | removeSecurityProxy(person).setPreferredEmail(email) |
215 | 3387 | db_updated = True | 3423 | db_updated = True |
216 | 3388 | else: | 3424 | else: |
219 | 3389 | # Account is active, so nothing to do. | 3425 | raise AssertionError( |
220 | 3390 | pass | 3426 | "Unhandled account status: %r" % person.account.status) |
221 | 3391 | 3427 | ||
222 | 3392 | return person, db_updated | 3428 | return person, db_updated |
223 | 3393 | 3429 | ||
224 | 3430 | def getOrCreateSoftwareCenterCustomer(self, user, openid_identifier, | ||
225 | 3431 | email_address, display_name): | ||
226 | 3432 | """See `IPersonSet`.""" | ||
227 | 3433 | if user != getUtility(ILaunchpadCelebrities).software_center_agent: | ||
228 | 3434 | raise Unauthorized() | ||
229 | 3435 | person, _ = getUtility( | ||
230 | 3436 | IPersonSet).getOrCreateByOpenIDIdentifier( | ||
231 | 3437 | openid_identifier, email_address, display_name, | ||
232 | 3438 | PersonCreationRationale.SOFTWARE_CENTER_PURCHASE, | ||
233 | 3439 | "when purchasing an application via Software Center.", | ||
234 | 3440 | trust_email=False) | ||
235 | 3441 | return person | ||
236 | 3442 | |||
237 | 3394 | def newTeam(self, teamowner, name, displayname, teamdescription=None, | 3443 | def newTeam(self, teamowner, name, displayname, teamdescription=None, |
238 | 3395 | membership_policy=TeamMembershipPolicy.MODERATED, | 3444 | membership_policy=TeamMembershipPolicy.MODERATED, |
239 | 3396 | defaultmembershipperiod=None, defaultrenewalperiod=None, | 3445 | defaultmembershipperiod=None, defaultrenewalperiod=None, |
240 | 3397 | 3446 | ||
241 | === modified file 'lib/lp/registry/tests/test_personset.py' | |||
242 | --- lib/lp/registry/tests/test_personset.py 2015-01-07 00:35:53 +0000 | |||
243 | +++ lib/lp/registry/tests/test_personset.py 2015-03-10 22:08:18 +0000 | |||
244 | @@ -1,4 +1,4 @@ | |||
246 | 1 | # Copyright 2009-2013 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
247 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
248 | 3 | 3 | ||
249 | 4 | """Tests for PersonSet.""" | 4 | """Tests for PersonSet.""" |
250 | @@ -9,6 +9,7 @@ | |||
251 | 9 | from testtools.matchers import LessThan | 9 | from testtools.matchers import LessThan |
252 | 10 | import transaction | 10 | import transaction |
253 | 11 | from zope.component import getUtility | 11 | from zope.component import getUtility |
254 | 12 | from zope.security.interfaces import Unauthorized | ||
255 | 12 | from zope.security.proxy import removeSecurityProxy | 13 | from zope.security.proxy import removeSecurityProxy |
256 | 13 | 14 | ||
257 | 14 | from lp.code.tests.helpers import remove_all_sample_data_branches | 15 | from lp.code.tests.helpers import remove_all_sample_data_branches |
258 | @@ -41,13 +42,17 @@ | |||
259 | 41 | from lp.services.identity.interfaces.emailaddress import ( | 42 | from lp.services.identity.interfaces.emailaddress import ( |
260 | 42 | EmailAddressAlreadyTaken, | 43 | EmailAddressAlreadyTaken, |
261 | 43 | EmailAddressStatus, | 44 | EmailAddressStatus, |
262 | 45 | IEmailAddressSet, | ||
263 | 44 | InvalidEmailAddress, | 46 | InvalidEmailAddress, |
264 | 45 | ) | 47 | ) |
265 | 46 | from lp.services.identity.model.account import Account | 48 | from lp.services.identity.model.account import Account |
266 | 47 | from lp.services.identity.model.emailaddress import EmailAddress | 49 | from lp.services.identity.model.emailaddress import EmailAddress |
267 | 48 | from lp.services.openid.model.openididentifier import OpenIdIdentifier | 50 | from lp.services.openid.model.openididentifier import OpenIdIdentifier |
268 | 51 | from lp.services.webapp.interfaces import ILaunchBag | ||
269 | 49 | from lp.testing import ( | 52 | from lp.testing import ( |
270 | 53 | admin_logged_in, | ||
271 | 50 | ANONYMOUS, | 54 | ANONYMOUS, |
272 | 55 | anonymous_logged_in, | ||
273 | 51 | login, | 56 | login, |
274 | 52 | logout, | 57 | logout, |
275 | 53 | person_logged_in, | 58 | person_logged_in, |
276 | @@ -590,3 +595,135 @@ | |||
277 | 590 | u'other-openid-identifier' in [ | 595 | u'other-openid-identifier' in [ |
278 | 591 | identifier.identifier for identifier in removeSecurityProxy( | 596 | identifier.identifier for identifier in removeSecurityProxy( |
279 | 592 | person.account).openid_identifiers]) | 597 | person.account).openid_identifiers]) |
280 | 598 | |||
281 | 599 | |||
282 | 600 | class TestPersonSetGetOrCreateSoftwareCenterCustomer(TestCaseWithFactory): | ||
283 | 601 | |||
284 | 602 | layer = DatabaseFunctionalLayer | ||
285 | 603 | |||
286 | 604 | def setUp(self): | ||
287 | 605 | super(TestPersonSetGetOrCreateSoftwareCenterCustomer, self).setUp() | ||
288 | 606 | self.sca = getUtility(IPersonSet).getByName('software-center-agent') | ||
289 | 607 | |||
290 | 608 | def makeOpenIdIdentifier(self, account, identifier): | ||
291 | 609 | openid_identifier = OpenIdIdentifier() | ||
292 | 610 | openid_identifier.identifier = identifier | ||
293 | 611 | openid_identifier.account = account | ||
294 | 612 | return IStore(OpenIdIdentifier).add(openid_identifier) | ||
295 | 613 | |||
296 | 614 | def test_restricted_to_sca(self): | ||
297 | 615 | # Only the software-center-agent celebrity can invoke this | ||
298 | 616 | # privileged method. | ||
299 | 617 | def do_it(): | ||
300 | 618 | return getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( | ||
301 | 619 | getUtility(ILaunchBag).user, u'somebody', | ||
302 | 620 | 'somebody@example.com', 'Example') | ||
303 | 621 | random = self.factory.makePerson() | ||
304 | 622 | admin = self.factory.makePerson( | ||
305 | 623 | member_of=[getUtility(IPersonSet).getByName('admins')]) | ||
306 | 624 | |||
307 | 625 | # Anonymous, random or admin users can't invoke the method. | ||
308 | 626 | with anonymous_logged_in(): | ||
309 | 627 | self.assertRaises(Unauthorized, do_it) | ||
310 | 628 | with person_logged_in(random): | ||
311 | 629 | self.assertRaises(Unauthorized, do_it) | ||
312 | 630 | with person_logged_in(admin): | ||
313 | 631 | self.assertRaises(Unauthorized, do_it) | ||
314 | 632 | |||
315 | 633 | with person_logged_in(self.sca): | ||
316 | 634 | person = do_it() | ||
317 | 635 | self.assertIsInstance(person, Person) | ||
318 | 636 | |||
319 | 637 | def test_finds_by_openid(self): | ||
320 | 638 | # A Person with the requested OpenID identifier is returned. | ||
321 | 639 | somebody = self.factory.makePerson() | ||
322 | 640 | self.makeOpenIdIdentifier(somebody.account, u'somebody') | ||
323 | 641 | with person_logged_in(self.sca): | ||
324 | 642 | got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( | ||
325 | 643 | self.sca, u'somebody', 'somebody@example.com', 'Example') | ||
326 | 644 | self.assertEqual(somebody, got) | ||
327 | 645 | |||
328 | 646 | # The email address doesn't get linked, as that could change how | ||
329 | 647 | # future logins work. | ||
330 | 648 | self.assertIs( | ||
331 | 649 | None, | ||
332 | 650 | getUtility(IEmailAddressSet).getByEmail('somebody@example.com')) | ||
333 | 651 | |||
334 | 652 | def test_creates_new(self): | ||
335 | 653 | # If an unknown OpenID identifier and email address are | ||
336 | 654 | # provided, a new account is created and returned. | ||
337 | 655 | with person_logged_in(self.sca): | ||
338 | 656 | made = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( | ||
339 | 657 | self.sca, u'somebody', 'somebody@example.com', 'Example') | ||
340 | 658 | with admin_logged_in(): | ||
341 | 659 | self.assertEqual('Example', made.displayname) | ||
342 | 660 | self.assertEqual('somebody@example.com', made.preferredemail.email) | ||
343 | 661 | |||
344 | 662 | # The email address is linked, since it can't compromise an | ||
345 | 663 | # account that is being created just for it. | ||
346 | 664 | email = getUtility(IEmailAddressSet).getByEmail('somebody@example.com') | ||
347 | 665 | self.assertEqual(made, email.person) | ||
348 | 666 | |||
349 | 667 | def test_activates_unactivated(self): | ||
350 | 668 | # An unactivated account should be treated just like a new | ||
351 | 669 | # account -- it gets activated with the given email address. | ||
352 | 670 | somebody = self.factory.makePerson( | ||
353 | 671 | email='existing@example.com', | ||
354 | 672 | account_status=AccountStatus.NOACCOUNT) | ||
355 | 673 | self.makeOpenIdIdentifier(somebody.account, u'somebody') | ||
356 | 674 | self.assertEqual(AccountStatus.NOACCOUNT, somebody.account.status) | ||
357 | 675 | with person_logged_in(self.sca): | ||
358 | 676 | got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( | ||
359 | 677 | self.sca, u'somebody', 'somebody@example.com', 'Example') | ||
360 | 678 | self.assertEqual(somebody, got) | ||
361 | 679 | with admin_logged_in(): | ||
362 | 680 | self.assertEqual(AccountStatus.ACTIVE, somebody.account.status) | ||
363 | 681 | self.assertEqual( | ||
364 | 682 | 'somebody@example.com', somebody.preferredemail.email) | ||
365 | 683 | |||
366 | 684 | def test_fails_if_email_is_already_registered(self): | ||
367 | 685 | # Only the OpenID identifier is used to look up an account. If | ||
368 | 686 | # the OpenID identifier isn't already registered by the email | ||
369 | 687 | # address is, the request is rejected to avoid potentially | ||
370 | 688 | # adding an unwanted OpenID identifier to the address' account. | ||
371 | 689 | # | ||
372 | 690 | # The user must log into Launchpad directly first to register | ||
373 | 691 | # their OpenID identifier. | ||
374 | 692 | other = self.factory.makePerson(email='other@example.com') | ||
375 | 693 | with person_logged_in(self.sca): | ||
376 | 694 | self.assertRaises( | ||
377 | 695 | EmailAddressAlreadyTaken, | ||
378 | 696 | getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer, | ||
379 | 697 | self.sca, u'somebody', 'other@example.com', 'Example') | ||
380 | 698 | |||
381 | 699 | # The email address stays with the old owner. | ||
382 | 700 | email = getUtility(IEmailAddressSet).getByEmail('other@example.com') | ||
383 | 701 | self.assertEqual(other, email.person) | ||
384 | 702 | |||
385 | 703 | def test_fails_if_account_is_suspended(self): | ||
386 | 704 | # Suspended accounts cannot be returned. | ||
387 | 705 | somebody = self.factory.makePerson() | ||
388 | 706 | self.makeOpenIdIdentifier(somebody.account, u'somebody') | ||
389 | 707 | with admin_logged_in(): | ||
390 | 708 | somebody.setAccountStatus( | ||
391 | 709 | AccountStatus.SUSPENDED, None, "Go away!") | ||
392 | 710 | with person_logged_in(self.sca): | ||
393 | 711 | self.assertRaises( | ||
394 | 712 | AccountSuspendedError, | ||
395 | 713 | getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer, | ||
396 | 714 | self.sca, u'somebody', 'somebody@example.com', 'Example') | ||
397 | 715 | |||
398 | 716 | def test_fails_if_account_is_deactivated(self): | ||
399 | 717 | # We don't want to reactivate explicitly deactivated accounts, | ||
400 | 718 | # nor do we want to potentially compromise them with a bad email | ||
401 | 719 | # address. | ||
402 | 720 | somebody = self.factory.makePerson() | ||
403 | 721 | self.makeOpenIdIdentifier(somebody.account, u'somebody') | ||
404 | 722 | with admin_logged_in(): | ||
405 | 723 | somebody.setAccountStatus( | ||
406 | 724 | AccountStatus.DEACTIVATED, None, "Goodbye cruel world.") | ||
407 | 725 | with person_logged_in(self.sca): | ||
408 | 726 | self.assertRaises( | ||
409 | 727 | NameAlreadyTaken, | ||
410 | 728 | getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer, | ||
411 | 729 | self.sca, u'somebody', 'somebody@example.com', 'Example') | ||
412 | 593 | 730 | ||
413 | === modified file 'lib/lp/services/identity/interfaces/account.py' | |||
414 | --- lib/lp/services/identity/interfaces/account.py 2015-01-07 00:35:53 +0000 | |||
415 | +++ lib/lp/services/identity/interfaces/account.py 2015-03-10 22:08:18 +0000 | |||
416 | @@ -1,4 +1,4 @@ | |||
418 | 1 | # Copyright 2009-2012 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
419 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
420 | 3 | 3 | ||
421 | 4 | """Account interfaces.""" | 4 | """Account interfaces.""" |
422 | @@ -18,11 +18,13 @@ | |||
423 | 18 | 'INACTIVE_ACCOUNT_STATUSES', | 18 | 'INACTIVE_ACCOUNT_STATUSES', |
424 | 19 | ] | 19 | ] |
425 | 20 | 20 | ||
426 | 21 | import httplib | ||
427 | 21 | 22 | ||
428 | 22 | from lazr.enum import ( | 23 | from lazr.enum import ( |
429 | 23 | DBEnumeratedType, | 24 | DBEnumeratedType, |
430 | 24 | DBItem, | 25 | DBItem, |
431 | 25 | ) | 26 | ) |
432 | 27 | from lazr.restful.declarations import error_status | ||
433 | 26 | from zope.interface import ( | 28 | from zope.interface import ( |
434 | 27 | Attribute, | 29 | Attribute, |
435 | 28 | Interface, | 30 | Interface, |
436 | @@ -40,6 +42,7 @@ | |||
437 | 40 | from lp.services.fields import StrippedTextLine | 42 | from lp.services.fields import StrippedTextLine |
438 | 41 | 43 | ||
439 | 42 | 44 | ||
440 | 45 | @error_status(httplib.BAD_REQUEST) | ||
441 | 43 | class AccountSuspendedError(Exception): | 46 | class AccountSuspendedError(Exception): |
442 | 44 | """The account being accessed has been suspended.""" | 47 | """The account being accessed has been suspended.""" |
443 | 45 | 48 | ||
444 | 46 | 49 | ||
445 | === modified file 'lib/lp/services/identity/interfaces/emailaddress.py' | |||
446 | --- lib/lp/services/identity/interfaces/emailaddress.py 2013-01-07 02:40:55 +0000 | |||
447 | +++ lib/lp/services/identity/interfaces/emailaddress.py 2015-03-10 22:08:18 +0000 | |||
448 | @@ -1,4 +1,4 @@ | |||
450 | 1 | # Copyright 2009-2012 Canonical Ltd. This software is licensed under the | 1 | # Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
451 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). |
452 | 3 | 3 | ||
453 | 4 | """EmailAddress interfaces.""" | 4 | """EmailAddress interfaces.""" |
454 | @@ -12,11 +12,14 @@ | |||
455 | 12 | 'InvalidEmailAddress', | 12 | 'InvalidEmailAddress', |
456 | 13 | 'VALID_EMAIL_STATUSES'] | 13 | 'VALID_EMAIL_STATUSES'] |
457 | 14 | 14 | ||
458 | 15 | import httplib | ||
459 | 16 | |||
460 | 15 | from lazr.enum import ( | 17 | from lazr.enum import ( |
461 | 16 | DBEnumeratedType, | 18 | DBEnumeratedType, |
462 | 17 | DBItem, | 19 | DBItem, |
463 | 18 | ) | 20 | ) |
464 | 19 | from lazr.restful.declarations import ( | 21 | from lazr.restful.declarations import ( |
465 | 22 | error_status, | ||
466 | 20 | export_as_webservice_entry, | 23 | export_as_webservice_entry, |
467 | 21 | exported, | 24 | exported, |
468 | 22 | ) | 25 | ) |
469 | @@ -36,6 +39,7 @@ | |||
470 | 36 | """The email address is not valid.""" | 39 | """The email address is not valid.""" |
471 | 37 | 40 | ||
472 | 38 | 41 | ||
473 | 42 | @error_status(httplib.CONFLICT) | ||
474 | 39 | class EmailAddressAlreadyTaken(Exception): | 43 | class EmailAddressAlreadyTaken(Exception): |
475 | 40 | """The email address is already registered in Launchpad.""" | 44 | """The email address is already registered in Launchpad.""" |
476 | 41 | 45 |
Thanks William. Looks great - I'll see if we can get the sca work scheduled so we can close the private xmlrpc access.