Merge lp:~wallyworld/launchpad/private-owned-entity-traversal-2 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 14490
Proposed branch: lp:~wallyworld/launchpad/private-owned-entity-traversal-2
Merge into: lp:launchpad
Prerequisite: lp:~wallyworld/launchpad/private-owned-entity-traversal
Diff against target: 991 lines (+310/-170)
22 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+5/-5)
lib/canonical/launchpad/security.py (+38/-27)
lib/canonical/launchpad/webapp/authorization.py (+1/-1)
lib/canonical/launchpad/webapp/interaction.py (+2/-1)
lib/lp/app/browser/launchpad.py (+2/-2)
lib/lp/app/browser/tales.py (+8/-0)
lib/lp/app/tests/test_tales.py (+16/-9)
lib/lp/bugs/browser/tests/test_bugsubscription_views.py (+1/-1)
lib/lp/code/browser/tests/test_branchlisting.py (+4/-3)
lib/lp/registry/browser/tests/test_mailinglists.py (+8/-6)
lib/lp/registry/browser/tests/test_person_view.py (+12/-8)
lib/lp/registry/configure.zcml (+6/-4)
lib/lp/registry/doc/person.txt (+2/-1)
lib/lp/registry/doc/private-team-visibility.txt (+61/-0)
lib/lp/registry/interfaces/person.py (+79/-77)
lib/lp/registry/interfaces/role.py (+2/-1)
lib/lp/registry/model/personroles.py (+10/-8)
lib/lp/registry/stories/webservice/xx-derivedistroseries.txt (+2/-1)
lib/lp/registry/tests/test_person_vocabularies.py (+3/-2)
lib/lp/registry/tests/test_team_webservice.py (+4/-5)
lib/lp/soyuz/tests/test_archive_subscriptions.py (+33/-3)
lib/lp/testing/factory.py (+11/-5)
To merge this branch: bzr merge lp:~wallyworld/launchpad/private-owned-entity-traversal-2
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+85283@code.launchpad.net

Commit message

[r=sinzui] Users with access to private team's PPAs or branches have limited view permission on the team.

Description of the change

This branch is a re-submission of lp:~wallyworld/launchpad/private-owned-entity-traversal as described in mp
https://code.launchpad.net/~wallyworld/launchpad/private-owned-entity-traversal/+merge/84048

Some tweaks were made to fix a traversal issue found during qa on qastaging.

== Implementation ==

The issue was that rendering the ppa page required access to IPerson properties:
- title
- is_probationary

These properties were defined under IPersonViewRestricted and not IPersonLimitedView.
title simple delegates to displayname
is_probationary returns false for teams anyway

So I simply moved the properties to IPersonLimitedView.

== Tests ==

Add one new test to test the failure as seen on qastaging:
TestArchiveSubscriptions.test_subscriber_can_browse_private_team_ppa()

== Lint ==

Linting changed files:
  lib/canonical/launchpad/security.py
  lib/canonical/launchpad/interfaces/_schema_circular_imports.py
  lib/canonical/launchpad/webapp/authorization.py
  lib/canonical/launchpad/webapp/interaction.py
  lib/lp/app/browser/launchpad.py
  lib/lp/app/browser/tales.py
  lib/lp/app/tests/test_tales.py
  lib/lp/bugs/browser/tests/test_bugsubscription_views.py
  lib/lp/code/browser/tests/test_branchlisting.py
  lib/lp/registry/configure.zcml
  lib/lp/registry/browser/tests/test_mailinglists.py
  lib/lp/registry/browser/tests/test_person_view.py
  lib/lp/registry/doc/person.txt
  lib/lp/registry/doc/private-team-visibility.txt
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/interfaces/role.py
  lib/lp/registry/model/personroles.py
  lib/lp/registry/stories/webservice/xx-derivedistroseries.txt
  lib/lp/registry/tests/test_person_vocabularies.py
  lib/lp/registry/tests/test_team_webservice.py
  lib/lp/soyuz/tests/test_archive_subscriptions.py
  lib/lp/testing/factory.py

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for the prompt follow up. I am going to use ec2 land now so that this might be merged and in buildbot when you awake.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
2--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-12-12 02:45:10 +0000
3+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-12-12 05:28:08 +0000
4@@ -135,7 +135,7 @@
5 )
6 from lp.registry.interfaces.person import (
7 IPerson,
8- IPersonPublic,
9+ IPersonViewRestricted,
10 ITeam,
11 )
12 from lp.registry.interfaces.pillar import (
13@@ -310,10 +310,10 @@
14
15 IPreviewDiff['branch_merge_proposal'].schema = IBranchMergeProposal
16
17-patch_reference_property(IPersonPublic, 'archive', IArchive)
18-patch_collection_property(IPersonPublic, 'ppas', IArchive)
19-patch_entry_return_type(IPersonPublic, 'getPPAByName', IArchive)
20-patch_entry_return_type(IPersonPublic, 'createPPA', IArchive)
21+patch_reference_property(IPersonViewRestricted, 'archive', IArchive)
22+patch_collection_property(IPersonViewRestricted, 'ppas', IArchive)
23+patch_entry_return_type(IPersonViewRestricted, 'getPPAByName', IArchive)
24+patch_entry_return_type(IPersonViewRestricted, 'createPPA', IArchive)
25
26 IHasBuildRecords['getBuildRecords'].queryTaggedValue(
27 LAZR_WEBSERVICE_EXPORTED)[
28
29=== modified file 'lib/canonical/launchpad/security.py'
30--- lib/canonical/launchpad/security.py 2011-12-12 02:45:10 +0000
31+++ lib/canonical/launchpad/security.py 2011-12-12 05:28:08 +0000
32@@ -63,6 +63,7 @@
33 IBranch,
34 user_has_special_branch_access,
35 )
36+from lp.code.interfaces.branchcollection import IBranchCollection
37 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
38 from lp.code.interfaces.branchmergequeue import IBranchMergeQueue
39 from lp.code.interfaces.codeimport import ICodeImport
40@@ -800,6 +801,35 @@
41 if (invitee.is_team and
42 invitee in user.person.getAdministratedTeams()):
43 return True
44+ return False
45+
46+
47+class PublicOrPrivateTeamsExistence(AuthorizationBase):
48+ """Restrict knowing about private teams' existence.
49+
50+ Knowing the existence of a private team allow traversing to its URL and
51+ displaying basic information like name, displayname.
52+ """
53+ permission = 'launchpad.LimitedView'
54+ usedfor = IPersonLimitedView
55+
56+ def checkUnauthenticated(self):
57+ """Unauthenticated users can only view public teams."""
58+ if self.obj.visibility == PersonVisibility.PUBLIC:
59+ return True
60+ return False
61+
62+ def checkAuthenticated(self, user):
63+ """By default, we simply perform a View permission check.
64+
65+ We also grant limited viewability to users who can see PPAs and
66+ branches owned by the team. In other scenarios, the context in which
67+ the permission is required is responsible for pre-caching the
68+ launchpad.LimitedView permission on each team which requires it.
69+ """
70+ if self.forwardCheckAuthenticated(
71+ user, self.obj, 'launchpad.View'):
72+ return True
73
74 if (self.obj.is_team
75 and self.obj.visibility == PersonVisibility.PRIVATE):
76@@ -813,33 +843,14 @@
77 ppa.id for ppa in self.obj.ppas if ppa.private)
78 if len(subscriber_archive_ids.intersection(team_ppa_ids)) > 0:
79 return True
80- return False
81-
82-
83-class PublicOrPrivateTeamsExistence(AuthorizationBase):
84- """Restrict knowing about private teams' existence.
85-
86- Knowing the existence of a private team allow traversing to its URL and
87- displaying basic information like name, displayname.
88- """
89- permission = 'launchpad.LimitedView'
90- usedfor = IPersonLimitedView
91-
92- def checkUnauthenticated(self):
93- """Unauthenticated users can only view public teams."""
94- if self.obj.visibility == PersonVisibility.PUBLIC:
95- return True
96- return False
97-
98- def checkAuthenticated(self, user):
99- """By default, we simply perform a View permission check.
100-
101- The context in which the permission is required is
102- responsible for pre-caching the launchpad.LimitedView permission on
103- each team which requires it.
104- """
105- return self.forwardCheckAuthenticated(
106- user, self.obj, 'launchpad.View')
107+
108+ # Grant visibility to people with subscriptions to branches owned
109+ # by the private team.
110+ owned_branches = getUtility(IBranchCollection).ownedBy(self.obj)
111+ if owned_branches.visibleByUser(user.person).count() > 0:
112+ return True
113+
114+ return False
115
116
117 class EditPollByTeamOwnerOrTeamAdminsOrAdmins(
118
119=== modified file 'lib/canonical/launchpad/webapp/authorization.py'
120--- lib/canonical/launchpad/webapp/authorization.py 2011-12-12 02:45:10 +0000
121+++ lib/canonical/launchpad/webapp/authorization.py 2011-12-12 05:28:08 +0000
122@@ -192,7 +192,7 @@
123 objecttoauthorize, {})
124 if permission in object_cache:
125 return object_cache[permission]
126- principal = participation.principal
127+ principal = removeAllProxies(participation.principal)
128
129 if (principal is not None and
130 not isinstance(principal, UnauthenticatedPrincipal)):
131
132=== modified file 'lib/canonical/launchpad/webapp/interaction.py'
133--- lib/canonical/launchpad/webapp/interaction.py 2011-12-12 02:45:10 +0000
134+++ lib/canonical/launchpad/webapp/interaction.py 2011-12-12 05:28:08 +0000
135@@ -163,7 +163,8 @@
136 return setupInteraction(ANONYMOUS, participation)
137 else:
138 # Bypass zope's security because IEmailAddress.email is not public.
139- naked_email = removeSecurityProxy(person.preferredemail)
140+ naked_person = removeSecurityProxy(person)
141+ naked_email = removeSecurityProxy(naked_person.preferredemail)
142 return setupInteractionByEmail(naked_email.email, participation)
143
144
145
146=== modified file 'lib/lp/app/browser/launchpad.py'
147--- lib/lp/app/browser/launchpad.py 2011-12-12 02:45:10 +0000
148+++ lib/lp/app/browser/launchpad.py 2011-12-12 05:28:08 +0000
149@@ -724,8 +724,8 @@
150 # Check to see if this is a team, and if so, whether the
151 # logged in user is allowed to view the team, by virtue of
152 # team membership or Launchpad administration.
153- if (person.is_team
154- and not check_permission('launchpad.View', person)):
155+ if (person.is_team and
156+ not check_permission('launchpad.LimitedView', person)):
157 raise NotFound(self.context, name)
158 # Only admins are permitted to see suspended users.
159 if person.account_status == AccountStatus.SUSPENDED:
160
161=== modified file 'lib/lp/app/browser/tales.py'
162--- lib/lp/app/browser/tales.py 2011-12-12 02:45:10 +0000
163+++ lib/lp/app/browser/tales.py 2011-12-12 05:28:08 +0000
164@@ -1266,6 +1266,14 @@
165 self.hidden)
166 return super(TeamFormatterAPI, self).link(view_name, rootsite)
167
168+ def icon(self, view_name):
169+ team = self._context
170+ if not check_permission('launchpad.LimitedView', team):
171+ css_class = ObjectImageDisplayAPI(team).sprite_css()
172+ return '<span class="' + css_class + '"></span>'
173+ else:
174+ return super(TeamFormatterAPI, self).icon(view_name)
175+
176 def displayname(self, view_name, rootsite=None):
177 """See `PersonFormatterAPI`."""
178 person = self._context
179
180=== modified file 'lib/lp/app/tests/test_tales.py'
181--- lib/lp/app/tests/test_tales.py 2011-12-12 02:45:10 +0000
182+++ lib/lp/app/tests/test_tales.py 2011-12-12 05:28:08 +0000
183@@ -149,18 +149,20 @@
184 self.assertEqual(expected, result)
185
186
187-class TestTalesFormatterAPI(TestCaseWithFactory):
188- """ Test permissions required to access TalesFormatterAPI methods.
189+class TestTeamFormatterAPI(TestCaseWithFactory):
190+ """ Test permissions required to access TeamFormatterAPI methods.
191
192 A user must have launchpad.LimitedView permission to use
193- TestTalesFormatterAPI with private teams.
194+ TeamFormatterAPI with private teams.
195 """
196- layer = DatabaseFunctionalLayer
197+ layer = LaunchpadFunctionalLayer
198
199 def setUp(self):
200- super(TestTalesFormatterAPI, self).setUp()
201+ super(TestTeamFormatterAPI, self).setUp()
202+ icon = self.factory.makeLibraryFileAlias(
203+ filename='smurf.png', content_type='image/png')
204 self.team = self.factory.makeTeam(
205- name='team', displayname='a team',
206+ name='team', displayname='a team', icon=icon,
207 visibility=PersonVisibility.PRIVATE)
208
209 def _make_formatter(self, cache_permission=False):
210@@ -175,10 +177,11 @@
211 request, 'launchpad.LimitedView', [self.team])
212 return formatter, request, any_person
213
214- def _tales_value(self, attr, request):
215+ def _tales_value(self, attr, request, path='fmt'):
216 # Evaluate the given formatted attribute value on team.
217- return test_tales(
218- "team/fmt:%s" % attr, team=self.team, request=request)
219+ result = test_tales(
220+ "team/%s:%s" % (path, attr), team=self.team, request=request)
221+ return result
222
223 def _test_can_view_attribute_no_login(self, attr, hidden=None):
224 # Test attribute access with no login.
225@@ -228,6 +231,10 @@
226 def test_can_view_url(self):
227 self._test_can_view_attribute('url')
228
229+ def test_can_view_icon(self):
230+ self._test_can_view_attribute(
231+ 'icon', '<span class="sprite team"></span>')
232+
233
234 class TestObjectFormatterAPI(TestCaseWithFactory):
235 """Tests for ObjectFormatterAPI"""
236
237=== modified file 'lib/lp/bugs/browser/tests/test_bugsubscription_views.py'
238--- lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-12-12 02:45:10 +0000
239+++ lib/lp/bugs/browser/tests/test_bugsubscription_views.py 2011-12-12 05:28:08 +0000
240@@ -629,7 +629,7 @@
241 direct_subscriber = self.factory.makeTeam(
242 name='team', displayname='Team Name', owner=teamowner,
243 visibility=PersonVisibility.PRIVATE)
244- with person_logged_in(direct_subscriber.teamowner):
245+ with person_logged_in(teamowner):
246 bug.subscribe(direct_subscriber, direct_subscriber.teamowner,
247 level=BugNotificationLevel.LIFECYCLE)
248
249
250=== modified file 'lib/lp/code/browser/tests/test_branchlisting.py'
251--- lib/lp/code/browser/tests/test_branchlisting.py 2011-12-12 02:45:10 +0000
252+++ lib/lp/code/browser/tests/test_branchlisting.py 2011-12-12 05:28:08 +0000
253@@ -613,13 +613,14 @@
254 layer = DatabaseFunctionalLayer
255
256 def _make_branch_for_private_team(self):
257+ owner = self.factory.makePerson()
258 private_team = self.factory.makeTeam(
259- name='shh', displayname='Shh',
260+ name='shh', displayname='Shh', owner=owner,
261 visibility=PersonVisibility.PRIVATE)
262 member = self.factory.makePerson(
263 email='member@example.com', password='test')
264- with person_logged_in(private_team.teamowner):
265- private_team.addMember(member, private_team.teamowner)
266+ with person_logged_in(owner):
267+ private_team.addMember(member, owner)
268 branch = self.factory.makeProductBranch(owner=private_team)
269 return private_team, member, branch
270
271
272=== modified file 'lib/lp/registry/browser/tests/test_mailinglists.py'
273--- lib/lp/registry/browser/tests/test_mailinglists.py 2011-12-12 02:45:10 +0000
274+++ lib/lp/registry/browser/tests/test_mailinglists.py 2011-12-12 05:28:08 +0000
275@@ -28,11 +28,12 @@
276 """Verify the content in +mailing-list-portlet."""
277
278 def makeTeamWithMailingList(self, name=None, owner=None, visibility=None):
279+ if owner is None:
280+ owner = self.factory.makePerson()
281 team = self.factory.makeTeam(
282 name=name, owner=owner, visibility=visibility)
283- login_person(team.teamowner)
284- team_list = self.factory.makeMailingList(
285- team=team, owner=team.teamowner)
286+ login_person(owner)
287+ self.factory.makeMailingList(team=team, owner=owner)
288 return team
289
290
291@@ -129,10 +130,11 @@
292
293 def test_private_message_message_without_list(self):
294 # Private teams have private archives.
295+ owner = self.factory.makePerson()
296 team = self.factory.makeTeam(
297- visibility=PersonVisibility.PRIVATE)
298- login_person(team.teamowner)
299+ owner=owner, visibility=PersonVisibility.PRIVATE)
300+ login_person(owner)
301 view = create_initialized_view(
302- team, name='+mailinglist', principal=team.teamowner)
303+ team, name='+mailinglist', principal=owner)
304 element = find_tag_by_id(view(), 'mailing-list-archive')
305 self.assertEqual('private', extract_text(element))
306
307=== modified file 'lib/lp/registry/browser/tests/test_person_view.py'
308--- lib/lp/registry/browser/tests/test_person_view.py 2011-12-12 02:45:10 +0000
309+++ lib/lp/registry/browser/tests/test_person_view.py 2011-12-12 05:28:08 +0000
310@@ -475,9 +475,11 @@
311
312 def test_active_participations_with_direct_private_team(self):
313 # Users cannot see private teams that they are not members of.
314- team = self.factory.makeTeam(visibility=PersonVisibility.PRIVATE)
315- login_person(team.teamowner)
316- team.addMember(self.user, team.teamowner)
317+ owner = self.factory.makePerson()
318+ team = self.factory.makeTeam(
319+ owner=owner, visibility=PersonVisibility.PRIVATE)
320+ login_person(owner)
321+ team.addMember(self.user, owner)
322 # The team is included in active_participations.
323 login_person(self.user)
324 view = create_view(
325@@ -492,11 +494,13 @@
326
327 def test_active_participations_with_indirect_private_team(self):
328 # Users cannot see private teams that they are not members of.
329- team = self.factory.makeTeam(visibility=PersonVisibility.PRIVATE)
330- direct_team = self.factory.makeTeam(owner=team.teamowner)
331- login_person(team.teamowner)
332- direct_team.addMember(self.user, team.teamowner)
333- team.addMember(direct_team, team.teamowner)
334+ owner = self.factory.makePerson()
335+ team = self.factory.makeTeam(
336+ owner=owner, visibility=PersonVisibility.PRIVATE)
337+ direct_team = self.factory.makeTeam(owner=owner)
338+ login_person(owner)
339+ direct_team.addMember(self.user, owner)
340+ team.addMember(direct_team, owner)
341 # The team is included in active_participations.
342 login_person(self.user)
343 view = create_view(
344
345=== modified file 'lib/lp/registry/configure.zcml'
346--- lib/lp/registry/configure.zcml 2011-12-12 02:45:10 +0000
347+++ lib/lp/registry/configure.zcml 2011-12-12 05:28:08 +0000
348@@ -912,8 +912,6 @@
349 <allow
350 interface="lp.registry.interfaces.person.IPersonPublic"/>
351 <allow
352- interface="lp.registry.interfaces.person.ITeamPublic"/>
353- <allow
354 interface="lp.registry.interfaces.person.IHasStanding"/>
355 <allow
356 interface="lp.registry.interfaces.person.IPersonCommAdminWriteRestricted"/>
357@@ -924,6 +922,9 @@
358 permission="launchpad.View"
359 interface="lp.registry.interfaces.person.IPersonViewRestricted"/>
360 <require
361+ permission="launchpad.View"
362+ interface="lp.registry.interfaces.person.ITeamPublic"/>
363+ <require
364 permission="launchpad.Edit"
365 interface="lp.registry.interfaces.location.ISetLocation"/>
366 <require
367@@ -931,9 +932,10 @@
368 interface="lp.registry.interfaces.person.IPersonEditRestricted"/>
369 <require
370 permission="launchpad.Edit"
371- set_schema="lp.registry.interfaces.person.IPersonPublic
372+ set_schema="lp.registry.interfaces.person.IPersonViewRestricted
373+ lp.registry.interfaces.person.IPersonPublic
374 lp.registry.interfaces.person.ITeamPublic"
375- set_attributes="displayname"/>
376+ set_attributes="displayname icon logo"/>
377 <require
378 permission="launchpad.Moderate"
379 set_attributes="name"/>
380
381=== modified file 'lib/lp/registry/doc/person.txt'
382--- lib/lp/registry/doc/person.txt 2011-12-12 02:45:10 +0000
383+++ lib/lp/registry/doc/person.txt 2011-12-12 05:28:08 +0000
384@@ -696,6 +696,7 @@
385 >>> private_team = factory.makeTeam(salgado, name='private-team',
386 ... displayname='Private Team',
387 ... visibility=PersonVisibility.PRIVATE)
388+ >>> private_team_owner = private_team.teamowner
389 >>> bug_subscription = BugSubscription(bug=bug, person=private_team,
390 ... subscribed_by=guadamen)
391
392@@ -806,7 +807,7 @@
393
394 But Owner, a member of that team, will see it in the results.
395
396- >>> login_person(private_team.teamowner)
397+ >>> login_person(private_team_owner)
398 >>> print_people(personset.find('team'))
399 Another a new team (new3): []
400 Hoary Gnome Team (name21): []
401
402=== modified file 'lib/lp/registry/doc/private-team-visibility.txt'
403--- lib/lp/registry/doc/private-team-visibility.txt 2011-12-12 02:45:10 +0000
404+++ lib/lp/registry/doc/private-team-visibility.txt 2011-12-12 05:28:08 +0000
405@@ -107,3 +107,64 @@
406 ...
407 Unauthorized: (<Person at ... priv-team (Priv Team)>,
408 'name', 'launchpad.LimitedView')
409+
410+A person with visibility to any of the branches owned by the private team will
411+be granted limited view permission on the team.
412+
413+For private branches, a user needs to be subscribed to the branch for the
414+branch (and hence team) to be visible.
415+
416+ >>> login_person(priv_owner)
417+ >>> private_team_branch = factory.makeBranch(
418+ ... owner=priv_team, private=True)
419+ >>> login_person(pub_member)
420+ >>> check_permission('launchpad.LimitedView', priv_team)
421+ False
422+
423+ >>> login_person(priv_owner)
424+ >>> sub = factory.makeBranchSubscription(
425+ ... branch=private_team_branch, person=pub_member,
426+ ... subscribed_by=priv_owner)
427+
428+ >>> login_person(pub_member)
429+ >>> check_permission('launchpad.LimitedView', priv_team)
430+ True
431+
432+Subscribers to the teams private PPA have limited view permission.
433+
434+ >>> login_person(priv_owner)
435+ >>> archive = factory.makeArchive(private=True, owner=priv_team)
436+ >>> archive_subscriber = factory.makePerson()
437+ >>> sub = archive.newSubscription(
438+ ... archive_subscriber, registrant=archive.owner)
439+
440+ >>> login_person(archive_subscriber)
441+ >>> check_permission('launchpad.LimitedView', priv_team)
442+ True
443+
444+Users with LimitedView can know identifying information like name,
445+displayname, and unique_name, but cannot know other information like
446+teamowner.
447+
448+ >>> print priv_team.name
449+ priv-team
450+
451+ >>> print priv_team.displayname
452+ Priv Team
453+
454+ >>> print priv_team.unique_displayname
455+ Priv Team (priv-team)
456+
457+ >>> print priv_team.icon
458+ None
459+
460+ >>> print priv_team.allmembers
461+ Traceback (most recent call last):
462+ ...
463+ Unauthorized: (<Person at ... priv-team (Priv Team)>,
464+ 'allmembers', 'launchpad.View')
465+
466+Anonymous users do not have permission.
467+ >>> login(ANONYMOUS)
468+ >>> check_permission('launchpad.LimitedView', priv_team)
469+ False
470
471=== modified file 'lib/lp/registry/interfaces/person.py'
472--- lib/lp/registry/interfaces/person.py 2011-12-12 02:45:10 +0000
473+++ lib/lp/registry/interfaces/person.py 2011-12-12 05:28:08 +0000
474@@ -15,7 +15,7 @@
475 'IObjectReassignment',
476 'IPerson',
477 'IPersonClaim',
478- 'IPersonPublic', # Required for a monkey patch in interfaces/archive.py
479+ 'IPersonPublic',
480 'IPersonSet',
481 'IPersonSettings',
482 'ISoftwareCenterAgentAPI',
483@@ -657,26 +657,55 @@
484 required=False, default=False)
485
486
487-class IPersonPublic(IHasBranches, IHasSpecifications,
488- IHasMergeProposals, IHasLogo, IHasMugshot, IHasIcon,
489- IHasLocation, IHasRequestedReviews, IObjectWithLocation,
490- IPrivacy, IHasBugs, IHasRecipes, IHasTranslationImports,
491- IPersonSettings, IQuestionsPerson):
492- """Public attributes for a Person."""
493+class IPersonPublic(IPrivacy):
494+ """Public attributes for a Person.
495+
496+ Very few attributes on a person can be public because private teams
497+ are also persons. The public attributes are generally information
498+ needed by the system to determine if the principal in the current
499+ interaction can work with the object.
500+ """
501
502 id = Int(title=_('ID'), required=True, readonly=True)
503- account = Object(schema=IAccount)
504- accountID = Int(title=_('Account ID'), required=True, readonly=True)
505- password = PasswordField(
506- title=_('Password'), required=True, readonly=False)
507- karma = exported(
508- Int(title=_('Karma'), readonly=True,
509- description=_('The cached total karma for this person.')))
510- homepage_content = exported(
511- Text(title=_("Homepage Content"), required=False,
512- description=_(
513- "The content of your profile page. Use plain text, "
514- "paragraphs are preserved and URLs are linked in pages.")))
515+ # This is redefined from IPrivacy.private because the attribute is
516+ # read-only. It is a summary of the team's visibility.
517+ private = exported(Bool(
518+ title=_("This team is private"),
519+ readonly=True, required=False,
520+ description=_("Private teams are visible only to "
521+ "their members.")))
522+ is_valid_person = Bool(
523+ title=_("This is an active user and not a team."), readonly=True)
524+ is_valid_person_or_team = exported(
525+ Bool(title=_("This is an active user or a team."), readonly=True),
526+ exported_as='is_valid')
527+ is_merge_pending = exported(Bool(
528+ title=_("Is this person due to be merged with another?"),
529+ required=False, default=False))
530+ is_team = exported(
531+ Bool(title=_('Is this object a team?'), readonly=True))
532+
533+
534+class IPersonLimitedView(IHasIcon, IHasLogo):
535+ """IPerson attributes that require launchpad.LimitedView permission."""
536+
537+ name = exported(
538+ PersonNameField(
539+ title=_('Name'), required=True, readonly=False,
540+ constraint=name_validator,
541+ description=_(
542+ "A short unique name, beginning with a lower-case "
543+ "letter or number, and containing only letters, "
544+ "numbers, dots, hyphens, or plus signs.")))
545+ displayname = exported(
546+ StrippedTextLine(
547+ title=_('Display Name'), required=True, readonly=False,
548+ description=_(
549+ "Your name as you would like it displayed throughout "
550+ "Launchpad. Most people use their full name here.")),
551+ exported_as='display_name')
552+ unique_displayname = TextLine(
553+ title=_('Return a string of the form $displayname ($name).'))
554 # NB at this stage we do not allow individual people to have their own
555 # icon, only teams get that. People can however have a logo and mugshot
556 # The icon is only used for teams; that's why we use /@@/team as the
557@@ -690,7 +719,6 @@
558 "displayed whenever the team name is listed - for example "
559 "in listings of bugs or on a person's membership table."))
560 iconID = Int(title=_('Icon ID'), required=True, readonly=True)
561-
562 logo = exported(
563 LogoImageUpload(
564 title=_("Logo"), required=False,
565@@ -701,6 +729,30 @@
566 "is a logo, a small picture or a personal mascot. It should "
567 "be no bigger than 50kb in size.")))
568 logoID = Int(title=_('Logo ID'), required=True, readonly=True)
569+ # title is required for the Launchpad Page Layout main template
570+ title = Attribute('Person Page Title')
571+ is_probationary = exported(
572+ Bool(title=_("Is this a probationary user?"), readonly=True))
573+
574+
575+class IPersonViewRestricted(IHasBranches, IHasSpecifications,
576+ IHasMergeProposals, IHasMugshot,
577+ IHasLocation, IHasRequestedReviews, IObjectWithLocation,
578+ IHasBugs, IHasRecipes, IHasTranslationImports,
579+ IPersonSettings, IQuestionsPerson):
580+ """IPerson attributes that require launchpad.View permission."""
581+ account = Object(schema=IAccount)
582+ accountID = Int(title=_('Account ID'), required=True, readonly=True)
583+ password = PasswordField(
584+ title=_('Password'), required=True, readonly=False)
585+ karma = exported(
586+ Int(title=_('Karma'), readonly=True,
587+ description=_('The cached total karma for this person.')))
588+ homepage_content = exported(
589+ Text(title=_("Homepage Content"), required=False,
590+ description=_(
591+ "The content of your profile page. Use plain text, "
592+ "paragraphs are preserved and URLs are linked in pages.")))
593
594 mugshot = exported(MugshotImageUpload(
595 title=_("Mugshot"), required=False,
596@@ -766,15 +818,6 @@
597 # Properties of the Person object.
598 karma_category_caches = Attribute(
599 'The caches of karma scores, by karma category.')
600- is_team = exported(
601- Bool(title=_('Is this object a team?'), readonly=True))
602- is_valid_person = Bool(
603- title=_("This is an active user and not a team."), readonly=True)
604- is_valid_person_or_team = exported(
605- Bool(title=_("This is an active user or a team."), readonly=True),
606- exported_as='is_valid')
607- is_probationary = exported(
608- Bool(title=_("Is this a probationary user?"), readonly=True))
609 is_ubuntu_coc_signer = exported(
610 Bool(title=_("Signed Ubuntu Code of Conduct"),
611 readonly=True))
612@@ -879,7 +922,6 @@
613 exported_as='team_owner')
614 teamownerID = Int(title=_("The Team Owner's ID or None"), required=False,
615 readonly=True)
616-
617 preferredemail = exported(
618 Reference(title=_("Preferred email address"),
619 description=_("The preferred email address for this person. "
620@@ -916,9 +958,6 @@
621 "this is set to None, then this Person has not been merged "
622 "into another and is still valid"))
623
624- # title is required for the Launchpad Page Layout main template
625- title = Attribute('Person Page Title')
626-
627 archive = exported(
628 Reference(
629 title=_("Default PPA"),
630@@ -987,18 +1026,6 @@
631 readonly=True, required=False,
632 value_type=Reference(schema=Interface))) # HWSubmission
633
634- # This is redefined from IPrivacy.private because the attribute is
635- # read-only. It is a summary of the team's visibility.
636- private = exported(Bool(
637- title=_("This team is private"),
638- readonly=True, required=False,
639- description=_("Private teams are visible only to "
640- "their members.")))
641-
642- is_merge_pending = exported(Bool(
643- title=_("Is this person due to be merged with another?"),
644- required=False, default=False))
645-
646 administrated_teams = Attribute(
647 u"the teams that this person/team is an administrator of.")
648
649@@ -1463,32 +1490,6 @@
650 :return: a boolean.
651 """
652
653-
654-class IPersonLimitedView(Interface):
655- """IPerson attributes that require launchpad.LimitedView permission."""
656-
657- name = exported(
658- PersonNameField(
659- title=_('Name'), required=True, readonly=False,
660- constraint=name_validator,
661- description=_(
662- "A short unique name, beginning with a lower-case "
663- "letter or number, and containing only letters, "
664- "numbers, dots, hyphens, or plus signs.")))
665- displayname = exported(
666- StrippedTextLine(
667- title=_('Display Name'), required=True, readonly=False,
668- description=_(
669- "Your name as you would like it displayed throughout "
670- "Launchpad. Most people use their full name here.")),
671- exported_as='display_name')
672- unique_displayname = TextLine(
673- title=_('Return a string of the form $displayname ($name).'))
674-
675-
676-class IPersonViewRestricted(Interface):
677- """IPerson attributes that require launchpad.View permission."""
678-
679 active_member_count = Attribute(
680 "The number of real people who are members of this team.")
681 # activemembers.value_type.schema will be set to IPerson once
682@@ -2565,18 +2566,19 @@
683 ]:
684 IPersonViewRestricted[name].value_type.schema = IPerson
685
686-IPersonPublic['sub_teams'].value_type.schema = ITeam
687-IPersonPublic['super_teams'].value_type.schema = ITeam
688+IPersonViewRestricted['sub_teams'].value_type.schema = ITeam
689+IPersonViewRestricted['super_teams'].value_type.schema = ITeam
690 # XXX: salgado, 2008-08-01: Uncomment these when teams_*participated_in are
691 # exported again.
692-# IPersonPublic['teams_participated_in'].value_type.schema = ITeam
693-# IPersonPublic['teams_indirectly_participated_in'].value_type.schema = ITeam
694+# IPersonViewRestricted['teams_participated_in'].value_type.schema = ITeam
695+# IPersonViewRestricted[
696+# 'teams_indirectly_participated_in'].value_type.schema = ITeam
697
698 # Fix schema of operation parameters. We need zope.deferredimport!
699 params_to_fix = [
700 # XXX: salgado, 2008-08-01: Uncomment these when they are exported again.
701- # (IPersonPublic['findPathToTeam'], 'team'),
702- # (IPersonPublic['inTeam'], 'team'),
703+ # (IPersonViewRestricted['findPathToTeam'], 'team'),
704+ # (IPersonViewRestricted['inTeam'], 'team'),
705 (IPersonEditRestricted['join'], 'team'),
706 (IPersonEditRestricted['leave'], 'team'),
707 (IPersonEditRestricted['addMember'], 'person'),
708
709=== modified file 'lib/lp/registry/interfaces/role.py'
710--- lib/lp/registry/interfaces/role.py 2011-12-12 02:45:10 +0000
711+++ lib/lp/registry/interfaces/role.py 2011-12-12 05:28:08 +0000
712@@ -119,7 +119,8 @@
713 def inTeam(team):
714 """Is this person a member or the owner of `team`?
715
716- Passed through to the same method in 'IPersonPublic'.
717+ Passed through to the *unproxied* same method in
718+ `IPersonViewRestricted`.
719 """
720
721 def isOwner(obj):
722
723=== modified file 'lib/lp/registry/model/personroles.py'
724--- lib/lp/registry/model/personroles.py 2011-12-12 02:45:10 +0000
725+++ lib/lp/registry/model/personroles.py 2011-12-12 05:28:08 +0000
726@@ -11,6 +11,7 @@
727 getUtility,
728 )
729 from zope.interface import implements
730+from zope.security.proxy import removeSecurityProxy
731
732 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
733 from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
734@@ -29,7 +30,8 @@
735 def __init__(self, person):
736 self.person = person
737 self._celebrities = getUtility(ILaunchpadCelebrities)
738- self.inTeam = self.person.inTeam
739+ # Use an unproxied inTeam() method for security checks.
740+ self.inTeam = removeSecurityProxy(self.person).inTeam
741
742 def __getattr__(self, name):
743 """Handle all in_* attributes."""
744@@ -39,7 +41,7 @@
745 raise AttributeError(errortext)
746 attribute = name[len(prefix):]
747 try:
748- return self.person.inTeam(getattr(self._celebrities, attribute))
749+ return self.inTeam(getattr(self._celebrities, attribute))
750 except AttributeError:
751 raise AttributeError(errortext)
752
753@@ -49,28 +51,28 @@
754
755 def isOwner(self, obj):
756 """See IPersonRoles."""
757- return self.person.inTeam(obj.owner)
758+ return self.inTeam(obj.owner)
759
760 def isBugSupervisor(self, obj):
761 """See IPersonRoles."""
762 return (IHasBugSupervisor.providedBy(obj)
763- and self.person.inTeam(obj.bug_supervisor))
764+ and self.inTeam(obj.bug_supervisor))
765
766 def isSecurityContact(self, obj):
767 """See IPersonRoles."""
768 return (IHasSecurityContact.providedBy(obj)
769- and self.person.inTeam(obj.security_contact))
770+ and self.inTeam(obj.security_contact))
771
772 def isDriver(self, obj):
773 """See IPersonRoles."""
774- return self.person.inTeam(obj.driver)
775+ return self.inTeam(obj.driver)
776
777 def isOneOfDrivers(self, obj):
778 """See IPersonRoles."""
779 if not IHasDrivers.providedBy(obj):
780 return self.isDriver(obj)
781 for driver in obj.drivers:
782- if self.person.inTeam(driver):
783+ if self.inTeam(driver):
784 return True
785 return False
786
787@@ -78,6 +80,6 @@
788 """See IPersonRoles."""
789 for attr in attributes:
790 role = getattr(obj, attr)
791- if self.person.inTeam(role):
792+ if self.inTeam(role):
793 return True
794 return False
795
796=== modified file 'lib/lp/registry/stories/webservice/xx-derivedistroseries.txt'
797--- lib/lp/registry/stories/webservice/xx-derivedistroseries.txt 2011-12-12 02:45:10 +0000
798+++ lib/lp/registry/stories/webservice/xx-derivedistroseries.txt 2011-12-12 05:28:08 +0000
799@@ -19,6 +19,7 @@
800 >>> soyuz_team = factory.makeTeam(
801 ... name='soyuz-team',
802 ... subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
803+ >>> soyuz_team_owner = soyuz_team.teamowner
804 >>> parent_series = factory.makeDistroSeries(name="parentseries")
805 >>> arch = factory.makeDistroArchSeries(distroseries=parent_series)
806 >>> removeSecurityProxy(parent_series).nominatedarchindep = arch
807@@ -41,7 +42,7 @@
808 >>> logout()
809
810 >>> soyuz_team_webservice = webservice_for_person(
811- ... soyuz_team.teamowner, permission=OAuthPermission.WRITE_PUBLIC)
812+ ... soyuz_team_owner, permission=OAuthPermission.WRITE_PUBLIC)
813
814
815 Calling
816
817=== modified file 'lib/lp/registry/tests/test_person_vocabularies.py'
818--- lib/lp/registry/tests/test_person_vocabularies.py 2011-12-12 02:45:10 +0000
819+++ lib/lp/registry/tests/test_person_vocabularies.py 2011-12-12 05:28:08 +0000
820@@ -311,9 +311,10 @@
821 def test_private_team_cannot_be_a_member_of_itself(self):
822 # A private team should be filtered by the vocab.extra_clause
823 # when provided a search term.
824+ owner = self.factory.makePerson()
825 team = self.factory.makeTeam(
826- visibility=PersonVisibility.PRIVATE)
827- login_person(team.teamowner)
828+ owner=owner, visibility=PersonVisibility.PRIVATE)
829+ login_person(owner)
830 self.assertNotIn(team, self.searchVocabulary(team, team.name))
831
832
833
834=== modified file 'lib/lp/registry/tests/test_team_webservice.py'
835--- lib/lp/registry/tests/test_team_webservice.py 2011-12-12 02:45:10 +0000
836+++ lib/lp/registry/tests/test_team_webservice.py 2011-12-12 05:28:08 +0000
837@@ -5,8 +5,6 @@
838
839 import httplib
840
841-from zope.security.proxy import removeSecurityProxy
842-
843 from lazr.restfulclient.errors import HTTPError
844
845 from canonical.testing.layers import DatabaseFunctionalLayer
846@@ -79,8 +77,9 @@
847 # Calling person.join with a team that has an open membership
848 # subscription policy should add that that user to the team.
849 self.person = self.factory.makePerson(name='test-person')
850- self.team = self.factory.makeTeam(name='test-team')
851- login_person(self.team.teamowner)
852+ owner = self.factory.makePerson()
853+ self.team = self.factory.makeTeam(name='test-team', owner=owner)
854+ login_person(owner)
855 self.team.subscriptionpolicy = TeamSubscriptionPolicy.OPEN
856 logout()
857
858@@ -88,7 +87,7 @@
859 test_person = launchpad.people['test-person']
860 test_team = launchpad.people['test-team']
861 test_person.join(team=test_team.self_link)
862- login_person(self.team.teamowner)
863+ login_person(owner)
864 self.assertEqual(
865 ['test-team'],
866 [membership.team.name
867
868=== modified file 'lib/lp/soyuz/tests/test_archive_subscriptions.py'
869--- lib/lp/soyuz/tests/test_archive_subscriptions.py 2011-12-12 02:45:10 +0000
870+++ lib/lp/soyuz/tests/test_archive_subscriptions.py 2011-12-12 05:28:08 +0000
871@@ -13,6 +13,7 @@
872 TestCaseWithFactory,
873 )
874 from lp.testing.mail_helpers import pop_notifications
875+from lp.testing.views import create_initialized_view
876
877
878 class TestArchiveSubscriptions(TestCaseWithFactory):
879@@ -26,9 +27,11 @@
880 def setUp(self):
881 """Create a test archive."""
882 super(TestArchiveSubscriptions, self).setUp()
883+ self.owner = self.factory.makePerson()
884 self.private_team = self.factory.makeTeam(
885- visibility=PersonVisibility.PRIVATE, name="subscribertest")
886- login_person(self.private_team.teamowner)
887+ visibility=PersonVisibility.PRIVATE,
888+ name="subscribertest", owner=self.owner)
889+ login_person(self.owner)
890 self.archive = self.factory.makeArchive(
891 private=True, owner=self.private_team)
892 self.subscriber = self.factory.makePerson()
893@@ -45,7 +48,7 @@
894 login_person(self.subscriber)
895 self.assertRaises(Unauthorized, get_name)
896
897- login_person(self.private_team.teamowner)
898+ login_person(self.owner)
899 self.archive.newSubscription(
900 self.subscriber, registrant=self.archive.owner)
901
902@@ -53,6 +56,33 @@
903 login_person(self.subscriber)
904 self.assertEqual(self.archive.owner.name, "subscribertest")
905
906+ def test_subscriber_can_browse_private_team_ppa(self):
907+ # As per bug 597783, we need to make sure a subscriber can see
908+ # a private team's PPA after they have been given a subscription.
909+ # This test ensures the subscriber can correctly load the PPA's view,
910+ # thus ensuring that all attributes necessary to render the view have
911+ # the necessary security permissions.
912+
913+ # Before a subscription, accessing the view name will raise.
914+ login_person(self.subscriber)
915+ view = create_initialized_view(
916+ self.archive, '+index', principal=self.subscriber)
917+ self.assertRaises(Unauthorized, view.render)
918+
919+ login_person(self.owner)
920+ self.archive.newSubscription(
921+ self.subscriber, registrant=self.archive.owner)
922+
923+ # When a subscription exists, it's fine.
924+ login_person(self.subscriber)
925+ self.assertIn(self.archive.displayname, view.render())
926+
927+ # Just to double check, by default, the subscriber still can't see the
928+ # +packages view which requires extra permissions.
929+ self.assertRaises(
930+ Unauthorized, create_initialized_view,
931+ self.archive, '+packages', principal=self.subscriber)
932+
933 def test_new_subscription_sends_email(self):
934 # Creating a new subscription sends an email to all members
935 # of the person or team subscribed.
936
937=== modified file 'lib/lp/testing/factory.py'
938--- lib/lp/testing/factory.py 2011-12-12 02:45:10 +0000
939+++ lib/lp/testing/factory.py 2011-12-12 05:28:08 +0000
940@@ -772,7 +772,7 @@
941 address, person, email_status, account)
942
943 def makeTeam(self, owner=None, displayname=None, email=None, name=None,
944- description=None,
945+ description=None, icon=None, logo=None,
946 subscription_policy=TeamSubscriptionPolicy.OPEN,
947 visibility=None, members=None):
948 """Create and return a new, arbitrary Team.
949@@ -783,9 +783,11 @@
950 :param displayname: The team's display name. If not given we'll use
951 the auto-generated name.
952 :param description: Team team's description.
953- :type string:
954+ :type description string:
955 :param email: The email address to use as the team's contact address.
956 :type email: string
957+ :param icon: The team's icon.
958+ :param logo: The team's logo.
959 :param subscription_policy: The subscription policy of the team.
960 :type subscription_policy: `TeamSubscriptionPolicy`
961 :param visibility: The team's visibility. If it's None, the default
962@@ -810,15 +812,19 @@
963 team = getUtility(IPersonSet).newTeam(
964 owner, name, displayname, teamdescription=description,
965 subscriptionpolicy=subscription_policy)
966+ naked_team = removeSecurityProxy(team)
967 if visibility is not None:
968 # Visibility is normally restricted to launchpad.Commercial, so
969 # removing the security proxy as we don't care here.
970- removeSecurityProxy(team).visibility = visibility
971+ naked_team.visibility = visibility
972 if email is not None:
973 team.setContactAddress(
974 getUtility(IEmailAddressSet).new(email, team))
975+ if icon is not None:
976+ naked_team.icon = icon
977+ if logo is not None:
978+ naked_team.logo = logo
979 if members is not None:
980- naked_team = removeSecurityProxy(team)
981 for member in members:
982 naked_team.addMember(member, owner)
983 return team
984@@ -1124,7 +1130,7 @@
985
986 if registrant is None:
987 if owner.is_team:
988- registrant = owner.teamowner
989+ registrant = removeSecurityProxy(owner).teamowner
990 else:
991 registrant = owner
992