Merge lp:~rharding/launchpad/filter_more_products into lp:launchpad

Proposed by Richard Harding on 2012-11-20
Status: Merged
Approved by: Richard Harding on 2012-11-20
Approved revision: no longer in the source branch.
Merged at revision: 16295
Proposed branch: lp:~rharding/launchpad/filter_more_products
Merge into: lp:launchpad
Diff against target: 256 lines (+104/-19)
6 files modified
lib/lp/registry/model/product.py (+1/-1)
lib/lp/translations/browser/translationgroup.py (+13/-0)
lib/lp/translations/interfaces/translationgroup.py (+1/-1)
lib/lp/translations/model/translationgroup.py (+21/-6)
lib/lp/translations/templates/translationgroup-portlet-projects.pt (+7/-11)
lib/lp/translations/tests/test_translationgroup.py (+61/-0)
To merge this branch: bzr merge lp:~rharding/launchpad/filter_more_products
Reviewer Review Type Date Requested Status
Aaron Bentley (community) 2012-11-20 Approve on 2012-11-20
Review via email: mp+135164@code.launchpad.net

Commit Message

Update product filters to prevent leaking non-public products in the translationgroup code.

Description of the Change

= Summary =

A search for products brought up several places to look to make sure we filter
products so that non-public ones are not leaked.

== Pre Implementation ==

None, straight forward case of add filters.

== Implementation Notes ==

ITranslationGroup.products

The query needed to be adjusted to allow for use of the
getproductPrivacyFilter. Added tests for it.

ITranslationGroup.projects

This is actually querying for a list of ProjectGroups which do not have an
information type.

     ProjectGroup.selectBy(translationgroup=self.id, active=True)

For any returned project group you'd then have to query for the products
within it and those are all filtered appropriately.

ITranslationGroup.fetchProjectsForDisplay()

This needed to be updated to add the extra filter. Tests added to verify it
filters correctly.

ITranslationGroup.fetchProjectGroupsForDisplay()

This again is querying for ProjectGroups which fall under the .projects case
above.

ITranslationGroup.top_projects

This uses self.products, self.projects to get data. Since they've been checked
and updated it's safe to use.

== Tests ==

lib/lp/translations/tests/test_translationgroup.py

To post a comment you must log in.
Richard Harding (rharding) wrote :

I've updated the one method that was changed to take a user argument and updated the template and view to provide the user so that it doesn't use the launchbag. However, changing the other ones, which are properties, will cascade into a larger change that I'd prefer was done as a second branch.

Aaron Bentley (abentley) wrote :

Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/model/product.py'
2--- lib/lp/registry/model/product.py 2012-11-19 06:26:32 +0000
3+++ lib/lp/registry/model/product.py 2012-11-20 18:19:28 +0000
4@@ -300,7 +300,7 @@
5 def composeLicensesColumn(cls, for_class=None):
6 """Compose a Storm column specification for licences.
7
8- Use this to render a list of `Product` linkes without querying
9+ Use this to render a list of `Product` links without querying
10 licences for each one individually.
11
12 It lets you prefetch the licensing information in the same
13
14=== modified file 'lib/lp/translations/browser/translationgroup.py'
15--- lib/lp/translations/browser/translationgroup.py 2012-01-01 02:58:52 +0000
16+++ lib/lp/translations/browser/translationgroup.py 2012-11-20 18:19:28 +0000
17@@ -25,6 +25,7 @@
18 )
19 from lp.app.errors import NotFoundError
20 from lp.registry.browser.objectreassignment import ObjectReassignmentView
21+from lp.services.propertycache import cachedproperty
22 from lp.services.webapp import (
23 canonical_url,
24 GetitemNavigation,
25@@ -72,6 +73,18 @@
26 self.translation_groups = getUtility(ITranslationGroupSet)
27 self.user_can_edit = check_permission('launchpad.Edit', self.context)
28
29+ @cachedproperty
30+ def distributions(self):
31+ return self.context.fetchDistrosForDisplay()
32+
33+ @cachedproperty
34+ def projectgroups(self):
35+ return self.context.fetchProjectGroupsForDisplay()
36+
37+ @cachedproperty
38+ def projects(self):
39+ return self.context.fetchProjectsForDisplay(self.user)
40+
41 @property
42 def label(self):
43 return "%s translation group" % self.context.title
44
45=== modified file 'lib/lp/translations/interfaces/translationgroup.py'
46--- lib/lp/translations/interfaces/translationgroup.py 2012-04-16 23:02:44 +0000
47+++ lib/lp/translations/interfaces/translationgroup.py 2012-11-20 18:19:28 +0000
48@@ -138,7 +138,7 @@
49 ordered by language name in English.
50 """
51
52- def fetchProjectsForDisplay():
53+ def fetchProjectsForDisplay(user):
54 """Fetch `Product`s using this group, for display purposes.
55
56 Prefetches display-related properties.
57
58=== modified file 'lib/lp/translations/model/translationgroup.py'
59--- lib/lp/translations/model/translationgroup.py 2011-12-30 06:14:56 +0000
60+++ lib/lp/translations/model/translationgroup.py 2012-11-20 18:19:28 +0000
61@@ -24,6 +24,7 @@
62 )
63 from storm.store import Store
64 from zope.interface import implements
65+from zope.component import getUtility
66
67 from lp.app.errors import NotFoundError
68 from lp.registry.interfaces.person import validate_public_person
69@@ -41,6 +42,7 @@
70 LibraryFileAlias,
71 LibraryFileContent,
72 )
73+from lp.services.webapp.interfaces import ILaunchBag
74 from lp.services.worlddata.model.language import Language
75 from lp.translations.interfaces.translationgroup import (
76 ITranslationGroup,
77@@ -109,9 +111,17 @@
78 def products(self):
79 """See `ITranslationGroup`."""
80 # Avoid circular imports.
81- from lp.registry.model.product import Product
82-
83- return Product.selectBy(translationgroup=self.id, active=True)
84+ from lp.registry.model.product import (
85+ Product,
86+ ProductSet,
87+ )
88+ user = getUtility(ILaunchBag).user
89+ results = Store.of(self).find(
90+ Product,
91+ Product.active==True,
92+ Product.translationgroup==self.id,
93+ ProductSet.getProductPrivacyFilter(user))
94+ return results
95
96 @property
97 def projects(self):
98@@ -182,17 +192,20 @@
99 mapper = lambda row: row[slice(0, 3)]
100 return DecoratedResultSet(translator_data, mapper)
101
102- def fetchProjectsForDisplay(self):
103+ def fetchProjectsForDisplay(self, user):
104 """See `ITranslationGroup`."""
105 # Avoid circular imports.
106 from lp.registry.model.product import (
107 Product,
108+ ProductSet,
109 ProductWithLicenses,
110 )
111
112 using = [
113 Product,
114- LeftJoin(LibraryFileAlias, LibraryFileAlias.id == Product.iconID),
115+ LeftJoin(
116+ LibraryFileAlias,
117+ LibraryFileAlias.id == Product.iconID),
118 LeftJoin(
119 LibraryFileContent,
120 LibraryFileContent.id == LibraryFileAlias.contentID),
121@@ -205,7 +218,9 @@
122 )
123 product_data = ISlaveStore(Product).using(*using).find(
124 columns,
125- Product.translationgroupID == self.id, Product.active == True)
126+ Product.translationgroup == self.id,
127+ Product.active == True,
128+ ProductSet.getProductPrivacyFilter(user))
129 product_data = product_data.order_by(Product.displayname)
130
131 return [
132
133=== modified file 'lib/lp/translations/templates/translationgroup-portlet-projects.pt'
134--- lib/lp/translations/templates/translationgroup-portlet-projects.pt 2010-09-03 06:37:26 +0000
135+++ lib/lp/translations/templates/translationgroup-portlet-projects.pt 2012-11-20 18:19:28 +0000
136@@ -13,30 +13,26 @@
137 <a href="#teams">translation teams</a>.
138 </p>
139
140- <div id="related-projects"
141- tal:define="
142- distributions context/fetchDistrosForDisplay;
143- projectgroups context/fetchProjectGroupsForDisplay;
144- projects context/fetchProjectsForDisplay">
145- <div tal:condition="distributions" style="margin-top:1em;">
146+ <div id="related-projects">
147+ <div tal:condition="view/distributions" style="margin-top:1em;">
148 <h3 style="display: inline;">Distributions:</h3>
149- <tal:distribution repeat="distribution distributions">
150+ <tal:distribution repeat="distribution view/distributions">
151 <a href="#" tal:replace="structure distribution/fmt:link">Ubuntu
152 </a><tal:comma condition="not:repeat/distribution/end">, </tal:comma>
153 </tal:distribution>
154 </div>
155
156- <div tal:condition="projectgroups" style="margin-top:1em;">
157+ <div tal:condition="view/projectgroups" style="margin-top:1em;">
158 <h3 style="display: inline;">Project groups:</h3>
159- <tal:projectgroup repeat="projectgroup projectgroups">
160+ <tal:projectgroup repeat="projectgroup view/projectgroups">
161 <a href="#" tal:replace="structure projectgroup/fmt:link">GNOME
162 </a><tal:comma condition="not:repeat/projectgroup/end">, </tal:comma>
163 </tal:projectgroup>
164 </div>
165
166- <div tal:condition="projects" style="margin-top:1em;">
167+ <div tal:condition="view/projects" style="margin-top:1em;">
168 <h3 style="display: inline;">Projects:</h3>
169- <tal:project repeat="project projects">
170+ <tal:project repeat="project view/projects">
171 <a href="#" tal:replace="structure project/fmt:link">Firefox
172 </a><tal:comma condition="not:repeat/project/end">, </tal:comma>
173 </tal:project>
174
175=== modified file 'lib/lp/translations/tests/test_translationgroup.py'
176--- lib/lp/translations/tests/test_translationgroup.py 2012-01-01 02:58:52 +0000
177+++ lib/lp/translations/tests/test_translationgroup.py 2012-11-20 18:19:28 +0000
178@@ -9,11 +9,13 @@
179 import transaction
180 from zope.component import getUtility
181
182+from lp.app.enums import InformationType
183 from lp.registry.interfaces.teammembership import (
184 ITeamMembershipSet,
185 TeamMembershipStatus,
186 )
187 from lp.testing import (
188+ person_logged_in,
189 TestCaseWithFactory,
190 WebServiceTestCase,
191 )
192@@ -21,6 +23,65 @@
193 from lp.translations.interfaces.translationgroup import ITranslationGroupSet
194
195
196+class TestTranslationGroup(TestCaseWithFactory):
197+
198+ layer = ZopelessDatabaseLayer
199+
200+ def _setup_products(self):
201+ """Helper to setup one public one non-public product."""
202+ user = self.factory.makePerson()
203+ private_owner = self.factory.makePerson()
204+ group = self.factory.makeTranslationGroup()
205+
206+ with person_logged_in(user):
207+ public_product = self.factory.makeProduct()
208+ public_product.translationgroup = group
209+
210+ with person_logged_in(private_owner):
211+ private_product = self.factory.makeProduct(
212+ information_type=InformationType.PROPRIETARY,
213+ owner=private_owner)
214+ private_product.translationgroup = group
215+
216+ return group, user, private_owner
217+
218+ def test_non_public_products_hidden(self):
219+ """Non Public products are not returned via products attribute."""
220+ group, public_user, private_user = self._setup_products()
221+
222+ with person_logged_in(public_user):
223+ self.assertEqual(
224+ 1,
225+ group.products.count(),
226+ 'There is only one public product for this user')
227+
228+ with person_logged_in(private_user):
229+ self.assertEqual(
230+ 2,
231+ group.products.count(),
232+ 'There are two for the private user.')
233+
234+ def test_non_public_products_hidden_for_display(self):
235+ """Non Public products are not returned via fetchProjectsForDisplay."""
236+ group, public_user, private_user = self._setup_products()
237+
238+ # Magical transaction so our data shows up via ISlaveStore
239+ import transaction
240+ transaction.commit()
241+
242+ with person_logged_in(public_user):
243+ self.assertEqual(
244+ 1,
245+ len(group.fetchProjectsForDisplay(public_user)),
246+ 'There is only one public product for the public user')
247+
248+ with person_logged_in(private_user):
249+ self.assertEqual(
250+ 2,
251+ len(group.fetchProjectsForDisplay(private_user)),
252+ 'Both products show for the private user.')
253+
254+
255 class TestTranslationGroupSet(TestCaseWithFactory):
256 layer = ZopelessDatabaseLayer
257