Merge lp:~michael.nelson/launchpad/598464-get-or-create-in-devel into lp:launchpad

Proposed by Michael Nelson
Status: Merged
Approved by: Michael Nelson
Approved revision: no longer in the source branch.
Merged at revision: 11112
Proposed branch: lp:~michael.nelson/launchpad/598464-get-or-create-in-devel
Merge into: lp:launchpad
Diff against target: 853 lines (+462/-77)
14 files modified
lib/canonical/launchpad/interfaces/account.py (+13/-1)
lib/canonical/launchpad/interfaces/launchpad.py (+3/-0)
lib/canonical/launchpad/webapp/login.py (+20/-63)
lib/canonical/launchpad/webapp/tests/test_login.py (+13/-9)
lib/canonical/launchpad/xmlrpc/application.py (+6/-0)
lib/canonical/launchpad/xmlrpc/configure.zcml (+3/-0)
lib/canonical/launchpad/xmlrpc/faults.py (+11/-0)
lib/lp/registry/configure.zcml (+10/-0)
lib/lp/registry/interfaces/person.py (+58/-1)
lib/lp/registry/model/person.py (+56/-0)
lib/lp/registry/tests/test_personset.py (+92/-1)
lib/lp/registry/tests/test_xmlrpc.py (+125/-0)
lib/lp/registry/xmlrpc/softwarecenteragent.py (+47/-0)
lib/lp/testopenid/browser/server.py (+5/-2)
To merge this branch: bzr merge lp:~michael.nelson/launchpad/598464-get-or-create-in-devel
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+29442@code.launchpad.net

Commit message

Refactor login code that gets-or-creates an LP account based on an open_id onto IPersonSet and expose the functionality via private xml-rpc for use by the software center agent web-app.

Description of the change

Overview
========

This branch merges some approved work which didn't land on db-devel last cycle:

https://code.edge.launchpad.net/~michael.nelson/launchpad/db-598464-get-or-create-from-identity/+merge/28885
https://code.edge.launchpad.net/~michael.nelson/launchpad/db-598464-priv-xmlrpc-access-getOrCreate/+merge/29059

And cleans up by reverting some changes that had been made in the first MP above, but were unnecessary in the end (an extra registrant attribute for createPerson).

The diff of the extra changes since the last approved MP above is `bzr diff -r11095..11097`
http://pastebin.ubuntu.com/460538/

Initially the extra permissions were required as we were planning on exposing this functionality via the API, but after consideration, it was decided to expose it via private xmlrpc instead and re-use code that was already doing what we needed (but which needs to be run by anyone who logs in to LP).

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/interfaces/account.py'
--- lib/canonical/launchpad/interfaces/account.py 2009-12-24 11:06:04 +0000
+++ lib/canonical/launchpad/interfaces/account.py 2010-07-08 13:40:20 +0000
@@ -8,6 +8,7 @@
88
9__all__ = [9__all__ = [
10 'AccountStatus',10 'AccountStatus',
11 'AccountSuspendedError',
11 'AccountCreationRationale',12 'AccountCreationRationale',
12 'IAccount',13 'IAccount',
13 'IAccountPrivate',14 'IAccountPrivate',
@@ -27,6 +28,10 @@
27from lazr.restful.fields import CollectionField, Reference28from lazr.restful.fields import CollectionField, Reference
2829
2930
31class AccountSuspendedError(Exception):
32 """The account being accessed has been suspended."""
33
34
30class AccountStatus(DBEnumeratedType):35class AccountStatus(DBEnumeratedType):
31 """The status of an account."""36 """The status of an account."""
3237
@@ -179,6 +184,13 @@
179 commented on.184 commented on.
180 """)185 """)
181186
187 SOFTWARE_CENTER_PURCHASE = DBItem(16, """
188 Created by purchasing commercial software through Software Center.
189
190 A purchase of commercial software (ie. subscriptions to a private
191 and commercial archive) was made via Software Center.
192 """)
193
182194
183class IAccountPublic(Interface):195class IAccountPublic(Interface):
184 """Public information on an `IAccount`."""196 """Public information on an `IAccount`."""
@@ -261,7 +273,7 @@
261 password = PasswordField(273 password = PasswordField(
262 title=_("Password."), readonly=False, required=True)274 title=_("Password."), readonly=False, required=True)
263275
264 def createPerson(self, rationale, name=None, comment=None):276 def createPerson(rationale, name=None, comment=None):
265 """Create and return a new `IPerson` associated with this account.277 """Create and return a new `IPerson` associated with this account.
266278
267 :param rationale: A member of `AccountCreationRationale`.279 :param rationale: A member of `AccountCreationRationale`.
268280
=== modified file 'lib/canonical/launchpad/interfaces/launchpad.py'
--- lib/canonical/launchpad/interfaces/launchpad.py 2010-06-22 17:08:23 +0000
+++ lib/canonical/launchpad/interfaces/launchpad.py 2010-07-08 13:40:20 +0000
@@ -296,6 +296,9 @@
296296
297 bugs = Attribute("""Launchpad Bugs XML-RPC end point.""")297 bugs = Attribute("""Launchpad Bugs XML-RPC end point.""")
298298
299 softwarecenteragent = Attribute(
300 """Software center agent XML-RPC end point.""")
301
299302
300class IAuthServerApplication(ILaunchpadApplication):303class IAuthServerApplication(ILaunchpadApplication):
301 """Launchpad legacy AuthServer application root."""304 """Launchpad legacy AuthServer application root."""
302305
=== modified file 'lib/canonical/launchpad/webapp/login.py'
--- lib/canonical/launchpad/webapp/login.py 2010-05-15 17:43:59 +0000
+++ lib/canonical/launchpad/webapp/login.py 2010-07-08 13:40:20 +0000
@@ -32,10 +32,9 @@
32from canonical.cachedproperty import cachedproperty32from canonical.cachedproperty import cachedproperty
33from canonical.config import config33from canonical.config import config
34from canonical.launchpad import _34from canonical.launchpad import _
35from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet35from canonical.launchpad.interfaces.account import AccountSuspendedError
36from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
37from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore36from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore
38from lp.registry.interfaces.person import IPerson, PersonCreationRationale37from lp.registry.interfaces.person import IPersonSet, PersonCreationRationale
39from canonical.launchpad.readonly import is_read_only38from canonical.launchpad.readonly import is_read_only
40from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy39from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
41from canonical.launchpad.webapp.error import SystemErrorView40from canonical.launchpad.webapp.error import SystemErrorView
@@ -199,7 +198,7 @@
199 return_to = "%s?%s" % (return_to, starting_url)198 return_to = "%s?%s" % (return_to, starting_url)
200 form_html = self.openid_request.htmlMarkup(trust_root, return_to)199 form_html = self.openid_request.htmlMarkup(trust_root, return_to)
201200
202 # The consumer.begin() call above will insert rows into the 201 # The consumer.begin() call above will insert rows into the
203 # OpenIDAssociations table, but since this will be a GET request, the202 # OpenIDAssociations table, but since this will be a GET request, the
204 # transaction would be rolled back, so we need an explicit commit203 # transaction would be rolled back, so we need an explicit commit
205 # here.204 # here.
@@ -278,23 +277,14 @@
278 "No email address or full name found in sreg response")277 "No email address or full name found in sreg response")
279 return email_address, full_name278 return email_address, full_name
280279
281 def _createAccount(self, openid_identifier, email_address, full_name):
282 account, email = getUtility(IAccountSet).createAccountAndEmail(
283 email_address, PersonCreationRationale.OWNER_CREATED_LAUNCHPAD,
284 full_name, password=None, openid_identifier=openid_identifier)
285 return account
286
287 def processPositiveAssertion(self):280 def processPositiveAssertion(self):
288 """Process an OpenID response containing a positive assertion.281 """Process an OpenID response containing a positive assertion.
289282
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
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
292 - If the account is suspended, we stop and render an error page;285 that account.
293 - If the account is deactivated, we reactivate it and proceed;
294 - If the account is active, we just proceed.
295286
296 After that we ensure there's an IPerson associated with the account287 If the account is suspended, we stop and render an error page.
297 and login using that account.
298288
299 We also update the 'last_write' key in the session if we've done any289 We also update the 'last_write' key in the session if we've done any
300 DB writes, to ensure subsequent requests use the master DB and see290 DB writes, to ensure subsequent requests use the master DB and see
@@ -302,56 +292,23 @@
302 """292 """
303 identifier = self.openid_response.identity_url.split('/')[-1]293 identifier = self.openid_response.identity_url.split('/')[-1]
304 should_update_last_write = False294 should_update_last_write = False
305 email_set = getUtility(IEmailAddressSet)
306 # Force the use of the master database to make sure a lagged slave295 # Force the use of the master database to make sure a lagged slave
307 # doesn't fool us into creating a Person/Account when one already296 # doesn't fool us into creating a Person/Account when one already
308 # exists.297 # exists.
298 person_set = getUtility(IPersonSet)
299 email_address, full_name = self._getEmailAddressAndFullName()
300 try:
301 person, db_updated = person_set.getOrCreateByOpenIDIdentifier(
302 identifier, email_address, full_name,
303 comment='when logging in to Launchpad.',
304 creation_rationale=(
305 PersonCreationRationale.OWNER_CREATED_LAUNCHPAD))
306 should_update_last_write = db_updated
307 except AccountSuspendedError:
308 return self.suspended_account_template()
309
309 with MasterDatabasePolicy():310 with MasterDatabasePolicy():
310 try:311 self.login(person.account)
311 account = getUtility(IAccountSet).getByOpenIDIdentifier(
312 identifier)
313 except LookupError:
314 # The two lines below are duplicated a few more lines down,
315 # but to avoid this duplication we'd have to refactor most of
316 # our tests to provide an SREG response, which would be rather
317 # painful.
318 email_address, full_name = self._getEmailAddressAndFullName()
319 email = email_set.getByEmail(email_address)
320 if email is None:
321 # We got an OpenID response containing a positive
322 # assertion, but we don't have an account for the
323 # identifier or for the email address. We'll create one.
324 account = self._createAccount(
325 identifier, email_address, full_name)
326 else:
327 account = email.account
328 assert account is not None, (
329 "This email address should have an associated "
330 "account.")
331 removeSecurityProxy(account).openid_identifier = (
332 identifier)
333 should_update_last_write = True
334
335 if account.status == AccountStatus.SUSPENDED:
336 return self.suspended_account_template()
337 elif account.status in [AccountStatus.DEACTIVATED,
338 AccountStatus.NOACCOUNT]:
339 comment = 'Reactivated by the user'
340 password = '' # Needed just to please reactivate() below.
341 email_address, dummy = self._getEmailAddressAndFullName()
342 email = email_set.getByEmail(email_address)
343 if email is None:
344 email = email_set.new(email_address, account=account)
345 removeSecurityProxy(account).reactivate(
346 comment, password, removeSecurityProxy(email))
347 else:
348 # Account is active, so nothing to do.
349 pass
350 if IPerson(account, None) is None:
351 removeSecurityProxy(account).createPerson(
352 PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
353 should_update_last_write = True
354 self.login(account)
355312
356 if should_update_last_write:313 if should_update_last_write:
357 # This is a GET request but we changed the database, so update314 # This is a GET request but we changed the database, so update
358315
=== modified file 'lib/canonical/launchpad/webapp/tests/test_login.py'
--- lib/canonical/launchpad/webapp/tests/test_login.py 2010-04-27 15:50:02 +0000
+++ lib/canonical/launchpad/webapp/tests/test_login.py 2010-07-08 13:40:20 +0000
@@ -111,7 +111,8 @@
111 view_class=StubbedOpenIDCallbackView):111 view_class=StubbedOpenIDCallbackView):
112 openid_response = FakeOpenIDResponse(112 openid_response = FakeOpenIDResponse(
113 ITestOpenIDPersistentIdentity(account).openid_identity_url,113 ITestOpenIDPersistentIdentity(account).openid_identity_url,
114 status=response_status, message=response_msg)114 status=response_status, message=response_msg,
115 email='non-existent@example.com', full_name='Foo User')
115 return self._createAndRenderView(116 return self._createAndRenderView(
116 openid_response, view_class=view_class)117 openid_response, view_class=view_class)
117118
@@ -140,7 +141,8 @@
140 # In the common case we just login and redirect to the URL specified141 # In the common case we just login and redirect to the URL specified
141 # in the 'starting_url' query arg.142 # in the 'starting_url' query arg.
142 person = self.factory.makePerson()143 person = self.factory.makePerson()
143 view, html = self._createViewWithResponse(person.account)144 with SRegResponse_fromSuccessResponse_stubbed():
145 view, html = self._createViewWithResponse(person.account)
144 self.assertTrue(view.login_called)146 self.assertTrue(view.login_called)
145 response = view.request.response147 response = view.request.response
146 self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())148 self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())
@@ -157,7 +159,8 @@
157 # create one.159 # create one.
158 account = self.factory.makeAccount('Test account')160 account = self.factory.makeAccount('Test account')
159 self.assertIs(None, IPerson(account, None))161 self.assertIs(None, IPerson(account, None))
160 view, html = self._createViewWithResponse(account)162 with SRegResponse_fromSuccessResponse_stubbed():
163 view, html = self._createViewWithResponse(account)
161 self.assertIsNot(None, IPerson(account, None))164 self.assertIsNot(None, IPerson(account, None))
162 self.assertTrue(view.login_called)165 self.assertTrue(view.login_called)
163 response = view.request.response166 response = view.request.response
@@ -167,7 +170,7 @@
167170
168 # We also update the last_write flag in the session, to make sure171 # We also update the last_write flag in the session, to make sure
169 # further requests use the master DB and thus see the newly created172 # further requests use the master DB and thus see the newly created
170 # stuff. 173 # stuff.
171 self.assertLastWriteIsSet(view.request)174 self.assertLastWriteIsSet(view.request)
172175
173 def test_unseen_identity(self):176 def test_unseen_identity(self):
@@ -196,7 +199,7 @@
196199
197 # We also update the last_write flag in the session, to make sure200 # We also update the last_write flag in the session, to make sure
198 # further requests use the master DB and thus see the newly created201 # further requests use the master DB and thus see the newly created
199 # stuff. 202 # stuff.
200 self.assertLastWriteIsSet(view.request)203 self.assertLastWriteIsSet(view.request)
201204
202 def test_unseen_identity_with_registered_email(self):205 def test_unseen_identity_with_registered_email(self):
@@ -230,7 +233,7 @@
230233
231 # We also update the last_write flag in the session, to make sure234 # We also update the last_write flag in the session, to make sure
232 # further requests use the master DB and thus see the newly created235 # further requests use the master DB and thus see the newly created
233 # stuff. 236 # stuff.
234 self.assertLastWriteIsSet(view.request)237 self.assertLastWriteIsSet(view.request)
235238
236 def test_deactivated_account(self):239 def test_deactivated_account(self):
@@ -256,7 +259,7 @@
256 self.assertEquals(email, account.preferredemail.email)259 self.assertEquals(email, account.preferredemail.email)
257 # We also update the last_write flag in the session, to make sure260 # We also update the last_write flag in the session, to make sure
258 # further requests use the master DB and thus see the newly created261 # further requests use the master DB and thus see the newly created
259 # stuff. 262 # stuff.
260 self.assertLastWriteIsSet(view.request)263 self.assertLastWriteIsSet(view.request)
261264
262 def test_never_used_account(self):265 def test_never_used_account(self):
@@ -278,7 +281,7 @@
278 self.assertEquals(email, account.preferredemail.email)281 self.assertEquals(email, account.preferredemail.email)
279 # We also update the last_write flag in the session, to make sure282 # We also update the last_write flag in the session, to make sure
280 # further requests use the master DB and thus see the newly created283 # further requests use the master DB and thus see the newly created
281 # stuff. 284 # stuff.
282 self.assertLastWriteIsSet(view.request)285 self.assertLastWriteIsSet(view.request)
283286
284 def test_suspended_account(self):287 def test_suspended_account(self):
@@ -286,7 +289,8 @@
286 # login, but we must not allow that.289 # login, but we must not allow that.
287 account = self.factory.makeAccount(290 account = self.factory.makeAccount(
288 'Test account', status=AccountStatus.SUSPENDED)291 'Test account', status=AccountStatus.SUSPENDED)
289 view, html = self._createViewWithResponse(account)292 with SRegResponse_fromSuccessResponse_stubbed():
293 view, html = self._createViewWithResponse(account)
290 self.assertFalse(view.login_called)294 self.assertFalse(view.login_called)
291 main_content = extract_text(find_main_content(html))295 main_content = extract_text(find_main_content(html))
292 self.assertIn('This account has been suspended', main_content)296 self.assertIn('This account has been suspended', main_content)
293297
=== modified file 'lib/canonical/launchpad/xmlrpc/application.py'
--- lib/canonical/launchpad/xmlrpc/application.py 2010-04-19 06:35:23 +0000
+++ lib/canonical/launchpad/xmlrpc/application.py 2010-07-08 13:40:20 +0000
@@ -27,6 +27,7 @@
27from lp.code.interfaces.codehosting import ICodehostingApplication27from lp.code.interfaces.codehosting import ICodehostingApplication
28from lp.code.interfaces.codeimportscheduler import (28from lp.code.interfaces.codeimportscheduler import (
29 ICodeImportSchedulerApplication)29 ICodeImportSchedulerApplication)
30from lp.registry.interfaces.person import ISoftwareCenterAgentApplication
30from canonical.launchpad.webapp import LaunchpadXMLRPCView31from canonical.launchpad.webapp import LaunchpadXMLRPCView
3132
3233
@@ -58,6 +59,11 @@
58 """See `IPrivateApplication`."""59 """See `IPrivateApplication`."""
59 return getUtility(IPrivateMaloneApplication)60 return getUtility(IPrivateMaloneApplication)
6061
62 @property
63 def softwarecenteragent(self):
64 """See `IPrivateApplication`."""
65 return getUtility(ISoftwareCenterAgentApplication)
66
6167
62class ISelfTest(Interface):68class ISelfTest(Interface):
63 """XMLRPC external interface for testing the XMLRPC external interface."""69 """XMLRPC external interface for testing the XMLRPC external interface."""
6470
=== modified file 'lib/canonical/launchpad/xmlrpc/configure.zcml'
--- lib/canonical/launchpad/xmlrpc/configure.zcml 2010-04-19 23:35:41 +0000
+++ lib/canonical/launchpad/xmlrpc/configure.zcml 2010-07-08 13:40:20 +0000
@@ -204,4 +204,7 @@
204 <require like_class="xmlrpclib.Fault" />204 <require like_class="xmlrpclib.Fault" />
205 </class>205 </class>
206206
207 <class class="canonical.launchpad.xmlrpc.faults.AccountSuspended">
208 <require like_class="xmlrpclib.Fault" />
209 </class>
207</configure>210</configure>
208211
=== modified file 'lib/canonical/launchpad/xmlrpc/faults.py'
--- lib/canonical/launchpad/xmlrpc/faults.py 2010-04-09 12:58:01 +0000
+++ lib/canonical/launchpad/xmlrpc/faults.py 2010-07-08 13:40:20 +0000
@@ -450,3 +450,14 @@
450450
451 def __init__(self, job_id):451 def __init__(self, job_id):
452 LaunchpadFault.__init__(self, job_id=job_id)452 LaunchpadFault.__init__(self, job_id=job_id)
453
454
455class AccountSuspended(LaunchpadFault):
456 """Raised by `ISoftwareCenterAgentAPI` when an account is suspended."""
457
458 error_code = 370
459 msg_template = ('The openid_identifier \'%(openid_identifier)s\''
460 ' is linked to a suspended account.')
461
462 def __init__(self, openid_identifier):
463 LaunchpadFault.__init__(self, openid_identifier=openid_identifier)
453464
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2010-06-09 08:26:26 +0000
+++ lib/lp/registry/configure.zcml 2010-07-08 13:40:20 +0000
@@ -1006,6 +1006,16 @@
1006 interface="lp.registry.interfaces.mailinglist.IMailingListAPIView"1006 interface="lp.registry.interfaces.mailinglist.IMailingListAPIView"
1007 class="canonical.launchpad.xmlrpc.MailingListAPIView"1007 class="canonical.launchpad.xmlrpc.MailingListAPIView"
1008 permission="zope.Public"/>1008 permission="zope.Public"/>
1009 <securedutility
1010 class="lp.registry.xmlrpc.softwarecenteragent.SoftwareCenterAgentApplication"
1011 provides="lp.registry.interfaces.person.ISoftwareCenterAgentApplication">
1012 <allow interface="lp.registry.interfaces.person.ISoftwareCenterAgentApplication" />
1013 </securedutility>
1014 <xmlrpc:view
1015 for="lp.registry.interfaces.person.ISoftwareCenterAgentApplication"
1016 interface="lp.registry.interfaces.person.ISoftwareCenterAgentAPI"
1017 class="lp.registry.xmlrpc.softwarecenteragent.SoftwareCenterAgentAPI"
1018 permission="zope.Public"/>
10091019
1010 <!-- Helper page for held message approval -->1020 <!-- Helper page for held message approval -->
10111021
10121022
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2010-06-03 14:51:07 +0000
+++ lib/lp/registry/interfaces/person.py 2010-07-08 13:40:20 +0000
@@ -16,6 +16,8 @@
16 'IPersonClaim',16 'IPersonClaim',
17 'IPersonPublic', # Required for a monkey patch in interfaces/archive.py17 'IPersonPublic', # Required for a monkey patch in interfaces/archive.py
18 'IPersonSet',18 'IPersonSet',
19 'ISoftwareCenterAgentAPI',
20 'ISoftwareCenterAgentApplication',
19 'IPersonViewRestricted',21 'IPersonViewRestricted',
20 'IRequestPeopleMerge',22 'IRequestPeopleMerge',
21 'ITeam',23 'ITeam',
@@ -75,7 +77,8 @@
75from canonical.launchpad.validators.email import email_validator77from canonical.launchpad.validators.email import email_validator
76from canonical.launchpad.validators.name import name_validator78from canonical.launchpad.validators.name import name_validator
77from canonical.launchpad.webapp.authorization import check_permission79from canonical.launchpad.webapp.authorization import check_permission
78from canonical.launchpad.webapp.interfaces import NameLookupFailed80from canonical.launchpad.webapp.interfaces import (
81 ILaunchpadApplication, NameLookupFailed)
7982
80from lp.app.interfaces.headings import IRootContext83from lp.app.interfaces.headings import IRootContext
81from lp.blueprints.interfaces.specificationtarget import (84from lp.blueprints.interfaces.specificationtarget import (
@@ -297,6 +300,13 @@
297 commented on.300 commented on.
298 """)301 """)
299302
303 SOFTWARE_CENTER_PURCHASE = DBItem(16, """
304 Created by purchasing commercial software through Software Center.
305
306 A purchase of commercial software (ie. subscriptions to a private
307 and commercial archive) was made via Software Center.
308 """)
309
300310
301class TeamMembershipRenewalPolicy(DBEnumeratedType):311class TeamMembershipRenewalPolicy(DBEnumeratedType):
302 """TeamMembership Renewal Policy.312 """TeamMembership Renewal Policy.
@@ -1754,6 +1764,33 @@
1754 on the displayname or other arguments.1764 on the displayname or other arguments.
1755 """1765 """
17561766
1767 def getOrCreateByOpenIDIdentifier(openid_identifier, email,
1768 full_name, creation_rationale, comment):
1769 """Get or create a person for a given OpenID identifier.
1770
1771 This is used when users login. We get the account with the given
1772 OpenID identifier (creating one if it doesn't already exist) and
1773 act according to the account's state:
1774 - If the account is suspended, we stop and raise an error.
1775 - If the account is deactivated, we reactivate it and proceed;
1776 - If the account is active, we just proceed.
1777
1778 If there is no existing Launchpad person for the account, we
1779 create it.
1780
1781 :param openid_identifier: representing the authenticated user.
1782 :param email_address: the email address of the user.
1783 :param full_name: the full name of the user.
1784 :param creation_rationale: When an account or person needs to
1785 be created, this indicates why it was created.
1786 :param comment: If the account is reactivated or person created,
1787 this comment indicates why.
1788 :return: a tuple of `IPerson` and a boolean indicating whether the
1789 database was updated.
1790 :raises AccountSuspendedError: if the account associated with the
1791 identifier has been suspended.
1792 """
1793
1757 @call_with(teamowner=REQUEST_USER)1794 @call_with(teamowner=REQUEST_USER)
1758 @rename_parameters_as(1795 @rename_parameters_as(
1759 displayname='display_name', teamdescription='team_description',1796 displayname='display_name', teamdescription='team_description',
@@ -2052,6 +2089,26 @@
2052 required=True, vocabulary=TeamContactMethod)2089 required=True, vocabulary=TeamContactMethod)
20532090
20542091
2092class ISoftwareCenterAgentAPI(Interface):
2093 """XMLRPC API used by the software center agent."""
2094
2095 def getOrCreateSoftwareCenterCustomer(openid_identifier, email,
2096 full_name):
2097 """Get or create an LP person based on a given identifier.
2098
2099 See the method of the same name on `IPersonSet`. This XMLRPC version
2100 doesn't require the creation rationale and comment.
2101
2102 This is added as a private XMLRPC method instead of exposing via the
2103 API as it should not be needed long-term. Long term we should allow
2104 the software center to create subscriptions to private PPAs without
2105 requiring a Launchpad account.
2106 """
2107
2108class ISoftwareCenterAgentApplication(ILaunchpadApplication):
2109 """XMLRPC application root for ISoftwareCenterAgentAPI."""
2110
2111
2055class JoinNotAllowed(Exception):2112class JoinNotAllowed(Exception):
2056 """User is not allowed to join a given team."""2113 """User is not allowed to join a given team."""
20572114
20582115
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2010-06-22 11:19:34 +0000
+++ lib/lp/registry/model/person.py 2010-07-08 13:40:20 +0000
@@ -1,5 +1,6 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009 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).
3from __future__ import with_statement
34
4# vars() causes W06125# vars() causes W0612
5# pylint: disable-msg=E0611,W0212,W0612,C03226# pylint: disable-msg=E0611,W0212,W0612,C0322
@@ -61,6 +62,7 @@
61from canonical.lazr.utils import get_current_browser_request, safe_hasattr62from canonical.lazr.utils import get_current_browser_request, safe_hasattr
6263
63from canonical.launchpad.database.account import Account, AccountPassword64from canonical.launchpad.database.account import Account, AccountPassword
65from canonical.launchpad.interfaces.account import AccountSuspendedError
64from lp.bugs.model.bugtarget import HasBugsBase66from lp.bugs.model.bugtarget import HasBugsBase
65from canonical.launchpad.database.stormsugar import StartsWith67from canonical.launchpad.database.stormsugar import StartsWith
66from lp.registry.model.karma import KarmaCategory68from lp.registry.model.karma import KarmaCategory
@@ -152,6 +154,7 @@
152154
153from canonical.launchpad.validators.email import valid_email155from canonical.launchpad.validators.email import valid_email
154from canonical.launchpad.validators.name import sanitize_name, valid_name156from canonical.launchpad.validators.name import sanitize_name, valid_name
157from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
155from lp.registry.interfaces.person import validate_public_person158from lp.registry.interfaces.person import validate_public_person
156159
157160
@@ -2429,6 +2432,59 @@
2429 key=lambda obj: (obj.karma, obj.displayname, obj.id),2432 key=lambda obj: (obj.karma, obj.displayname, obj.id),
2430 reverse=True)2433 reverse=True)
24312434
2435 def getOrCreateByOpenIDIdentifier(
2436 self, openid_identifier, email_address, full_name,
2437 creation_rationale, comment):
2438 """See `IPersonSet`."""
2439 assert email_address is not None and full_name is not None, (
2440 "Both email address and full name are required to "
2441 "create an account.")
2442 db_updated = False
2443 with MasterDatabasePolicy():
2444 account_set = getUtility(IAccountSet)
2445 email_set = getUtility(IEmailAddressSet)
2446 email = email_set.getByEmail(email_address)
2447 try:
2448 account = account_set.getByOpenIDIdentifier(
2449 openid_identifier)
2450 except LookupError:
2451 if email is None:
2452 # There is no account associated with the identifier
2453 # nor an email associated with the email address.
2454 # We'll create one.
2455 account, email = account_set.createAccountAndEmail(
2456 email_address, creation_rationale, full_name,
2457 password=None,
2458 openid_identifier=openid_identifier)
2459 else:
2460 account = email.account
2461 assert account is not None, (
2462 "This email address should have an associated "
2463 "account.")
2464 removeSecurityProxy(account).openid_identifier = (
2465 openid_identifier)
2466 db_updated = True
2467
2468 if account.status == AccountStatus.SUSPENDED:
2469 raise AccountSuspendedError(
2470 "The account matching the identifier is suspended.")
2471 elif account.status in [AccountStatus.DEACTIVATED,
2472 AccountStatus.NOACCOUNT]:
2473 password = '' # Needed just to please reactivate() below.
2474 if email is None:
2475 email = email_set.new(email_address, account=account)
2476 removeSecurityProxy(account).reactivate(
2477 comment, password, removeSecurityProxy(email))
2478 else:
2479 # Account is active, so nothing to do.
2480 pass
2481 if IPerson(account, None) is None:
2482 removeSecurityProxy(account).createPerson(
2483 creation_rationale, comment=comment)
2484 db_updated = True
2485
2486 return IPerson(account), db_updated
2487
2432 def newTeam(self, teamowner, name, displayname, teamdescription=None,2488 def newTeam(self, teamowner, name, displayname, teamdescription=None,
2433 subscriptionpolicy=TeamSubscriptionPolicy.MODERATED,2489 subscriptionpolicy=TeamSubscriptionPolicy.MODERATED,
2434 defaultmembershipperiod=None, defaultrenewalperiod=None):2490 defaultmembershipperiod=None, defaultrenewalperiod=None):
24352491
=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py 2010-03-31 18:51:32 +0000
+++ lib/lp/registry/tests/test_personset.py 2010-07-08 13:40:20 +0000
@@ -16,9 +16,12 @@
16 MailingListAutoSubscribePolicy)16 MailingListAutoSubscribePolicy)
17from lp.registry.interfaces.person import (17from lp.registry.interfaces.person import (
18 PersonCreationRationale, IPersonSet)18 PersonCreationRationale, IPersonSet)
19from lp.testing import TestCaseWithFactory, login_person, logout19from lp.testing import (
20 TestCaseWithFactory, login_person, logout)
2021
21from canonical.database.sqlbase import cursor22from canonical.database.sqlbase import cursor
23from canonical.launchpad.interfaces.account import (
24 AccountStatus, AccountSuspendedError)
22from canonical.launchpad.testing.databasehelpers import (25from canonical.launchpad.testing.databasehelpers import (
23 remove_all_sample_data_branches)26 remove_all_sample_data_branches)
24from canonical.testing import DatabaseFunctionalLayer27from canonical.testing import DatabaseFunctionalLayer
@@ -146,5 +149,93 @@
146 self.assertEqual(1, self.cur.rowcount)149 self.assertEqual(1, self.cur.rowcount)
147150
148151
152class TestPersonSetGetOrCreateByOpenIDIdentifier(TestCaseWithFactory):
153
154 layer = DatabaseFunctionalLayer
155
156 def setUp(self):
157 super(TestPersonSetGetOrCreateByOpenIDIdentifier, self).setUp()
158 self.person_set = getUtility(IPersonSet)
159
160 def callGetOrCreate(self, identifier, email='a@b.com'):
161 return self.person_set.getOrCreateByOpenIDIdentifier(
162 identifier, email, "Joe Bloggs",
163 PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
164 "when purchasing an application via Software Center.")
165
166 def test_existing_person(self):
167 person = self.factory.makePerson()
168 openid_ident = removeSecurityProxy(person.account).openid_identifier
169 person_set = getUtility(IPersonSet)
170
171 result, db_updated = self.callGetOrCreate(openid_ident)
172
173 self.assertEqual(person, result)
174 self.assertFalse(db_updated)
175
176 def test_existing_account_no_person(self):
177 # A person is created with the correct rationale.
178 account = self.factory.makeAccount('purchaser')
179 openid_ident = removeSecurityProxy(account).openid_identifier
180
181 person, db_updated = self.callGetOrCreate(openid_ident)
182
183 self.assertEqual(account, person.account)
184 # The person is created with the correct rationale and creation
185 # comment.
186 self.assertEqual(
187 "when purchasing an application via Software Center.",
188 person.creation_comment)
189 self.assertEqual(
190 PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
191 person.creation_rationale)
192 self.assertTrue(db_updated)
193
194 def test_existing_deactivated_account(self):
195 # An existing deactivated account will be reactivated.
196 account = self.factory.makeAccount('purchaser',
197 status=AccountStatus.DEACTIVATED)
198 openid_ident = removeSecurityProxy(account).openid_identifier
199
200 person, db_updated = self.callGetOrCreate(openid_ident)
201 self.assertEqual(AccountStatus.ACTIVE, person.account.status)
202 self.assertTrue(db_updated)
203 self.assertEqual(
204 "when purchasing an application via Software Center.",
205 removeSecurityProxy(person.account).status_comment)
206
207 def test_existing_suspended_account(self):
208 # An existing suspended account will raise an exception.
209 account = self.factory.makeAccount('purchaser',
210 status=AccountStatus.SUSPENDED)
211 openid_ident = removeSecurityProxy(account).openid_identifier
212
213 self.assertRaises(
214 AccountSuspendedError, self.callGetOrCreate, openid_ident)
215
216 def test_no_account_or_email(self):
217 # An identifier can be used to create an account (it is assumed
218 # to be already authenticated with SSO).
219 person, db_updated = self.callGetOrCreate('openid-identifier')
220
221 self.assertEqual(
222 "openid-identifier",
223 removeSecurityProxy(person.account).openid_identifier)
224 self.assertTrue(db_updated)
225
226 def test_no_matching_account_existing_email(self):
227 # The openid_identity of the account matching the email will
228 # updated.
229 other_account = self.factory.makeAccount('test', email='a@b.com')
230
231 person, db_updated = self.callGetOrCreate(
232 'other-openid-identifier', 'a@b.com')
233
234 self.assertEqual(other_account, person.account)
235 self.assertEqual(
236 'other-openid-identifier',
237 removeSecurityProxy(person.account).openid_identifier)
238
239
149def test_suite():240def test_suite():
150 return TestLoader().loadTestsFromName(__name__)241 return TestLoader().loadTestsFromName(__name__)
151242
=== added file 'lib/lp/registry/tests/test_xmlrpc.py'
--- lib/lp/registry/tests/test_xmlrpc.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/tests/test_xmlrpc.py 2010-07-08 13:40:20 +0000
@@ -0,0 +1,125 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Testing registry-related xmlrpc calls."""
5
6__metaclass__ = type
7
8import unittest
9import xmlrpclib
10from zope.component import getUtility
11from zope.security.proxy import removeSecurityProxy
12
13from canonical.functional import XMLRPCTestTransport
14from canonical.launchpad.interfaces import IPrivateApplication
15from canonical.launchpad.interfaces.account import AccountStatus
16from canonical.launchpad.webapp.servers import LaunchpadTestRequest
17from canonical.testing.layers import LaunchpadFunctionalLayer
18from lp.registry.interfaces.person import (
19 IPersonSet, ISoftwareCenterAgentAPI, ISoftwareCenterAgentApplication,
20 PersonCreationRationale)
21from lp.registry.xmlrpc.softwarecenteragent import SoftwareCenterAgentAPI
22from lp.testing import TestCaseWithFactory
23
24
25class TestSoftwareCenterAgentAPI(TestCaseWithFactory):
26
27 layer = LaunchpadFunctionalLayer
28
29 def setUp(self):
30 super(TestSoftwareCenterAgentAPI, self).setUp()
31 self.private_root = getUtility(IPrivateApplication)
32 self.sca_api = SoftwareCenterAgentAPI(
33 context=self.private_root.softwarecenteragent,
34 request=LaunchpadTestRequest())
35
36 def test_provides_interface(self):
37 # The view interface is provided.
38 self.assertProvides(self.sca_api, ISoftwareCenterAgentAPI)
39
40 def test_getOrCreateSoftwareCenterCustomer(self):
41 # The method returns the username of the person, and sets the
42 # correct creation rational/comment.
43 user_name = self.sca_api.getOrCreateSoftwareCenterCustomer(
44 'openid-ident', 'alice@b.com', 'Joe Blogs')
45
46 self.assertEqual('alice', user_name)
47 person = getUtility(IPersonSet).getByName(user_name)
48 self.assertEqual(
49 'openid-ident',
50 removeSecurityProxy(person.account).openid_identifier)
51 self.assertEqual(
52 PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
53 person.creation_rationale)
54 self.assertEqual(
55 "when purchasing an application via Software Center.",
56 person.creation_comment)
57
58
59class TestSoftwareCenterAgentApplication(TestCaseWithFactory):
60
61 layer = LaunchpadFunctionalLayer
62
63 def setUp(self):
64 super(TestSoftwareCenterAgentApplication, self).setUp()
65 self.private_root = getUtility(IPrivateApplication)
66 self.rpc_proxy = xmlrpclib.ServerProxy(
67 'http://xmlrpc-private.launchpad.dev:8087/softwarecenteragent',
68 transport=XMLRPCTestTransport())
69
70 def test_provides_interface(self):
71 # The application is provided.
72 self.assertProvides(
73 self.private_root.softwarecenteragent,
74 ISoftwareCenterAgentApplication)
75
76 def test_getOrCreateSoftwareCenterCustomer_xmlrpc(self):
77 # The method can be called via xmlrpc
78 user_name = self.rpc_proxy.getOrCreateSoftwareCenterCustomer(
79 'openid-ident', 'a@b.com', 'Joe Blogs')
80 person = getUtility(IPersonSet).getByName(user_name)
81 self.assertEqual(
82 'openid-ident',
83 removeSecurityProxy(person.account).openid_identifier)
84
85 def test_getOrCreateSoftwareCenterCustomer_xmlrpc_error(self):
86 # A suspended account results in an appropriate xmlrpc fault.
87 suspended_account = self.factory.makeAccount(
88 'Joe Blogs', email='a@b.com', status=AccountStatus.SUSPENDED)
89 openid_identifier = removeSecurityProxy(
90 suspended_account).openid_identifier
91
92 # assertRaises doesn't let us check the type of Fault.
93 fault_raised = False
94 try:
95 self.rpc_proxy.getOrCreateSoftwareCenterCustomer(
96 openid_identifier, 'a@b.com', 'Joe Blogs')
97 except xmlrpclib.Fault, e:
98 fault_raised = True
99 self.assertEqual(370, e.faultCode)
100 self.assertIn(openid_identifier, e.faultString)
101
102 self.assertTrue(fault_raised)
103
104 def test_not_available_on_public_api(self):
105 # The person set api is not available on the public xmlrpc
106 # service.
107 public_rpc_proxy = xmlrpclib.ServerProxy(
108 'http://test@canonical.com:test@'
109 'xmlrpc.launchpad.dev/softwarecenteragent',
110 transport=XMLRPCTestTransport())
111
112 # assertRaises doesn't let us check the type of Fault.
113 protocol_error_raised = False
114 try:
115 public_rpc_proxy.getOrCreateSoftwareCenterCustomer(
116 'openid-ident', 'a@b.com', 'Joe Blogs')
117 except xmlrpclib.ProtocolError, e:
118 protocol_error_raised = True
119 self.assertEqual(404, e.errcode)
120
121 self.assertTrue(protocol_error_raised)
122
123
124def test_suite():
125 return unittest.TestLoader().loadTestsFromName(__name__)
0126
=== added directory 'lib/lp/registry/xmlrpc'
=== added file 'lib/lp/registry/xmlrpc/__init__.py'
=== added file 'lib/lp/registry/xmlrpc/softwarecenteragent.py'
--- lib/lp/registry/xmlrpc/softwarecenteragent.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/xmlrpc/softwarecenteragent.py 2010-07-08 13:40:20 +0000
@@ -0,0 +1,47 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""XMLRPC APIs for person set."""
5
6__metaclass__ = type
7__all__ = [
8 'SoftwareCenterAgentAPI',
9 ]
10
11
12from zope.component import getUtility
13from zope.interface import implements
14
15from canonical.launchpad.interfaces.account import AccountSuspendedError
16from canonical.launchpad.webapp import LaunchpadXMLRPCView
17from canonical.launchpad.xmlrpc import faults
18from lp.registry.interfaces.person import (
19 IPersonSet, ISoftwareCenterAgentAPI, ISoftwareCenterAgentApplication,
20 PersonCreationRationale)
21
22
23class SoftwareCenterAgentAPI(LaunchpadXMLRPCView):
24 """See `ISoftwareCenterAgentAPI`."""
25
26 implements(ISoftwareCenterAgentAPI)
27
28 def getOrCreateSoftwareCenterCustomer(self, openid_identifier, email,
29 full_name):
30 try:
31 person, db_updated = getUtility(
32 IPersonSet).getOrCreateByOpenIDIdentifier(
33 openid_identifier, email, full_name,
34 PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
35 "when purchasing an application via Software Center.")
36 except AccountSuspendedError:
37 return faults.AccountSuspended(openid_identifier)
38
39 return person.name
40
41
42class SoftwareCenterAgentApplication:
43 """Software center agent end-point."""
44 implements(ISoftwareCenterAgentApplication)
45
46 title = "Software Center Agent API"
47
048
=== modified file 'lib/lp/testopenid/browser/server.py'
--- lib/lp/testopenid/browser/server.py 2010-05-15 17:43:59 +0000
+++ lib/lp/testopenid/browser/server.py 2010-07-08 13:40:20 +0000
@@ -198,7 +198,10 @@
198 else:198 else:
199 response = self.openid_request.answer(True)199 response = self.openid_request.answer(True)
200200
201 sreg_fields = dict(nickname=IPerson(self.account).name)201 sreg_fields = dict(
202 nickname=IPerson(self.account).name,
203 email=self.account.preferredemail.email,
204 fullname=self.account.displayname)
202 sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request)205 sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request)
203 sreg_response = SRegResponse.extractResponse(206 sreg_response = SRegResponse.extractResponse(
204 sreg_request, sreg_fields)207 sreg_request, sreg_fields)
@@ -224,7 +227,7 @@
224 This class implements an OpenID endpoint using the python-openid227 This class implements an OpenID endpoint using the python-openid
225 library. In addition to the normal modes of operation, it also228 library. In addition to the normal modes of operation, it also
226 implements the OpenID 2.0 identifier select mode.229 implements the OpenID 2.0 identifier select mode.
227 230
228 Note that the checkid_immediate mode is not supported.231 Note that the checkid_immediate mode is not supported.
229 """232 """
230233