← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/projectgroup-private-projects into lp:launchpad.

Commit message:
Fix milestone and projectgroup privacy bugs.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1063291 in Launchpad itself: "Project groups are broken by private projects"
  https://bugs.launchpad.net/launchpad/+bug/1063291

For more details, see:
https://code.launchpad.net/~abentley/launchpad/projectgroup-private-projects/+merge/130590

= 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
-- 
https://code.launchpad.net/~abentley/launchpad/projectgroup-private-projects/+merge/130590
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/projectgroup-private-projects into lp:launchpad.
=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
--- lib/lp/bugs/model/bugtasksearch.py	2012-10-18 04:26:34 +0000
+++ lib/lp/bugs/model/bugtasksearch.py	2012-10-19 15:54:26 +0000
@@ -80,7 +80,10 @@
 from lp.registry.model.milestone import Milestone
 from lp.registry.model.milestonetag import MilestoneTag
 from lp.registry.model.person import Person
-from lp.registry.model.product import Product
+from lp.registry.model.product import (
+    Product,
+    ProductSet,
+    )
 from lp.registry.model.teammembership import TeamParticipation
 from lp.services.database.bulk import load
 from lp.services.database.decoratedresultset import DecoratedResultSet
@@ -387,7 +390,8 @@
                         where=And(
                             Product.project == params.milestone.target,
                             Milestone.productID == Product.id,
-                            Milestone.name == params.milestone.name))))
+                            Milestone.name == params.milestone.name,
+                            ProductSet.getProductPrivacyFilter(params.user)))))
         else:
             extra_clauses.append(
                 search_value_to_storm_where_condition(

=== modified file 'lib/lp/registry/browser/__init__.py'
--- lib/lp/registry/browser/__init__.py	2012-09-27 19:11:59 +0000
+++ lib/lp/registry/browser/__init__.py	2012-10-19 15:54:26 +0000
@@ -34,6 +34,7 @@
 from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
 from lp.registry.interfaces.productseries import IProductSeries
 from lp.registry.interfaces.series import SeriesStatus
+from lp.services.webapp.interfaces import ILaunchBag
 from lp.services.webapp.publisher import (
     canonical_url,
     DataDownloadView,
@@ -175,7 +176,8 @@
         if IProductSeries.providedBy(target):
             return list(target._all_specifications)
         else:
-            return list(target.specifications)
+            user = getUtility(ILaunchBag).user
+            return list(target.getSpecifications(user))
 
     def _getProductRelease(self, milestone):
         """The `IProductRelease` associated with the milestone."""

=== modified file 'lib/lp/registry/browser/milestone.py'
--- lib/lp/registry/browser/milestone.py	2012-10-04 20:38:22 +0000
+++ lib/lp/registry/browser/milestone.py	2012-10-19 15:54:26 +0000
@@ -214,7 +214,7 @@
     @cachedproperty
     def specifications(self):
         """The list of specifications targeted to this milestone."""
-        return list(self.context.specifications)
+        return list(self.context.getSpecifications(self.user))
 
     @cachedproperty
     def _bugtasks(self):

=== modified file 'lib/lp/registry/browser/tests/test_projectgroup.py'
--- lib/lp/registry/browser/tests/test_projectgroup.py	2012-10-05 00:00:15 +0000
+++ lib/lp/registry/browser/tests/test_projectgroup.py	2012-10-19 15:54:26 +0000
@@ -22,6 +22,7 @@
 from lp.services.webapp import canonical_url
 from lp.services.webapp.interfaces import ILaunchBag
 from lp.testing import (
+    BrowserTestCase,
     celebrity_logged_in,
     person_logged_in,
     TestCaseWithFactory,
@@ -32,7 +33,7 @@
 from lp.testing.views import create_initialized_view
 
 
-class TestProjectGroupView(TestCaseWithFactory):
+class TestProjectGroupView(BrowserTestCase):
     """Tests the +index view."""
 
     layer = DatabaseFunctionalLayer
@@ -53,6 +54,48 @@
             team_membership_policy_data,
             cache.objects['team_membership_policy_data'])
 
+    def test_proprietary_product(self):
+        # Proprietary projects are not listed for people without access to
+        # them.
+        owner = self.factory.makePerson()
+        product = self.factory.makeProduct(
+            information_type=InformationType.PROPRIETARY,
+            project=self.project_group, owner=owner)
+        owner_browser = self.getViewBrowser(self.project_group,
+                                            user=owner)
+        with person_logged_in(owner):
+            product_name = product.name
+        self.assertIn(product_name, owner_browser.contents)
+        browser = self.getViewBrowser(self.project_group)
+        self.assertNotIn(product_name, browser.contents)
+
+    def test_proprietary_product_milestone(self):
+        # Proprietary projects are not listed for people without access to
+        # them.
+        owner = self.factory.makePerson()
+        public_product = self.factory.makeProduct(
+            information_type=InformationType.PUBLIC,
+            project=self.project_group, owner=owner)
+        public_milestone = self.factory.makeMilestone(product=public_product)
+        product = self.factory.makeProduct(
+            information_type=InformationType.PROPRIETARY,
+            project=self.project_group, owner=owner)
+        milestone = self.factory.makeMilestone(product=product,
+                                               name=public_milestone.name)
+        (group_milestone,) = self.project_group.milestones
+        self.factory.makeSpecification(milestone=public_milestone)
+        with person_logged_in(owner):
+            self.factory.makeSpecification(milestone=milestone)
+            product_name = product.displayname
+        with person_logged_in(None):
+            owner_browser = self.getViewBrowser(group_milestone, user=owner)
+            browser = self.getViewBrowser(group_milestone)
+
+        self.assertIn(product_name, owner_browser.contents)
+        self.assertIn(public_product.displayname, owner_browser.contents)
+        self.assertNotIn(product_name, browser.contents)
+        self.assertIn(public_product.displayname, browser.contents)
+
 
 class TestProjectGroupEditView(TestCaseWithFactory):
     """Tests the edit view."""

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2012-10-18 14:09:19 +0000
+++ lib/lp/registry/configure.zcml	2012-10-19 15:54:26 +0000
@@ -1038,6 +1038,7 @@
                 displayname
                 distribution
                 distroseries
+                getSpecifications
                 getTags
                 getTagsData
                 name
@@ -1045,7 +1046,6 @@
                 product_release
                 productseries
                 series_target
-                specifications
                 summary
                 target
                 title

=== modified file 'lib/lp/registry/doc/milestone.txt'
--- lib/lp/registry/doc/milestone.txt	2012-09-27 14:25:32 +0000
+++ lib/lp/registry/doc/milestone.txt	2012-10-19 15:54:26 +0000
@@ -296,7 +296,7 @@
     netapplet []
     gnomebaker []
 
-    >>> print list(gnome.getMilestone('1.1').specifications)
+    >>> print list(gnome.getMilestone('1.1').getSpecifications(None))
     []
 
 When a specification for a product is created and assigned to a product
@@ -306,7 +306,8 @@
     >>> [spec.name for spec in applets._all_specifications]
     [u'applets-specification']
 
-    >>> [spec.name for spec in gnome.getMilestone('1.1').specifications]
+    >>> specs = gnome.getMilestone('1.1').getSpecifications(None)
+    >>> [spec.name for spec in specs]
     [u'applets-specification']
 
 

=== modified file 'lib/lp/registry/interfaces/milestone.py'
--- lib/lp/registry/interfaces/milestone.py	2012-10-16 14:47:54 +0000
+++ lib/lp/registry/interfaces/milestone.py	2012-10-19 15:54:26 +0000
@@ -123,8 +123,6 @@
                 "The product, distribution, or project group for this "
                 "milestone."),
             required=False))
-    specifications = Attribute(
-        "A list of specifications targeted to this object.")
     dateexpected = exported(
         FormattableDate(title=_("Date Targeted"), required=False,
              description=_("Example: 2005-11-24")),
@@ -143,6 +141,9 @@
     def bugtasks(user):
         """Get a list of non-conjoined bugtasks visible to this user."""
 
+    def getSpecifications(user):
+        """Return the specifications visible to this user."""
+
 
 class IAbstractMilestone(IMilestoneData):
     """An intermediate interface for milestone, or a targeting point for bugs

=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py	2012-10-17 20:18:20 +0000
+++ lib/lp/registry/model/milestone.py	2012-10-19 15:54:26 +0000
@@ -64,6 +64,7 @@
 from lp.services.database.lpstorm import IStore
 from lp.services.database.sqlbase import SQLBase
 from lp.services.propertycache import get_property_cache
+from lp.services.webapp.interfaces import ILaunchBag
 from lp.services.webapp.sorting import expand_numbers
 
 
@@ -147,15 +148,15 @@
     def title(self):
         raise NotImplementedError
 
-    @property
-    def specifications(self):
+    def getSpecifications(self, user):
+        """See `IMilestoneData`"""
         from lp.registry.model.person import Person
         store = Store.of(self.target)
         origin = [
             Specification,
             LeftJoin(Person, Specification.assigneeID == Person.id),
             ]
-        milestones = self._milestone_ids_expr
+        milestones = self._milestone_ids_expr(user)
 
         results = store.using(*origin).find(
             (Specification, Person),
@@ -219,8 +220,7 @@
     summary = StringCol(notNull=False, default=None)
     code_name = StringCol(dbName='codename', notNull=False, default=None)
 
-    @property
-    def _milestone_ids_expr(self):
+    def _milestone_ids_expr(self, user):
         return (self.id,)
 
     @property
@@ -294,13 +294,14 @@
         params = BugTaskSearchParams(milestone=self, user=None)
         bugtasks = getUtility(IBugTaskSet).search(params)
         subscriptions = IResultSet(self.getSubscriptions())
+        user = getUtility(ILaunchBag).user
         assert subscriptions.is_empty(), (
             "You cannot delete a milestone which has structural "
             "subscriptions.")
         assert bugtasks.count() == 0, (
             "You cannot delete a milestone which has bugtasks targeted "
             "to it.")
-        assert self.specifications.count() == 0, (
+        assert self.getSpecifications(user).count() == 0, (
             "You cannot delete a milestone which has specifications targeted "
             "to it.")
         assert self.product_release is None, (
@@ -440,16 +441,19 @@
         self.series_target = None
         self.summary = None
 
-    @property
-    def _milestone_ids_expr(self):
-        from lp.registry.model.product import Product
+    def _milestone_ids_expr(self, user):
+        from lp.registry.model.product import (
+            Product,
+            ProductSet,
+            )
         return Select(
             Milestone.id,
             tables=[Milestone, Product],
             where=And(
                 Milestone.name == self.name,
                 Milestone.productID == Product.id,
-                Product.project == self.target))
+                Product.project == self.target,
+                ProductSet.getProductPrivacyFilter(user)))
 
     @property
     def displayname(self):

=== modified file 'lib/lp/registry/model/milestonetag.py'
--- lib/lp/registry/model/milestonetag.py	2012-10-02 03:31:03 +0000
+++ lib/lp/registry/model/milestonetag.py	2012-10-19 15:54:26 +0000
@@ -77,8 +77,7 @@
         """See IMilestoneData."""
         return self.displayname
 
-    @property
-    def _milestone_ids_expr(self):
+    def _milestone_ids_expr(self, user):
         tag_constraints = And(*[
             Exists(
                 Select(

=== modified file 'lib/lp/registry/model/projectgroup.py'
--- lib/lp/registry/model/projectgroup.py	2012-10-16 16:03:38 +0000
+++ lib/lp/registry/model/projectgroup.py	2012-10-19 15:54:26 +0000
@@ -88,7 +88,10 @@
     ProjectMilestone,
     )
 from lp.registry.model.pillar import HasAliasMixin
-from lp.registry.model.product import Product
+from lp.registry.model.product import (
+    Product,
+    ProductSet,
+    )
 from lp.registry.model.productseries import ProductSeries
 from lp.services.database.constants import UTC_NOW
 from lp.services.database.datetimecol import UtcDateTimeCol
@@ -101,6 +104,7 @@
 from lp.services.helpers import shortlist
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp.authorization import check_permission
+from lp.services.webapp.interfaces import ILaunchBag
 from lp.services.worlddata.model.language import Language
 from lp.translations.enums import TranslationPermission
 from lp.translations.model.potemplate import POTemplate
@@ -170,13 +174,15 @@
         """See `IPillar`."""
         return "Project Group"
 
-    def getProducts(self):
-        return Product.selectBy(
-            project=self, active=True, orderBy='displayname')
+    def getProducts(self, user):
+        results = Store.of(self).find(
+            Product, Product.project == self, Product.active == True,
+            ProductSet.getProductPrivacyFilter(user))
+        return results.order_by(Product.displayname)
 
     @cachedproperty
     def products(self):
-        return list(self.getProducts())
+        return list(self.getProducts(getUtility(ILaunchBag).user))
 
     def getProduct(self, name):
         return Product.selectOneBy(project=self, name=name)
@@ -404,7 +410,7 @@
         return And(Milestone.productID == Product.id,
                    Product.projectID == self.id)
 
-    def _getMilestones(self, only_active):
+    def _getMilestones(self, user, only_active):
         """Return a list of milestones for this project group.
 
         If only_active is True, only active milestones are returned,
@@ -422,7 +428,8 @@
             )
         conditions = And(Milestone.product == Product.id,
                          Product.project == self,
-                         Product.active == True)
+                         Product.active == True,
+                         ProductSet.getProductPrivacyFilter(user))
         result = store.find(columns, conditions)
         result.group_by(Milestone.name)
         if only_active:
@@ -466,7 +473,8 @@
     @property
     def milestones(self):
         """See `IProjectGroup`."""
-        return self._getMilestones(only_active=True)
+        user = getUtility(ILaunchBag).user
+        return self._getMilestones(user, only_active=True)
 
     @property
     def product_milestones(self):
@@ -478,7 +486,8 @@
     @property
     def all_milestones(self):
         """See `IProjectGroup`."""
-        return self._getMilestones(only_active=False)
+        user = getUtility(ILaunchBag).user
+        return self._getMilestones(user, only_active=False)
 
     def getMilestone(self, name):
         """See `IProjectGroup`."""

=== modified file 'lib/lp/registry/tests/test_milestone.py'
--- lib/lp/registry/tests/test_milestone.py	2012-10-18 16:56:54 +0000
+++ lib/lp/registry/tests/test_milestone.py	2012-10-19 15:54:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Milestone related test helper."""
@@ -15,13 +15,15 @@
     getChecker,
     )
 from zope.security.interfaces import Unauthorized
-from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
 from lp.app.errors import NotFoundError
 from lp.app.interfaces.informationtype import IInformationType
 from lp.app.interfaces.services import IService
-from lp.registry.enums import SharingPermission
+from lp.registry.enums import (
+    BugSharingPolicy,
+    SharingPermission,
+    )
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.milestone import (
     IHasMilestones,
@@ -138,14 +140,14 @@
             'active', 'bug_subscriptions', 'bugtasks', 'code_name',
             'dateexpected', 'displayname', 'distribution', 'distroseries',
             '_getOfficialTagClause', 'getBugSummaryContextWhereClause',
-            'getBugTaskWeightFunction', 'getSubscription',
-            'getSubscriptions', 'getTags', 'getTagsData',
+            'getBugTaskWeightFunction', 'getSpecifications',
+            'getSubscription', 'getSubscriptions', 'getTags', 'getTagsData',
             'getUsedBugTagsWithOpenCounts', 'name', 'official_bug_tags',
             'parent_subscription_target', 'product', 'product_release',
             'productseries', 'searchTasks', 'series_target',
-            'specifications', 'summary', 'target', 'target_type_display',
-            'title', 'userCanAlterBugSubscription',
-            'userCanAlterSubscription', 'userHasBugSubscriptions',
+            'summary', 'target', 'target_type_display', 'title',
+            'userCanAlterBugSubscription', 'userCanAlterSubscription',
+            'userHasBugSubscriptions',
             )),
         'launchpad.AnyAllowedPerson': set((
             'addBugSubscription', 'addBugSubscriptionFilter',
@@ -423,7 +425,8 @@
             owner=self.owner,
             product=self.product,
             )
-        self.assertContentEqual(specifications, self.milestone.specifications)
+        self.assertContentEqual(specifications,
+                                self.milestone.getSpecifications(None))
 
 
 class MilestonesContainsPartialSpecifications(TestCaseWithFactory):
@@ -452,12 +455,14 @@
     def test_milestones_on_product(self):
         spec, target_milestone = self._create_milestones_on_target(
             product=self.factory.makeProduct())
-        self.assertContentEqual([spec], target_milestone.specifications)
+        self.assertContentEqual([spec],
+                                target_milestone.getSpecifications(None))
 
     def test_milestones_on_distribution(self):
         spec, target_milestone = self._create_milestones_on_target(
             distribution=self.factory.makeDistribution())
-        self.assertContentEqual([spec], target_milestone.specifications)
+        self.assertContentEqual([spec],
+                                target_milestone.getSpecifications(None))
 
     def test_milestones_on_project(self):
         # A Project (Project Group) milestone contains all specifications
@@ -468,7 +473,53 @@
         spec, target_milestone = self._create_milestones_on_target(
             product=product)
         milestone = projectgroup.getMilestone(name=target_milestone.name)
-        self.assertContentEqual([spec], milestone.specifications)
+        self.assertContentEqual([spec], milestone.getSpecifications(None))
+
+    def makeMixedMilestone(self):
+        projectgroup = self.factory.makeProject()
+        owner = self.factory.makePerson()
+        public_product = self.factory.makeProduct(project=projectgroup)
+        public_milestone = self.factory.makeMilestone(product=public_product)
+        product = self.factory.makeProduct(
+            owner=owner, information_type=InformationType.PROPRIETARY,
+            project=projectgroup, bug_sharing_policy=BugSharingPolicy.PUBLIC)
+        target_milestone = self.factory.makeMilestone(
+            product=product, name=public_milestone.name)
+        milestone = projectgroup.getMilestone(name=public_milestone.name)
+        return milestone, target_milestone, owner
+
+    def test_getSpecifications_milestone_privacy(self):
+        # Ensure getSpecifications respects milestone privacy.
+        # This looks wrong, because the specification is actually public, and
+        # we don't normally hide specifications based on the visibility of
+        # their products.  But we're not trying to hide the specification.
+        # We're hiding the fact that this specification is associated with
+        # a proprietary Product milestone.  We create a proprietary product
+        # because that's the only way to get a proprietary milestone.
+        milestone, target_milestone, owner = self.makeMixedMilestone()
+        with person_logged_in(owner):
+            spec = self.factory.makeSpecification(milestone=target_milestone)
+        self.assertContentEqual([],
+                                milestone.getSpecifications(None))
+        self.assertContentEqual([spec],
+                                milestone.getSpecifications(owner))
+
+    def test_bugtasks_milestone_privacy(self):
+        # Ensure bugtasks respects milestone privacy.
+        # This looks wrong, because the bugtask is actually public, and we
+        # don't normally hide bugtasks based on the visibility of their
+        # products.  But we're not trying to hide the bugtask.  We're hiding
+        # the fact that this bugtask is associated with a proprietary Product
+        # milestone.  We create a proprietary product because that's the only
+        # way to get a proprietary milestone.
+        milestone, target_milestone, owner = self.makeMixedMilestone()
+        with person_logged_in(owner):
+            bugtask = self.factory.makeBugTask(
+                target=target_milestone.product)
+        with person_logged_in(bugtask.owner):
+            bugtask.transitionToMilestone(target_milestone, owner)
+        self.assertContentEqual([], milestone.bugtasks(None))
+        self.assertContentEqual([bugtask], milestone.bugtasks(owner))
 
     def test_milestones_with_deleted_workitems(self):
         # Deleted work items do not cause the specification to show up
@@ -479,7 +530,7 @@
             product=milestone.product)
         self.factory.makeSpecificationWorkItem(
             specification=specification, milestone=milestone, deleted=True)
-        self.assertContentEqual([], milestone.specifications)
+        self.assertContentEqual([], milestone.getSpecifications(None))
 
 
 class TestMilestoneInformationType(TestCaseWithFactory):

=== modified file 'lib/lp/registry/tests/test_milestonetag.py'
--- lib/lp/registry/tests/test_milestonetag.py	2012-02-28 04:24:19 +0000
+++ lib/lp/registry/tests/test_milestonetag.py	2012-10-19 15:54:26 +0000
@@ -189,21 +189,21 @@
         # Ensure that all specifications on a milestone can be retrieved.
         specs, milestonetag = self._create_items_for_retrieval(
             self._create_specifications)
-        self.assertContentEqual(specs, milestonetag.specifications)
+        self.assertContentEqual(specs, milestonetag.getSpecifications(None))
 
     def test_specifications_for_untagged_milestone(self):
         # Ensure that specifications for a project group are retrieved
         # only if associated with milestones having specified tags.
         specs, milestonetag = self._create_items_for_untagged_milestone(
             self._create_specifications)
-        self.assertContentEqual(specs, milestonetag.specifications)
+        self.assertContentEqual(specs, milestonetag.getSpecifications(None))
 
     def test_specifications_multiple_tags(self):
         # Ensure that, in presence of multiple tags, only specifications
         # for milestones associated with all the tags are retrieved.
         specs, milestonetag = self._create_items_for_multiple_tags(
             self._create_specifications)
-        self.assertContentEqual(specs, milestonetag.specifications)
+        self.assertContentEqual(specs, milestonetag.getSpecifications(None))
 
 
 class MilestoneTagWebServiceTest(WebServiceTestCase):

=== modified file 'lib/lp/registry/tests/test_project_milestone.py'
--- lib/lp/registry/tests/test_project_milestone.py	2012-08-07 02:31:56 +0000
+++ lib/lp/registry/tests/test_project_milestone.py	2012-10-19 15:54:26 +0000
@@ -227,7 +227,7 @@
         # The spec for firefox (not a gnome product) is not included
         # in the specifications, while the other two specs are included.
         self.assertEqual(
-            [spec.name for spec in gnome_milestone.specifications],
+            [spec.name for spec in gnome_milestone.getSpecifications(None)],
             ['evolution-specification', 'gnomebaker-specification'])
 
     def _createProductBugtask(self, product_name, milestone_name):

=== modified file 'lib/lp/registry/tests/test_projectgroup.py'
--- lib/lp/registry/tests/test_projectgroup.py	2012-08-13 21:04:17 +0000
+++ lib/lp/registry/tests/test_projectgroup.py	2012-10-19 15:54:26 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -6,7 +6,9 @@
 from lazr.restfulclient.errors import ClientError
 from zope.component import getUtility
 from zope.security.interfaces import Unauthorized
+from zope.security.proxy import removeSecurityProxy
 
+from lp.app.enums import InformationType
 from lp.registry.enums import (
     EXCLUSIVE_TEAM_POLICY,
     INCLUSIVE_TEAM_POLICY,
@@ -17,6 +19,7 @@
     launchpadlib_for,
     login_celebrity,
     login_person,
+    person_logged_in,
     TestCaseWithFactory,
     )
 from lp.testing.layers import (
@@ -49,6 +52,19 @@
             closed_team = self.factory.makeTeam(membership_policy=policy)
             self.factory.makeProject(owner=closed_team)
 
+    def test_getProducts_with_proprietary(self):
+        # Proprietary projects are not listed for users without access to
+        # them.
+        project_group = removeSecurityProxy(self.factory.makeProject())
+        owner = self.factory.makePerson()
+        product = self.factory.makeProduct(
+            project=project_group, owner=owner,
+            information_type=InformationType.PROPRIETARY)
+        self.assertNotIn(product, project_group.getProducts(None))
+        outsider = self.factory.makePerson()
+        self.assertNotIn(product, project_group.getProducts(outsider))
+        self.assertIn(product, project_group.getProducts(owner))
+
 
 class ProjectGroupSearchTestCase(TestCaseWithFactory):
 
@@ -163,6 +179,37 @@
         self.pg.owner = self.factory.makePerson(name='project-group-owner')
 
 
+class TestMilestones(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_milestones_privacy(self):
+        """ProjectGroup.milestones uses logged-in user."""
+        owner = self.factory.makePerson()
+        project_group = self.factory.makeProject()
+        product = self.factory.makeProduct(
+            information_type=InformationType.PROPRIETARY, owner=owner,
+            project=project_group)
+        milestone = self.factory.makeMilestone(product=product)
+        self.assertContentEqual([], project_group.milestones)
+        with person_logged_in(owner):
+            names = [ms.name for ms in project_group.milestones]
+            self.assertEqual([milestone.name], names)
+
+    def test_all_milestones_privacy(self):
+        """ProjectGroup.milestones uses logged-in user."""
+        owner = self.factory.makePerson()
+        project_group = self.factory.makeProject()
+        product = self.factory.makeProduct(
+            information_type=InformationType.PROPRIETARY, owner=owner,
+            project=project_group)
+        milestone = self.factory.makeMilestone(product=product)
+        self.assertContentEqual([], project_group.milestones)
+        with person_logged_in(owner):
+            names = [ms.name for ms in project_group.all_milestones]
+            self.assertEqual([milestone.name], names)
+
+
 class TestLaunchpadlibAPI(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer


Follow ups