Merge lp:~jcsackett/launchpad/bad-bugtasks-how-dare-you-break-details 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: 15071
Proposed branch: lp:~jcsackett/launchpad/bad-bugtasks-how-dare-you-break-details
Merge into: lp:launchpad
Diff against target: 195 lines (+77/-31)
2 files modified
lib/lp/registry/browser/pillar.py (+46/-23)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+31/-8)
To merge this branch: bzr merge lp:~jcsackett/launchpad/bad-bugtasks-how-dare-you-break-details
Reviewer Review Type Date Requested Status
Steve Kowalik (community) code Approve
Review via email: mp+100677@code.launchpad.net

Commit message

Filters out bugs the user doesn't have authorization to see in the sharing details view.

Description of the change

Summary
=======
The sharing details view currently attempts to show all shared bugs in a
project. However, as the details view requires a permission of
`launchpad.Driver`, bugs exist that cannot be seen by everyone who can see
sharing details.

To remedy this, bugs should be filtered using the searching machinery to
safely filter out anything that the user shouldn't be able to see.

Preimp
======
Spoke with William Grant.

Additional discussion can be found in the #launchpad-dev irclogs. [1]

Implementation
==============

The bug ids found by querying the flattened artifact source are now passed
into a new method which uses `BugTaskSet.search` to get back a list of safe
bugtasks. These are then used to build the mustache data.

Additionally, testing this branch demonstrated that if an artifact was shared
via two policies, it would be listed twice; as a drive by, this branch
switches to using sets to accumulate the artifacts rather than lists,
preventing duplication.

Tests
=====
bin/test -vvct pillar_sharing

QA
==
Open https://qastaging.launchpad.net/launchpad/+sharingdetails/adeuring. It
should open safely, showing only bugs you, as the user, should be able to see.

Lint
====

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/browser/pillar.py
  lib/lp/registry/browser/tests/test_pillar_sharing.py

[1]: http://irclogs.ubuntu.com/2012/04/02/%23launchpad-dev.html#t03:28

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) wrote :

This looks like great work, my only concerns are the imports you've added on lines 11 to 17 of the diff -- I'd like to see a comment explaining circular imports, and I really don't think the zope import belongs there at all. Please try and move the imports if you can, but I won't block on it.

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

Oh lord, those were put there as expedient with the notion I'd move them; I don't believe it's a circular issue at all, just a miss on my part. I'll move those before landing, thanks.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/pillar.py'
2--- lib/lp/registry/browser/pillar.py 2012-04-05 18:47:58 +0000
3+++ lib/lp/registry/browser/pillar.py 2012-04-09 20:45:27 +0000
4@@ -44,13 +44,23 @@
5 StructuralSubscriptionMenuMixin,
6 )
7 from lp.bugs.interfaces.bug import IBug
8+from lp.bugs.interfaces.bugtask import (
9+ BugTaskSearchParams,
10+ IBugTaskSet,
11+ )
12 from lp.code.interfaces.branch import IBranch
13+from lp.registry.interfaces.accesspolicy import (
14+ IAccessPolicyGrantFlatSource,
15+ IAccessPolicySource,
16+ )
17+from lp.registry.interfaces.distribution import IDistribution
18 from lp.registry.interfaces.distributionsourcepackage import (
19 IDistributionSourcePackage,
20 )
21 from lp.registry.interfaces.distroseries import IDistroSeries
22 from lp.registry.interfaces.person import IPersonSet
23 from lp.registry.interfaces.pillar import IPillar
24+from lp.registry.interfaces.product import IProduct
25 from lp.registry.interfaces.projectgroup import IProjectGroup
26 from lp.registry.model.pillar import PillarPerson
27 from lp.services.config import config
28@@ -400,21 +410,43 @@
29 cache.objects['sharing_write_enabled'] = (write_flag_enabled
30 and check_permission('launchpad.Edit', self.pillar))
31
32+ def _getSafeBugs(self, bugs):
33+ """Uses the bugsearch tools to safely get the list of bugs the user is
34+ allowed to see."""
35+ if bugs == set([]):
36+ return []
37+ params = []
38+ for b in bugs:
39+ param = BugTaskSearchParams(user=self.user, bug=b)
40+ if IProduct.providedBy(self.pillar):
41+ param.setProduct(self.pillar)
42+ elif IDistribution.providedBy(self.pillar):
43+ param.setDistribution(self.pillar)
44+ params.append(param)
45+
46+ safe_bugs = getUtility(IBugTaskSet).search(params[0], *params[1:])
47+ return list(safe_bugs)
48+
49 def _loadSharedArtifacts(self):
50- bugs = []
51- branches = []
52+ # As a concrete can by linked via more than one policy, we use sets to
53+ # filter out dupes.
54+ bugs = set()
55+ branches = set()
56 for artifact in self.sharing_service.getSharedArtifacts(
57 self.pillar, self.person):
58 concrete = artifact.concrete_artifact
59 if IBug.providedBy(concrete):
60- bugs.append(concrete)
61+ bugs.add(concrete)
62 elif IBranch.providedBy(concrete):
63- branches.append(concrete)
64+ branches.add(concrete)
65
66- self.bugs = bugs
67+ # For security reasons, the bugs have to be refetched by ID through
68+ # the normal querying mechanism. This prevents bugs the user shouldn't
69+ # be able to see from being displayed.
70+ self.bugs = self._getSafeBugs(bugs)
71 self.branches = branches
72- self.shared_bugs_count = len(bugs)
73- self.shared_branches_count = len(branches)
74+ self.shared_bugs_count = len(self.bugs)
75+ self.shared_branches_count = len(self.branches)
76
77 def _build_branch_template_data(self, branches, request):
78 branch_data = []
79@@ -426,26 +458,17 @@
80 branch_id=branch.id))
81 return branch_data
82
83- def _build_bug_template_data(self, bugs, request):
84+ def _build_bug_template_data(self, bugtasks, request):
85 bug_data = []
86- for bug in bugs:
87- [bugtask] = [task for task in bug.bugtasks if
88- task.target == self.pillar]
89- if bugtask is not None:
90- web_link = canonical_url(bugtask, path_only_if_possible=True)
91- self_link = absoluteURL(bug, request)
92- importance = bugtask.importance.title.lower()
93- else:
94- # This shouldn't ever happen, but if it does there's no reason
95- # to crash.
96- web_link = canonical_url(bug, path_only_if_possible=True)
97- self_link = absoluteURL(bug, request)
98- importance = bug.default_bugtask.importance.title.lower()
99+ for bugtask in bugtasks:
100+ web_link = canonical_url(bugtask, path_only_if_possible=True)
101+ self_link = absoluteURL(bugtask.bug, request)
102+ importance = bugtask.importance.title.lower()
103
104 bug_data.append(dict(
105 self_link=self_link,
106 web_link=web_link,
107- bug_summary=bug.title,
108- bug_id=bug.id,
109+ bug_summary=bugtask.bug.title,
110+ bug_id=bugtask.bug.id,
111 bug_importance=importance))
112 return bug_data
113
114=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
115--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-04-05 18:47:58 +0000
116+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-04-09 20:45:27 +0000
117@@ -54,14 +54,15 @@
118
119 layer = DatabaseFunctionalLayer
120
121- def getPillarPerson(self, person=None, with_sharing=True):
122- if person is None:
123- person = self.factory.makePerson()
124- if with_sharing:
125+ def _create_sharing(self, grantee, security=False):
126+ if security:
127+ owner = self.factory.makePerson()
128+ else:
129+ owner = self.pillar.owner
130 if self.pillar_type == 'product':
131 self.bug = self.factory.makeBug(
132 product=self.pillar,
133- owner=self.pillar.owner,
134+ owner=owner,
135 private=True)
136 self.branch = self.factory.makeBranch(
137 product=self.pillar,
138@@ -71,25 +72,46 @@
139 self.branch = None
140 self.bug = self.factory.makeBug(
141 distribution=self.pillar,
142- owner=self.pillar.owner,
143+ owner=owner,
144 private=True)
145 artifact = self.factory.makeAccessArtifact(concrete=self.bug)
146 policy = self.factory.makeAccessPolicy(pillar=self.pillar)
147 self.factory.makeAccessPolicyArtifact(
148 artifact=artifact, policy=policy)
149 self.factory.makeAccessArtifactGrant(
150- artifact=artifact, grantee=person, grantor=self.pillar.owner)
151+ artifact=artifact, grantee=grantee, grantor=self.pillar.owner)
152+
153 if self.branch:
154 artifact = self.factory.makeAccessArtifact(
155 concrete=self.branch)
156 self.factory.makeAccessPolicyArtifact(
157 artifact=artifact, policy=policy)
158 self.factory.makeAccessArtifactGrant(
159- artifact=artifact, grantee=person,
160+ artifact=artifact, grantee=grantee,
161 grantor=self.pillar.owner)
162
163+ def getPillarPerson(self, person=None, with_sharing=True):
164+ if person is None:
165+ person = self.factory.makePerson()
166+ if with_sharing:
167+ self._create_sharing(person)
168+
169 return PillarPerson(self.pillar, person)
170
171+ def test_view_filters_security_wisely(self):
172+ # There are bugs in the sharingdetails view that not everyone with
173+ # `launchpad.Driver` -- the permission level for the page -- should be
174+ # able to see.
175+ with FeatureFixture(DETAILS_ENABLED_FLAG):
176+ pillarperson = self.getPillarPerson(with_sharing=False)
177+ self._create_sharing(grantee=pillarperson.person, security=True)
178+ view = create_initialized_view(pillarperson, '+index')
179+ # The page loads
180+ self.assertEqual(pillarperson.person.displayname, view.page_title)
181+ # The bug, which is not shared with the owner, is not included.
182+
183+ self.assertEqual(0, view.shared_bugs_count)
184+
185 def test_view_traverses_plus_sharingdetails(self):
186 # The traversed url in the app is pillar/+sharing/person
187 with FeatureFixture(DETAILS_ENABLED_FLAG):
188@@ -130,6 +152,7 @@
189 pillarperson = self.getPillarPerson()
190 view = create_initialized_view(pillarperson, '+index')
191 self.assertEqual(pillarperson.person.displayname, view.page_title)
192+ self.assertEqual(1, view.shared_bugs_count)
193
194 def test_view_data_model(self):
195 # Test that the json request cache contains the view data model.