Merge lp:~wgrant/launchpad/sca-api into lp:launchpad

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
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.getOrCreateSoftwareCenterCustomer and export it on the webservice. The xmlrpc-private method is deprecated.

Description of the change

Implement PersonSet.getOrCreateSoftwareCenterCustomer and export it on the webservice.

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
Revision history for this message
Michael Nelson (michael.nelson) wrote :

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

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/tests/test_person_webservice.py'
--- lib/lp/registry/browser/tests/test_person_webservice.py 2015-01-07 00:35:53 +0000
+++ lib/lp/registry/browser/tests/test_person_webservice.py 2015-03-10 22:08:18 +0000
@@ -1,8 +1,9 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
55
6from storm.store import Store
6from testtools.matchers import Equals7from testtools.matchers import Equals
7from zope.component import getUtility8from zope.component import getUtility
8from zope.security.management import endInteraction9from zope.security.management import endInteraction
@@ -14,6 +15,7 @@
14 )15 )
15from lp.registry.interfaces.teammembership import ITeamMembershipSet16from lp.registry.interfaces.teammembership import ITeamMembershipSet
16from lp.services.identity.interfaces.account import AccountStatus17from lp.services.identity.interfaces.account import AccountStatus
18from lp.services.openid.model.openididentifier import OpenIdIdentifier
17from lp.services.webapp.interfaces import OAuthPermission19from lp.services.webapp.interfaces import OAuthPermission
18from lp.testing import (20from lp.testing import (
19 admin_logged_in,21 admin_logged_in,
@@ -282,3 +284,61 @@
282 'identifier=http://login1.dev/%%2Bid/%s'284 'identifier=http://login1.dev/%%2Bid/%s'
283 % person_openid,285 % person_openid,
284 api_version='devel').jsonBody()['name'])286 api_version='devel').jsonBody()['name'])
287
288 def getOrCreateSoftwareCenterCustomer(self, user):
289 webservice = webservice_for_person(
290 user, permission=OAuthPermission.WRITE_PRIVATE)
291 response = webservice.named_post(
292 '/people', 'getOrCreateSoftwareCenterCustomer',
293 openid_identifier='somebody',
294 email_address='somebody@example.com', display_name='Somebody',
295 api_version='devel')
296 return response
297
298 def test_getOrCreateSoftwareCenterCustomer(self):
299 # Software Center Agent (SCA) can get or create people by OpenID
300 # identifier.
301 with admin_logged_in():
302 sca = getUtility(IPersonSet).getByName('software-center-agent')
303 response = self.getOrCreateSoftwareCenterCustomer(sca)
304 self.assertEqual('Somebody', response.jsonBody()['display_name'])
305 with admin_logged_in():
306 person = getUtility(IPersonSet).getByEmail('somebody@example.com')
307 self.assertEqual('Somebody', person.displayname)
308 self.assertEqual(
309 ['somebody'],
310 [oid.identifier for oid in person.account.openid_identifiers])
311 self.assertEqual(
312 'somebody@example.com', person.preferredemail.email)
313
314 def test_getOrCreateSoftwareCenterCustomer_is_restricted(self):
315 # The method may only be invoked by the ~software-center-agent
316 # celebrity user, as it is security-sensitive.
317 with admin_logged_in():
318 random = self.factory.makePerson()
319 response = self.getOrCreateSoftwareCenterCustomer(random)
320 self.assertEqual(401, response.status)
321
322 def test_getOrCreateSoftwareCenterCustomer_rejects_email_conflicts(self):
323 # An unknown OpenID identifier with a known email address causes
324 # the request to fail with 409 Conflict, as we'd otherwise end
325 # up linking the OpenID identifier to an existing account.
326 with admin_logged_in():
327 self.factory.makePerson(email='somebody@example.com')
328 sca = getUtility(IPersonSet).getByName('software-center-agent')
329 response = self.getOrCreateSoftwareCenterCustomer(sca)
330 self.assertEqual(409, response.status)
331
332 def test_getOrCreateSoftwareCenterCustomer_rejects_suspended(self):
333 # Suspended accounts are not returned.
334 with admin_logged_in():
335 existing = self.factory.makePerson(
336 email='somebody@example.com',
337 account_status=AccountStatus.SUSPENDED)
338 oid = OpenIdIdentifier()
339 oid.account = existing.account
340 oid.identifier = u'somebody'
341 Store.of(existing).add(oid)
342 sca = getUtility(IPersonSet).getByName('software-center-agent')
343 response = self.getOrCreateSoftwareCenterCustomer(sca)
344 self.assertEqual(400, response.status)
285345
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2015-02-26 11:34:47 +0000
+++ lib/lp/registry/interfaces/person.py 2015-03-10 22:08:18 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2014 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Person interfaces."""4"""Person interfaces."""
@@ -2173,6 +2173,30 @@
2173 identifier has been suspended.2173 identifier has been suspended.
2174 """2174 """
21752175
2176 @call_with(user=REQUEST_USER)
2177 @operation_parameters(
2178 openid_identifier=TextLine(
2179 title=_("OpenID identifier suffix"), required=True),
2180 email_address=TextLine(title=_("Email address"), required=True),
2181 display_name=TextLine(title=_("Display name"), required=True))
2182 @export_write_operation()
2183 @operation_for_version("devel")
2184 def getOrCreateSoftwareCenterCustomer(user, openid_identifier,
2185 email_address, display_name):
2186 """Restricted person creation API for Software Center Agent.
2187
2188 This method can only be called by Software Center Agent. It gets
2189 a person by OpenID identifier or creates a new Launchpad person
2190 from the OpenID identifier, email address and display name.
2191
2192 :param user: the `IPerson` performing the operation. Only the
2193 software-center-agent celebrity is allowed.
2194 :param openid_identifier: OpenID identifier suffix for the user.
2195 This is *not* the full URL, just the unique suffix portion.
2196 :param email_address: the email address of the user.
2197 :param full_name: the full name of the user.
2198 """
2199
2176 @call_with(teamowner=REQUEST_USER)2200 @call_with(teamowner=REQUEST_USER)
2177 @rename_parameters_as(2201 @rename_parameters_as(
2178 displayname='display_name', teamdescription='team_description',2202 displayname='display_name', teamdescription='team_description',
21792203
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2015-03-04 19:05:47 +0000
+++ lib/lp/registry/model/person.py 2015-03-10 22:08:18 +0000
@@ -268,6 +268,7 @@
268 INACTIVE_ACCOUNT_STATUSES,268 INACTIVE_ACCOUNT_STATUSES,
269 )269 )
270from lp.services.identity.interfaces.emailaddress import (270from lp.services.identity.interfaces.emailaddress import (
271 EmailAddressAlreadyTaken,
271 EmailAddressStatus,272 EmailAddressStatus,
272 IEmailAddress,273 IEmailAddress,
273 IEmailAddressSet,274 IEmailAddressSet,
@@ -3317,8 +3318,26 @@
3317 return IPerson(account)3318 return IPerson(account)
33183319
3319 def getOrCreateByOpenIDIdentifier(self, openid_identifier, email_address,3320 def getOrCreateByOpenIDIdentifier(self, openid_identifier, email_address,
3320 full_name, creation_rationale, comment):3321 full_name, creation_rationale, comment,
3322 trust_email=True):
3321 """See `IPersonSet`."""3323 """See `IPersonSet`."""
3324 # trust_email is an internal flag used by
3325 # getOrCreateSoftwareCenterCustomer. We don't want SCA to be
3326 # able to use the API to associate arbitrary OpenID identifiers
3327 # and email addresses when that could compromise existing
3328 # accounts.
3329 #
3330 # To that end, if trust_email is not set then the given
3331 # email address will only be used to create an account. It will
3332 # never be used to look up an account, nor will it be added to
3333 # an existing account. This causes two additional cases to be
3334 # rejected: unknown OpenID identifier but known email address,
3335 # and deactivated account.
3336 #
3337 # Exempting account creation and activation from this rule opens
3338 # us to a potential account fixation attack, but the risk is
3339 # minimal.
3340
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, (
3323 "Both email address and full name are required to create an "3342 "Both email address and full name are required to create an "
3324 "account.")3343 "account.")
@@ -3341,13 +3360,19 @@
3341 # We don't know about the OpenID identifier yet, so try3360 # We don't know about the OpenID identifier yet, so try
3342 # to match a person by email address, or as a last3361 # to match a person by email address, or as a last
3343 # resort create a new one.3362 # resort create a new one.
3344 if email is not None:3363 if email is None:
3345 person = email.person
3346 else:
3347 person_set = getUtility(IPersonSet)3364 person_set = getUtility(IPersonSet)
3348 person, email = person_set.createPersonAndEmail(3365 person, email = person_set.createPersonAndEmail(
3349 email_address, creation_rationale, comment=comment,3366 email_address, creation_rationale, comment=comment,
3350 displayname=full_name)3367 displayname=full_name)
3368 elif trust_email:
3369 person = email.person
3370 else:
3371 # The email address originated from a source that's
3372 # not completely trustworth (eg. SCA), so we can't
3373 # use it to link the OpenID identifier to an
3374 # existing person.
3375 raise EmailAddressAlreadyTaken()
33513376
3352 # It's possible that the email address is owned by a3377 # It's possible that the email address is owned by a
3353 # team. Reject the login attempt, and wait for the user3378 # team. Reject the login attempt, and wait for the user
@@ -3373,10 +3398,21 @@
3373 person = IPerson(identifier.account, None)3398 person = IPerson(identifier.account, None)
3374 assert person is not None, ('Received a personless account.')3399 assert person is not None, ('Received a personless account.')
33753400
3376 if person.account.status == AccountStatus.SUSPENDED:3401 status = person.account.status
3402 if status == AccountStatus.ACTIVE:
3403 # Account is active, so nothing to do.
3404 pass
3405 elif status == AccountStatus.SUSPENDED:
3377 raise AccountSuspendedError(3406 raise AccountSuspendedError(
3378 "The account matching the identifier is suspended.")3407 "The account matching the identifier is suspended.")
33793408 elif not trust_email and status != AccountStatus.NOACCOUNT:
3409 # If the email address is not completely trustworthy
3410 # (ie. it comes from SCA) and the account has already
3411 # been used, then we don't want to proceed as we might
3412 # end up adding a malicious OpenID identifier to an
3413 # existing account.
3414 raise NameAlreadyTaken(
3415 "The account matching the identifier is inactive.")
3380 elif person.account.status in [AccountStatus.DEACTIVATED,3416 elif person.account.status in [AccountStatus.DEACTIVATED,
3381 AccountStatus.NOACCOUNT]:3417 AccountStatus.NOACCOUNT]:
3382 removeSecurityProxy(person.account).reactivate(comment)3418 removeSecurityProxy(person.account).reactivate(comment)
@@ -3386,11 +3422,24 @@
3386 removeSecurityProxy(person).setPreferredEmail(email)3422 removeSecurityProxy(person).setPreferredEmail(email)
3387 db_updated = True3423 db_updated = True
3388 else:3424 else:
3389 # Account is active, so nothing to do.3425 raise AssertionError(
3390 pass3426 "Unhandled account status: %r" % person.account.status)
33913427
3392 return person, db_updated3428 return person, db_updated
33933429
3430 def getOrCreateSoftwareCenterCustomer(self, user, openid_identifier,
3431 email_address, display_name):
3432 """See `IPersonSet`."""
3433 if user != getUtility(ILaunchpadCelebrities).software_center_agent:
3434 raise Unauthorized()
3435 person, _ = getUtility(
3436 IPersonSet).getOrCreateByOpenIDIdentifier(
3437 openid_identifier, email_address, display_name,
3438 PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
3439 "when purchasing an application via Software Center.",
3440 trust_email=False)
3441 return person
3442
3394 def newTeam(self, teamowner, name, displayname, teamdescription=None,3443 def newTeam(self, teamowner, name, displayname, teamdescription=None,
3395 membership_policy=TeamMembershipPolicy.MODERATED,3444 membership_policy=TeamMembershipPolicy.MODERATED,
3396 defaultmembershipperiod=None, defaultrenewalperiod=None,3445 defaultmembershipperiod=None, defaultrenewalperiod=None,
33973446
=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py 2015-01-07 00:35:53 +0000
+++ lib/lp/registry/tests/test_personset.py 2015-03-10 22:08:18 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2013 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for PersonSet."""4"""Tests for PersonSet."""
@@ -9,6 +9,7 @@
9from testtools.matchers import LessThan9from testtools.matchers import LessThan
10import transaction10import transaction
11from zope.component import getUtility11from zope.component import getUtility
12from zope.security.interfaces import Unauthorized
12from zope.security.proxy import removeSecurityProxy13from zope.security.proxy import removeSecurityProxy
1314
14from lp.code.tests.helpers import remove_all_sample_data_branches15from lp.code.tests.helpers import remove_all_sample_data_branches
@@ -41,13 +42,17 @@
41from lp.services.identity.interfaces.emailaddress import (42from lp.services.identity.interfaces.emailaddress import (
42 EmailAddressAlreadyTaken,43 EmailAddressAlreadyTaken,
43 EmailAddressStatus,44 EmailAddressStatus,
45 IEmailAddressSet,
44 InvalidEmailAddress,46 InvalidEmailAddress,
45 )47 )
46from lp.services.identity.model.account import Account48from lp.services.identity.model.account import Account
47from lp.services.identity.model.emailaddress import EmailAddress49from lp.services.identity.model.emailaddress import EmailAddress
48from lp.services.openid.model.openididentifier import OpenIdIdentifier50from lp.services.openid.model.openididentifier import OpenIdIdentifier
51from lp.services.webapp.interfaces import ILaunchBag
49from lp.testing import (52from lp.testing import (
53 admin_logged_in,
50 ANONYMOUS,54 ANONYMOUS,
55 anonymous_logged_in,
51 login,56 login,
52 logout,57 logout,
53 person_logged_in,58 person_logged_in,
@@ -590,3 +595,135 @@
590 u'other-openid-identifier' in [595 u'other-openid-identifier' in [
591 identifier.identifier for identifier in removeSecurityProxy(596 identifier.identifier for identifier in removeSecurityProxy(
592 person.account).openid_identifiers])597 person.account).openid_identifiers])
598
599
600class TestPersonSetGetOrCreateSoftwareCenterCustomer(TestCaseWithFactory):
601
602 layer = DatabaseFunctionalLayer
603
604 def setUp(self):
605 super(TestPersonSetGetOrCreateSoftwareCenterCustomer, self).setUp()
606 self.sca = getUtility(IPersonSet).getByName('software-center-agent')
607
608 def makeOpenIdIdentifier(self, account, identifier):
609 openid_identifier = OpenIdIdentifier()
610 openid_identifier.identifier = identifier
611 openid_identifier.account = account
612 return IStore(OpenIdIdentifier).add(openid_identifier)
613
614 def test_restricted_to_sca(self):
615 # Only the software-center-agent celebrity can invoke this
616 # privileged method.
617 def do_it():
618 return getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer(
619 getUtility(ILaunchBag).user, u'somebody',
620 'somebody@example.com', 'Example')
621 random = self.factory.makePerson()
622 admin = self.factory.makePerson(
623 member_of=[getUtility(IPersonSet).getByName('admins')])
624
625 # Anonymous, random or admin users can't invoke the method.
626 with anonymous_logged_in():
627 self.assertRaises(Unauthorized, do_it)
628 with person_logged_in(random):
629 self.assertRaises(Unauthorized, do_it)
630 with person_logged_in(admin):
631 self.assertRaises(Unauthorized, do_it)
632
633 with person_logged_in(self.sca):
634 person = do_it()
635 self.assertIsInstance(person, Person)
636
637 def test_finds_by_openid(self):
638 # A Person with the requested OpenID identifier is returned.
639 somebody = self.factory.makePerson()
640 self.makeOpenIdIdentifier(somebody.account, u'somebody')
641 with person_logged_in(self.sca):
642 got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer(
643 self.sca, u'somebody', 'somebody@example.com', 'Example')
644 self.assertEqual(somebody, got)
645
646 # The email address doesn't get linked, as that could change how
647 # future logins work.
648 self.assertIs(
649 None,
650 getUtility(IEmailAddressSet).getByEmail('somebody@example.com'))
651
652 def test_creates_new(self):
653 # If an unknown OpenID identifier and email address are
654 # provided, a new account is created and returned.
655 with person_logged_in(self.sca):
656 made = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer(
657 self.sca, u'somebody', 'somebody@example.com', 'Example')
658 with admin_logged_in():
659 self.assertEqual('Example', made.displayname)
660 self.assertEqual('somebody@example.com', made.preferredemail.email)
661
662 # The email address is linked, since it can't compromise an
663 # account that is being created just for it.
664 email = getUtility(IEmailAddressSet).getByEmail('somebody@example.com')
665 self.assertEqual(made, email.person)
666
667 def test_activates_unactivated(self):
668 # An unactivated account should be treated just like a new
669 # account -- it gets activated with the given email address.
670 somebody = self.factory.makePerson(
671 email='existing@example.com',
672 account_status=AccountStatus.NOACCOUNT)
673 self.makeOpenIdIdentifier(somebody.account, u'somebody')
674 self.assertEqual(AccountStatus.NOACCOUNT, somebody.account.status)
675 with person_logged_in(self.sca):
676 got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer(
677 self.sca, u'somebody', 'somebody@example.com', 'Example')
678 self.assertEqual(somebody, got)
679 with admin_logged_in():
680 self.assertEqual(AccountStatus.ACTIVE, somebody.account.status)
681 self.assertEqual(
682 'somebody@example.com', somebody.preferredemail.email)
683
684 def test_fails_if_email_is_already_registered(self):
685 # Only the OpenID identifier is used to look up an account. If
686 # the OpenID identifier isn't already registered by the email
687 # address is, the request is rejected to avoid potentially
688 # adding an unwanted OpenID identifier to the address' account.
689 #
690 # The user must log into Launchpad directly first to register
691 # their OpenID identifier.
692 other = self.factory.makePerson(email='other@example.com')
693 with person_logged_in(self.sca):
694 self.assertRaises(
695 EmailAddressAlreadyTaken,
696 getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer,
697 self.sca, u'somebody', 'other@example.com', 'Example')
698
699 # The email address stays with the old owner.
700 email = getUtility(IEmailAddressSet).getByEmail('other@example.com')
701 self.assertEqual(other, email.person)
702
703 def test_fails_if_account_is_suspended(self):
704 # Suspended accounts cannot be returned.
705 somebody = self.factory.makePerson()
706 self.makeOpenIdIdentifier(somebody.account, u'somebody')
707 with admin_logged_in():
708 somebody.setAccountStatus(
709 AccountStatus.SUSPENDED, None, "Go away!")
710 with person_logged_in(self.sca):
711 self.assertRaises(
712 AccountSuspendedError,
713 getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer,
714 self.sca, u'somebody', 'somebody@example.com', 'Example')
715
716 def test_fails_if_account_is_deactivated(self):
717 # We don't want to reactivate explicitly deactivated accounts,
718 # nor do we want to potentially compromise them with a bad email
719 # address.
720 somebody = self.factory.makePerson()
721 self.makeOpenIdIdentifier(somebody.account, u'somebody')
722 with admin_logged_in():
723 somebody.setAccountStatus(
724 AccountStatus.DEACTIVATED, None, "Goodbye cruel world.")
725 with person_logged_in(self.sca):
726 self.assertRaises(
727 NameAlreadyTaken,
728 getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer,
729 self.sca, u'somebody', 'somebody@example.com', 'Example')
593730
=== modified file 'lib/lp/services/identity/interfaces/account.py'
--- lib/lp/services/identity/interfaces/account.py 2015-01-07 00:35:53 +0000
+++ lib/lp/services/identity/interfaces/account.py 2015-03-10 22:08:18 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Account interfaces."""4"""Account interfaces."""
@@ -18,11 +18,13 @@
18 'INACTIVE_ACCOUNT_STATUSES',18 'INACTIVE_ACCOUNT_STATUSES',
19 ]19 ]
2020
21import httplib
2122
22from lazr.enum import (23from lazr.enum import (
23 DBEnumeratedType,24 DBEnumeratedType,
24 DBItem,25 DBItem,
25 )26 )
27from lazr.restful.declarations import error_status
26from zope.interface import (28from zope.interface import (
27 Attribute,29 Attribute,
28 Interface,30 Interface,
@@ -40,6 +42,7 @@
40from lp.services.fields import StrippedTextLine42from lp.services.fields import StrippedTextLine
4143
4244
45@error_status(httplib.BAD_REQUEST)
43class AccountSuspendedError(Exception):46class AccountSuspendedError(Exception):
44 """The account being accessed has been suspended."""47 """The account being accessed has been suspended."""
4548
4649
=== modified file 'lib/lp/services/identity/interfaces/emailaddress.py'
--- lib/lp/services/identity/interfaces/emailaddress.py 2013-01-07 02:40:55 +0000
+++ lib/lp/services/identity/interfaces/emailaddress.py 2015-03-10 22:08:18 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2012 Canonical Ltd. This software is licensed under the1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""EmailAddress interfaces."""4"""EmailAddress interfaces."""
@@ -12,11 +12,14 @@
12 'InvalidEmailAddress',12 'InvalidEmailAddress',
13 'VALID_EMAIL_STATUSES']13 'VALID_EMAIL_STATUSES']
1414
15import httplib
16
15from lazr.enum import (17from lazr.enum import (
16 DBEnumeratedType,18 DBEnumeratedType,
17 DBItem,19 DBItem,
18 )20 )
19from lazr.restful.declarations import (21from lazr.restful.declarations import (
22 error_status,
20 export_as_webservice_entry,23 export_as_webservice_entry,
21 exported,24 exported,
22 )25 )
@@ -36,6 +39,7 @@
36 """The email address is not valid."""39 """The email address is not valid."""
3740
3841
42@error_status(httplib.CONFLICT)
39class EmailAddressAlreadyTaken(Exception):43class EmailAddressAlreadyTaken(Exception):
40 """The email address is already registered in Launchpad."""44 """The email address is already registered in Launchpad."""
4145