Merge lp:~jcsackett/launchpad/anonymous-api-access-emails-681815 into lp:launchpad

Proposed by j.c.sackett
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 12048
Proposed branch: lp:~jcsackett/launchpad/anonymous-api-access-emails-681815
Merge into: lp:launchpad
Diff against target: 1048 lines (+205/-101)
32 files modified
lib/canonical/launchpad/browser/tests/test_logintoken.py (+2/-1)
lib/canonical/launchpad/doc/emailaddress.txt (+1/-0)
lib/canonical/launchpad/doc/location-widget.txt (+3/-3)
lib/canonical/launchpad/doc/notification-recipient-set.txt (+1/-0)
lib/canonical/launchpad/doc/vocabulary-json.txt (+1/-1)
lib/canonical/launchpad/security.py (+2/-5)
lib/canonical/launchpad/webapp/tests/test_login.py (+3/-2)
lib/canonical/launchpad/webapp/tests/test_loginsource.py (+4/-3)
lib/lp/blueprints/stories/standalone/subscribing.txt (+2/-2)
lib/lp/code/browser/tests/test_branch.py (+1/-1)
lib/lp/code/mail/branch.py (+4/-2)
lib/lp/code/mail/tests/test_branch.py (+4/-1)
lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py (+1/-1)
lib/lp/code/model/recipebuilder.py (+6/-1)
lib/lp/code/model/tests/test_recipebuilder.py (+1/-0)
lib/lp/code/stories/feeds/xx-revision-atom.txt (+2/-1)
lib/lp/code/tests/test_directbranchcommit.py (+4/-1)
lib/lp/registry/browser/tests/person-views.txt (+4/-2)
lib/lp/registry/browser/tests/test_person_webservice.py (+41/-1)
lib/lp/registry/browser/tests/user-to-user-views.txt (+1/-1)
lib/lp/registry/doc/message-holds.txt (+1/-1)
lib/lp/registry/doc/person.txt (+3/-2)
lib/lp/registry/scripts/personnotification.py (+2/-1)
lib/lp/registry/stories/webservice/xx-person.txt (+81/-46)
lib/lp/registry/tests/test_personset.py (+3/-3)
lib/lp/registry/tests/test_product.py (+7/-6)
lib/lp/registry/tests/test_project.py (+2/-1)
lib/lp/services/mail/sendmail.py (+3/-1)
lib/lp/services/mailman/testing/__init__.py (+2/-1)
lib/lp/services/mailman/tests/test_lpmoderate.py (+4/-2)
lib/lp/testing/factory.py (+7/-6)
lib/lp/testing/tests/test_login.py (+2/-2)
To merge this branch: bzr merge lp:~jcsackett/launchpad/anonymous-api-access-emails-681815
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code Approve
Benji York (community) code* Approve
Review via email: mp+42309@code.launchpad.net

Commit message

[r=benji,edwin-grubbs][ui=none][bug=681815] Patches a hole in the API allowing anonymous gathering of emails.

Description of the change

Summary
=======
The webservice currently has a hole through which a person can anonymously collect email addresses. This is because the security check employed for it allows email addresses to be returned if they're attached to a user. To be consistent with the web interface, you shouldn't get email addresses unless you're logged in.

This branch changes the unauthenticated check so that it never allows emails to be returned.

Proposed fix
============
Change the ViewEmailAddress checkUnauthenticated to always return false; there is never a circumstance in which the API should anonymously return emails.

Preimplementation talk
======================
Spoke with Curtis Hovey.

Implementation details
======================
As in Proposed.

Tests
=====
bin/test -t registry.*webservice.*xx-person\.txt

Demo & QA
=========
There are several scripts attached to the bug that should pass if this whole is patched.

Lint
====
make lint output:

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/security.py
  lib/lp/registry/stories/webservice/xx-person.txt

./lib/canonical/launchpad/security.py
     733: E302 expected 2 blank lines, found 1
    1266: E302 expected 2 blank lines, found 1
    1476: E302 expected 2 blank lines, found 1
./lib/lp/registry/stories/webservice/xx-person.txt
      14: want exceeds 78 characters.
      16: want exceeds 78 characters.
      20: want exceeds 78 characters.
      38: want exceeds 78 characters.
      41: want exceeds 78 characters.
      44: want exceeds 78 characters.
      61: want exceeds 78 characters.
      63: want exceeds 78 characters.
      67: want exceeds 78 characters.
      69: want exceeds 78 characters.
      72: want exceeds 78 characters.
      86: want exceeds 78 characters.
      87: want exceeds 78 characters.
      90: want exceeds 78 characters.
      93: want exceeds 78 characters.
      95: want exceeds 78 characters.
     161: source exceeds 78 characters.
     162: source exceeds 78 characters.
     163: want has trailing whitespace.
     191: narrative exceeds 78 characters.
     266: source exceeds 78 characters.
     349: narrative has trailing whitespace.
     359: narrative has trailing whitespace.
     482: source exceeds 78 characters.
     506: source exceeds 78 characters.
     569: source exceeds 78 characters.
     822: source has bad indentation.

The blank line error in security.py is because of lint's issues with comments and blanklines.

The wants over 78 lines don't appear to be something that can easily be moved about; they are arguably more readable in their current form.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good. The reST-ification of the section headings is a nice touch.

As for the long "want" lines, the ones I looked at where of the form "foo: bar\n", so could be wrapped to "foo:\n bar\n" (i.e., line break at the colon and indent the next line) and still retain their readability. However, I agree that their current arrangement isn't too bad either.

review: Approve (code*)
Revision history for this message
j.c.sackett (jcsackett) wrote :

I've pushed up a change which makes this a better test, but it's still going to pass in either event; oddly, while in test mode, this security hole doesn't seem to exist.

I encourage any thoughts as to how to deal with this; otherwise, I think the tests show this patch hasn't broken anything, and the scripts attached to the bug can be used to QA it once it's live on qastaging.

Revision history for this message
j.c.sackett (jcsackett) wrote :

The big change is the new tests. I've attached a diff of just those.

=== modified file 'lib/lp/registry/browser/tests/test_person_webservice.py'
--- lib/lp/registry/browser/tests/test_person_webservice.py 2010-10-04 19:50:45 +0000
+++ lib/lp/registry/browser/tests/test_person_webservice.py 2010-12-02 17:53:46 +0000
@@ -5,12 +5,52 @@
55
6import unittest6import unittest
77
8from zope.security.management import endInteraction
8from zope.security.proxy import removeSecurityProxy9from zope.security.proxy import removeSecurityProxy
910
10from canonical.launchpad.ftests import login11from canonical.launchpad.ftests import login
11from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller12from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
12from canonical.testing.layers import DatabaseFunctionalLayer13from canonical.testing.layers import DatabaseFunctionalLayer
13from lp.testing import TestCaseWithFactory14from lp.testing import (
15 launchpadlib_for,
16 launchpadlib_for_anonymous,
17 TestCaseWithFactory,
18 )
19
20
21class TestPersonEmailSecurity(TestCaseWithFactory):
22
23 layer = DatabaseFunctionalLayer
24
25 def setUp(self):
26 super(TestPersonEmailSecurity, self).setUp()
27 self.target = self.factory.makePerson(name='target')
28 self.email_one = self.factory.makeEmail(
29 'test1@example.com', self.target)
30 self.email_two = self.factory.makeEmail(
31 'test2@example.com', self.target)
32
33 def test_logged_in_can_access(self):
34 # A logged in launchpadlib connection can see confirmed email
35 # addresses.
36 accessor = self.factory.makePerson()
37 lp = launchpadlib_for("test", accessor.name)
38 person = lp.people['target']
39 emails = sorted(list(person.confirmed_email_addresses))
40 self.assertNotEqual(
41 sorted([self.email_one, self.email_two]),
42 len(emails))
43
44 def test_anonymous_cannot_access(self):
45 # An anonymous launchpadlib connection cannot see email addresses.
46
47 # Need to endInteraction() because launchpadlib_for_anonymous() will
48 # setup a new one.
49 endInteraction()
50 lp = launchpadlib_for_anonymous('test', version='devel')
51 person = lp.people['target']
52 emails = list(person.confirmed_email_addresses)
53 self.assertEqual([], emails)
1454
1555
16class TestPersonRepresentation(TestCaseWithFactory):56class TestPersonRepresentation(TestCaseWithFactory):
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi JC,

Thanks for adding tests that actually fail without your changes to checkUnauthenticated(). I like benji's suggestion to wrap lines in the doctests to make lint complain less.

-Edwin

review: Approve (code)
Revision history for this message
j.c.sackett (jcsackett) wrote :

Edwin, Benji--

I've made the changes to make lint happy.

Thanks for the review, guys!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/browser/tests/test_logintoken.py'
--- lib/canonical/launchpad/browser/tests/test_logintoken.py 2010-10-04 19:50:45 +0000
+++ lib/canonical/launchpad/browser/tests/test_logintoken.py 2010-12-13 13:34:01 +0000
@@ -4,6 +4,7 @@
4import unittest4import unittest
55
6from zope.component import getUtility6from zope.component import getUtility
7from zope.security.proxy import removeSecurityProxy
78
8from canonical.launchpad.browser.logintoken import (9from canonical.launchpad.browser.logintoken import (
9 ClaimTeamView,10 ClaimTeamView,
@@ -29,7 +30,7 @@
29 def setUp(self):30 def setUp(self):
30 TestCaseWithFactory.setUp(self)31 TestCaseWithFactory.setUp(self)
31 self.person = self.factory.makePerson(name='test-user')32 self.person = self.factory.makePerson(name='test-user')
32 self.email = self.person.preferredemail.email33 self.email = removeSecurityProxy(self.person).preferredemail.email
33 self.expected_next_url = 'http://127.0.0.1/~test-user'34 self.expected_next_url = 'http://127.0.0.1/~test-user'
3435
35 def test_ClaimTeamView(self):36 def test_ClaimTeamView(self):
3637
=== modified file 'lib/canonical/launchpad/doc/emailaddress.txt'
--- lib/canonical/launchpad/doc/emailaddress.txt 2010-11-15 21:19:45 +0000
+++ lib/canonical/launchpad/doc/emailaddress.txt 2010-12-13 13:34:01 +0000
@@ -12,6 +12,7 @@
1212
13We can get an email address through IEmailAddressSet.getByEmail().13We can get an email address through IEmailAddressSet.getByEmail().
1414
15 >>> login_person(person)
15 >>> email = emailset.getByEmail(person.preferredemail.email)16 >>> email = emailset.getByEmail(person.preferredemail.email)
16 >>> email == person.preferredemail17 >>> email == person.preferredemail
17 True18 True
1819
=== modified file 'lib/canonical/launchpad/doc/location-widget.txt'
--- lib/canonical/launchpad/doc/location-widget.txt 2010-11-16 18:55:26 +0000
+++ lib/canonical/launchpad/doc/location-widget.txt 2010-12-13 13:34:01 +0000
@@ -28,7 +28,7 @@
28map around the location where he seems to be, based on the IP address of28map around the location where he seems to be, based on the IP address of
29the request.29the request.
3030
31 >>> login(salgado.preferredemail.email)31 >>> login_person(salgado)
32 >>> widget = LocationWidget(bound_field, request)32 >>> widget = LocationWidget(bound_field, request)
33 >>> widget.zoom33 >>> widget.zoom
34 734 7
@@ -65,7 +65,7 @@
6565
66 >>> kamion = getUtility(IPersonSet).getByName('kamion')66 >>> kamion = getUtility(IPersonSet).getByName('kamion')
67 >>> bound_field = field.bind(kamion)67 >>> bound_field = field.bind(kamion)
68 >>> login(kamion.preferredemail.email)68 >>> login_person(kamion)
69 >>> widget = LocationWidget(bound_field, request)69 >>> widget = LocationWidget(bound_field, request)
70 >>> widget.zoom70 >>> widget.zoom
71 971 9
@@ -148,7 +148,7 @@
148 >>> bound_field = field.bind(kamion)148 >>> bound_field = field.bind(kamion)
149 >>> request = LaunchpadTestRequest(149 >>> request = LaunchpadTestRequest(
150 ... environ={'REMOTE_ADDR': '201.13.165.145'})150 ... environ={'REMOTE_ADDR': '201.13.165.145'})
151 >>> login(kamion.preferredemail.email)151 >>> login_person(kamion)
152 >>> widget = LocationWidget(bound_field, request)152 >>> widget = LocationWidget(bound_field, request)
153 >>> print widget()153 >>> print widget()
154 <input...name="field.location.latitude"...type="hidden"...154 <input...name="field.location.latitude"...type="hidden"...
155155
=== modified file 'lib/canonical/launchpad/doc/notification-recipient-set.txt'
--- lib/canonical/launchpad/doc/notification-recipient-set.txt 2010-10-18 22:24:59 +0000
+++ lib/canonical/launchpad/doc/notification-recipient-set.txt 2010-12-13 13:34:01 +0000
@@ -123,6 +123,7 @@
123other person:123other person:
124124
125 >>> ubuntu_team = person_set.getByName('ubuntu-team')125 >>> ubuntu_team = person_set.getByName('ubuntu-team')
126 >>> login_person(ubuntu_team.teamowner)
126 >>> print ubuntu_team.preferredemail.email127 >>> print ubuntu_team.preferredemail.email
127 support@ubuntu.com128 support@ubuntu.com
128 >>> recipients.add(ubuntu_team, 'You are notified for fun.', 'Fun')129 >>> recipients.add(ubuntu_team, 'You are notified for fun.', 'Fun')
129130
=== modified file 'lib/canonical/launchpad/doc/vocabulary-json.txt'
--- lib/canonical/launchpad/doc/vocabulary-json.txt 2010-10-03 15:30:06 +0000
+++ lib/canonical/launchpad/doc/vocabulary-json.txt 2010-12-13 13:34:01 +0000
@@ -96,7 +96,7 @@
96 {96 {
97 "api_uri": "/~name16",97 "api_uri": "/~name16",
98 "css": "sprite person",98 "css": "sprite person",
99 "description": "foo.bar@canonical.com",99 "description": "<email address hidden>",
100 "title": "Foo Bar",100 "title": "Foo Bar",
101 "value": "name16"101 "value": "name16"
102 }102 }
103103
=== modified file 'lib/canonical/launchpad/security.py'
--- lib/canonical/launchpad/security.py 2010-12-07 14:15:37 +0000
+++ lib/canonical/launchpad/security.py 2010-12-13 13:34:01 +0000
@@ -2422,11 +2422,8 @@
24222422
2423 def checkUnauthenticated(self):2423 def checkUnauthenticated(self):
2424 """See `AuthorizationBase`."""2424 """See `AuthorizationBase`."""
2425 # Email addresses without an associated Person cannot be seen by2425 # Anonymous users can never see email addresses.
2426 # anonymous users.2426 return False
2427 if self.obj.person is None:
2428 return False
2429 return not self.obj.person.hide_email_addresses
24302427
2431 def checkAccountAuthenticated(self, account):2428 def checkAccountAuthenticated(self, account):
2432 """Can the user see the details of this email address?2429 """Can the user see the details of this email address?
24332430
=== modified file 'lib/canonical/launchpad/webapp/tests/test_login.py'
--- lib/canonical/launchpad/webapp/tests/test_login.py 2010-10-27 14:20:21 +0000
+++ lib/canonical/launchpad/webapp/tests/test_login.py 2010-12-13 13:34:01 +0000
@@ -188,10 +188,11 @@
188 def test_full_fledged_account(self):188 def test_full_fledged_account(self):
189 # In the common case we just login and redirect to the URL specified189 # In the common case we just login and redirect to the URL specified
190 # in the 'starting_url' query arg.190 # in the 'starting_url' query arg.
191 person = self.factory.makePerson()191 test_email = 'test-example@example.com'
192 person = self.factory.makePerson(email=test_email)
192 with SRegResponse_fromSuccessResponse_stubbed():193 with SRegResponse_fromSuccessResponse_stubbed():
193 view, html = self._createViewWithResponse(194 view, html = self._createViewWithResponse(
194 person.account, email=person.preferredemail.email)195 person.account, email=test_email)
195 self.assertTrue(view.login_called)196 self.assertTrue(view.login_called)
196 response = view.request.response197 response = view.request.response
197 self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())198 self.assertEquals(httplib.TEMPORARY_REDIRECT, response.getStatus())
198199
=== modified file 'lib/canonical/launchpad/webapp/tests/test_loginsource.py'
--- lib/canonical/launchpad/webapp/tests/test_loginsource.py 2010-10-20 20:51:26 +0000
+++ lib/canonical/launchpad/webapp/tests/test_loginsource.py 2010-12-13 13:34:01 +0000
@@ -4,6 +4,7 @@
4import unittest4import unittest
55
6from zope.component import getUtility6from zope.component import getUtility
7from zope.security.proxy import removeSecurityProxy
78
8from canonical.launchpad.ftests import (9from canonical.launchpad.ftests import (
9 ANONYMOUS,10 ANONYMOUS,
@@ -29,8 +30,8 @@
29 """30 """
30 principal = self.login_source.getPrincipal(self.mark.account.id)31 principal = self.login_source.getPrincipal(self.mark.account.id)
31 self.assertEqual(principal.access_level, AccessLevel.WRITE_PRIVATE)32 self.assertEqual(principal.access_level, AccessLevel.WRITE_PRIVATE)
32 principal = self.login_source.getPrincipalByLogin(33 marks_email = removeSecurityProxy(self.mark).preferredemail.email
33 self.mark.preferredemail.email)34 principal = self.login_source.getPrincipalByLogin(marks_email)
34 self.assertEqual(principal.access_level, AccessLevel.WRITE_PRIVATE)35 self.assertEqual(principal.access_level, AccessLevel.WRITE_PRIVATE)
3536
36 def test_given_access_level_is_used(self):37 def test_given_access_level_is_used(self):
@@ -41,7 +42,7 @@
41 self.mark.account.id, access_level=AccessLevel.WRITE_PUBLIC)42 self.mark.account.id, access_level=AccessLevel.WRITE_PUBLIC)
42 self.assertEqual(principal.access_level, AccessLevel.WRITE_PUBLIC)43 self.assertEqual(principal.access_level, AccessLevel.WRITE_PUBLIC)
43 principal = self.login_source.getPrincipalByLogin(44 principal = self.login_source.getPrincipalByLogin(
44 self.mark.preferredemail.email, AccessLevel.READ_PUBLIC)45 removeSecurityProxy(self.mark).preferredemail.email, AccessLevel.READ_PUBLIC)
45 self.assertEqual(principal.access_level, AccessLevel.READ_PUBLIC)46 self.assertEqual(principal.access_level, AccessLevel.READ_PUBLIC)
4647
4748
4849
=== modified file 'lib/lp/blueprints/stories/standalone/subscribing.txt'
--- lib/lp/blueprints/stories/standalone/subscribing.txt 2010-07-30 12:56:27 +0000
+++ lib/lp/blueprints/stories/standalone/subscribing.txt 2010-12-13 13:34:01 +0000
@@ -235,7 +235,7 @@
235We subscribe the Ubuntu Team and an email is sent to the team's235We subscribe the Ubuntu Team and an email is sent to the team's
236preferred email address.236preferred email address.
237237
238 >>> login(ANONYMOUS)238 >>> login('no-priv@canonical.com')
239 >>> ([message['To'] for message in pop_notifications()] ==239 >>> ([message['To'] for message in pop_notifications()] ==
240 ... [str(ubuntu_team.preferredemail.email)])240 ... [str(ubuntu_team.preferredemail.email)])
241 True241 True
@@ -250,7 +250,7 @@
250We modified the Ubuntu Team's subscription and again, an email is sent250We modified the Ubuntu Team's subscription and again, an email is sent
251to the team's preferred email address.251to the team's preferred email address.
252252
253 >>> login(ANONYMOUS)253 >>> login('no-priv@canonical.com')
254 >>> ([message['To'] for message in pop_notifications()] ==254 >>> ([message['To'] for message in pop_notifications()] ==
255 ... [str(ubuntu_team.preferredemail.email)])255 ... [str(ubuntu_team.preferredemail.email)])
256 True256 True
257257
=== modified file 'lib/lp/code/browser/tests/test_branch.py'
--- lib/lp/code/browser/tests/test_branch.py 2010-11-09 18:42:38 +0000
+++ lib/lp/code/browser/tests/test_branch.py 2010-12-13 13:34:01 +0000
@@ -172,7 +172,7 @@
172 """Registering a mirrored branch requests a mirror."""172 """Registering a mirrored branch requests a mirror."""
173 arbitrary_person = self.factory.makePerson()173 arbitrary_person = self.factory.makePerson()
174 arbitrary_product = self.factory.makeProduct()174 arbitrary_product = self.factory.makeProduct()
175 login(arbitrary_person.preferredemail.email)175 login_person(arbitrary_person)
176 try:176 try:
177 add_view = BranchAddView(arbitrary_person, self.request)177 add_view = BranchAddView(arbitrary_person, self.request)
178 add_view.initialize()178 add_view.initialize()
179179
=== modified file 'lib/lp/code/mail/branch.py'
--- lib/lp/code/mail/branch.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/mail/branch.py 2010-12-13 13:34:01 +0000
@@ -5,6 +5,7 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8from zope.security.proxy import removeSecurityProxy
89
9from canonical.launchpad.mail import format_address10from canonical.launchpad.mail import format_address
10from canonical.launchpad.webapp import canonical_url11from canonical.launchpad.webapp import canonical_url
@@ -192,7 +193,7 @@
192 RecipientReason.forBranchSubscriber(193 RecipientReason.forBranchSubscriber(
193 subscription, recipient, rationale)194 subscription, recipient, rationale)
194 from_address = format_address(195 from_address = format_address(
195 user.displayname, user.preferredemail.email)196 user.displayname, removeSecurityProxy(user).preferredemail.email)
196 return cls(197 return cls(
197 '[Branch %(unique_name)s]', 'branch-modified.txt',198 '[Branch %(unique_name)s]', 'branch-modified.txt',
198 actual_recipients, from_address, delta=delta,199 actual_recipients, from_address, delta=delta,
@@ -304,4 +305,5 @@
304305
305 @staticmethod306 @staticmethod
306 def _format_user_address(user):307 def _format_user_address(user):
307 return format_address(user.displayname, user.preferredemail.email)308 naked_email = removeSecurityProxy(user).preferredemail.email
309 return format_address(user.displayname, naked_email)
308310
=== modified file 'lib/lp/code/mail/tests/test_branch.py'
--- lib/lp/code/mail/tests/test_branch.py 2010-10-04 19:50:45 +0000
+++ lib/lp/code/mail/tests/test_branch.py 2010-12-13 13:34:01 +0000
@@ -5,6 +5,8 @@
55
6from unittest import TestLoader6from unittest import TestLoader
77
8from zope.security.proxy import removeSecurityProxy
9
8from canonical.testing.layers import DatabaseFunctionalLayer10from canonical.testing.layers import DatabaseFunctionalLayer
9from lp.code.enums import (11from lp.code.enums import (
10 BranchSubscriptionDiffSize,12 BranchSubscriptionDiffSize,
@@ -259,8 +261,9 @@
259 mailer = BranchMailer.forRevision(261 mailer = BranchMailer.forRevision(
260 branch, 1, 'test@example.com', 'content', 'diff',262 branch, 1, 'test@example.com', 'content', 'diff',
261 'Testing %j foo')263 'Testing %j foo')
264 branch_owner_email = removeSecurityProxy(branch.owner).preferredemail.email
262 self.assertEqual('Testing %j foo', mailer._getSubject(265 self.assertEqual('Testing %j foo', mailer._getSubject(
263 branch.owner.preferredemail.email))266 branch_owner_email))
264267
265268
266def test_suite():269def test_suite():
267270
=== modified file 'lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py'
--- lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py 2010-10-04 19:50:45 +0000
+++ lib/lp/code/mail/tests/test_sourcepackagerecipebuild.py 2010-12-13 13:34:01 +0000
@@ -48,7 +48,7 @@
4848
49 def makeStatusEmail(self, build):49 def makeStatusEmail(self, build):
50 mailer = SourcePackageRecipeBuildMailer.forStatus(build)50 mailer = SourcePackageRecipeBuildMailer.forStatus(build)
51 email = build.requester.preferredemail.email51 email = removeSecurityProxy(build.requester).preferredemail.email
52 return mailer.generateEmail(email, build.requester)52 return mailer.generateEmail(email, build.requester)
5353
54 def test_generateEmail(self):54 def test_generateEmail(self):
5555
=== modified file 'lib/lp/code/model/recipebuilder.py'
--- lib/lp/code/model/recipebuilder.py 2010-11-17 14:00:22 +0000
+++ lib/lp/code/model/recipebuilder.py 2010-12-13 13:34:01 +0000
@@ -12,6 +12,7 @@
1212
13from zope.component import adapts13from zope.component import adapts
14from zope.interface import implements14from zope.interface import implements
15from zope.security.proxy import removeSecurityProxy
1516
16from canonical.config import config17from canonical.config import config
17from lp.buildmaster.interfaces.builder import CannotBuild18from lp.buildmaster.interfaces.builder import CannotBuild
@@ -72,7 +73,11 @@
72 args["author_email"] = config.canonical.noreply_from_address73 args["author_email"] = config.canonical.noreply_from_address
73 else:74 else:
74 args["author_name"] = requester.displayname75 args["author_name"] = requester.displayname
75 args["author_email"] = requester.preferredemail.email76 # We have to remove the security proxy here b/c there's not a
77 # logged in entity, and anonymous email lookups aren't allowed.
78 # Don't keep the naked requester around though.
79 args["author_email"] = removeSecurityProxy(
80 requester).preferredemail.email
76 args["recipe_text"] = str(self.build.recipe.builder_recipe)81 args["recipe_text"] = str(self.build.recipe.builder_recipe)
77 args['archive_purpose'] = self.build.archive.purpose.name82 args['archive_purpose'] = self.build.archive.purpose.name
78 args["ogrecomponent"] = get_primary_current_component(83 args["ogrecomponent"] = get_primary_current_component(
7984
=== modified file 'lib/lp/code/model/tests/test_recipebuilder.py'
--- lib/lp/code/model/tests/test_recipebuilder.py 2010-11-27 07:17:34 +0000
+++ lib/lp/code/model/tests/test_recipebuilder.py 2010-12-13 13:34:01 +0000
@@ -298,6 +298,7 @@
298 # dispatchBuildToSlave will fail when there is not chroot tarball298 # dispatchBuildToSlave will fail when there is not chroot tarball
299 # available for the distroseries to build for.299 # available for the distroseries to build for.
300 job = self.makeJob()300 job = self.makeJob()
301 test_publisher = SoyuzTestPublisher()
301 builder = MockBuilder("bob-de-bouwer", OkSlave())302 builder = MockBuilder("bob-de-bouwer", OkSlave())
302 processorfamily = ProcessorFamilySet().getByProcessorName('386')303 processorfamily = ProcessorFamilySet().getByProcessorName('386')
303 builder.processor = processorfamily.processors[0]304 builder.processor = processorfamily.processors[0]
304305
=== modified file 'lib/lp/code/stories/feeds/xx-revision-atom.txt'
--- lib/lp/code/stories/feeds/xx-revision-atom.txt 2010-10-18 22:24:59 +0000
+++ lib/lp/code/stories/feeds/xx-revision-atom.txt 2010-12-13 13:34:01 +0000
@@ -32,10 +32,11 @@
32 >>> fooey_branch = factory.makeProductBranch(32 >>> fooey_branch = factory.makeProductBranch(
33 ... name='feature-x', product=fooey, owner=mike)33 ... name='feature-x', product=fooey, owner=mike)
3434
35 >>> from zope.security.proxy import removeSecurityProxy
35 >>> def makeRevision(author, rev_id, log_body):36 >>> def makeRevision(author, rev_id, log_body):
36 ... global factory, date_generator37 ... global factory, date_generator
37 ... return factory.makeRevision(38 ... return factory.makeRevision(
38 ... author=author.preferredemail.email,39 ... author=removeSecurityProxy(author).preferredemail.email,
39 ... revision_date=date_generator.next(),40 ... revision_date=date_generator.next(),
40 ... rev_id=rev_id, log_body=log_body)41 ... rev_id=rev_id, log_body=log_body)
41 >>> ignored = fooey_branch.createBranchRevision(42 >>> ignored = fooey_branch.createBranchRevision(
4243
=== modified file 'lib/lp/code/tests/test_directbranchcommit.py'
--- lib/lp/code/tests/test_directbranchcommit.py 2010-10-02 11:41:43 +0000
+++ lib/lp/code/tests/test_directbranchcommit.py 2010-12-13 13:34:01 +0000
@@ -5,6 +5,8 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8from zope.security.proxy import removeSecurityProxy
9
8from canonical.testing.layers import (10from canonical.testing.layers import (
9 DatabaseFunctionalLayer,11 DatabaseFunctionalLayer,
10 ZopelessDatabaseLayer,12 ZopelessDatabaseLayer,
@@ -309,7 +311,8 @@
309 committer = DirectBranchCommit(branch)311 committer = DirectBranchCommit(branch)
310 self.addCleanup(committer.unlock)312 self.addCleanup(committer.unlock)
311 self.assertIn(313 self.assertIn(
312 branch.owner.preferredemail.email, committer.getBzrCommitterID())314 removeSecurityProxy(branch.owner).preferredemail.email,
315 committer.getBzrCommitterID())
313316
314 def test_falls_back_to_noreply(self):317 def test_falls_back_to_noreply(self):
315 # If all else fails, getBzrCommitterID uses the noreply318 # If all else fails, getBzrCommitterID uses the noreply
316319
=== modified file 'lib/lp/registry/browser/tests/person-views.txt'
--- lib/lp/registry/browser/tests/person-views.txt 2010-11-01 03:46:17 +0000
+++ lib/lp/registry/browser/tests/person-views.txt 2010-12-13 13:34:01 +0000
@@ -106,14 +106,16 @@
106can see them106can see them
107107
108Mark has a registered email address, and he has chosen to disclose it to108Mark has a registered email address, and he has chosen to disclose it to
109the world.109anyone in Launchpad..
110110
111 >>> login('test@canonical.com')
111 >>> mark = person_set.getByEmail('mark@example.com')112 >>> mark = person_set.getByEmail('mark@example.com')
112 >>> mark.preferredemail.email113 >>> mark.preferredemail.email
113 u'mark@example.com'114 u'mark@example.com'
114115
115 >>> mark.hide_email_addresses116 >>> mark.hide_email_addresses
116 False117 False
118 >>> logout()
117119
118Anonymous users cannot see any Launchpad user's email addresses. The120Anonymous users cannot see any Launchpad user's email addresses. The
119email addresses state is LOGIN_REQUIRED, there is no description, nor121email addresses state is LOGIN_REQUIRED, there is no description, nor
@@ -332,7 +334,7 @@
332334
333 >>> from lp.blueprints.enums import SpecificationImplementationStatus335 >>> from lp.blueprints.enums import SpecificationImplementationStatus
334336
335 >>> login(user.preferredemail.email)337 >>> login_person(user)
336 >>> product = factory.makeProduct(name="tool", owner=user)338 >>> product = factory.makeProduct(name="tool", owner=user)
337 >>> spec = factory.makeSpecification(339 >>> spec = factory.makeSpecification(
338 ... product=product, title='Specs need stories')340 ... product=product, title='Specs need stories')
339341
=== modified file 'lib/lp/registry/browser/tests/test_person_webservice.py'
--- lib/lp/registry/browser/tests/test_person_webservice.py 2010-10-04 19:50:45 +0000
+++ lib/lp/registry/browser/tests/test_person_webservice.py 2010-12-13 13:34:01 +0000
@@ -5,12 +5,52 @@
55
6import unittest6import unittest
77
8from zope.security.management import endInteraction
8from zope.security.proxy import removeSecurityProxy9from zope.security.proxy import removeSecurityProxy
910
10from canonical.launchpad.ftests import login11from canonical.launchpad.ftests import login
11from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller12from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
12from canonical.testing.layers import DatabaseFunctionalLayer13from canonical.testing.layers import DatabaseFunctionalLayer
13from lp.testing import TestCaseWithFactory14from lp.testing import (
15 launchpadlib_for,
16 launchpadlib_for_anonymous,
17 TestCaseWithFactory,
18 )
19
20
21class TestPersonEmailSecurity(TestCaseWithFactory):
22
23 layer = DatabaseFunctionalLayer
24
25 def setUp(self):
26 super(TestPersonEmailSecurity, self).setUp()
27 self.target = self.factory.makePerson(name='target')
28 self.email_one = self.factory.makeEmail(
29 'test1@example.com', self.target)
30 self.email_two = self.factory.makeEmail(
31 'test2@example.com', self.target)
32
33 def test_logged_in_can_access(self):
34 # A logged in launchpadlib connection can see confirmed email
35 # addresses.
36 accessor = self.factory.makePerson()
37 lp = launchpadlib_for("test", accessor.name)
38 person = lp.people['target']
39 emails = sorted(list(person.confirmed_email_addresses))
40 self.assertNotEqual(
41 sorted([self.email_one, self.email_two]),
42 len(emails))
43
44 def test_anonymous_cannot_access(self):
45 # An anonymous launchpadlib connection cannot see email addresses.
46
47 # Need to endInteraction() because launchpadlib_for_anonymous() will
48 # setup a new one.
49 endInteraction()
50 lp = launchpadlib_for_anonymous('test', version='devel')
51 person = lp.people['target']
52 emails = list(person.confirmed_email_addresses)
53 self.assertEqual([], emails)
1454
1555
16class TestPersonRepresentation(TestCaseWithFactory):56class TestPersonRepresentation(TestCaseWithFactory):
1757
=== modified file 'lib/lp/registry/browser/tests/user-to-user-views.txt'
--- lib/lp/registry/browser/tests/user-to-user-views.txt 2010-08-17 15:57:37 +0000
+++ lib/lp/registry/browser/tests/user-to-user-views.txt 2010-12-13 13:34:01 +0000
@@ -24,7 +24,7 @@
24...No Priv would start by going to Salgado's +contactuser page.24...No Priv would start by going to Salgado's +contactuser page.
2525
26 >>> from canonical.launchpad.ftests import login26 >>> from canonical.launchpad.ftests import login
27 >>> login(no_priv.preferredemail.email)27 >>> login_person(no_priv)
28 >>> view = create_view(no_priv, salgado)28 >>> view = create_view(no_priv, salgado)
2929
30This contact is allowed.30This contact is allowed.
3131
=== modified file 'lib/lp/registry/doc/message-holds.txt'
--- lib/lp/registry/doc/message-holds.txt 2010-10-26 02:18:08 +0000
+++ lib/lp/registry/doc/message-holds.txt 2010-12-13 13:34:01 +0000
@@ -378,10 +378,10 @@
378378
379379
380== Posting privileges ==380== Posting privileges ==
381
382Message approvals are also used to derived posting privileges for non-team381Message approvals are also used to derived posting privileges for non-team
383members. To start with, only the team owner can post to the list.382members. To start with, only the team owner can post to the list.
384383
384 >>> login('no-priv@canonical.com')
385 >>> team_three, list_three = mailinglists_helper.new_team(385 >>> team_three, list_three = mailinglists_helper.new_team(
386 ... 'test-three', True)386 ... 'test-three', True)
387 >>> sorted(email.email for email in list_three.getSenderAddresses())387 >>> sorted(email.email for email in list_three.getSenderAddresses())
388388
=== modified file 'lib/lp/registry/doc/person.txt'
--- lib/lp/registry/doc/person.txt 2010-11-02 05:48:54 +0000
+++ lib/lp/registry/doc/person.txt 2010-12-13 13:34:01 +0000
@@ -639,6 +639,7 @@
639If a team has a contact email address, all notifications we send to the639If a team has a contact email address, all notifications we send to the
640team will go to that address.640team will go to that address.
641641
642 >>> login('no-priv@canonical.com')
642 >>> ubuntu_team = personset.getByName('ubuntu-team')643 >>> ubuntu_team = personset.getByName('ubuntu-team')
643 >>> ubuntu_team.preferredemail.email644 >>> ubuntu_team.preferredemail.email
644 u'support@ubuntu.com'645 u'support@ubuntu.com'
@@ -750,11 +751,11 @@
750 # First we'll define a utility function to help us displaying751 # First we'll define a utility function to help us displaying
751 # the results.752 # the results.
752753
753 >>> emailset = getUtility(IEmailAddressSet)754 >>> naked_emailset = removeSecurityProxy(getUtility(IEmailAddressSet))
754 >>> def print_people(results):755 >>> def print_people(results):
755 ... for person in results:756 ... for person in results:
756 ... emails = [email.email757 ... emails = [email.email
757 ... for email in emailset.getByPerson(person)]758 ... for email in naked_emailset.getByPerson(person)]
758 ... print "%s (%s): %s" % (759 ... print "%s (%s): %s" % (
759 ... person.displayname, person.name, emails)760 ... person.displayname, person.name, emails)
760761
761762
=== modified file 'lib/lp/registry/scripts/personnotification.py'
--- lib/lp/registry/scripts/personnotification.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/scripts/personnotification.py 2010-12-13 13:34:01 +0000
@@ -15,6 +15,7 @@
1515
16import pytz16import pytz
17from zope.component import getUtility17from zope.component import getUtility
18from zope.security.proxy import removeSecurityProxy
1819
19from canonical.config import config20from canonical.config import config
20from lp.registry.interfaces.personnotification import IPersonNotificationSet21from lp.registry.interfaces.personnotification import IPersonNotificationSet
@@ -44,7 +45,7 @@
44 continue45 continue
45 self.logger.info(46 self.logger.info(
46 "Sending notification to %s <%s>."47 "Sending notification to %s <%s>."
47 % (person.name, person.preferredemail.email))48 % (person.name, removeSecurityProxy(person).preferredemail.email))
48 notification.send()49 notification.send()
49 notifications_sent = True50 notifications_sent = True
50 # Commit after each email sent, so that we won't re-mail the51 # Commit after each email sent, so that we won't re-mail the
5152
=== modified file 'lib/lp/registry/stories/webservice/xx-person.txt'
--- lib/lp/registry/stories/webservice/xx-person.txt 2010-11-22 15:30:05 +0000
+++ lib/lp/registry/stories/webservice/xx-person.txt 2010-12-13 13:34:01 +0000
@@ -1,4 +1,5 @@
1= People and teams =1People and teams
2================
23
3Since we use a single class (Person) to represent a person or a team,4Since we use a single class (Person) to represent a person or a team,
4representations of people and teams are supposed to have nearly the5representations of people and teams are supposed to have nearly the
@@ -10,13 +11,16 @@
10 >>> pprint_entry(salgado)11 >>> pprint_entry(salgado)
11 admins_collection_link: u'http://.../~salgado/admins'12 admins_collection_link: u'http://.../~salgado/admins'
12 archive_link: None13 archive_link: None
13 confirmed_email_addresses_collection_link: u'http://.../~salgado/confirmed_email_addresses'14 confirmed_email_addresses_collection_link:
15 u'http://.../~salgado/confirmed_email_addresses'
14 date_created: u'2005-06-06T08:59:51.596025+00:00'16 date_created: u'2005-06-06T08:59:51.596025+00:00'
15 deactivated_members_collection_link: u'http://.../~salgado/deactivated_members'17 deactivated_members_collection_link:
18 u'http://.../~salgado/deactivated_members'
16 display_name: u'Guilherme Salgado'19 display_name: u'Guilherme Salgado'
17 expired_members_collection_link: u'http://.../~salgado/expired_members'20 expired_members_collection_link: u'http://.../~salgado/expired_members'
18 gpg_keys_collection_link: u'http://.../~salgado/gpg_keys'21 gpg_keys_collection_link: u'http://.../~salgado/gpg_keys'
19 hardware_submissions_collection_link: u'http://.../~salgado/hardware_submissions'22 hardware_submissions_collection_link:
23 u'http://.../~salgado/hardware_submissions'
20 hide_email_addresses: False24 hide_email_addresses: False
21 homepage_content: None25 homepage_content: None
22 invited_members_collection_link: u'http://.../~salgado/invited_members'26 invited_members_collection_link: u'http://.../~salgado/invited_members'
@@ -34,13 +38,16 @@
34 mailing_list_auto_subscribe_policy: u'Ask me when I join a team'38 mailing_list_auto_subscribe_policy: u'Ask me when I join a team'
35 members_collection_link: u'http://.../~salgado/members'39 members_collection_link: u'http://.../~salgado/members'
36 members_details_collection_link: u'http://.../~salgado/members_details'40 members_details_collection_link: u'http://.../~salgado/members_details'
37 memberships_details_collection_link: u'http://.../~salgado/memberships_details'41 memberships_details_collection_link:
42 u'http://.../~salgado/memberships_details'
38 mugshot_link: u'http://.../~salgado/mugshot'43 mugshot_link: u'http://.../~salgado/mugshot'
39 name: u'salgado'44 name: u'salgado'
40 open_membership_invitations_collection_link: u'http://.../~salgado/open_membership_invitations'45 open_membership_invitations_collection_link:
46 u'http://.../~salgado/open_membership_invitations'
41 participants_collection_link: u'http://.../~salgado/participants'47 participants_collection_link: u'http://.../~salgado/participants'
42 ppas_collection_link: u'http://.../~salgado/ppas'48 ppas_collection_link: u'http://.../~salgado/ppas'
43 preferred_email_address_link: u'http://.../~salgado/+email/guilherme.salgado@canonical.com'49 preferred_email_address_link:
50 u'http://.../~salgado/+email/guilherme.salgado@canonical.com'
44 private: False51 private: False
45 proposed_members_collection_link: u'http://.../~salgado/proposed_members'52 proposed_members_collection_link: u'http://.../~salgado/proposed_members'
46 resource_type_link: u'http://.../#person'53 resource_type_link: u'http://.../#person'
@@ -57,18 +64,23 @@
57 >>> pprint_entry(ubuntu_team)64 >>> pprint_entry(ubuntu_team)
58 admins_collection_link: u'http://.../~ubuntu-team/admins'65 admins_collection_link: u'http://.../~ubuntu-team/admins'
59 archive_link: None66 archive_link: None
60 confirmed_email_addresses_collection_link: u'http://.../~ubuntu-team/confirmed_email_addresses'67 confirmed_email_addresses_collection_link:
68 u'http://.../~ubuntu-team/confirmed_email_addresses'
61 date_created: u'2005-06-06T08:59:51.605760+00:00'69 date_created: u'2005-06-06T08:59:51.605760+00:00'
62 deactivated_members_collection_link: u'http://.../~ubuntu-team/deactivated_members'70 deactivated_members_collection_link:
71 u'http://.../~ubuntu-team/deactivated_members'
63 default_membership_period: None72 default_membership_period: None
64 default_renewal_period: None73 default_renewal_period: None
65 display_name: u'Ubuntu Team'74 display_name: u'Ubuntu Team'
66 expired_members_collection_link: u'http://.../~ubuntu-team/expired_members'75 expired_members_collection_link:
76 u'http://.../~ubuntu-team/expired_members'
67 gpg_keys_collection_link: u'http://.../~ubuntu-team/gpg_keys'77 gpg_keys_collection_link: u'http://.../~ubuntu-team/gpg_keys'
68 hardware_submissions_collection_link: u'http://.../~ubuntu-team/hardware_submissions'78 hardware_submissions_collection_link:
79 u'http://.../~ubuntu-team/hardware_submissions'
69 hide_email_addresses: False80 hide_email_addresses: False
70 homepage_content: None81 homepage_content: None
71 invited_members_collection_link: u'http://.../~ubuntu-team/invited_members'82 invited_members_collection_link:
83 u'http://.../~ubuntu-team/invited_members'
72 irc_nicknames_collection_link: u'http://.../~ubuntu-team/irc_nicknames'84 irc_nicknames_collection_link: u'http://.../~ubuntu-team/irc_nicknames'
73 is_probationary: False85 is_probationary: False
74 is_team: True86 is_team: True
@@ -82,16 +94,21 @@
82 longitude: None94 longitude: None
83 mailing_list_auto_subscribe_policy: u'Ask me when I join a team'95 mailing_list_auto_subscribe_policy: u'Ask me when I join a team'
84 members_collection_link: u'http://.../~ubuntu-team/members'96 members_collection_link: u'http://.../~ubuntu-team/members'
85 members_details_collection_link: u'http://.../~ubuntu-team/members_details'97 members_details_collection_link:
86 memberships_details_collection_link: u'http://.../~ubuntu-team/memberships_details'98 u'http://.../~ubuntu-team/members_details'
99 memberships_details_collection_link:
100 u'http://.../~ubuntu-team/memberships_details'
87 mugshot_link: u'http://.../~ubuntu-team/mugshot'101 mugshot_link: u'http://.../~ubuntu-team/mugshot'
88 name: u'ubuntu-team'102 name: u'ubuntu-team'
89 open_membership_invitations_collection_link: u'http://.../~ubuntu-team/open_membership_invitations'103 open_membership_invitations_collection_link:
104 u'http://.../~ubuntu-team/open_membership_invitations'
90 participants_collection_link: u'http://.../~ubuntu-team/participants'105 participants_collection_link: u'http://.../~ubuntu-team/participants'
91 ppas_collection_link: u'http://.../~ubuntu-team/ppas'106 ppas_collection_link: u'http://.../~ubuntu-team/ppas'
92 preferred_email_address_link: u'http://.../~ubuntu-team/+email/support@ubuntu.com'107 preferred_email_address_link:
108 u'http://.../~ubuntu-team/+email/support@ubuntu.com'
93 private: False109 private: False
94 proposed_members_collection_link: u'http://.../~ubuntu-team/proposed_members'110 proposed_members_collection_link:
111 u'http://.../~ubuntu-team/proposed_members'
95 renewal_policy: u'invite them to apply for renewal'112 renewal_policy: u'invite them to apply for renewal'
96 resource_type_link: u'http://.../#team'113 resource_type_link: u'http://.../#team'
97 self_link: u'http://.../~ubuntu-team'114 self_link: u'http://.../~ubuntu-team'
@@ -113,13 +130,15 @@
113 []130 []
114131
115132
116== Links to related things ==133Links to related things
134-----------------------
117135
118As seen above, many attributes of a person are actually links to other136As seen above, many attributes of a person are actually links to other
119things (or collections).137things (or collections).
120138
121139
122=== Email addresses ===140Email addresses
141...............
123142
124Apart from the link to the preferred email, there is a link to the143Apart from the link to the preferred email, there is a link to the
125collection of other confirmed email addresses of that person/team.144collection of other confirmed email addresses of that person/team.
@@ -151,7 +170,8 @@
151 HTTP/1.1 404 Not Found170 HTTP/1.1 404 Not Found
152 ...171 ...
153172
154== SSH keys ===173SSH keys
174........
155175
156People have SSH keys which we can manipulate over the API.176People have SSH keys which we can manipulate over the API.
157177
@@ -175,8 +195,8 @@
175 >>> ssh_key = factory.makeSSHKey(ssh_user)195 >>> ssh_key = factory.makeSSHKey(ssh_user)
176 >>> logout()196 >>> logout()
177197
178Now when we get the sshkey collection for 'sssh-user' again, the key should show198Now when we get the sshkey collection for 'sssh-user' again, the key should
179up:199show up:
180200
181 >>> keys = webservice.get(sshkeys).jsonBody()201 >>> keys = webservice.get(sshkeys).jsonBody()
182 >>> print_self_link_of_entries(keys)202 >>> print_self_link_of_entries(keys)
@@ -192,7 +212,8 @@
192 resource_type_link: u'http://.../#ssh_key'212 resource_type_link: u'http://.../#ssh_key'
193 self_link: u'http://.../~ssh-user/+ssh-keys/...'213 self_link: u'http://.../~ssh-user/+ssh-keys/...'
194214
195=== GPG keys ===215GPG keys
216........
196217
197People have GPG keys which we can manipulate over the API.218People have GPG keys which we can manipulate over the API.
198219
@@ -230,7 +251,8 @@
230 self_link: u'http://.../~name12/+gpg-keys/...'251 self_link: u'http://.../~name12/+gpg-keys/...'
231252
232253
233=== Team memberships ===254Team memberships
255................
234256
235A person is linked to their team memberships.257A person is linked to their team memberships.
236258
@@ -248,7 +270,8 @@
248And to all membership invitations sent to it.270And to all membership invitations sent to it.
249271
250 >>> lp_team = webservice.get("/~launchpad").jsonBody()272 >>> lp_team = webservice.get("/~launchpad").jsonBody()
251 >>> lp_invitations = lp_team['open_membership_invitations_collection_link']273 >>> lp_invitations = lp_team[
274 ... 'open_membership_invitations_collection_link']
252 >>> lp_invitations275 >>> lp_invitations
253 u'http://.../~launchpad/open_membership_invitations'276 u'http://.../~launchpad/open_membership_invitations'
254277
@@ -331,7 +354,7 @@
331 >>> print webservice.get(354 >>> print webservice.get(
332 ... salgado_landscape['self_link']).jsonBody()['status']355 ... salgado_landscape['self_link']).jsonBody()['status']
333 Deactivated356 Deactivated
334 357
335 >>> print webservice.named_post(358 >>> print webservice.named_post(
336 ... salgado_landscape['self_link'], 'setStatus', {},359 ... salgado_landscape['self_link'], 'setStatus', {},
337 ... status='Approved', silent=True)360 ... status='Approved', silent=True)
@@ -341,7 +364,7 @@
341 >>> print webservice.get(364 >>> print webservice.get(
342 ... salgado_landscape['self_link']).jsonBody()['status']365 ... salgado_landscape['self_link']).jsonBody()['status']
343 Approved366 Approved
344 367
345 >>> print webservice.named_post(368 >>> print webservice.named_post(
346 ... salgado_landscape['self_link'], 'setStatus', {},369 ... salgado_landscape['self_link'], 'setStatus', {},
347 ... status='Deactivated', silent=True)370 ... status='Deactivated', silent=True)
@@ -357,7 +380,8 @@
357 ...380 ...
358381
359382
360=== Members ===383Members
384.......
361385
362A list of team memberships is distinct from a list of a team's386A list of team memberships is distinct from a list of a team's
363members. Members are people; memberships are TeamMemberships. You've387members. Members are people; memberships are TeamMemberships. You've
@@ -427,7 +451,8 @@
427 http://.../~karl451 http://.../~karl
428452
429453
430=== Sub-teams and super-teams ===454Sub-teams and super-teams
455.........................
431456
432Teams can be members of other teams, and sometimes it's useful to know457Teams can be members of other teams, and sometimes it's useful to know
433which teams are members of any given team as well as the ones it is a458which teams are members of any given team as well as the ones it is a
@@ -442,7 +467,8 @@
442 http://.../~guadamen467 http://.../~guadamen
443468
444469
445=== Wiki names ===470Wiki names
471..........
446472
447All wiki names associated to a person/team are also linked to that473All wiki names associated to a person/team are also linked to that
448person/team.474person/team.
@@ -461,7 +487,8 @@
461Wiki names are first-class objects with their own URLs and487Wiki names are first-class objects with their own URLs and
462representations too.488representations too.
463489
464 >>> wiki_name = sorted(webservice.get(wikis_link).jsonBody()['entries'])[0]490 >>> wiki_name = sorted(
491 ... webservice.get(wikis_link).jsonBody()['entries'])[0]
465 >>> pprint_entry(wiki_name)492 >>> pprint_entry(wiki_name)
466 person_link: u'http://.../~salgado'493 person_link: u'http://.../~salgado'
467 resource_type_link: u'http://.../#wiki_name'494 resource_type_link: u'http://.../#wiki_name'
@@ -485,7 +512,8 @@
485 ... u'wikiname': 'MrExample'}512 ... u'wikiname': 'MrExample'}
486 >>> response = webservice.patch(513 >>> response = webservice.patch(
487 ... wiki_name['self_link'], 'application/json', dumps(patch))514 ... wiki_name['self_link'], 'application/json', dumps(patch))
488 >>> wiki_name = sorted(webservice.get(wikis_link).jsonBody()['entries'])[0]515 >>> wiki_name = sorted(
516 ... webservice.get(wikis_link).jsonBody()['entries'])[0]
489 >>> print wiki_name['url']517 >>> print wiki_name['url']
490 http://www.example.com/MrExample518 http://www.example.com/MrExample
491519
@@ -503,7 +531,8 @@
503 Only URIs with the following schemes may be used: http, https531 Only URIs with the following schemes may be used: http, https
504532
505533
506=== Jabber IDs ===534Jabber IDs
535..........
507536
508Jabber IDs of a person are also linked.537Jabber IDs of a person are also linked.
509538
@@ -534,7 +563,8 @@
534 ...563 ...
535564
536565
537=== IRC nicknames ===566IRC nicknames
567.............
538568
539The same for IRC nicknames569The same for IRC nicknames
540570
@@ -546,7 +576,8 @@
546576
547Anonymous listing is possible.577Anonymous listing is possible.
548578
549 >>> print_self_link_of_entries(anon_webservice.get(irc_ids_link).jsonBody())579 >>> print_self_link_of_entries(
580 ... anon_webservice.get(irc_ids_link).jsonBody())
550 http://.../~mark/+ircnick/1581 http://.../~mark/+ircnick/1
551582
552IRC IDs are first-class objects with their own URLs and representations583IRC IDs are first-class objects with their own URLs and representations
@@ -568,7 +599,8 @@
568 ...599 ...
569600
570601
571=== PPAs ===602PPAs
603....
572604
573We can get to the person's default PPA via the 'archive' property:605We can get to the person's default PPA via the 'archive' property:
574606
@@ -634,7 +666,8 @@
634 [u'http://mark:testtoken@private-ppa.launchpad.dev/mark/p3a/ubuntu']666 [u'http://mark:testtoken@private-ppa.launchpad.dev/mark/p3a/ubuntu']
635667
636668
637== Custom operations ==669Custom operations
670-----------------
638671
639IPerson supports a bunch of operations.672IPerson supports a bunch of operations.
640673
@@ -669,7 +702,8 @@
669 ---702 ---
670703
671704
672=== Team membership operations ===705Team membership operations
706..........................
673707
674Joining and leaving teams:708Joining and leaving teams:
675709
@@ -791,14 +825,15 @@
791 # http://.../~mailing-list-experts825 # http://.../~mailing-list-experts
792826
793827
794== Restrictions ==828Restrictions
829------------
795830
796A team can't be its own owner.831A team can't be its own owner.
797832
798 >>> import simplejson833 >>> import simplejson
799 >>> doc = {'team_owner_link' : webservice.getAbsoluteUrl("/~admins")}834 >>> doc = {'team_owner_link' : webservice.getAbsoluteUrl("/~admins")}
800 >>> print webservice.patch(835 >>> print webservice.patch(
801 ... "/~admins", 'application/json', simplejson.dumps(doc))836 ... "/~admins", 'application/json', simplejson.dumps(doc))
802 HTTP/1.1 400 Bad Request837 HTTP/1.1 400 Bad Request
803 ...838 ...
804 team_owner_link: Constraint not satisfied.839 team_owner_link: Constraint not satisfied.
805840
=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py 2010-10-04 19:50:45 +0000
+++ lib/lp/registry/tests/test_personset.py 2010-12-13 13:34:01 +0000
@@ -172,13 +172,13 @@
172 "when purchasing an application via Software Center.")172 "when purchasing an application via Software Center.")
173173
174 def test_existing_person(self):174 def test_existing_person(self):
175 person = self.factory.makePerson()175 email = 'test-email@example.com'
176 person = self.factory.makePerson(email=email)
176 openid_ident = removeSecurityProxy(177 openid_ident = removeSecurityProxy(
177 person.account).openid_identifiers.any().identifier178 person.account).openid_identifiers.any().identifier
178 person_set = getUtility(IPersonSet)179 person_set = getUtility(IPersonSet)
179180
180 result, db_updated = self.callGetOrCreate(181 result, db_updated = self.callGetOrCreate(openid_ident, email=email)
181 openid_ident, email=person.preferredemail.email)
182182
183 self.assertEqual(person, result)183 self.assertEqual(person, result)
184 self.assertFalse(db_updated)184 self.assertFalse(db_updated)
185185
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2010-11-22 22:00:28 +0000
+++ lib/lp/registry/tests/test_product.py 2010-12-13 13:34:01 +0000
@@ -12,10 +12,7 @@
12import transaction12import transaction
13from zope.component import getUtility13from zope.component import getUtility
1414
15from canonical.launchpad.ftests import (15from canonical.launchpad.ftests import syncUpdate
16 login,
17 syncUpdate,
18 )
19from canonical.launchpad.testing.pages import (16from canonical.launchpad.testing.pages import (
20 find_main_content,17 find_main_content,
21 get_feedback_messages,18 get_feedback_messages,
@@ -37,7 +34,11 @@
37 UnDeactivateable,34 UnDeactivateable,
38 )35 )
39from lp.registry.model.productlicense import ProductLicense36from lp.registry.model.productlicense import ProductLicense
40from lp.testing import TestCaseWithFactory37from lp.testing import (
38 login,
39 login_person,
40 TestCaseWithFactory,
41 )
4142
4243
43class TestProduct(TestCaseWithFactory):44class TestProduct(TestCaseWithFactory):
@@ -339,7 +340,7 @@
339 super(BugSupervisorTestCase, self).setUp()340 super(BugSupervisorTestCase, self).setUp()
340 self.person = self.factory.makePerson()341 self.person = self.factory.makePerson()
341 self.product = self.factory.makeProduct(owner=self.person)342 self.product = self.factory.makeProduct(owner=self.person)
342 login(self.person.preferredemail.email)343 login_person(self.person)
343344
344 def testPersonCanSetSelfAsSupervisor(self):345 def testPersonCanSetSelfAsSupervisor(self):
345 # A person can set themselves as bug supervisor for a product.346 # A person can set themselves as bug supervisor for a product.
346347
=== modified file 'lib/lp/registry/tests/test_project.py'
--- lib/lp/registry/tests/test_project.py 2010-10-04 19:50:45 +0000
+++ lib/lp/registry/tests/test_project.py 2010-12-13 13:34:01 +0000
@@ -17,6 +17,7 @@
17from lp.registry.interfaces.projectgroup import IProjectGroupSet17from lp.registry.interfaces.projectgroup import IProjectGroupSet
18from lp.testing import (18from lp.testing import (
19 launchpadlib_for,19 launchpadlib_for,
20 login_person,
20 TestCaseWithFactory,21 TestCaseWithFactory,
21 )22 )
2223
@@ -36,7 +37,7 @@
36 name="razzle-dazzle", owner=self.person,37 name="razzle-dazzle", owner=self.person,
37 description="Giving 110% at all times.")38 description="Giving 110% at all times.")
38 self.projectset = getUtility(IProjectGroupSet)39 self.projectset = getUtility(IProjectGroupSet)
39 login(self.person.preferredemail.email)40 login_person(self.person)
4041
41 def testSearchNoMatch(self):42 def testSearchNoMatch(self):
42 # Search for a string that does not exist.43 # Search for a string that does not exist.
4344
=== modified file 'lib/lp/services/mail/sendmail.py'
--- lib/lp/services/mail/sendmail.py 2010-10-20 01:23:52 +0000
+++ lib/lp/services/mail/sendmail.py 2010-12-13 13:34:01 +0000
@@ -46,6 +46,7 @@
46from lazr.restful.utils import get_current_browser_request46from lazr.restful.utils import get_current_browser_request
47from zope.app import zapi47from zope.app import zapi
48from zope.security.proxy import isinstance as zisinstance48from zope.security.proxy import isinstance as zisinstance
49from zope.security.proxy import removeSecurityProxy
49from zope.sendmail.interfaces import IMailDelivery50from zope.sendmail.interfaces import IMailDelivery
5051
51from canonical.config import config52from canonical.config import config
@@ -155,7 +156,8 @@
155156
156def format_address_for_person(person):157def format_address_for_person(person):
157 """Helper function to call format_address for a person."""158 """Helper function to call format_address for a person."""
158 return format_address(person.displayname, person.preferredemail.email)159 email_address = removeSecurityProxy(person.preferredemail).email
160 return format_address(person.displayname, email_address)
159161
160162
161def simple_sendmail(from_addr, to_addrs, subject, body, headers=None,163def simple_sendmail(from_addr, to_addrs, subject, body, headers=None,
162164
=== modified file 'lib/lp/services/mailman/testing/__init__.py'
--- lib/lp/services/mailman/testing/__init__.py 2010-10-06 11:46:51 +0000
+++ lib/lp/services/mailman/testing/__init__.py 2010-12-13 13:34:01 +0000
@@ -17,6 +17,7 @@
17 mm_cfg,17 mm_cfg,
18 )18 )
19from Mailman.Queue import XMLRPCRunner19from Mailman.Queue import XMLRPCRunner
20from zope.security.proxy import removeSecurityProxy
2021
21from canonical.testing.layers import DatabaseFunctionalLayer22from canonical.testing.layers import DatabaseFunctionalLayer
2223
@@ -56,7 +57,7 @@
56 # This utility is based on mailman/tests/TestBase.py.57 # This utility is based on mailman/tests/TestBase.py.
57 mlist = MailList.MailList()58 mlist = MailList.MailList()
58 team = lp_mailing_list.team59 team = lp_mailing_list.team
59 owner_email = team.teamowner.preferredemail.email60 owner_email = removeSecurityProxy(team.teamowner).preferredemail.email
60 mlist.Create(team.name, owner_email, 'password')61 mlist.Create(team.name, owner_email, 'password')
61 mlist.host_name = 'lists.launchpad.dev'62 mlist.host_name = 'lists.launchpad.dev'
62 mlist.web_page_url = 'http://lists.launchpad.dev/mailman/'63 mlist.web_page_url = 'http://lists.launchpad.dev/mailman/'
6364
=== modified file 'lib/lp/services/mailman/tests/test_lpmoderate.py'
--- lib/lp/services/mailman/tests/test_lpmoderate.py 2010-10-26 15:47:24 +0000
+++ lib/lp/services/mailman/tests/test_lpmoderate.py 2010-12-13 13:34:01 +0000
@@ -7,6 +7,7 @@
77
8from Mailman import Errors8from Mailman import Errors
9from Mailman.Handlers import LPModerate9from Mailman.Handlers import LPModerate
10from zope.security.proxy import removeSecurityProxy
1011
11from canonical.testing.layers import LaunchpadFunctionalLayer12from canonical.testing.layers import LaunchpadFunctionalLayer
12from lp.services.mailman.testing import MailmanTestCase13from lp.services.mailman.testing import MailmanTestCase
@@ -31,7 +32,7 @@
31 'team-1', 'team-1-owner')32 'team-1', 'team-1-owner')
32 self.mm_list = self.makeMailmanList(self.mailing_list)33 self.mm_list = self.makeMailmanList(self.mailing_list)
33 self.lp_user = self.factory.makePerson()34 self.lp_user = self.factory.makePerson()
34 self.lp_user_email = self.lp_user.preferredemail.email35 self.lp_user_email = removeSecurityProxy(self.lp_user).preferredemail.email
3536
36 def tearDown(self):37 def tearDown(self):
37 super(TestLPModerateTestCase, self).tearDown()38 super(TestLPModerateTestCase, self).tearDown()
@@ -47,7 +48,8 @@
4748
48 def test_process_message_from_subscriber(self):49 def test_process_message_from_subscriber(self):
49 # Messages from subscribers silently complete the process.50 # Messages from subscribers silently complete the process.
50 subscriber_email = self.team.teamowner.preferredemail.email51 subscriber_email = removeSecurityProxy(
52 self.team.teamowner).preferredemail.email
51 message = self.makeMailmanMessage(53 message = self.makeMailmanMessage(
52 self.mm_list, subscriber_email, 'subject', 'any content.')54 self.mm_list, subscriber_email, 'subject', 'any content.')
53 msg_data = {}55 msg_data = {}
5456
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2010-12-09 11:03:53 +0000
+++ lib/lp/testing/factory.py 2010-12-13 13:34:01 +0000
@@ -1315,7 +1315,7 @@
1315 if author is None:1315 if author is None:
1316 author = self.getUniqueString('author')1316 author = self.getUniqueString('author')
1317 elif IPerson.providedBy(author):1317 elif IPerson.providedBy(author):
1318 author = author.preferredemail.email1318 author = removeSecurityProxy(author).preferredemail.email
1319 if revision_date is None:1319 if revision_date is None:
1320 revision_date = datetime.now(pytz.UTC)1320 revision_date = datetime.now(pytz.UTC)
1321 if parent_ids is None:1321 if parent_ids is None:
@@ -1626,9 +1626,10 @@
1626 mail = SignedMessage()1626 mail = SignedMessage()
1627 if email_address is None:1627 if email_address is None:
1628 person = self.makePerson()1628 person = self.makePerson()
1629 email_address = person.preferredemail.email1629 email_address = removeSecurityProxy(person).preferredemail.email
1630 mail['From'] = email_address1630 mail['From'] = email_address
1631 mail['To'] = self.makePerson().preferredemail.email1631 mail['To'] = removeSecurityProxy(
1632 self.makePerson()).preferredemail.email
1632 if subject is None:1633 if subject is None:
1633 subject = self.getUniqueString('subject')1634 subject = self.getUniqueString('subject')
1634 mail['Subject'] = subject1635 mail['Subject'] = subject
@@ -2716,7 +2717,7 @@
2716 if dsc_maintainer_rfc822 is None:2717 if dsc_maintainer_rfc822 is None:
2717 dsc_maintainer_rfc822 = '%s <%s>' % (2718 dsc_maintainer_rfc822 = '%s <%s>' % (
2718 maintainer.displayname,2719 maintainer.displayname,
2719 maintainer.preferredemail.email)2720 removeSecurityProxy(maintainer).preferredemail.email)
27202721
2721 if creator is None:2722 if creator is None:
2722 creator = self.makePerson()2723 creator = self.makePerson()
@@ -3105,7 +3106,7 @@
3105 msg['Message-Id'] = make_msgid('launchpad')3106 msg['Message-Id'] = make_msgid('launchpad')
3106 msg['Date'] = formatdate()3107 msg['Date'] = formatdate()
3107 msg['To'] = to3108 msg['To'] = to
3108 msg['From'] = sender.preferredemail.email3109 msg['From'] = removeSecurityProxy(sender).preferredemail.email
3109 msg['Subject'] = 'Sample'3110 msg['Subject'] = 'Sample'
31103111
3111 if attachments is None:3112 if attachments is None:
@@ -3142,7 +3143,7 @@
3142 timezone=0)3143 timezone=0)
3143 email = None3144 email = None
3144 if sender is not None:3145 if sender is not None:
3145 email = sender.preferredemail.email3146 email = removeSecurityProxy(sender).preferredemail.email
3146 return self.makeSignedMessage(3147 return self.makeSignedMessage(
3147 body='My body', subject='My subject',3148 body='My body', subject='My subject',
3148 attachment_contents=''.join(md.to_lines()),3149 attachment_contents=''.join(md.to_lines()),
31493150
=== modified file 'lib/lp/testing/tests/test_login.py'
--- lib/lp/testing/tests/test_login.py 2010-10-26 15:47:24 +0000
+++ lib/lp/testing/tests/test_login.py 2010-12-13 13:34:01 +0000
@@ -122,8 +122,8 @@
122122
123 def test_login_with_email(self):123 def test_login_with_email(self):
124 # login() logs a person in by email.124 # login() logs a person in by email.
125 person = self.factory.makePerson()125 email = 'test-email@example.com'
126 email = person.preferredemail.email126 person = self.factory.makePerson(email=email)
127 logout()127 logout()
128 login(email)128 login(email)
129 self.assertLoggedIn(person)129 self.assertLoggedIn(person)