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

Proposed by Ian Booth
Status: Merged
Merged at revision: 14477
Proposed branch: lp:~wallyworld/launchpad/private-owned-entity-traversal
Merge into: lp:launchpad
Diff against target: 263 lines (+118/-38)
5 files modified
lib/canonical/launchpad/security.py (+38/-27)
lib/lp/app/browser/launchpad.py (+2/-2)
lib/lp/app/tests/test_tales.py (+30/-5)
lib/lp/registry/doc/private-team-visibility.txt (+38/-0)
lib/lp/testing/factory.py (+10/-4)
To merge this branch: bzr merge lp:~wallyworld/launchpad/private-owned-entity-traversal
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+84048@code.launchpad.net

Commit message

Users with access to private team's PPAs or branches have limited view permission on the team.

Description of the change

Users with access to private team's PPAs or branches have limited view permission on the team. This allows traversal to the branch or PPPA and also the team's name and URL to be known.

== Implementation ==

1. Change the traversal rules so that the "launchpad.LimitedView" permission is required to traverse private teams.
2. Enhance the private team security adaptor to check that the users with access to the team's PPAs or branches are authorised.

This change removes the hack put in place for bug 597783 which allowed subscribers to the private team's PPA to also see info they shouldn't have like team members etc.

== Tests ==

Re-run the TestArchiveSubscriptions.test_subscriber_can_access_private_team_ppa() test.
Add to the private-team-visibility.txt doc test.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/security.py
  lib/lp/app/browser/launchpad.py
  lib/lp/registry/doc/private-team-visibility.txt

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

I changed the archive bug you are fixing because the older bug was about not having access to the archive. The newer bug is about having access to non-traversed objects. I think your implementation is a good foundation that we can extend.

We might be missing a test. I like your additions to the doctest that we use to quickly say yes or no to a private team relationship. I see Julian's lib/lp/soyuz/tests/test_archive_subscriptions.py test that verifies access to team.name, which was the the *first* attr the code tried to access then oopsed. I do not see a test that verifies either the archive page or the branch page will render in this scenario. Look at
    https://launchpad.net/~launchpad/+archive/ppa
and
    https://code.launchpad.net/~launchpad/launchpad-help-moin-theme/moin_launchpad

In both pages I see the team *icon* with the team displayname that is a link made using the name. I do not certain limitedview permits me to see icon. Maybe we should also grant icon to limited view because we often use it with name and displayname. Does the tales formatter need to be aware that it cannot use an icon if the user has only LimiteView?

> === modified file 'lib/canonical/launchpad/security.py'
> --- lib/canonical/launchpad/security.py 2011-11-22 13:36:28 +0000
> +++ lib/canonical/launchpad/security.py 2011-12-01 15:21:25 +0000
...
> +class PublicOrPrivateTeamsExistence(AuthorizationBase):
> + """Restrict knowing about private teams' existence.
> +
> + Knowing the existence of a private team allow traversing to its URL and
> + displaying basic information like name, displayname.
> + """
> + permission = 'launchpad.LimitedView'
> + usedfor = IPersonLimitedView
> +
> + def checkUnauthenticated(self):
> + """Unauthenticated users can only view public teams."""
> + if self.obj.visibility == PersonVisibility.PUBLIC:
> + return True
> + return False
> +
> + def checkAuthenticated(self, user):
> + """By default, we simply perform a View permission check.
> +
> + We also grant limited viewability to users who can see PPAs and
> + branches owned by the team. In other scenarios, the context in which
> + the permission is required is responsible for pre-caching the
> + launchpad.LimitedView permission on each team which requires it.
> + """
> + if self.forwardCheckAuthenticated(
> + user, self.obj, 'launchpad.View'):
> + return True

checkAuthenticated returns false by default and I was puzzled. I think this guard
needs a comment to ensure dimwitted people like myself know that the forward
check covers public teams and users, which defaults to True.

        if self.forwardCheckAuthenticated(user, self.obj, 'launchpad.View'):
            # The user has full visibility of the user, the public team,
            # or has view permission of the private team.
            return True

> if (self.obj.is_team
> and self.obj.visibility == PersonVisibility.PRIVATE):
>
> + # Grant visibility to people with subscriptions to branches owned
> + # by the private team.
> + owned_branches = ge...

Read more...

review: Needs Information (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

We spoke about this and agreed we need only one test is needed to verify our tales code handles limitedview of a private team. We expect this to fail because the link formatter wants access to the icon (which is absent from the existing tests of private teams being rendered) and the IPrimaryContext tales adapter to render the logo. We want allow users with limitedview to see name, displayname, icon, and logo.

review: Approve (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

I moved this back to needs review because we need to check the interaction with IPersonPublic.

Revision history for this message
Curtis Hovey (sinzui) wrote :

We want IPersonPublic and ITeamPublic to require launchpad.View to ensure information is not revealed when the user has launchpad.LimitedView

review: Needs Fixing (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Ian.

I have a working version of you branch that I am trying to land. Buildbot has been in a bad state. I landed to cleanup branches to address brittle/faulty tests that your attempt to refactor IPersonPublic revealed. My branch updates some authentication code to know it could be working with a secured person, and updates a dozen tests that could not login because it could not access the private team. I will land your other branch too if I can get my branch landed before your Monday.

Revision history for this message
Curtis Hovey (sinzui) wrote :

The branch was merged with my interface and test fixes.

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/security.py'
2--- lib/canonical/launchpad/security.py 2011-11-22 13:36:28 +0000
3+++ lib/canonical/launchpad/security.py 2011-12-02 22:51:25 +0000
4@@ -67,6 +67,7 @@
5 IBranch,
6 user_has_special_branch_access,
7 )
8+from lp.code.interfaces.branchcollection import IBranchCollection
9 from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
10 from lp.code.interfaces.branchmergequeue import IBranchMergeQueue
11 from lp.code.interfaces.codeimport import ICodeImport
12@@ -800,6 +801,35 @@
13 if (invitee.is_team and
14 invitee in user.person.getAdministratedTeams()):
15 return True
16+ return False
17+
18+
19+class PublicOrPrivateTeamsExistence(AuthorizationBase):
20+ """Restrict knowing about private teams' existence.
21+
22+ Knowing the existence of a private team allow traversing to its URL and
23+ displaying basic information like name, displayname.
24+ """
25+ permission = 'launchpad.LimitedView'
26+ usedfor = IPersonLimitedView
27+
28+ def checkUnauthenticated(self):
29+ """Unauthenticated users can only view public teams."""
30+ if self.obj.visibility == PersonVisibility.PUBLIC:
31+ return True
32+ return False
33+
34+ def checkAuthenticated(self, user):
35+ """By default, we simply perform a View permission check.
36+
37+ We also grant limited viewability to users who can see PPAs and
38+ branches owned by the team. In other scenarios, the context in which
39+ the permission is required is responsible for pre-caching the
40+ launchpad.LimitedView permission on each team which requires it.
41+ """
42+ if self.forwardCheckAuthenticated(
43+ user, self.obj, 'launchpad.View'):
44+ return True
45
46 if (self.obj.is_team
47 and self.obj.visibility == PersonVisibility.PRIVATE):
48@@ -813,33 +843,14 @@
49 ppa.id for ppa in self.obj.ppas if ppa.private)
50 if len(subscriber_archive_ids.intersection(team_ppa_ids)) > 0:
51 return True
52- return False
53-
54-
55-class PublicOrPrivateTeamsExistence(AuthorizationBase):
56- """Restrict knowing about private teams' existence.
57-
58- Knowing the existence of a private team allow traversing to its URL and
59- displaying basic information like name, displayname.
60- """
61- permission = 'launchpad.LimitedView'
62- usedfor = IPersonLimitedView
63-
64- def checkUnauthenticated(self):
65- """Unauthenticated users can only view public teams."""
66- if self.obj.visibility == PersonVisibility.PUBLIC:
67- return True
68- return False
69-
70- def checkAuthenticated(self, user):
71- """By default, we simply perform a View permission check.
72-
73- The context in which the permission is required is
74- responsible for pre-caching the launchpad.LimitedView permission on
75- each team which requires it.
76- """
77- return self.forwardCheckAuthenticated(
78- user, self.obj, 'launchpad.View')
79+
80+ # Grant visibility to people with subscriptions to branches owned
81+ # by the private team.
82+ owned_branches = getUtility(IBranchCollection).ownedBy(self.obj)
83+ if owned_branches.visibleByUser(user.person).count() > 0:
84+ return True
85+
86+ return False
87
88
89 class EditPollByTeamOwnerOrTeamAdminsOrAdmins(
90
91=== modified file 'lib/lp/app/browser/launchpad.py'
92--- lib/lp/app/browser/launchpad.py 2011-11-30 14:41:22 +0000
93+++ lib/lp/app/browser/launchpad.py 2011-12-02 22:51:25 +0000
94@@ -738,8 +738,8 @@
95 # Check to see if this is a team, and if so, whether the
96 # logged in user is allowed to view the team, by virtue of
97 # team membership or Launchpad administration.
98- if (person.is_team
99- and not check_permission('launchpad.View', person)):
100+ if (person.is_team and
101+ not check_permission('launchpad.LimitedView', person)):
102 raise NotFound(self.context, name)
103 # Only admins are permitted to see suspended users.
104 if person.account_status == AccountStatus.SUSPENDED:
105
106=== modified file 'lib/lp/app/tests/test_tales.py'
107--- lib/lp/app/tests/test_tales.py 2011-11-16 00:09:49 +0000
108+++ lib/lp/app/tests/test_tales.py 2011-12-02 22:51:25 +0000
109@@ -155,12 +155,16 @@
110 A user must have launchpad.LimitedView permission to use
111 TestTalesFormatterAPI with private teams.
112 """
113- layer = DatabaseFunctionalLayer
114+ layer = LaunchpadFunctionalLayer
115
116 def setUp(self):
117 super(TestTalesFormatterAPI, self).setUp()
118+ icon = self.factory.makeLibraryFileAlias(
119+ filename='smurf.png', content_type='image/png')
120+ logo = self.factory.makeLibraryFileAlias(
121+ filename='papa.png', content_type='image/png')
122 self.team = self.factory.makeTeam(
123- name='team', displayname='a team',
124+ name='team', displayname='a team', icon=icon, logo=logo,
125 visibility=PersonVisibility.PRIVATE)
126
127 def _make_formatter(self, cache_permission=False):
128@@ -175,10 +179,11 @@
129 request, 'launchpad.LimitedView', [self.team])
130 return formatter, request, any_person
131
132- def _tales_value(self, attr, request):
133+ def _tales_value(self, attr, request, path='fmt'):
134 # Evaluate the given formatted attribute value on team.
135- return test_tales(
136- "team/fmt:%s" % attr, team=self.team, request=request)
137+ result = test_tales(
138+ "team/%s:%s" % (path, attr), team=self.team, request=request)
139+ return result
140
141 def _test_can_view_attribute_no_login(self, attr, hidden=None):
142 # Test attribute access with no login.
143@@ -228,6 +233,26 @@
144 def test_can_view_url(self):
145 self._test_can_view_attribute('url')
146
147+ def test_can_view_icon(self):
148+ # Any user can view private team icons so there's no need to set up
149+ # launchpad.LimitedView permissions to test that.
150+ formatter, request, ignore = self._make_formatter()
151+ value = self._tales_value('icon', request)
152+ self.assertEqual(
153+ '<img src="%s" width="14" height="14" />'
154+ % self.team.icon.http_url,
155+ value)
156+
157+ def test_can_view_logo(self):
158+ # Any user can view private team logos so there's no need to set up
159+ # launchpad.LimitedView permissions to test that.
160+ formatter, request, ignore = self._make_formatter()
161+ value = self._tales_value('logo', request, 'image')
162+ self.assertEqual(
163+ '<img alt="" width="64" height="64" src="%s" />'
164+ % self.team.logo.http_url,
165+ value)
166+
167
168 class TestObjectFormatterAPI(TestCaseWithFactory):
169 """Tests for ObjectFormatterAPI"""
170
171=== modified file 'lib/lp/registry/doc/private-team-visibility.txt'
172--- lib/lp/registry/doc/private-team-visibility.txt 2011-11-16 00:09:49 +0000
173+++ lib/lp/registry/doc/private-team-visibility.txt 2011-12-02 22:51:25 +0000
174@@ -107,3 +107,41 @@
175 ...
176 Unauthorized: (<Person at ... priv-team (Priv Team)>,
177 'name', 'launchpad.LimitedView')
178+
179+A person with visibility to any of the branches owned by the private team will
180+be granted limited view permission on the team.
181+
182+For private branches, a user needs to be subscribed to the branch for the
183+branch (and hence team) to be visible.
184+
185+ >>> private_team_branch = factory.makeBranch(
186+ ... owner=priv_team, private=True)
187+ >>> login_person(pub_member)
188+ >>> check_permission('launchpad.LimitedView', priv_team)
189+ False
190+
191+ >>> login_person(priv_owner)
192+ >>> sub = factory.makeBranchSubscription(
193+ ... branch=private_team_branch, person=pub_member,
194+ ... subscribed_by=priv_owner)
195+
196+ >>> login_person(pub_member)
197+ >>> check_permission('launchpad.LimitedView', priv_team)
198+ True
199+
200+Subscribers to the teams private PPA have limited view permission.
201+
202+ >>> archive = factory.makeArchive(private=True, owner=priv_team)
203+ >>> archive_subscriber = factory.makePerson()
204+ >>> login_person(priv_owner)
205+ >>> sub = archive.newSubscription(
206+ ... archive_subscriber, registrant=archive.owner)
207+
208+ >>> login_person(archive_subscriber)
209+ >>> check_permission('launchpad.LimitedView', priv_team)
210+ True
211+
212+Anonymous users do not have permission.
213+ >>> login(ANONYMOUS)
214+ >>> check_permission('launchpad.LimitedView', priv_team)
215+ False
216
217=== modified file 'lib/lp/testing/factory.py'
218--- lib/lp/testing/factory.py 2011-11-29 16:48:56 +0000
219+++ lib/lp/testing/factory.py 2011-12-02 22:51:25 +0000
220@@ -772,7 +772,7 @@
221 address, person, email_status, account)
222
223 def makeTeam(self, owner=None, displayname=None, email=None, name=None,
224- description=None,
225+ description=None, icon=None, logo=None,
226 subscription_policy=TeamSubscriptionPolicy.OPEN,
227 visibility=None, members=None):
228 """Create and return a new, arbitrary Team.
229@@ -783,9 +783,11 @@
230 :param displayname: The team's display name. If not given we'll use
231 the auto-generated name.
232 :param description: Team team's description.
233- :type string:
234+ :type description string:
235 :param email: The email address to use as the team's contact address.
236 :type email: string
237+ :param icon: The team's icon.
238+ :param logo: The team's logo.
239 :param subscription_policy: The subscription policy of the team.
240 :type subscription_policy: `TeamSubscriptionPolicy`
241 :param visibility: The team's visibility. If it's None, the default
242@@ -810,15 +812,19 @@
243 team = getUtility(IPersonSet).newTeam(
244 owner, name, displayname, teamdescription=description,
245 subscriptionpolicy=subscription_policy)
246+ naked_team = removeSecurityProxy(team)
247 if visibility is not None:
248 # Visibility is normally restricted to launchpad.Commercial, so
249 # removing the security proxy as we don't care here.
250- removeSecurityProxy(team).visibility = visibility
251+ naked_team.visibility = visibility
252 if email is not None:
253 team.setContactAddress(
254 getUtility(IEmailAddressSet).new(email, team))
255+ if icon is not None:
256+ naked_team.icon = icon
257+ if logo is not None:
258+ naked_team.logo = logo
259 if members is not None:
260- naked_team = removeSecurityProxy(team)
261 for member in members:
262 naked_team.addMember(member, owner)
263 return team