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
1=== modified file 'lib/canonical/launchpad/interfaces/account.py'
2--- lib/canonical/launchpad/interfaces/account.py 2009-12-24 11:06:04 +0000
3+++ lib/canonical/launchpad/interfaces/account.py 2010-07-08 13:40:20 +0000
4@@ -8,6 +8,7 @@
5
6 __all__ = [
7 'AccountStatus',
8+ 'AccountSuspendedError',
9 'AccountCreationRationale',
10 'IAccount',
11 'IAccountPrivate',
12@@ -27,6 +28,10 @@
13 from lazr.restful.fields import CollectionField, Reference
14
15
16+class AccountSuspendedError(Exception):
17+ """The account being accessed has been suspended."""
18+
19+
20 class AccountStatus(DBEnumeratedType):
21 """The status of an account."""
22
23@@ -179,6 +184,13 @@
24 commented on.
25 """)
26
27+ SOFTWARE_CENTER_PURCHASE = DBItem(16, """
28+ Created by purchasing commercial software through Software Center.
29+
30+ A purchase of commercial software (ie. subscriptions to a private
31+ and commercial archive) was made via Software Center.
32+ """)
33+
34
35 class IAccountPublic(Interface):
36 """Public information on an `IAccount`."""
37@@ -261,7 +273,7 @@
38 password = PasswordField(
39 title=_("Password."), readonly=False, required=True)
40
41- def createPerson(self, rationale, name=None, comment=None):
42+ def createPerson(rationale, name=None, comment=None):
43 """Create and return a new `IPerson` associated with this account.
44
45 :param rationale: A member of `AccountCreationRationale`.
46
47=== modified file 'lib/canonical/launchpad/interfaces/launchpad.py'
48--- lib/canonical/launchpad/interfaces/launchpad.py 2010-06-22 17:08:23 +0000
49+++ lib/canonical/launchpad/interfaces/launchpad.py 2010-07-08 13:40:20 +0000
50@@ -296,6 +296,9 @@
51
52 bugs = Attribute("""Launchpad Bugs XML-RPC end point.""")
53
54+ softwarecenteragent = Attribute(
55+ """Software center agent XML-RPC end point.""")
56+
57
58 class IAuthServerApplication(ILaunchpadApplication):
59 """Launchpad legacy AuthServer application root."""
60
61=== modified file 'lib/canonical/launchpad/webapp/login.py'
62--- lib/canonical/launchpad/webapp/login.py 2010-05-15 17:43:59 +0000
63+++ lib/canonical/launchpad/webapp/login.py 2010-07-08 13:40:20 +0000
64@@ -32,10 +32,9 @@
65 from canonical.cachedproperty import cachedproperty
66 from canonical.config import config
67 from canonical.launchpad import _
68-from canonical.launchpad.interfaces.account import AccountStatus, IAccountSet
69-from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
70+from canonical.launchpad.interfaces.account import AccountSuspendedError
71 from canonical.launchpad.interfaces.openidconsumer import IOpenIDConsumerStore
72-from lp.registry.interfaces.person import IPerson, PersonCreationRationale
73+from lp.registry.interfaces.person import IPersonSet, PersonCreationRationale
74 from canonical.launchpad.readonly import is_read_only
75 from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
76 from canonical.launchpad.webapp.error import SystemErrorView
77@@ -199,7 +198,7 @@
78 return_to = "%s?%s" % (return_to, starting_url)
79 form_html = self.openid_request.htmlMarkup(trust_root, return_to)
80
81- # The consumer.begin() call above will insert rows into the
82+ # The consumer.begin() call above will insert rows into the
83 # OpenIDAssociations table, but since this will be a GET request, the
84 # transaction would be rolled back, so we need an explicit commit
85 # here.
86@@ -278,23 +277,14 @@
87 "No email address or full name found in sreg response")
88 return email_address, full_name
89
90- def _createAccount(self, openid_identifier, email_address, full_name):
91- account, email = getUtility(IAccountSet).createAccountAndEmail(
92- email_address, PersonCreationRationale.OWNER_CREATED_LAUNCHPAD,
93- full_name, password=None, openid_identifier=openid_identifier)
94- return account
95-
96 def processPositiveAssertion(self):
97 """Process an OpenID response containing a positive assertion.
98
99- We'll get the account with the given OpenID identifier (creating one
100- if it doesn't already exist) and act according to the account's state:
101- - If the account is suspended, we stop and render an error page;
102- - If the account is deactivated, we reactivate it and proceed;
103- - If the account is active, we just proceed.
104+ We'll get the person and account with the given OpenID
105+ identifier (creating one if necessary), and then login using
106+ that account.
107
108- After that we ensure there's an IPerson associated with the account
109- and login using that account.
110+ If the account is suspended, we stop and render an error page.
111
112 We also update the 'last_write' key in the session if we've done any
113 DB writes, to ensure subsequent requests use the master DB and see
114@@ -302,56 +292,23 @@
115 """
116 identifier = self.openid_response.identity_url.split('/')[-1]
117 should_update_last_write = False
118- email_set = getUtility(IEmailAddressSet)
119 # Force the use of the master database to make sure a lagged slave
120 # doesn't fool us into creating a Person/Account when one already
121 # exists.
122+ person_set = getUtility(IPersonSet)
123+ email_address, full_name = self._getEmailAddressAndFullName()
124+ try:
125+ person, db_updated = person_set.getOrCreateByOpenIDIdentifier(
126+ identifier, email_address, full_name,
127+ comment='when logging in to Launchpad.',
128+ creation_rationale=(
129+ PersonCreationRationale.OWNER_CREATED_LAUNCHPAD))
130+ should_update_last_write = db_updated
131+ except AccountSuspendedError:
132+ return self.suspended_account_template()
133+
134 with MasterDatabasePolicy():
135- try:
136- account = getUtility(IAccountSet).getByOpenIDIdentifier(
137- identifier)
138- except LookupError:
139- # The two lines below are duplicated a few more lines down,
140- # but to avoid this duplication we'd have to refactor most of
141- # our tests to provide an SREG response, which would be rather
142- # painful.
143- email_address, full_name = self._getEmailAddressAndFullName()
144- email = email_set.getByEmail(email_address)
145- if email is None:
146- # We got an OpenID response containing a positive
147- # assertion, but we don't have an account for the
148- # identifier or for the email address. We'll create one.
149- account = self._createAccount(
150- identifier, email_address, full_name)
151- else:
152- account = email.account
153- assert account is not None, (
154- "This email address should have an associated "
155- "account.")
156- removeSecurityProxy(account).openid_identifier = (
157- identifier)
158- should_update_last_write = True
159-
160- if account.status == AccountStatus.SUSPENDED:
161- return self.suspended_account_template()
162- elif account.status in [AccountStatus.DEACTIVATED,
163- AccountStatus.NOACCOUNT]:
164- comment = 'Reactivated by the user'
165- password = '' # Needed just to please reactivate() below.
166- email_address, dummy = self._getEmailAddressAndFullName()
167- email = email_set.getByEmail(email_address)
168- if email is None:
169- email = email_set.new(email_address, account=account)
170- removeSecurityProxy(account).reactivate(
171- comment, password, removeSecurityProxy(email))
172- else:
173- # Account is active, so nothing to do.
174- pass
175- if IPerson(account, None) is None:
176- removeSecurityProxy(account).createPerson(
177- PersonCreationRationale.OWNER_CREATED_LAUNCHPAD)
178- should_update_last_write = True
179- self.login(account)
180+ self.login(person.account)
181
182 if should_update_last_write:
183 # This is a GET request but we changed the database, so update
184
185=== modified file 'lib/canonical/launchpad/webapp/tests/test_login.py'
186--- lib/canonical/launchpad/webapp/tests/test_login.py 2010-04-27 15:50:02 +0000
187+++ lib/canonical/launchpad/webapp/tests/test_login.py 2010-07-08 13:40:20 +0000
188@@ -111,7 +111,8 @@
189 view_class=StubbedOpenIDCallbackView):
190 openid_response = FakeOpenIDResponse(
191 ITestOpenIDPersistentIdentity(account).openid_identity_url,
192- status=response_status, message=response_msg)
193+ status=response_status, message=response_msg,
194+ email='non-existent@example.com', full_name='Foo User')
195 return self._createAndRenderView(
196 openid_response, view_class=view_class)
197
198@@ -140,7 +141,8 @@
199 # In the common case we just login and redirect to the URL specified
200 # in the 'starting_url' query arg.
201 person = self.factory.makePerson()
202- view, html = self._createViewWithResponse(person.account)
203+ with SRegResponse_fromSuccessResponse_stubbed():
204+ view, html = self._createViewWithResponse(person.account)
205 self.assertTrue(view.login_called)
206 response = view.request.response
207 self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())
208@@ -157,7 +159,8 @@
209 # create one.
210 account = self.factory.makeAccount('Test account')
211 self.assertIs(None, IPerson(account, None))
212- view, html = self._createViewWithResponse(account)
213+ with SRegResponse_fromSuccessResponse_stubbed():
214+ view, html = self._createViewWithResponse(account)
215 self.assertIsNot(None, IPerson(account, None))
216 self.assertTrue(view.login_called)
217 response = view.request.response
218@@ -167,7 +170,7 @@
219
220 # We also update the last_write flag in the session, to make sure
221 # further requests use the master DB and thus see the newly created
222- # stuff.
223+ # stuff.
224 self.assertLastWriteIsSet(view.request)
225
226 def test_unseen_identity(self):
227@@ -196,7 +199,7 @@
228
229 # We also update the last_write flag in the session, to make sure
230 # further requests use the master DB and thus see the newly created
231- # stuff.
232+ # stuff.
233 self.assertLastWriteIsSet(view.request)
234
235 def test_unseen_identity_with_registered_email(self):
236@@ -230,7 +233,7 @@
237
238 # We also update the last_write flag in the session, to make sure
239 # further requests use the master DB and thus see the newly created
240- # stuff.
241+ # stuff.
242 self.assertLastWriteIsSet(view.request)
243
244 def test_deactivated_account(self):
245@@ -256,7 +259,7 @@
246 self.assertEquals(email, account.preferredemail.email)
247 # We also update the last_write flag in the session, to make sure
248 # further requests use the master DB and thus see the newly created
249- # stuff.
250+ # stuff.
251 self.assertLastWriteIsSet(view.request)
252
253 def test_never_used_account(self):
254@@ -278,7 +281,7 @@
255 self.assertEquals(email, account.preferredemail.email)
256 # We also update the last_write flag in the session, to make sure
257 # further requests use the master DB and thus see the newly created
258- # stuff.
259+ # stuff.
260 self.assertLastWriteIsSet(view.request)
261
262 def test_suspended_account(self):
263@@ -286,7 +289,8 @@
264 # login, but we must not allow that.
265 account = self.factory.makeAccount(
266 'Test account', status=AccountStatus.SUSPENDED)
267- view, html = self._createViewWithResponse(account)
268+ with SRegResponse_fromSuccessResponse_stubbed():
269+ view, html = self._createViewWithResponse(account)
270 self.assertFalse(view.login_called)
271 main_content = extract_text(find_main_content(html))
272 self.assertIn('This account has been suspended', main_content)
273
274=== modified file 'lib/canonical/launchpad/xmlrpc/application.py'
275--- lib/canonical/launchpad/xmlrpc/application.py 2010-04-19 06:35:23 +0000
276+++ lib/canonical/launchpad/xmlrpc/application.py 2010-07-08 13:40:20 +0000
277@@ -27,6 +27,7 @@
278 from lp.code.interfaces.codehosting import ICodehostingApplication
279 from lp.code.interfaces.codeimportscheduler import (
280 ICodeImportSchedulerApplication)
281+from lp.registry.interfaces.person import ISoftwareCenterAgentApplication
282 from canonical.launchpad.webapp import LaunchpadXMLRPCView
283
284
285@@ -58,6 +59,11 @@
286 """See `IPrivateApplication`."""
287 return getUtility(IPrivateMaloneApplication)
288
289+ @property
290+ def softwarecenteragent(self):
291+ """See `IPrivateApplication`."""
292+ return getUtility(ISoftwareCenterAgentApplication)
293+
294
295 class ISelfTest(Interface):
296 """XMLRPC external interface for testing the XMLRPC external interface."""
297
298=== modified file 'lib/canonical/launchpad/xmlrpc/configure.zcml'
299--- lib/canonical/launchpad/xmlrpc/configure.zcml 2010-04-19 23:35:41 +0000
300+++ lib/canonical/launchpad/xmlrpc/configure.zcml 2010-07-08 13:40:20 +0000
301@@ -204,4 +204,7 @@
302 <require like_class="xmlrpclib.Fault" />
303 </class>
304
305+ <class class="canonical.launchpad.xmlrpc.faults.AccountSuspended">
306+ <require like_class="xmlrpclib.Fault" />
307+ </class>
308 </configure>
309
310=== modified file 'lib/canonical/launchpad/xmlrpc/faults.py'
311--- lib/canonical/launchpad/xmlrpc/faults.py 2010-04-09 12:58:01 +0000
312+++ lib/canonical/launchpad/xmlrpc/faults.py 2010-07-08 13:40:20 +0000
313@@ -450,3 +450,14 @@
314
315 def __init__(self, job_id):
316 LaunchpadFault.__init__(self, job_id=job_id)
317+
318+
319+class AccountSuspended(LaunchpadFault):
320+ """Raised by `ISoftwareCenterAgentAPI` when an account is suspended."""
321+
322+ error_code = 370
323+ msg_template = ('The openid_identifier \'%(openid_identifier)s\''
324+ ' is linked to a suspended account.')
325+
326+ def __init__(self, openid_identifier):
327+ LaunchpadFault.__init__(self, openid_identifier=openid_identifier)
328
329=== modified file 'lib/lp/registry/configure.zcml'
330--- lib/lp/registry/configure.zcml 2010-06-09 08:26:26 +0000
331+++ lib/lp/registry/configure.zcml 2010-07-08 13:40:20 +0000
332@@ -1006,6 +1006,16 @@
333 interface="lp.registry.interfaces.mailinglist.IMailingListAPIView"
334 class="canonical.launchpad.xmlrpc.MailingListAPIView"
335 permission="zope.Public"/>
336+ <securedutility
337+ class="lp.registry.xmlrpc.softwarecenteragent.SoftwareCenterAgentApplication"
338+ provides="lp.registry.interfaces.person.ISoftwareCenterAgentApplication">
339+ <allow interface="lp.registry.interfaces.person.ISoftwareCenterAgentApplication" />
340+ </securedutility>
341+ <xmlrpc:view
342+ for="lp.registry.interfaces.person.ISoftwareCenterAgentApplication"
343+ interface="lp.registry.interfaces.person.ISoftwareCenterAgentAPI"
344+ class="lp.registry.xmlrpc.softwarecenteragent.SoftwareCenterAgentAPI"
345+ permission="zope.Public"/>
346
347 <!-- Helper page for held message approval -->
348
349
350=== modified file 'lib/lp/registry/interfaces/person.py'
351--- lib/lp/registry/interfaces/person.py 2010-06-03 14:51:07 +0000
352+++ lib/lp/registry/interfaces/person.py 2010-07-08 13:40:20 +0000
353@@ -16,6 +16,8 @@
354 'IPersonClaim',
355 'IPersonPublic', # Required for a monkey patch in interfaces/archive.py
356 'IPersonSet',
357+ 'ISoftwareCenterAgentAPI',
358+ 'ISoftwareCenterAgentApplication',
359 'IPersonViewRestricted',
360 'IRequestPeopleMerge',
361 'ITeam',
362@@ -75,7 +77,8 @@
363 from canonical.launchpad.validators.email import email_validator
364 from canonical.launchpad.validators.name import name_validator
365 from canonical.launchpad.webapp.authorization import check_permission
366-from canonical.launchpad.webapp.interfaces import NameLookupFailed
367+from canonical.launchpad.webapp.interfaces import (
368+ ILaunchpadApplication, NameLookupFailed)
369
370 from lp.app.interfaces.headings import IRootContext
371 from lp.blueprints.interfaces.specificationtarget import (
372@@ -297,6 +300,13 @@
373 commented on.
374 """)
375
376+ SOFTWARE_CENTER_PURCHASE = DBItem(16, """
377+ Created by purchasing commercial software through Software Center.
378+
379+ A purchase of commercial software (ie. subscriptions to a private
380+ and commercial archive) was made via Software Center.
381+ """)
382+
383
384 class TeamMembershipRenewalPolicy(DBEnumeratedType):
385 """TeamMembership Renewal Policy.
386@@ -1754,6 +1764,33 @@
387 on the displayname or other arguments.
388 """
389
390+ def getOrCreateByOpenIDIdentifier(openid_identifier, email,
391+ full_name, creation_rationale, comment):
392+ """Get or create a person for a given OpenID identifier.
393+
394+ This is used when users login. We get the account with the given
395+ OpenID identifier (creating one if it doesn't already exist) and
396+ act according to the account's state:
397+ - If the account is suspended, we stop and raise an error.
398+ - If the account is deactivated, we reactivate it and proceed;
399+ - If the account is active, we just proceed.
400+
401+ If there is no existing Launchpad person for the account, we
402+ create it.
403+
404+ :param openid_identifier: representing the authenticated user.
405+ :param email_address: the email address of the user.
406+ :param full_name: the full name of the user.
407+ :param creation_rationale: When an account or person needs to
408+ be created, this indicates why it was created.
409+ :param comment: If the account is reactivated or person created,
410+ this comment indicates why.
411+ :return: a tuple of `IPerson` and a boolean indicating whether the
412+ database was updated.
413+ :raises AccountSuspendedError: if the account associated with the
414+ identifier has been suspended.
415+ """
416+
417 @call_with(teamowner=REQUEST_USER)
418 @rename_parameters_as(
419 displayname='display_name', teamdescription='team_description',
420@@ -2052,6 +2089,26 @@
421 required=True, vocabulary=TeamContactMethod)
422
423
424+class ISoftwareCenterAgentAPI(Interface):
425+ """XMLRPC API used by the software center agent."""
426+
427+ def getOrCreateSoftwareCenterCustomer(openid_identifier, email,
428+ full_name):
429+ """Get or create an LP person based on a given identifier.
430+
431+ See the method of the same name on `IPersonSet`. This XMLRPC version
432+ doesn't require the creation rationale and comment.
433+
434+ This is added as a private XMLRPC method instead of exposing via the
435+ API as it should not be needed long-term. Long term we should allow
436+ the software center to create subscriptions to private PPAs without
437+ requiring a Launchpad account.
438+ """
439+
440+class ISoftwareCenterAgentApplication(ILaunchpadApplication):
441+ """XMLRPC application root for ISoftwareCenterAgentAPI."""
442+
443+
444 class JoinNotAllowed(Exception):
445 """User is not allowed to join a given team."""
446
447
448=== modified file 'lib/lp/registry/model/person.py'
449--- lib/lp/registry/model/person.py 2010-06-22 11:19:34 +0000
450+++ lib/lp/registry/model/person.py 2010-07-08 13:40:20 +0000
451@@ -1,5 +1,6 @@
452 # Copyright 2009 Canonical Ltd. This software is licensed under the
453 # GNU Affero General Public License version 3 (see the file LICENSE).
454+from __future__ import with_statement
455
456 # vars() causes W0612
457 # pylint: disable-msg=E0611,W0212,W0612,C0322
458@@ -61,6 +62,7 @@
459 from canonical.lazr.utils import get_current_browser_request, safe_hasattr
460
461 from canonical.launchpad.database.account import Account, AccountPassword
462+from canonical.launchpad.interfaces.account import AccountSuspendedError
463 from lp.bugs.model.bugtarget import HasBugsBase
464 from canonical.launchpad.database.stormsugar import StartsWith
465 from lp.registry.model.karma import KarmaCategory
466@@ -152,6 +154,7 @@
467
468 from canonical.launchpad.validators.email import valid_email
469 from canonical.launchpad.validators.name import sanitize_name, valid_name
470+from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
471 from lp.registry.interfaces.person import validate_public_person
472
473
474@@ -2429,6 +2432,59 @@
475 key=lambda obj: (obj.karma, obj.displayname, obj.id),
476 reverse=True)
477
478+ def getOrCreateByOpenIDIdentifier(
479+ self, openid_identifier, email_address, full_name,
480+ creation_rationale, comment):
481+ """See `IPersonSet`."""
482+ assert email_address is not None and full_name is not None, (
483+ "Both email address and full name are required to "
484+ "create an account.")
485+ db_updated = False
486+ with MasterDatabasePolicy():
487+ account_set = getUtility(IAccountSet)
488+ email_set = getUtility(IEmailAddressSet)
489+ email = email_set.getByEmail(email_address)
490+ try:
491+ account = account_set.getByOpenIDIdentifier(
492+ openid_identifier)
493+ except LookupError:
494+ if email is None:
495+ # There is no account associated with the identifier
496+ # nor an email associated with the email address.
497+ # We'll create one.
498+ account, email = account_set.createAccountAndEmail(
499+ email_address, creation_rationale, full_name,
500+ password=None,
501+ openid_identifier=openid_identifier)
502+ else:
503+ account = email.account
504+ assert account is not None, (
505+ "This email address should have an associated "
506+ "account.")
507+ removeSecurityProxy(account).openid_identifier = (
508+ openid_identifier)
509+ db_updated = True
510+
511+ if account.status == AccountStatus.SUSPENDED:
512+ raise AccountSuspendedError(
513+ "The account matching the identifier is suspended.")
514+ elif account.status in [AccountStatus.DEACTIVATED,
515+ AccountStatus.NOACCOUNT]:
516+ password = '' # Needed just to please reactivate() below.
517+ if email is None:
518+ email = email_set.new(email_address, account=account)
519+ removeSecurityProxy(account).reactivate(
520+ comment, password, removeSecurityProxy(email))
521+ else:
522+ # Account is active, so nothing to do.
523+ pass
524+ if IPerson(account, None) is None:
525+ removeSecurityProxy(account).createPerson(
526+ creation_rationale, comment=comment)
527+ db_updated = True
528+
529+ return IPerson(account), db_updated
530+
531 def newTeam(self, teamowner, name, displayname, teamdescription=None,
532 subscriptionpolicy=TeamSubscriptionPolicy.MODERATED,
533 defaultmembershipperiod=None, defaultrenewalperiod=None):
534
535=== modified file 'lib/lp/registry/tests/test_personset.py'
536--- lib/lp/registry/tests/test_personset.py 2010-03-31 18:51:32 +0000
537+++ lib/lp/registry/tests/test_personset.py 2010-07-08 13:40:20 +0000
538@@ -16,9 +16,12 @@
539 MailingListAutoSubscribePolicy)
540 from lp.registry.interfaces.person import (
541 PersonCreationRationale, IPersonSet)
542-from lp.testing import TestCaseWithFactory, login_person, logout
543+from lp.testing import (
544+ TestCaseWithFactory, login_person, logout)
545
546 from canonical.database.sqlbase import cursor
547+from canonical.launchpad.interfaces.account import (
548+ AccountStatus, AccountSuspendedError)
549 from canonical.launchpad.testing.databasehelpers import (
550 remove_all_sample_data_branches)
551 from canonical.testing import DatabaseFunctionalLayer
552@@ -146,5 +149,93 @@
553 self.assertEqual(1, self.cur.rowcount)
554
555
556+class TestPersonSetGetOrCreateByOpenIDIdentifier(TestCaseWithFactory):
557+
558+ layer = DatabaseFunctionalLayer
559+
560+ def setUp(self):
561+ super(TestPersonSetGetOrCreateByOpenIDIdentifier, self).setUp()
562+ self.person_set = getUtility(IPersonSet)
563+
564+ def callGetOrCreate(self, identifier, email='a@b.com'):
565+ return self.person_set.getOrCreateByOpenIDIdentifier(
566+ identifier, email, "Joe Bloggs",
567+ PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
568+ "when purchasing an application via Software Center.")
569+
570+ def test_existing_person(self):
571+ person = self.factory.makePerson()
572+ openid_ident = removeSecurityProxy(person.account).openid_identifier
573+ person_set = getUtility(IPersonSet)
574+
575+ result, db_updated = self.callGetOrCreate(openid_ident)
576+
577+ self.assertEqual(person, result)
578+ self.assertFalse(db_updated)
579+
580+ def test_existing_account_no_person(self):
581+ # A person is created with the correct rationale.
582+ account = self.factory.makeAccount('purchaser')
583+ openid_ident = removeSecurityProxy(account).openid_identifier
584+
585+ person, db_updated = self.callGetOrCreate(openid_ident)
586+
587+ self.assertEqual(account, person.account)
588+ # The person is created with the correct rationale and creation
589+ # comment.
590+ self.assertEqual(
591+ "when purchasing an application via Software Center.",
592+ person.creation_comment)
593+ self.assertEqual(
594+ PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
595+ person.creation_rationale)
596+ self.assertTrue(db_updated)
597+
598+ def test_existing_deactivated_account(self):
599+ # An existing deactivated account will be reactivated.
600+ account = self.factory.makeAccount('purchaser',
601+ status=AccountStatus.DEACTIVATED)
602+ openid_ident = removeSecurityProxy(account).openid_identifier
603+
604+ person, db_updated = self.callGetOrCreate(openid_ident)
605+ self.assertEqual(AccountStatus.ACTIVE, person.account.status)
606+ self.assertTrue(db_updated)
607+ self.assertEqual(
608+ "when purchasing an application via Software Center.",
609+ removeSecurityProxy(person.account).status_comment)
610+
611+ def test_existing_suspended_account(self):
612+ # An existing suspended account will raise an exception.
613+ account = self.factory.makeAccount('purchaser',
614+ status=AccountStatus.SUSPENDED)
615+ openid_ident = removeSecurityProxy(account).openid_identifier
616+
617+ self.assertRaises(
618+ AccountSuspendedError, self.callGetOrCreate, openid_ident)
619+
620+ def test_no_account_or_email(self):
621+ # An identifier can be used to create an account (it is assumed
622+ # to be already authenticated with SSO).
623+ person, db_updated = self.callGetOrCreate('openid-identifier')
624+
625+ self.assertEqual(
626+ "openid-identifier",
627+ removeSecurityProxy(person.account).openid_identifier)
628+ self.assertTrue(db_updated)
629+
630+ def test_no_matching_account_existing_email(self):
631+ # The openid_identity of the account matching the email will
632+ # updated.
633+ other_account = self.factory.makeAccount('test', email='a@b.com')
634+
635+ person, db_updated = self.callGetOrCreate(
636+ 'other-openid-identifier', 'a@b.com')
637+
638+ self.assertEqual(other_account, person.account)
639+ self.assertEqual(
640+ 'other-openid-identifier',
641+ removeSecurityProxy(person.account).openid_identifier)
642+
643+
644 def test_suite():
645 return TestLoader().loadTestsFromName(__name__)
646
647=== added file 'lib/lp/registry/tests/test_xmlrpc.py'
648--- lib/lp/registry/tests/test_xmlrpc.py 1970-01-01 00:00:00 +0000
649+++ lib/lp/registry/tests/test_xmlrpc.py 2010-07-08 13:40:20 +0000
650@@ -0,0 +1,125 @@
651+# Copyright 2010 Canonical Ltd. This software is licensed under the
652+# GNU Affero General Public License version 3 (see the file LICENSE).
653+
654+"""Testing registry-related xmlrpc calls."""
655+
656+__metaclass__ = type
657+
658+import unittest
659+import xmlrpclib
660+from zope.component import getUtility
661+from zope.security.proxy import removeSecurityProxy
662+
663+from canonical.functional import XMLRPCTestTransport
664+from canonical.launchpad.interfaces import IPrivateApplication
665+from canonical.launchpad.interfaces.account import AccountStatus
666+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
667+from canonical.testing.layers import LaunchpadFunctionalLayer
668+from lp.registry.interfaces.person import (
669+ IPersonSet, ISoftwareCenterAgentAPI, ISoftwareCenterAgentApplication,
670+ PersonCreationRationale)
671+from lp.registry.xmlrpc.softwarecenteragent import SoftwareCenterAgentAPI
672+from lp.testing import TestCaseWithFactory
673+
674+
675+class TestSoftwareCenterAgentAPI(TestCaseWithFactory):
676+
677+ layer = LaunchpadFunctionalLayer
678+
679+ def setUp(self):
680+ super(TestSoftwareCenterAgentAPI, self).setUp()
681+ self.private_root = getUtility(IPrivateApplication)
682+ self.sca_api = SoftwareCenterAgentAPI(
683+ context=self.private_root.softwarecenteragent,
684+ request=LaunchpadTestRequest())
685+
686+ def test_provides_interface(self):
687+ # The view interface is provided.
688+ self.assertProvides(self.sca_api, ISoftwareCenterAgentAPI)
689+
690+ def test_getOrCreateSoftwareCenterCustomer(self):
691+ # The method returns the username of the person, and sets the
692+ # correct creation rational/comment.
693+ user_name = self.sca_api.getOrCreateSoftwareCenterCustomer(
694+ 'openid-ident', 'alice@b.com', 'Joe Blogs')
695+
696+ self.assertEqual('alice', user_name)
697+ person = getUtility(IPersonSet).getByName(user_name)
698+ self.assertEqual(
699+ 'openid-ident',
700+ removeSecurityProxy(person.account).openid_identifier)
701+ self.assertEqual(
702+ PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
703+ person.creation_rationale)
704+ self.assertEqual(
705+ "when purchasing an application via Software Center.",
706+ person.creation_comment)
707+
708+
709+class TestSoftwareCenterAgentApplication(TestCaseWithFactory):
710+
711+ layer = LaunchpadFunctionalLayer
712+
713+ def setUp(self):
714+ super(TestSoftwareCenterAgentApplication, self).setUp()
715+ self.private_root = getUtility(IPrivateApplication)
716+ self.rpc_proxy = xmlrpclib.ServerProxy(
717+ 'http://xmlrpc-private.launchpad.dev:8087/softwarecenteragent',
718+ transport=XMLRPCTestTransport())
719+
720+ def test_provides_interface(self):
721+ # The application is provided.
722+ self.assertProvides(
723+ self.private_root.softwarecenteragent,
724+ ISoftwareCenterAgentApplication)
725+
726+ def test_getOrCreateSoftwareCenterCustomer_xmlrpc(self):
727+ # The method can be called via xmlrpc
728+ user_name = self.rpc_proxy.getOrCreateSoftwareCenterCustomer(
729+ 'openid-ident', 'a@b.com', 'Joe Blogs')
730+ person = getUtility(IPersonSet).getByName(user_name)
731+ self.assertEqual(
732+ 'openid-ident',
733+ removeSecurityProxy(person.account).openid_identifier)
734+
735+ def test_getOrCreateSoftwareCenterCustomer_xmlrpc_error(self):
736+ # A suspended account results in an appropriate xmlrpc fault.
737+ suspended_account = self.factory.makeAccount(
738+ 'Joe Blogs', email='a@b.com', status=AccountStatus.SUSPENDED)
739+ openid_identifier = removeSecurityProxy(
740+ suspended_account).openid_identifier
741+
742+ # assertRaises doesn't let us check the type of Fault.
743+ fault_raised = False
744+ try:
745+ self.rpc_proxy.getOrCreateSoftwareCenterCustomer(
746+ openid_identifier, 'a@b.com', 'Joe Blogs')
747+ except xmlrpclib.Fault, e:
748+ fault_raised = True
749+ self.assertEqual(370, e.faultCode)
750+ self.assertIn(openid_identifier, e.faultString)
751+
752+ self.assertTrue(fault_raised)
753+
754+ def test_not_available_on_public_api(self):
755+ # The person set api is not available on the public xmlrpc
756+ # service.
757+ public_rpc_proxy = xmlrpclib.ServerProxy(
758+ 'http://test@canonical.com:test@'
759+ 'xmlrpc.launchpad.dev/softwarecenteragent',
760+ transport=XMLRPCTestTransport())
761+
762+ # assertRaises doesn't let us check the type of Fault.
763+ protocol_error_raised = False
764+ try:
765+ public_rpc_proxy.getOrCreateSoftwareCenterCustomer(
766+ 'openid-ident', 'a@b.com', 'Joe Blogs')
767+ except xmlrpclib.ProtocolError, e:
768+ protocol_error_raised = True
769+ self.assertEqual(404, e.errcode)
770+
771+ self.assertTrue(protocol_error_raised)
772+
773+
774+def test_suite():
775+ return unittest.TestLoader().loadTestsFromName(__name__)
776
777=== added directory 'lib/lp/registry/xmlrpc'
778=== added file 'lib/lp/registry/xmlrpc/__init__.py'
779=== added file 'lib/lp/registry/xmlrpc/softwarecenteragent.py'
780--- lib/lp/registry/xmlrpc/softwarecenteragent.py 1970-01-01 00:00:00 +0000
781+++ lib/lp/registry/xmlrpc/softwarecenteragent.py 2010-07-08 13:40:20 +0000
782@@ -0,0 +1,47 @@
783+# Copyright 2010 Canonical Ltd. This software is licensed under the
784+# GNU Affero General Public License version 3 (see the file LICENSE).
785+
786+"""XMLRPC APIs for person set."""
787+
788+__metaclass__ = type
789+__all__ = [
790+ 'SoftwareCenterAgentAPI',
791+ ]
792+
793+
794+from zope.component import getUtility
795+from zope.interface import implements
796+
797+from canonical.launchpad.interfaces.account import AccountSuspendedError
798+from canonical.launchpad.webapp import LaunchpadXMLRPCView
799+from canonical.launchpad.xmlrpc import faults
800+from lp.registry.interfaces.person import (
801+ IPersonSet, ISoftwareCenterAgentAPI, ISoftwareCenterAgentApplication,
802+ PersonCreationRationale)
803+
804+
805+class SoftwareCenterAgentAPI(LaunchpadXMLRPCView):
806+ """See `ISoftwareCenterAgentAPI`."""
807+
808+ implements(ISoftwareCenterAgentAPI)
809+
810+ def getOrCreateSoftwareCenterCustomer(self, openid_identifier, email,
811+ full_name):
812+ try:
813+ person, db_updated = getUtility(
814+ IPersonSet).getOrCreateByOpenIDIdentifier(
815+ openid_identifier, email, full_name,
816+ PersonCreationRationale.SOFTWARE_CENTER_PURCHASE,
817+ "when purchasing an application via Software Center.")
818+ except AccountSuspendedError:
819+ return faults.AccountSuspended(openid_identifier)
820+
821+ return person.name
822+
823+
824+class SoftwareCenterAgentApplication:
825+ """Software center agent end-point."""
826+ implements(ISoftwareCenterAgentApplication)
827+
828+ title = "Software Center Agent API"
829+
830
831=== modified file 'lib/lp/testopenid/browser/server.py'
832--- lib/lp/testopenid/browser/server.py 2010-05-15 17:43:59 +0000
833+++ lib/lp/testopenid/browser/server.py 2010-07-08 13:40:20 +0000
834@@ -198,7 +198,10 @@
835 else:
836 response = self.openid_request.answer(True)
837
838- sreg_fields = dict(nickname=IPerson(self.account).name)
839+ sreg_fields = dict(
840+ nickname=IPerson(self.account).name,
841+ email=self.account.preferredemail.email,
842+ fullname=self.account.displayname)
843 sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request)
844 sreg_response = SRegResponse.extractResponse(
845 sreg_request, sreg_fields)
846@@ -224,7 +227,7 @@
847 This class implements an OpenID endpoint using the python-openid
848 library. In addition to the normal modes of operation, it also
849 implements the OpenID 2.0 identifier select mode.
850-
851+
852 Note that the checkid_immediate mode is not supported.
853 """
854