Merge lp:~wgrant/launchpad/sca-api into lp:launchpad
- sca-api
- Merge into devel
Proposed by
William Grant
on 2015-03-10
| 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) | 2015-03-10 | Approve on 2015-04-13 | |
| Colin Watson | 2015-03-10 | Approve on 2015-03-10 | |
|
Review via email:
|
|||
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.
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 @@ |
| 5 | -# Copyright 2009 Canonical Ltd. This software is licensed under the |
| 6 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
| 7 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 8 | |
| 9 | __metaclass__ = type |
| 10 | |
| 11 | +from storm.store import Store |
| 12 | from testtools.matchers import Equals |
| 13 | from zope.component import getUtility |
| 14 | from zope.security.management import endInteraction |
| 15 | @@ -14,6 +15,7 @@ |
| 16 | ) |
| 17 | from lp.registry.interfaces.teammembership import ITeamMembershipSet |
| 18 | from lp.services.identity.interfaces.account import AccountStatus |
| 19 | +from lp.services.openid.model.openididentifier import OpenIdIdentifier |
| 20 | from lp.services.webapp.interfaces import OAuthPermission |
| 21 | from lp.testing import ( |
| 22 | admin_logged_in, |
| 23 | @@ -282,3 +284,61 @@ |
| 24 | 'identifier=http://login1.dev/%%2Bid/%s' |
| 25 | % person_openid, |
| 26 | api_version='devel').jsonBody()['name']) |
| 27 | + |
| 28 | + def getOrCreateSoftwareCenterCustomer(self, user): |
| 29 | + webservice = webservice_for_person( |
| 30 | + user, permission=OAuthPermission.WRITE_PRIVATE) |
| 31 | + response = webservice.named_post( |
| 32 | + '/people', 'getOrCreateSoftwareCenterCustomer', |
| 33 | + openid_identifier='somebody', |
| 34 | + email_address='somebody@example.com', display_name='Somebody', |
| 35 | + api_version='devel') |
| 36 | + return response |
| 37 | + |
| 38 | + def test_getOrCreateSoftwareCenterCustomer(self): |
| 39 | + # Software Center Agent (SCA) can get or create people by OpenID |
| 40 | + # identifier. |
| 41 | + with admin_logged_in(): |
| 42 | + sca = getUtility(IPersonSet).getByName('software-center-agent') |
| 43 | + response = self.getOrCreateSoftwareCenterCustomer(sca) |
| 44 | + self.assertEqual('Somebody', response.jsonBody()['display_name']) |
| 45 | + with admin_logged_in(): |
| 46 | + person = getUtility(IPersonSet).getByEmail('somebody@example.com') |
| 47 | + self.assertEqual('Somebody', person.displayname) |
| 48 | + self.assertEqual( |
| 49 | + ['somebody'], |
| 50 | + [oid.identifier for oid in person.account.openid_identifiers]) |
| 51 | + self.assertEqual( |
| 52 | + 'somebody@example.com', person.preferredemail.email) |
| 53 | + |
| 54 | + def test_getOrCreateSoftwareCenterCustomer_is_restricted(self): |
| 55 | + # The method may only be invoked by the ~software-center-agent |
| 56 | + # celebrity user, as it is security-sensitive. |
| 57 | + with admin_logged_in(): |
| 58 | + random = self.factory.makePerson() |
| 59 | + response = self.getOrCreateSoftwareCenterCustomer(random) |
| 60 | + self.assertEqual(401, response.status) |
| 61 | + |
| 62 | + def test_getOrCreateSoftwareCenterCustomer_rejects_email_conflicts(self): |
| 63 | + # An unknown OpenID identifier with a known email address causes |
| 64 | + # the request to fail with 409 Conflict, as we'd otherwise end |
| 65 | + # up linking the OpenID identifier to an existing account. |
| 66 | + with admin_logged_in(): |
| 67 | + self.factory.makePerson(email='somebody@example.com') |
| 68 | + sca = getUtility(IPersonSet).getByName('software-center-agent') |
| 69 | + response = self.getOrCreateSoftwareCenterCustomer(sca) |
| 70 | + self.assertEqual(409, response.status) |
| 71 | + |
| 72 | + def test_getOrCreateSoftwareCenterCustomer_rejects_suspended(self): |
| 73 | + # Suspended accounts are not returned. |
| 74 | + with admin_logged_in(): |
| 75 | + existing = self.factory.makePerson( |
| 76 | + email='somebody@example.com', |
| 77 | + account_status=AccountStatus.SUSPENDED) |
| 78 | + oid = OpenIdIdentifier() |
| 79 | + oid.account = existing.account |
| 80 | + oid.identifier = u'somebody' |
| 81 | + Store.of(existing).add(oid) |
| 82 | + sca = getUtility(IPersonSet).getByName('software-center-agent') |
| 83 | + response = self.getOrCreateSoftwareCenterCustomer(sca) |
| 84 | + self.assertEqual(400, response.status) |
| 85 | |
| 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 @@ |
| 90 | -# Copyright 2009-2014 Canonical Ltd. This software is licensed under the |
| 91 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
| 92 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 93 | |
| 94 | """Person interfaces.""" |
| 95 | @@ -2173,6 +2173,30 @@ |
| 96 | identifier has been suspended. |
| 97 | """ |
| 98 | |
| 99 | + @call_with(user=REQUEST_USER) |
| 100 | + @operation_parameters( |
| 101 | + openid_identifier=TextLine( |
| 102 | + title=_("OpenID identifier suffix"), required=True), |
| 103 | + email_address=TextLine(title=_("Email address"), required=True), |
| 104 | + display_name=TextLine(title=_("Display name"), required=True)) |
| 105 | + @export_write_operation() |
| 106 | + @operation_for_version("devel") |
| 107 | + def getOrCreateSoftwareCenterCustomer(user, openid_identifier, |
| 108 | + email_address, display_name): |
| 109 | + """Restricted person creation API for Software Center Agent. |
| 110 | + |
| 111 | + This method can only be called by Software Center Agent. It gets |
| 112 | + a person by OpenID identifier or creates a new Launchpad person |
| 113 | + from the OpenID identifier, email address and display name. |
| 114 | + |
| 115 | + :param user: the `IPerson` performing the operation. Only the |
| 116 | + software-center-agent celebrity is allowed. |
| 117 | + :param openid_identifier: OpenID identifier suffix for the user. |
| 118 | + This is *not* the full URL, just the unique suffix portion. |
| 119 | + :param email_address: the email address of the user. |
| 120 | + :param full_name: the full name of the user. |
| 121 | + """ |
| 122 | + |
| 123 | @call_with(teamowner=REQUEST_USER) |
| 124 | @rename_parameters_as( |
| 125 | displayname='display_name', teamdescription='team_description', |
| 126 | |
| 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 | INACTIVE_ACCOUNT_STATUSES, |
| 132 | ) |
| 133 | from lp.services.identity.interfaces.emailaddress import ( |
| 134 | + EmailAddressAlreadyTaken, |
| 135 | EmailAddressStatus, |
| 136 | IEmailAddress, |
| 137 | IEmailAddressSet, |
| 138 | @@ -3317,8 +3318,26 @@ |
| 139 | return IPerson(account) |
| 140 | |
| 141 | def getOrCreateByOpenIDIdentifier(self, openid_identifier, email_address, |
| 142 | - full_name, creation_rationale, comment): |
| 143 | + full_name, creation_rationale, comment, |
| 144 | + trust_email=True): |
| 145 | """See `IPersonSet`.""" |
| 146 | + # trust_email is an internal flag used by |
| 147 | + # getOrCreateSoftwareCenterCustomer. We don't want SCA to be |
| 148 | + # able to use the API to associate arbitrary OpenID identifiers |
| 149 | + # and email addresses when that could compromise existing |
| 150 | + # accounts. |
| 151 | + # |
| 152 | + # To that end, if trust_email is not set then the given |
| 153 | + # email address will only be used to create an account. It will |
| 154 | + # never be used to look up an account, nor will it be added to |
| 155 | + # an existing account. This causes two additional cases to be |
| 156 | + # rejected: unknown OpenID identifier but known email address, |
| 157 | + # and deactivated account. |
| 158 | + # |
| 159 | + # Exempting account creation and activation from this rule opens |
| 160 | + # us to a potential account fixation attack, but the risk is |
| 161 | + # minimal. |
| 162 | + |
| 163 | assert email_address is not None and full_name is not None, ( |
| 164 | "Both email address and full name are required to create an " |
| 165 | "account.") |
| 166 | @@ -3341,13 +3360,19 @@ |
| 167 | # We don't know about the OpenID identifier yet, so try |
| 168 | # to match a person by email address, or as a last |
| 169 | # resort create a new one. |
| 170 | - if email is not None: |
| 171 | - person = email.person |
| 172 | - else: |
| 173 | + if email is None: |
| 174 | person_set = getUtility(IPersonSet) |
| 175 | person, email = person_set.createPersonAndEmail( |
| 176 | email_address, creation_rationale, comment=comment, |
| 177 | displayname=full_name) |
| 178 | + elif trust_email: |
| 179 | + person = email.person |
| 180 | + else: |
| 181 | + # The email address originated from a source that's |
| 182 | + # not completely trustworth (eg. SCA), so we can't |
| 183 | + # use it to link the OpenID identifier to an |
| 184 | + # existing person. |
| 185 | + raise EmailAddressAlreadyTaken() |
| 186 | |
| 187 | # It's possible that the email address is owned by a |
| 188 | # team. Reject the login attempt, and wait for the user |
| 189 | @@ -3373,10 +3398,21 @@ |
| 190 | person = IPerson(identifier.account, None) |
| 191 | assert person is not None, ('Received a personless account.') |
| 192 | |
| 193 | - if person.account.status == AccountStatus.SUSPENDED: |
| 194 | + status = person.account.status |
| 195 | + if status == AccountStatus.ACTIVE: |
| 196 | + # Account is active, so nothing to do. |
| 197 | + pass |
| 198 | + elif status == AccountStatus.SUSPENDED: |
| 199 | raise AccountSuspendedError( |
| 200 | "The account matching the identifier is suspended.") |
| 201 | - |
| 202 | + elif not trust_email and status != AccountStatus.NOACCOUNT: |
| 203 | + # If the email address is not completely trustworthy |
| 204 | + # (ie. it comes from SCA) and the account has already |
| 205 | + # been used, then we don't want to proceed as we might |
| 206 | + # end up adding a malicious OpenID identifier to an |
| 207 | + # existing account. |
| 208 | + raise NameAlreadyTaken( |
| 209 | + "The account matching the identifier is inactive.") |
| 210 | elif person.account.status in [AccountStatus.DEACTIVATED, |
| 211 | AccountStatus.NOACCOUNT]: |
| 212 | removeSecurityProxy(person.account).reactivate(comment) |
| 213 | @@ -3386,11 +3422,24 @@ |
| 214 | removeSecurityProxy(person).setPreferredEmail(email) |
| 215 | db_updated = True |
| 216 | else: |
| 217 | - # Account is active, so nothing to do. |
| 218 | - pass |
| 219 | + raise AssertionError( |
| 220 | + "Unhandled account status: %r" % person.account.status) |
| 221 | |
| 222 | return person, db_updated |
| 223 | |
| 224 | + def getOrCreateSoftwareCenterCustomer(self, user, openid_identifier, |
| 225 | + email_address, display_name): |
| 226 | + """See `IPersonSet`.""" |
| 227 | + if user != getUtility(ILaunchpadCelebrities).software_center_agent: |
| 228 | + raise Unauthorized() |
| 229 | + person, _ = getUtility( |
| 230 | + IPersonSet).getOrCreateByOpenIDIdentifier( |
| 231 | + openid_identifier, email_address, display_name, |
| 232 | + PersonCreationRationale.SOFTWARE_CENTER_PURCHASE, |
| 233 | + "when purchasing an application via Software Center.", |
| 234 | + trust_email=False) |
| 235 | + return person |
| 236 | + |
| 237 | def newTeam(self, teamowner, name, displayname, teamdescription=None, |
| 238 | membership_policy=TeamMembershipPolicy.MODERATED, |
| 239 | defaultmembershipperiod=None, defaultrenewalperiod=None, |
| 240 | |
| 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 @@ |
| 245 | -# Copyright 2009-2013 Canonical Ltd. This software is licensed under the |
| 246 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
| 247 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 248 | |
| 249 | """Tests for PersonSet.""" |
| 250 | @@ -9,6 +9,7 @@ |
| 251 | from testtools.matchers import LessThan |
| 252 | import transaction |
| 253 | from zope.component import getUtility |
| 254 | +from zope.security.interfaces import Unauthorized |
| 255 | from zope.security.proxy import removeSecurityProxy |
| 256 | |
| 257 | from lp.code.tests.helpers import remove_all_sample_data_branches |
| 258 | @@ -41,13 +42,17 @@ |
| 259 | from lp.services.identity.interfaces.emailaddress import ( |
| 260 | EmailAddressAlreadyTaken, |
| 261 | EmailAddressStatus, |
| 262 | + IEmailAddressSet, |
| 263 | InvalidEmailAddress, |
| 264 | ) |
| 265 | from lp.services.identity.model.account import Account |
| 266 | from lp.services.identity.model.emailaddress import EmailAddress |
| 267 | from lp.services.openid.model.openididentifier import OpenIdIdentifier |
| 268 | +from lp.services.webapp.interfaces import ILaunchBag |
| 269 | from lp.testing import ( |
| 270 | + admin_logged_in, |
| 271 | ANONYMOUS, |
| 272 | + anonymous_logged_in, |
| 273 | login, |
| 274 | logout, |
| 275 | person_logged_in, |
| 276 | @@ -590,3 +595,135 @@ |
| 277 | u'other-openid-identifier' in [ |
| 278 | identifier.identifier for identifier in removeSecurityProxy( |
| 279 | person.account).openid_identifiers]) |
| 280 | + |
| 281 | + |
| 282 | +class TestPersonSetGetOrCreateSoftwareCenterCustomer(TestCaseWithFactory): |
| 283 | + |
| 284 | + layer = DatabaseFunctionalLayer |
| 285 | + |
| 286 | + def setUp(self): |
| 287 | + super(TestPersonSetGetOrCreateSoftwareCenterCustomer, self).setUp() |
| 288 | + self.sca = getUtility(IPersonSet).getByName('software-center-agent') |
| 289 | + |
| 290 | + def makeOpenIdIdentifier(self, account, identifier): |
| 291 | + openid_identifier = OpenIdIdentifier() |
| 292 | + openid_identifier.identifier = identifier |
| 293 | + openid_identifier.account = account |
| 294 | + return IStore(OpenIdIdentifier).add(openid_identifier) |
| 295 | + |
| 296 | + def test_restricted_to_sca(self): |
| 297 | + # Only the software-center-agent celebrity can invoke this |
| 298 | + # privileged method. |
| 299 | + def do_it(): |
| 300 | + return getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( |
| 301 | + getUtility(ILaunchBag).user, u'somebody', |
| 302 | + 'somebody@example.com', 'Example') |
| 303 | + random = self.factory.makePerson() |
| 304 | + admin = self.factory.makePerson( |
| 305 | + member_of=[getUtility(IPersonSet).getByName('admins')]) |
| 306 | + |
| 307 | + # Anonymous, random or admin users can't invoke the method. |
| 308 | + with anonymous_logged_in(): |
| 309 | + self.assertRaises(Unauthorized, do_it) |
| 310 | + with person_logged_in(random): |
| 311 | + self.assertRaises(Unauthorized, do_it) |
| 312 | + with person_logged_in(admin): |
| 313 | + self.assertRaises(Unauthorized, do_it) |
| 314 | + |
| 315 | + with person_logged_in(self.sca): |
| 316 | + person = do_it() |
| 317 | + self.assertIsInstance(person, Person) |
| 318 | + |
| 319 | + def test_finds_by_openid(self): |
| 320 | + # A Person with the requested OpenID identifier is returned. |
| 321 | + somebody = self.factory.makePerson() |
| 322 | + self.makeOpenIdIdentifier(somebody.account, u'somebody') |
| 323 | + with person_logged_in(self.sca): |
| 324 | + got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( |
| 325 | + self.sca, u'somebody', 'somebody@example.com', 'Example') |
| 326 | + self.assertEqual(somebody, got) |
| 327 | + |
| 328 | + # The email address doesn't get linked, as that could change how |
| 329 | + # future logins work. |
| 330 | + self.assertIs( |
| 331 | + None, |
| 332 | + getUtility(IEmailAddressSet).getByEmail('somebody@example.com')) |
| 333 | + |
| 334 | + def test_creates_new(self): |
| 335 | + # If an unknown OpenID identifier and email address are |
| 336 | + # provided, a new account is created and returned. |
| 337 | + with person_logged_in(self.sca): |
| 338 | + made = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( |
| 339 | + self.sca, u'somebody', 'somebody@example.com', 'Example') |
| 340 | + with admin_logged_in(): |
| 341 | + self.assertEqual('Example', made.displayname) |
| 342 | + self.assertEqual('somebody@example.com', made.preferredemail.email) |
| 343 | + |
| 344 | + # The email address is linked, since it can't compromise an |
| 345 | + # account that is being created just for it. |
| 346 | + email = getUtility(IEmailAddressSet).getByEmail('somebody@example.com') |
| 347 | + self.assertEqual(made, email.person) |
| 348 | + |
| 349 | + def test_activates_unactivated(self): |
| 350 | + # An unactivated account should be treated just like a new |
| 351 | + # account -- it gets activated with the given email address. |
| 352 | + somebody = self.factory.makePerson( |
| 353 | + email='existing@example.com', |
| 354 | + account_status=AccountStatus.NOACCOUNT) |
| 355 | + self.makeOpenIdIdentifier(somebody.account, u'somebody') |
| 356 | + self.assertEqual(AccountStatus.NOACCOUNT, somebody.account.status) |
| 357 | + with person_logged_in(self.sca): |
| 358 | + got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( |
| 359 | + self.sca, u'somebody', 'somebody@example.com', 'Example') |
| 360 | + self.assertEqual(somebody, got) |
| 361 | + with admin_logged_in(): |
| 362 | + self.assertEqual(AccountStatus.ACTIVE, somebody.account.status) |
| 363 | + self.assertEqual( |
| 364 | + 'somebody@example.com', somebody.preferredemail.email) |
| 365 | + |
| 366 | + def test_fails_if_email_is_already_registered(self): |
| 367 | + # Only the OpenID identifier is used to look up an account. If |
| 368 | + # the OpenID identifier isn't already registered by the email |
| 369 | + # address is, the request is rejected to avoid potentially |
| 370 | + # adding an unwanted OpenID identifier to the address' account. |
| 371 | + # |
| 372 | + # The user must log into Launchpad directly first to register |
| 373 | + # their OpenID identifier. |
| 374 | + other = self.factory.makePerson(email='other@example.com') |
| 375 | + with person_logged_in(self.sca): |
| 376 | + self.assertRaises( |
| 377 | + EmailAddressAlreadyTaken, |
| 378 | + getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer, |
| 379 | + self.sca, u'somebody', 'other@example.com', 'Example') |
| 380 | + |
| 381 | + # The email address stays with the old owner. |
| 382 | + email = getUtility(IEmailAddressSet).getByEmail('other@example.com') |
| 383 | + self.assertEqual(other, email.person) |
| 384 | + |
| 385 | + def test_fails_if_account_is_suspended(self): |
| 386 | + # Suspended accounts cannot be returned. |
| 387 | + somebody = self.factory.makePerson() |
| 388 | + self.makeOpenIdIdentifier(somebody.account, u'somebody') |
| 389 | + with admin_logged_in(): |
| 390 | + somebody.setAccountStatus( |
| 391 | + AccountStatus.SUSPENDED, None, "Go away!") |
| 392 | + with person_logged_in(self.sca): |
| 393 | + self.assertRaises( |
| 394 | + AccountSuspendedError, |
| 395 | + getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer, |
| 396 | + self.sca, u'somebody', 'somebody@example.com', 'Example') |
| 397 | + |
| 398 | + def test_fails_if_account_is_deactivated(self): |
| 399 | + # We don't want to reactivate explicitly deactivated accounts, |
| 400 | + # nor do we want to potentially compromise them with a bad email |
| 401 | + # address. |
| 402 | + somebody = self.factory.makePerson() |
| 403 | + self.makeOpenIdIdentifier(somebody.account, u'somebody') |
| 404 | + with admin_logged_in(): |
| 405 | + somebody.setAccountStatus( |
| 406 | + AccountStatus.DEACTIVATED, None, "Goodbye cruel world.") |
| 407 | + with person_logged_in(self.sca): |
| 408 | + self.assertRaises( |
| 409 | + NameAlreadyTaken, |
| 410 | + getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer, |
| 411 | + self.sca, u'somebody', 'somebody@example.com', 'Example') |
| 412 | |
| 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 @@ |
| 417 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
| 418 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
| 419 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 420 | |
| 421 | """Account interfaces.""" |
| 422 | @@ -18,11 +18,13 @@ |
| 423 | 'INACTIVE_ACCOUNT_STATUSES', |
| 424 | ] |
| 425 | |
| 426 | +import httplib |
| 427 | |
| 428 | from lazr.enum import ( |
| 429 | DBEnumeratedType, |
| 430 | DBItem, |
| 431 | ) |
| 432 | +from lazr.restful.declarations import error_status |
| 433 | from zope.interface import ( |
| 434 | Attribute, |
| 435 | Interface, |
| 436 | @@ -40,6 +42,7 @@ |
| 437 | from lp.services.fields import StrippedTextLine |
| 438 | |
| 439 | |
| 440 | +@error_status(httplib.BAD_REQUEST) |
| 441 | class AccountSuspendedError(Exception): |
| 442 | """The account being accessed has been suspended.""" |
| 443 | |
| 444 | |
| 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 @@ |
| 449 | -# Copyright 2009-2012 Canonical Ltd. This software is licensed under the |
| 450 | +# Copyright 2009-2015 Canonical Ltd. This software is licensed under the |
| 451 | # GNU Affero General Public License version 3 (see the file LICENSE). |
| 452 | |
| 453 | """EmailAddress interfaces.""" |
| 454 | @@ -12,11 +12,14 @@ |
| 455 | 'InvalidEmailAddress', |
| 456 | 'VALID_EMAIL_STATUSES'] |
| 457 | |
| 458 | +import httplib |
| 459 | + |
| 460 | from lazr.enum import ( |
| 461 | DBEnumeratedType, |
| 462 | DBItem, |
| 463 | ) |
| 464 | from lazr.restful.declarations import ( |
| 465 | + error_status, |
| 466 | export_as_webservice_entry, |
| 467 | exported, |
| 468 | ) |
| 469 | @@ -36,6 +39,7 @@ |
| 470 | """The email address is not valid.""" |
| 471 | |
| 472 | |
| 473 | +@error_status(httplib.CONFLICT) |
| 474 | class EmailAddressAlreadyTaken(Exception): |
| 475 | """The email address is already registered in Launchpad.""" |
| 476 |

Thanks William. Looks great - I'll see if we can get the sca work scheduled so we can close the private xmlrpc access.