Merge lp:~abentley/launchpad/projectgroup-private-projects into lp:launchpad

Proposed by Aaron Bentley on 2012-10-19
Status: Merged
Merged at revision: 16179
Proposed branch: lp:~abentley/launchpad/projectgroup-private-projects
Merge into: lp:launchpad
Diff against target: 641 lines (+210/-49)
14 files modified
lib/lp/bugs/model/bugtasksearch.py (+6/-2)
lib/lp/registry/browser/__init__.py (+3/-1)
lib/lp/registry/browser/milestone.py (+1/-1)
lib/lp/registry/browser/tests/test_projectgroup.py (+44/-1)
lib/lp/registry/configure.zcml (+1/-1)
lib/lp/registry/doc/milestone.txt (+3/-2)
lib/lp/registry/interfaces/milestone.py (+3/-2)
lib/lp/registry/model/milestone.py (+14/-10)
lib/lp/registry/model/milestonetag.py (+1/-2)
lib/lp/registry/model/projectgroup.py (+18/-9)
lib/lp/registry/tests/test_milestone.py (+64/-13)
lib/lp/registry/tests/test_milestonetag.py (+3/-3)
lib/lp/registry/tests/test_project_milestone.py (+1/-1)
lib/lp/registry/tests/test_projectgroup.py (+48/-1)
To merge this branch: bzr merge lp:~abentley/launchpad/projectgroup-private-projects
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code 2012-10-19 Approve on 2012-10-19
Review via email: mp+130590@code.launchpad.net

Commit Message

Fix milestone and projectgroup privacy bugs.

Description of the Change

= Summary =
Fix bug #1063291: Project groups are broken by private projects

== Proposed fix ==
Change Milestone.specifications to privacy-aware getSpecifications.

Change bugtasksearch._build_query to make Milestone.bugtasks privacy-aware.

Change ProjectGroup.milestones and all_milestones to be privacy-aware.

== Pre-implementation notes ==
Had a call with deryck to discuss test_getSpecifications_milestone_privacy. We concluded it was correct, and I summarized our discussion in the comment.

== LOC Rationale ==
part of Private Projects

== Implementation details ==
ProjectGroup.milestones and all_milestones are exported in the web service, so they could not be converted into user-taking function.

Deletion code currently only considers items the user can see. We expect that deletion may oops in cases where, e.g. a milestone has a proprietary blueprint assigned, but will fix in a follow-up.

== Tests ==
bin/test -t test_getSpecifications_milestone_privacy -t test_bugtasks_milestone_privacy -t test_getProducts_with_proprietary -t test_milestones_privacy -t test_all_milestones_privacy

== Demo and Q/A ==
- Add a public and proprietary project to a project group
- Add two milestones to the proprietary project.
- Add one milestone to the public project with the same name as one of the proprietary projects.
- Add public specifications and bug tasks to all the milestones
- Log in as an unprivileged user. You should see only one milestone, and on that milestone, only the bug tasks and milestones for the public project.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/model/bugtasksearch.py
  lib/lp/registry/doc/milestone.txt
  lib/lp/registry/model/projectgroup.py
  lib/lp/registry/model/milestonetag.py
  lib/lp/registry/tests/test_project_milestone.py
  lib/lp/registry/configure.zcml
  lib/lp/registry/tests/test_projectgroup.py
  lib/lp/registry/interfaces/milestone.py
  lib/lp/registry/browser/__init__.py
  lib/lp/registry/tests/test_milestone.py
  lib/lp/registry/browser/tests/test_projectgroup.py
  lib/lp/registry/tests/test_milestonetag.py
  lib/lp/registry/model/milestone.py
  lib/lp/registry/browser/milestone.py

To post a comment you must log in.
Brad Crittenden (bac) wrote :

Hi Aaron,

This change looks good. Thanks.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
2--- lib/lp/bugs/model/bugtasksearch.py 2012-10-18 04:26:34 +0000
3+++ lib/lp/bugs/model/bugtasksearch.py 2012-10-19 15:54:26 +0000
4@@ -80,7 +80,10 @@
5 from lp.registry.model.milestone import Milestone
6 from lp.registry.model.milestonetag import MilestoneTag
7 from lp.registry.model.person import Person
8-from lp.registry.model.product import Product
9+from lp.registry.model.product import (
10+ Product,
11+ ProductSet,
12+ )
13 from lp.registry.model.teammembership import TeamParticipation
14 from lp.services.database.bulk import load
15 from lp.services.database.decoratedresultset import DecoratedResultSet
16@@ -387,7 +390,8 @@
17 where=And(
18 Product.project == params.milestone.target,
19 Milestone.productID == Product.id,
20- Milestone.name == params.milestone.name))))
21+ Milestone.name == params.milestone.name,
22+ ProductSet.getProductPrivacyFilter(params.user)))))
23 else:
24 extra_clauses.append(
25 search_value_to_storm_where_condition(
26
27=== modified file 'lib/lp/registry/browser/__init__.py'
28--- lib/lp/registry/browser/__init__.py 2012-09-27 19:11:59 +0000
29+++ lib/lp/registry/browser/__init__.py 2012-10-19 15:54:26 +0000
30@@ -34,6 +34,7 @@
31 from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
32 from lp.registry.interfaces.productseries import IProductSeries
33 from lp.registry.interfaces.series import SeriesStatus
34+from lp.services.webapp.interfaces import ILaunchBag
35 from lp.services.webapp.publisher import (
36 canonical_url,
37 DataDownloadView,
38@@ -175,7 +176,8 @@
39 if IProductSeries.providedBy(target):
40 return list(target._all_specifications)
41 else:
42- return list(target.specifications)
43+ user = getUtility(ILaunchBag).user
44+ return list(target.getSpecifications(user))
45
46 def _getProductRelease(self, milestone):
47 """The `IProductRelease` associated with the milestone."""
48
49=== modified file 'lib/lp/registry/browser/milestone.py'
50--- lib/lp/registry/browser/milestone.py 2012-10-04 20:38:22 +0000
51+++ lib/lp/registry/browser/milestone.py 2012-10-19 15:54:26 +0000
52@@ -214,7 +214,7 @@
53 @cachedproperty
54 def specifications(self):
55 """The list of specifications targeted to this milestone."""
56- return list(self.context.specifications)
57+ return list(self.context.getSpecifications(self.user))
58
59 @cachedproperty
60 def _bugtasks(self):
61
62=== modified file 'lib/lp/registry/browser/tests/test_projectgroup.py'
63--- lib/lp/registry/browser/tests/test_projectgroup.py 2012-10-05 00:00:15 +0000
64+++ lib/lp/registry/browser/tests/test_projectgroup.py 2012-10-19 15:54:26 +0000
65@@ -22,6 +22,7 @@
66 from lp.services.webapp import canonical_url
67 from lp.services.webapp.interfaces import ILaunchBag
68 from lp.testing import (
69+ BrowserTestCase,
70 celebrity_logged_in,
71 person_logged_in,
72 TestCaseWithFactory,
73@@ -32,7 +33,7 @@
74 from lp.testing.views import create_initialized_view
75
76
77-class TestProjectGroupView(TestCaseWithFactory):
78+class TestProjectGroupView(BrowserTestCase):
79 """Tests the +index view."""
80
81 layer = DatabaseFunctionalLayer
82@@ -53,6 +54,48 @@
83 team_membership_policy_data,
84 cache.objects['team_membership_policy_data'])
85
86+ def test_proprietary_product(self):
87+ # Proprietary projects are not listed for people without access to
88+ # them.
89+ owner = self.factory.makePerson()
90+ product = self.factory.makeProduct(
91+ information_type=InformationType.PROPRIETARY,
92+ project=self.project_group, owner=owner)
93+ owner_browser = self.getViewBrowser(self.project_group,
94+ user=owner)
95+ with person_logged_in(owner):
96+ product_name = product.name
97+ self.assertIn(product_name, owner_browser.contents)
98+ browser = self.getViewBrowser(self.project_group)
99+ self.assertNotIn(product_name, browser.contents)
100+
101+ def test_proprietary_product_milestone(self):
102+ # Proprietary projects are not listed for people without access to
103+ # them.
104+ owner = self.factory.makePerson()
105+ public_product = self.factory.makeProduct(
106+ information_type=InformationType.PUBLIC,
107+ project=self.project_group, owner=owner)
108+ public_milestone = self.factory.makeMilestone(product=public_product)
109+ product = self.factory.makeProduct(
110+ information_type=InformationType.PROPRIETARY,
111+ project=self.project_group, owner=owner)
112+ milestone = self.factory.makeMilestone(product=product,
113+ name=public_milestone.name)
114+ (group_milestone,) = self.project_group.milestones
115+ self.factory.makeSpecification(milestone=public_milestone)
116+ with person_logged_in(owner):
117+ self.factory.makeSpecification(milestone=milestone)
118+ product_name = product.displayname
119+ with person_logged_in(None):
120+ owner_browser = self.getViewBrowser(group_milestone, user=owner)
121+ browser = self.getViewBrowser(group_milestone)
122+
123+ self.assertIn(product_name, owner_browser.contents)
124+ self.assertIn(public_product.displayname, owner_browser.contents)
125+ self.assertNotIn(product_name, browser.contents)
126+ self.assertIn(public_product.displayname, browser.contents)
127+
128
129 class TestProjectGroupEditView(TestCaseWithFactory):
130 """Tests the edit view."""
131
132=== modified file 'lib/lp/registry/configure.zcml'
133--- lib/lp/registry/configure.zcml 2012-10-18 14:09:19 +0000
134+++ lib/lp/registry/configure.zcml 2012-10-19 15:54:26 +0000
135@@ -1038,6 +1038,7 @@
136 displayname
137 distribution
138 distroseries
139+ getSpecifications
140 getTags
141 getTagsData
142 name
143@@ -1045,7 +1046,6 @@
144 product_release
145 productseries
146 series_target
147- specifications
148 summary
149 target
150 title
151
152=== modified file 'lib/lp/registry/doc/milestone.txt'
153--- lib/lp/registry/doc/milestone.txt 2012-09-27 14:25:32 +0000
154+++ lib/lp/registry/doc/milestone.txt 2012-10-19 15:54:26 +0000
155@@ -296,7 +296,7 @@
156 netapplet []
157 gnomebaker []
158
159- >>> print list(gnome.getMilestone('1.1').specifications)
160+ >>> print list(gnome.getMilestone('1.1').getSpecifications(None))
161 []
162
163 When a specification for a product is created and assigned to a product
164@@ -306,7 +306,8 @@
165 >>> [spec.name for spec in applets._all_specifications]
166 [u'applets-specification']
167
168- >>> [spec.name for spec in gnome.getMilestone('1.1').specifications]
169+ >>> specs = gnome.getMilestone('1.1').getSpecifications(None)
170+ >>> [spec.name for spec in specs]
171 [u'applets-specification']
172
173
174
175=== modified file 'lib/lp/registry/interfaces/milestone.py'
176--- lib/lp/registry/interfaces/milestone.py 2012-10-16 14:47:54 +0000
177+++ lib/lp/registry/interfaces/milestone.py 2012-10-19 15:54:26 +0000
178@@ -123,8 +123,6 @@
179 "The product, distribution, or project group for this "
180 "milestone."),
181 required=False))
182- specifications = Attribute(
183- "A list of specifications targeted to this object.")
184 dateexpected = exported(
185 FormattableDate(title=_("Date Targeted"), required=False,
186 description=_("Example: 2005-11-24")),
187@@ -143,6 +141,9 @@
188 def bugtasks(user):
189 """Get a list of non-conjoined bugtasks visible to this user."""
190
191+ def getSpecifications(user):
192+ """Return the specifications visible to this user."""
193+
194
195 class IAbstractMilestone(IMilestoneData):
196 """An intermediate interface for milestone, or a targeting point for bugs
197
198=== modified file 'lib/lp/registry/model/milestone.py'
199--- lib/lp/registry/model/milestone.py 2012-10-17 20:18:20 +0000
200+++ lib/lp/registry/model/milestone.py 2012-10-19 15:54:26 +0000
201@@ -64,6 +64,7 @@
202 from lp.services.database.lpstorm import IStore
203 from lp.services.database.sqlbase import SQLBase
204 from lp.services.propertycache import get_property_cache
205+from lp.services.webapp.interfaces import ILaunchBag
206 from lp.services.webapp.sorting import expand_numbers
207
208
209@@ -147,15 +148,15 @@
210 def title(self):
211 raise NotImplementedError
212
213- @property
214- def specifications(self):
215+ def getSpecifications(self, user):
216+ """See `IMilestoneData`"""
217 from lp.registry.model.person import Person
218 store = Store.of(self.target)
219 origin = [
220 Specification,
221 LeftJoin(Person, Specification.assigneeID == Person.id),
222 ]
223- milestones = self._milestone_ids_expr
224+ milestones = self._milestone_ids_expr(user)
225
226 results = store.using(*origin).find(
227 (Specification, Person),
228@@ -219,8 +220,7 @@
229 summary = StringCol(notNull=False, default=None)
230 code_name = StringCol(dbName='codename', notNull=False, default=None)
231
232- @property
233- def _milestone_ids_expr(self):
234+ def _milestone_ids_expr(self, user):
235 return (self.id,)
236
237 @property
238@@ -294,13 +294,14 @@
239 params = BugTaskSearchParams(milestone=self, user=None)
240 bugtasks = getUtility(IBugTaskSet).search(params)
241 subscriptions = IResultSet(self.getSubscriptions())
242+ user = getUtility(ILaunchBag).user
243 assert subscriptions.is_empty(), (
244 "You cannot delete a milestone which has structural "
245 "subscriptions.")
246 assert bugtasks.count() == 0, (
247 "You cannot delete a milestone which has bugtasks targeted "
248 "to it.")
249- assert self.specifications.count() == 0, (
250+ assert self.getSpecifications(user).count() == 0, (
251 "You cannot delete a milestone which has specifications targeted "
252 "to it.")
253 assert self.product_release is None, (
254@@ -440,16 +441,19 @@
255 self.series_target = None
256 self.summary = None
257
258- @property
259- def _milestone_ids_expr(self):
260- from lp.registry.model.product import Product
261+ def _milestone_ids_expr(self, user):
262+ from lp.registry.model.product import (
263+ Product,
264+ ProductSet,
265+ )
266 return Select(
267 Milestone.id,
268 tables=[Milestone, Product],
269 where=And(
270 Milestone.name == self.name,
271 Milestone.productID == Product.id,
272- Product.project == self.target))
273+ Product.project == self.target,
274+ ProductSet.getProductPrivacyFilter(user)))
275
276 @property
277 def displayname(self):
278
279=== modified file 'lib/lp/registry/model/milestonetag.py'
280--- lib/lp/registry/model/milestonetag.py 2012-10-02 03:31:03 +0000
281+++ lib/lp/registry/model/milestonetag.py 2012-10-19 15:54:26 +0000
282@@ -77,8 +77,7 @@
283 """See IMilestoneData."""
284 return self.displayname
285
286- @property
287- def _milestone_ids_expr(self):
288+ def _milestone_ids_expr(self, user):
289 tag_constraints = And(*[
290 Exists(
291 Select(
292
293=== modified file 'lib/lp/registry/model/projectgroup.py'
294--- lib/lp/registry/model/projectgroup.py 2012-10-16 16:03:38 +0000
295+++ lib/lp/registry/model/projectgroup.py 2012-10-19 15:54:26 +0000
296@@ -88,7 +88,10 @@
297 ProjectMilestone,
298 )
299 from lp.registry.model.pillar import HasAliasMixin
300-from lp.registry.model.product import Product
301+from lp.registry.model.product import (
302+ Product,
303+ ProductSet,
304+ )
305 from lp.registry.model.productseries import ProductSeries
306 from lp.services.database.constants import UTC_NOW
307 from lp.services.database.datetimecol import UtcDateTimeCol
308@@ -101,6 +104,7 @@
309 from lp.services.helpers import shortlist
310 from lp.services.propertycache import cachedproperty
311 from lp.services.webapp.authorization import check_permission
312+from lp.services.webapp.interfaces import ILaunchBag
313 from lp.services.worlddata.model.language import Language
314 from lp.translations.enums import TranslationPermission
315 from lp.translations.model.potemplate import POTemplate
316@@ -170,13 +174,15 @@
317 """See `IPillar`."""
318 return "Project Group"
319
320- def getProducts(self):
321- return Product.selectBy(
322- project=self, active=True, orderBy='displayname')
323+ def getProducts(self, user):
324+ results = Store.of(self).find(
325+ Product, Product.project == self, Product.active == True,
326+ ProductSet.getProductPrivacyFilter(user))
327+ return results.order_by(Product.displayname)
328
329 @cachedproperty
330 def products(self):
331- return list(self.getProducts())
332+ return list(self.getProducts(getUtility(ILaunchBag).user))
333
334 def getProduct(self, name):
335 return Product.selectOneBy(project=self, name=name)
336@@ -404,7 +410,7 @@
337 return And(Milestone.productID == Product.id,
338 Product.projectID == self.id)
339
340- def _getMilestones(self, only_active):
341+ def _getMilestones(self, user, only_active):
342 """Return a list of milestones for this project group.
343
344 If only_active is True, only active milestones are returned,
345@@ -422,7 +428,8 @@
346 )
347 conditions = And(Milestone.product == Product.id,
348 Product.project == self,
349- Product.active == True)
350+ Product.active == True,
351+ ProductSet.getProductPrivacyFilter(user))
352 result = store.find(columns, conditions)
353 result.group_by(Milestone.name)
354 if only_active:
355@@ -466,7 +473,8 @@
356 @property
357 def milestones(self):
358 """See `IProjectGroup`."""
359- return self._getMilestones(only_active=True)
360+ user = getUtility(ILaunchBag).user
361+ return self._getMilestones(user, only_active=True)
362
363 @property
364 def product_milestones(self):
365@@ -478,7 +486,8 @@
366 @property
367 def all_milestones(self):
368 """See `IProjectGroup`."""
369- return self._getMilestones(only_active=False)
370+ user = getUtility(ILaunchBag).user
371+ return self._getMilestones(user, only_active=False)
372
373 def getMilestone(self, name):
374 """See `IProjectGroup`."""
375
376=== modified file 'lib/lp/registry/tests/test_milestone.py'
377--- lib/lp/registry/tests/test_milestone.py 2012-10-18 16:56:54 +0000
378+++ lib/lp/registry/tests/test_milestone.py 2012-10-19 15:54:26 +0000
379@@ -1,4 +1,4 @@
380-# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
381+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
382 # GNU Affero General Public License version 3 (see the file LICENSE).
383
384 """Milestone related test helper."""
385@@ -15,13 +15,15 @@
386 getChecker,
387 )
388 from zope.security.interfaces import Unauthorized
389-from zope.security.proxy import removeSecurityProxy
390
391 from lp.app.enums import InformationType
392 from lp.app.errors import NotFoundError
393 from lp.app.interfaces.informationtype import IInformationType
394 from lp.app.interfaces.services import IService
395-from lp.registry.enums import SharingPermission
396+from lp.registry.enums import (
397+ BugSharingPolicy,
398+ SharingPermission,
399+ )
400 from lp.registry.interfaces.distribution import IDistributionSet
401 from lp.registry.interfaces.milestone import (
402 IHasMilestones,
403@@ -138,14 +140,14 @@
404 'active', 'bug_subscriptions', 'bugtasks', 'code_name',
405 'dateexpected', 'displayname', 'distribution', 'distroseries',
406 '_getOfficialTagClause', 'getBugSummaryContextWhereClause',
407- 'getBugTaskWeightFunction', 'getSubscription',
408- 'getSubscriptions', 'getTags', 'getTagsData',
409+ 'getBugTaskWeightFunction', 'getSpecifications',
410+ 'getSubscription', 'getSubscriptions', 'getTags', 'getTagsData',
411 'getUsedBugTagsWithOpenCounts', 'name', 'official_bug_tags',
412 'parent_subscription_target', 'product', 'product_release',
413 'productseries', 'searchTasks', 'series_target',
414- 'specifications', 'summary', 'target', 'target_type_display',
415- 'title', 'userCanAlterBugSubscription',
416- 'userCanAlterSubscription', 'userHasBugSubscriptions',
417+ 'summary', 'target', 'target_type_display', 'title',
418+ 'userCanAlterBugSubscription', 'userCanAlterSubscription',
419+ 'userHasBugSubscriptions',
420 )),
421 'launchpad.AnyAllowedPerson': set((
422 'addBugSubscription', 'addBugSubscriptionFilter',
423@@ -423,7 +425,8 @@
424 owner=self.owner,
425 product=self.product,
426 )
427- self.assertContentEqual(specifications, self.milestone.specifications)
428+ self.assertContentEqual(specifications,
429+ self.milestone.getSpecifications(None))
430
431
432 class MilestonesContainsPartialSpecifications(TestCaseWithFactory):
433@@ -452,12 +455,14 @@
434 def test_milestones_on_product(self):
435 spec, target_milestone = self._create_milestones_on_target(
436 product=self.factory.makeProduct())
437- self.assertContentEqual([spec], target_milestone.specifications)
438+ self.assertContentEqual([spec],
439+ target_milestone.getSpecifications(None))
440
441 def test_milestones_on_distribution(self):
442 spec, target_milestone = self._create_milestones_on_target(
443 distribution=self.factory.makeDistribution())
444- self.assertContentEqual([spec], target_milestone.specifications)
445+ self.assertContentEqual([spec],
446+ target_milestone.getSpecifications(None))
447
448 def test_milestones_on_project(self):
449 # A Project (Project Group) milestone contains all specifications
450@@ -468,7 +473,53 @@
451 spec, target_milestone = self._create_milestones_on_target(
452 product=product)
453 milestone = projectgroup.getMilestone(name=target_milestone.name)
454- self.assertContentEqual([spec], milestone.specifications)
455+ self.assertContentEqual([spec], milestone.getSpecifications(None))
456+
457+ def makeMixedMilestone(self):
458+ projectgroup = self.factory.makeProject()
459+ owner = self.factory.makePerson()
460+ public_product = self.factory.makeProduct(project=projectgroup)
461+ public_milestone = self.factory.makeMilestone(product=public_product)
462+ product = self.factory.makeProduct(
463+ owner=owner, information_type=InformationType.PROPRIETARY,
464+ project=projectgroup, bug_sharing_policy=BugSharingPolicy.PUBLIC)
465+ target_milestone = self.factory.makeMilestone(
466+ product=product, name=public_milestone.name)
467+ milestone = projectgroup.getMilestone(name=public_milestone.name)
468+ return milestone, target_milestone, owner
469+
470+ def test_getSpecifications_milestone_privacy(self):
471+ # Ensure getSpecifications respects milestone privacy.
472+ # This looks wrong, because the specification is actually public, and
473+ # we don't normally hide specifications based on the visibility of
474+ # their products. But we're not trying to hide the specification.
475+ # We're hiding the fact that this specification is associated with
476+ # a proprietary Product milestone. We create a proprietary product
477+ # because that's the only way to get a proprietary milestone.
478+ milestone, target_milestone, owner = self.makeMixedMilestone()
479+ with person_logged_in(owner):
480+ spec = self.factory.makeSpecification(milestone=target_milestone)
481+ self.assertContentEqual([],
482+ milestone.getSpecifications(None))
483+ self.assertContentEqual([spec],
484+ milestone.getSpecifications(owner))
485+
486+ def test_bugtasks_milestone_privacy(self):
487+ # Ensure bugtasks respects milestone privacy.
488+ # This looks wrong, because the bugtask is actually public, and we
489+ # don't normally hide bugtasks based on the visibility of their
490+ # products. But we're not trying to hide the bugtask. We're hiding
491+ # the fact that this bugtask is associated with a proprietary Product
492+ # milestone. We create a proprietary product because that's the only
493+ # way to get a proprietary milestone.
494+ milestone, target_milestone, owner = self.makeMixedMilestone()
495+ with person_logged_in(owner):
496+ bugtask = self.factory.makeBugTask(
497+ target=target_milestone.product)
498+ with person_logged_in(bugtask.owner):
499+ bugtask.transitionToMilestone(target_milestone, owner)
500+ self.assertContentEqual([], milestone.bugtasks(None))
501+ self.assertContentEqual([bugtask], milestone.bugtasks(owner))
502
503 def test_milestones_with_deleted_workitems(self):
504 # Deleted work items do not cause the specification to show up
505@@ -479,7 +530,7 @@
506 product=milestone.product)
507 self.factory.makeSpecificationWorkItem(
508 specification=specification, milestone=milestone, deleted=True)
509- self.assertContentEqual([], milestone.specifications)
510+ self.assertContentEqual([], milestone.getSpecifications(None))
511
512
513 class TestMilestoneInformationType(TestCaseWithFactory):
514
515=== modified file 'lib/lp/registry/tests/test_milestonetag.py'
516--- lib/lp/registry/tests/test_milestonetag.py 2012-02-28 04:24:19 +0000
517+++ lib/lp/registry/tests/test_milestonetag.py 2012-10-19 15:54:26 +0000
518@@ -189,21 +189,21 @@
519 # Ensure that all specifications on a milestone can be retrieved.
520 specs, milestonetag = self._create_items_for_retrieval(
521 self._create_specifications)
522- self.assertContentEqual(specs, milestonetag.specifications)
523+ self.assertContentEqual(specs, milestonetag.getSpecifications(None))
524
525 def test_specifications_for_untagged_milestone(self):
526 # Ensure that specifications for a project group are retrieved
527 # only if associated with milestones having specified tags.
528 specs, milestonetag = self._create_items_for_untagged_milestone(
529 self._create_specifications)
530- self.assertContentEqual(specs, milestonetag.specifications)
531+ self.assertContentEqual(specs, milestonetag.getSpecifications(None))
532
533 def test_specifications_multiple_tags(self):
534 # Ensure that, in presence of multiple tags, only specifications
535 # for milestones associated with all the tags are retrieved.
536 specs, milestonetag = self._create_items_for_multiple_tags(
537 self._create_specifications)
538- self.assertContentEqual(specs, milestonetag.specifications)
539+ self.assertContentEqual(specs, milestonetag.getSpecifications(None))
540
541
542 class MilestoneTagWebServiceTest(WebServiceTestCase):
543
544=== modified file 'lib/lp/registry/tests/test_project_milestone.py'
545--- lib/lp/registry/tests/test_project_milestone.py 2012-08-07 02:31:56 +0000
546+++ lib/lp/registry/tests/test_project_milestone.py 2012-10-19 15:54:26 +0000
547@@ -227,7 +227,7 @@
548 # The spec for firefox (not a gnome product) is not included
549 # in the specifications, while the other two specs are included.
550 self.assertEqual(
551- [spec.name for spec in gnome_milestone.specifications],
552+ [spec.name for spec in gnome_milestone.getSpecifications(None)],
553 ['evolution-specification', 'gnomebaker-specification'])
554
555 def _createProductBugtask(self, product_name, milestone_name):
556
557=== modified file 'lib/lp/registry/tests/test_projectgroup.py'
558--- lib/lp/registry/tests/test_projectgroup.py 2012-08-13 21:04:17 +0000
559+++ lib/lp/registry/tests/test_projectgroup.py 2012-10-19 15:54:26 +0000
560@@ -1,4 +1,4 @@
561-# Copyright 2009 Canonical Ltd. This software is licensed under the
562+# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
563 # GNU Affero General Public License version 3 (see the file LICENSE).
564
565 __metaclass__ = type
566@@ -6,7 +6,9 @@
567 from lazr.restfulclient.errors import ClientError
568 from zope.component import getUtility
569 from zope.security.interfaces import Unauthorized
570+from zope.security.proxy import removeSecurityProxy
571
572+from lp.app.enums import InformationType
573 from lp.registry.enums import (
574 EXCLUSIVE_TEAM_POLICY,
575 INCLUSIVE_TEAM_POLICY,
576@@ -17,6 +19,7 @@
577 launchpadlib_for,
578 login_celebrity,
579 login_person,
580+ person_logged_in,
581 TestCaseWithFactory,
582 )
583 from lp.testing.layers import (
584@@ -49,6 +52,19 @@
585 closed_team = self.factory.makeTeam(membership_policy=policy)
586 self.factory.makeProject(owner=closed_team)
587
588+ def test_getProducts_with_proprietary(self):
589+ # Proprietary projects are not listed for users without access to
590+ # them.
591+ project_group = removeSecurityProxy(self.factory.makeProject())
592+ owner = self.factory.makePerson()
593+ product = self.factory.makeProduct(
594+ project=project_group, owner=owner,
595+ information_type=InformationType.PROPRIETARY)
596+ self.assertNotIn(product, project_group.getProducts(None))
597+ outsider = self.factory.makePerson()
598+ self.assertNotIn(product, project_group.getProducts(outsider))
599+ self.assertIn(product, project_group.getProducts(owner))
600+
601
602 class ProjectGroupSearchTestCase(TestCaseWithFactory):
603
604@@ -163,6 +179,37 @@
605 self.pg.owner = self.factory.makePerson(name='project-group-owner')
606
607
608+class TestMilestones(TestCaseWithFactory):
609+
610+ layer = DatabaseFunctionalLayer
611+
612+ def test_milestones_privacy(self):
613+ """ProjectGroup.milestones uses logged-in user."""
614+ owner = self.factory.makePerson()
615+ project_group = self.factory.makeProject()
616+ product = self.factory.makeProduct(
617+ information_type=InformationType.PROPRIETARY, owner=owner,
618+ project=project_group)
619+ milestone = self.factory.makeMilestone(product=product)
620+ self.assertContentEqual([], project_group.milestones)
621+ with person_logged_in(owner):
622+ names = [ms.name for ms in project_group.milestones]
623+ self.assertEqual([milestone.name], names)
624+
625+ def test_all_milestones_privacy(self):
626+ """ProjectGroup.milestones uses logged-in user."""
627+ owner = self.factory.makePerson()
628+ project_group = self.factory.makeProject()
629+ product = self.factory.makeProduct(
630+ information_type=InformationType.PROPRIETARY, owner=owner,
631+ project=project_group)
632+ milestone = self.factory.makeMilestone(product=product)
633+ self.assertContentEqual([], project_group.milestones)
634+ with person_logged_in(owner):
635+ names = [ms.name for ms in project_group.all_milestones]
636+ self.assertEqual([milestone.name], names)
637+
638+
639 class TestLaunchpadlibAPI(TestCaseWithFactory):
640
641 layer = DatabaseFunctionalLayer