Merge lp:~michael.nelson/launchpad/598464-get-or-create-in-devel into lp:launchpad
- 598464-get-or-create-in-devel
- Merge into devel
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 | ||||
Related bugs: |
|
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:/
https:/
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://
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).
Abel Deuring (adeuring) : | # |
Preview Diff
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 |