← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~adeuring/launchpad/milestone-sec-adapter into lp:launchpad

 

Abel Deuring has proposed merging lp:~adeuring/launchpad/milestone-sec-adapter into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/milestone-sec-adapter/+merge/129907

This branch adds a "sharing wawre" security adapter for IMilestone.

Milestones related to private products are now only shown to persons
with policy grants for the product; the only exception are some
attributes that leak no information: the database ID, and the methods
userCanView(), checkAuthenticated(), checkUnauthenticated().

The two latter methods should not appear at all -- we use adapters
for the security checks, so there is no need to define them in the
main class. And in fact they are not implemented at all.

Similary, IMilestone and some other interface classes included in
IMilestone define a few attributes that not implemented by class
Milestone.

We do not have very much time to finish the "Private products" story,
so I simply tried to ignore these inconsistencies.

Details of the change:

lp/registry/configure.zcml:

Access to most attributes and to "partial" interfaces that were public
now requires the permission launchpad.View; the permission
launchpad.AnyPerson is replaced with launchapd.AnyAllowedPerson.

(lp.services.webapp.authorization.LaunchpadSecurityPolicy.checkPermission()
has a "shortcut" for the permission launchpad.AnyPerson: no
dedicated security adapters are looked up for this permission,
so the new rule "data for milestones of private product should only
be visible for person having a policy grant" cannot be implemented
with this permission.)

I also sorted the attributes requiring lp.View alphabetically.

lp/security.py:

two new adapters for IMilestone and the permissions lp.View and
lp.AnyAllowedPerson, respectively.

Both methods delegate the security check to the new method
Milestone.userCanView()

I akso removed some unnecessary imports.

lp/registry/model/milestone.py:

The new method userCanView(). The acutal permission check is done
by IProduct.userCanView()

lp/registry/tests/test_milestone.py:

Tests for the permissions.

The test class properties expected_get_permissions and
expected_set_permissions are also intended to document which permissions
are acutally used for IMilestone.

The tests are similar to the test pattern I also used for the
security adapters of IProduct and ISpecification, so I moved the
common method checkPermissions() to lp.testing.TestCase.

test:
./bin/test registry -vvt lp.registry.tests.test_milestone.MilestoneSecurityAdaperTestCase

no lint

-- 
https://code.launchpad.net/~adeuring/launchpad/milestone-sec-adapter/+merge/129907
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/milestone-sec-adapter into lp:launchpad.
=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
=== modified file 'lib/lp/blueprints/tests/test_specification.py'
--- lib/lp/blueprints/tests/test_specification.py	2012-09-24 13:43:00 +0000
+++ lib/lp/blueprints/tests/test_specification.py	2012-10-16 14:36:28 +0000
@@ -139,26 +139,6 @@
         self.assertRaises(
             Unauthorized, getattr, specification, 'setTarget')
 
-    def check_permissions(self, expected_permissions, used_permissions,
-                             type_):
-        expected = set(expected_permissions.keys())
-        self.assertEqual(
-            expected, set(used_permissions.values()),
-            'Unexpected %s permissions' % type_)
-        for permission in expected_permissions:
-            attribute_names = set(
-                name for name, value in used_permissions.items()
-                if value == permission)
-            self.assertEqual(
-                expected_permissions[permission], attribute_names,
-                'Unexpected set of attributes with %s permission %s:\n'
-                'Defined but not expected: %s\n'
-                'Expected but not defined: %s'
-                % (
-                    type_, permission,
-                    attribute_names - expected_permissions[permission],
-                    expected_permissions[permission] - attribute_names))
-
     def test_get_permissions(self):
         expected_get_permissions = {
             CheckerPublic: set((
@@ -196,7 +176,7 @@
             }
         specification = self.factory.makeSpecification()
         checker = getChecker(specification)
-        self.check_permissions(
+        self.checkPermissions(
             expected_get_permissions, checker.get_permissions, 'get')
 
     def test_set_permissions(self):
@@ -211,7 +191,7 @@
             }
         specification = self.factory.makeSpecification()
         checker = getChecker(specification)
-        self.check_permissions(
+        self.checkPermissions(
             expected_get_permissions, checker.set_permissions, 'set')
 
     def test_security_adapters(self):

=== modified file 'lib/lp/bugs/browser/bugtask.py'
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py	2012-10-15 09:15:29 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py	2012-10-16 14:36:28 +0000
@@ -157,7 +157,11 @@
     def test_rendered_query_counts_constant_with_attachments(self):
         with celebrity_logged_in('admin'):
             browses_under_limit = BrowsesWithQueryLimit(
+<<<<<<< TREE
                 85, self.factory.makePerson())
+=======
+                95, self.factory.makePerson())
+>>>>>>> MERGE-SOURCE
 
             # First test with a single attachment.
             task = self.factory.makeBugTask()
@@ -240,6 +244,7 @@
         [activity] = view.interesting_activity
         self.assertEqual("description", activity.whatchanged)
 
+<<<<<<< TREE
     def test_rendered_query_counts_constant_with_activities(self):
         # More queries are not used for extra bug activities.
         task = self.factory.makeBugTask()
@@ -286,6 +291,33 @@
 
         self.assertThat(bug, browses_under_limit)
 
+=======
+    def test_rendered_query_counts_constant_with_activities(self):
+        # More queries are not used for extra bug activities.
+        task = self.factory.makeBugTask()
+
+        def add_activity(what, who):
+            getUtility(IBugActivitySet).new(
+                task.bug, datetime.now(UTC), who, whatchanged=what)
+
+        # Render the view with one activity.
+        with celebrity_logged_in('admin'):
+            browses_under_limit = BrowsesWithQueryLimit(
+                93, self.factory.makePerson())
+            person = self.factory.makePerson()
+            add_activity("description", person)
+
+        self.assertThat(task, browses_under_limit)
+
+        # Render the view with many more activities by different people.
+        with celebrity_logged_in('admin'):
+            for _ in range(20):
+                person = self.factory.makePerson()
+                add_activity("description", person)
+
+        self.assertThat(task, browses_under_limit)
+
+>>>>>>> MERGE-SOURCE
     def test_error_for_changing_target_with_invalid_status(self):
         # If a user moves a bug task with a restricted status (say,
         # Triaged) to a target where they do not have permission to set

=== modified file 'lib/lp/bugs/model/bug.py'
=== modified file 'lib/lp/bugs/tests/test_bugs_webservice.py'
--- lib/lp/bugs/tests/test_bugs_webservice.py	2012-10-12 14:53:10 +0000
+++ lib/lp/bugs/tests/test_bugs_webservice.py	2012-10-16 14:36:28 +0000
@@ -400,6 +400,40 @@
 
     layer = DatabaseFunctionalLayer
 
+<<<<<<< TREE
+=======
+    def test_default_private_bugs_true(self):
+        # Verify the path through user submission, to MaloneApplication to
+        # BugSet, and back to the user creates a private bug according
+        # to the project's bugs are private by default rule.
+        project = self.factory.makeLegacyProduct(
+            licenses=[License.OTHER_PROPRIETARY])
+        target_url = api_url(project)
+        with person_logged_in(project.owner):
+            project.setPrivateBugs(True, project.owner)
+        webservice = launchpadlib_for('test', 'salgado')
+        bugs_collection = webservice.load('/bugs')
+        bug = bugs_collection.createBug(
+            target=target_url, title='title', description='desc')
+        self.assertEqual('Private', bug.information_type)
+
+    def test_explicit_private_private_bugs_true(self):
+        # Verify the path through user submission, to MaloneApplication to
+        # BugSet, and back to the user creates a private bug because the
+        # user commands it.
+        project = self.factory.makeLegacyProduct(
+            licenses=[License.OTHER_PROPRIETARY])
+        target_url = api_url(project)
+        with person_logged_in(project.owner):
+            project.setPrivateBugs(True, project.owner)
+        webservice = launchpadlib_for('test', 'salgado')
+        bugs_collection = webservice.load('/bugs')
+        bug = bugs_collection.createBug(
+            target=target_url, title='title', description='desc',
+            private=True)
+        self.assertEqual('Private', bug.information_type)
+
+>>>>>>> MERGE-SOURCE
     def test_default_sharing_policy_proprietary(self):
         # Verify the path through user submission, to MaloneApplication to
         # BugSet, and back to the user creates a private bug according

=== modified file 'lib/lp/code/browser/tests/test_branch.py'
=== modified file 'lib/lp/code/browser/tests/test_product.py'
=== modified file 'lib/lp/code/model/branchnamespace.py'
--- lib/lp/code/model/branchnamespace.py	2012-10-11 08:09:12 +0000
+++ lib/lp/code/model/branchnamespace.py	2012-10-16 14:36:28 +0000
@@ -369,6 +369,7 @@
 
     def getAllowedInformationTypes(self, who=None):
         """See `IBranchNamespace`."""
+<<<<<<< TREE
         # The project uses the new simplified branch_sharing_policy
         # rules, so check them.
 
@@ -387,6 +388,60 @@
 
         return BRANCH_POLICY_ALLOWED_TYPES[
             self.product.branch_sharing_policy]
+=======
+        if not self._using_branchvisibilitypolicy:
+            # The project uses the new simplified branch_sharing_policy
+            # rules, so check them.
+
+            # Some policies require that the branch owner or current user have
+            # full access to an information type. If it's required and the user
+            # doesn't hold it, no information types are legal.
+            required_grant = BRANCH_POLICY_REQUIRED_GRANTS[
+                self.product.branch_sharing_policy]
+            if (required_grant is not None
+                and not getUtility(IService, 'sharing').checkPillarAccess(
+                    [self.product], required_grant, self.owner)
+                and (who is None
+                    or not getUtility(IService, 'sharing').checkPillarAccess(
+                        [self.product], required_grant, who))):
+                return []
+
+            return BRANCH_POLICY_ALLOWED_TYPES[
+                self.product.branch_sharing_policy]
+
+        # The project still uses BranchVisibilityPolicy, so check that.
+        private_rules = (
+            BranchVisibilityRule.PRIVATE,
+            BranchVisibilityRule.PRIVATE_ONLY)
+
+        rule = self.product.getBranchVisibilityRuleForTeam(self.owner)
+        if rule is not None:
+            # If there is an explicit rule for the namespace owner, use that.
+            private = rule in private_rules
+            public = rule != BranchVisibilityRule.PRIVATE_ONLY
+        else:
+            # Otherwise find all the rules for the owner's teams.
+            related_rules = set(p.rule for p in self._getRelatedPolicies())
+
+            # If any of the rules allow private branches, allow them.
+            private = bool(related_rules.intersection(private_rules))
+
+            # If any of the rules allow public branches, allow them.
+            if related_rules.difference([BranchVisibilityRule.PRIVATE_ONLY]):
+                public = True
+            else:
+                # There's no team-specific rules, or none of them allow
+                # public branches. Fall back to the default rule.
+                base_rule = self.product.getBaseBranchVisibilityRule()
+                public = base_rule == BranchVisibilityRule.PUBLIC
+
+        types = []
+        if public:
+            types.extend(PUBLIC_INFORMATION_TYPES)
+        if private:
+            types.extend(FREE_PRIVATE_INFORMATION_TYPES)
+        return types
+>>>>>>> MERGE-SOURCE
 
     def getDefaultInformationType(self, who=None):
         """See `IBranchNamespace`."""

=== modified file 'lib/lp/code/stories/branches/xx-product-branches.txt'
=== modified file 'lib/lp/code/stories/webservice/xx-code-import.txt'
=== modified file 'lib/lp/registry/browser/configure.zcml'
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2012-10-12 19:34:12 +0000
+++ lib/lp/registry/browser/product.py	2012-10-16 14:36:28 +0000
@@ -1392,6 +1392,7 @@
         ]
     custom_widget('licenses', LicenseWidget)
     custom_widget('license_info', GhostWidget)
+<<<<<<< TREE
     custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
 
     @property
@@ -1405,6 +1406,19 @@
             return canonical_url(getUtility(IProductSet))
 
     cancel_url = next_url
+=======
+    custom_widget('information_type', LaunchpadRadioWidgetWithDescription)
+
+    @property
+    def next_url(self):
+        """See `LaunchpadFormView`."""
+        if self.context.active:
+            return canonical_url(self.context)
+        else:
+            return canonical_url(getUtility(IProductSet))
+
+    cancel_url = next_url
+>>>>>>> MERGE-SOURCE
 
     @property
     def page_title(self):
@@ -1441,11 +1455,16 @@
 
     @action("Change", name='change')
     def change_action(self, action, data):
+<<<<<<< TREE
         try:
             self.updateContextFromData(data)
         except CannotChangeInformationType as e:
             self.setFieldError('information_type', str(e))
 
+=======
+        self.updateContextFromData(data)
+
+>>>>>>> MERGE-SOURCE
 
 class ProductValidationMixin:
 

=== modified file 'lib/lp/registry/browser/productseries.py'
=== modified file 'lib/lp/registry/browser/tests/test_product.py'
--- lib/lp/registry/browser/tests/test_product.py	2012-10-15 09:15:29 +0000
+++ lib/lp/registry/browser/tests/test_product.py	2012-10-16 14:36:28 +0000
@@ -432,6 +432,7 @@
             cache.objects['team_membership_policy_data'])
 
 
+<<<<<<< TREE
 class TestProductEditView(BrowserTestCase):
     """Tests for the ProductEditView"""
 
@@ -517,6 +518,76 @@
                 InformationType.PUBLIC, updated_product.information_type)
 
 
+=======
+class TestProductEditView(TestCaseWithFactory):
+    """Tests for the ProductEditView"""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(TestProductEditView, self).setUp()
+
+    def _make_product_edit_form(self, product, proprietary=False):
+        """Return form data for product edit.
+
+        :param product: Factory product to base the form data base of.
+        """
+        if proprietary:
+            licenses = License.OTHER_PROPRIETARY.title
+            license_info = 'Commercial Subscription'
+            information_type = 'PROPRIETARY'
+        else:
+            licenses = ['MIT']
+            license_info = ''
+            information_type = 'PUBLIC'
+
+        return {
+            'field.actions.change': 'Change',
+            'field.name': product.name,
+            'field.displayname': product.displayname,
+            'field.title': product.title,
+            'field.summary': product.summary,
+            'field.information_type': information_type,
+            'field.licenses': licenses,
+            'field.license_info': license_info,
+        }
+
+    def test_change_information_type_proprietary(self):
+        product = self.factory.makeProduct(name='fnord')
+        with FeatureFixture({u'disclosure.private_projects.enabled': u'on'}):
+            login_person(product.owner)
+            form = self._make_product_edit_form(product, proprietary=True)
+            view = create_initialized_view(product, '+edit', form=form)
+            self.assertEqual(0, len(view.errors))
+
+            product_set = getUtility(IProductSet)
+            updated_product = product_set.getByName('fnord')
+            self.assertEqual(
+                InformationType.PROPRIETARY, updated_product.information_type)
+            # A complimentary commercial subscription is auto generated for
+            # the product when the information type is changed.
+            self.assertIsNotNone(updated_product.commercial_subscription)
+
+    def test_change_information_type_public(self):
+        owner = self.factory.makePerson(name='pting')
+        product = self.factory.makeProduct(
+            name='fnord',
+            information_type=InformationType.PROPRIETARY,
+            owner=owner,
+        )
+        with FeatureFixture({u'disclosure.private_projects.enabled': u'on'}):
+            login_person(owner)
+            form = self._make_product_edit_form(product)
+            view = create_initialized_view(product, '+edit', form=form)
+            self.assertEqual(0, len(view.errors))
+
+            product_set = getUtility(IProductSet)
+            updated_product = product_set.getByName('fnord')
+            self.assertEqual(
+                InformationType.PUBLIC, updated_product.information_type)
+
+
+>>>>>>> MERGE-SOURCE
 class ProductSetReviewLicensesViewTestCase(TestCaseWithFactory):
     """Tests the ProductSetReviewLicensesView."""
 

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2012-10-09 10:28:02 +0000
+++ lib/lp/registry/configure.zcml	2012-10-16 14:36:28 +0000
@@ -1026,22 +1026,30 @@
         <allow
             attributes="
                 id
-                name
+                userCanView
+                " />
+        <require
+            permission="launchpad.View"
+            attributes="
+                active
+                bugtasks
                 code_name
+                dateexpected
+                displayname
+                distribution
+                distroseries
+                getTags
+                getTagsData
+                name
                 product
-                distribution
+                product_release
                 productseries
-                distroseries
-                dateexpected
-                active
-                summary
-                target
                 series_target
-                displayname
-                title
-                bugtasks
                 specifications
-                product_release"/>
+                summary
+                target
+                title
+                "/>
         <require
             permission="launchpad.Edit"
             attributes="
@@ -1051,15 +1059,13 @@
                 setTags
                 "/>
         <require
-            permission="zope.Public"
-            attributes="
-                getTags
-                getTagsData
-                "/>
-        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
-        <allow
+            permission="launchpad.View"
+            interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
+        <require
+            permission="launchpad.View"
             interface="lp.bugs.interfaces.bugtarget.IHasBugs"/>
-        <allow
+        <require
+            permission="launchpad.View"
             interface="lp.bugs.interfaces.bugtarget.IHasOfficialBugTags"/>
         <allow
             interface="lp.app.interfaces.security.IAuthorization"/>
@@ -1070,10 +1076,11 @@
 
         <!-- IStructuralSubscriptionTarget -->
 
-        <allow
+        <require
+            permission="launchpad.View"
             interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
         <require
-            permission="launchpad.AnyPerson"
+            permission="launchpad.AnyAllowedPerson"
             interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />
 
     </class>
@@ -1229,6 +1236,7 @@
         class="lp.registry.model.product.Product">
         <allow
             interface="lp.registry.interfaces.product.IProductPublic"/>
+<<<<<<< TREE
         <allow
             interface="lp.registry.interfaces.pillar.IPillar"/>
         <require
@@ -1239,6 +1247,19 @@
             interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
         <require
             permission="launchpad.View"
+=======
+        <require
+            permission="launchpad.View"
+            interface="lp.registry.interfaces.product.IPillar"/>
+        <require
+            permission="launchpad.View"
+            interface="lp.registry.interfaces.product.IProductLimitedView"/>
+        <require
+            permission="launchpad.View"
+            interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
+        <require
+            permission="launchpad.View"
+>>>>>>> MERGE-SOURCE
             interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/>
         <require
             permission="launchpad.AnyAllowedPerson"
@@ -1371,8 +1392,14 @@
 
         <!-- IStructuralSubscriptionTarget -->
 
+<<<<<<< TREE
         <require
             permission="launchpad.View"
+=======
+        <!-- XXX bug=1065162 Abel Deuring 2012-10-10:
+             This interface should require the permission launchpad.View. -->
+        <allow
+>>>>>>> MERGE-SOURCE
             interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
         <require
             permission="launchpad.AnyAllowedPerson"

=== modified file 'lib/lp/registry/interfaces/milestone.py'
--- lib/lp/registry/interfaces/milestone.py	2012-10-15 02:32:30 +0000
+++ lib/lp/registry/interfaces/milestone.py	2012-10-16 14:36:28 +0000
@@ -267,6 +267,9 @@
         why this is not a property.
         """
 
+    def userCanView(user):
+        """True if the given user has access to this product."""
+
 
 # Avoid circular imports
 IBugTask['milestone'].schema = IMilestone

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py	2012-10-12 14:53:10 +0000
+++ lib/lp/registry/interfaces/product.py	2012-10-16 14:36:28 +0000
@@ -420,15 +420,45 @@
                 "Not applicable to 'Other/Proprietary'.")))
 
 
-class IProductPublic(Interface):
-
-    id = Int(title=_('The Project ID'))
-
-    def userCanView(user):
-        """True if the given user has access to this product."""
-
-
-class IProductLimitedView(
+<<<<<<< TREE
+class IProductPublic(Interface):
+
+    id = Int(title=_('The Project ID'))
+
+    def userCanView(user):
+        """True if the given user has access to this product."""
+
+
+class IProductLimitedView(
+=======
+class IProductPublic(Interface):
+
+    id = Int(title=_('The Project ID'))
+
+    def userCanView(user):
+        """True if the given user has access to this product."""
+
+    # bug=1065162 Abel Deuring 2012-10-10:
+    # name and displayname should be defined in IProductLimitedView
+    name = exported(
+        ProductNameField(
+            title=_('Name'),
+            constraint=name_validator,
+            description=_(
+                "At least one lowercase letter or number, followed by "
+                "letters, numbers, dots, hyphens or pluses. "
+                "Keep this name short; it is used in URLs as shown above.")))
+
+    displayname = exported(
+        TextLine(
+            title=_('Display Name'),
+            description=_("""The name of the project as it would appear in a
+                paragraph.""")),
+        exported_as='display_name')
+
+
+class IProductLimitedView(
+>>>>>>> MERGE-SOURCE
     IBugTarget, ICanGetMilestonesDirectly, IHasAppointedDriver, IHasBranches,
     IHasDrivers, IHasExternalBugTracker, IHasIcon,
     IHasLogo, IHasMergeProposals, IHasMilestones, IHasExpirableBugs,
@@ -488,22 +518,6 @@
         "required because there might be a project driver and also a "
         "driver appointed in the overarching project group.")
 
-    name = exported(
-        ProductNameField(
-            title=_('Name'),
-            constraint=name_validator,
-            description=_(
-                "At least one lowercase letter or number, followed by "
-                "letters, numbers, dots, hyphens or pluses. "
-                "Keep this name short; it is used in URLs as shown above.")))
-
-    displayname = exported(
-        TextLine(
-            title=_('Display Name'),
-            description=_("""The name of the project as it would appear in a
-                paragraph.""")),
-        exported_as='display_name')
-
     title = exported(
         Title(
             title=_('Title'),

=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py	2012-10-06 23:14:35 +0000
+++ lib/lp/registry/model/milestone.py	2012-10-16 14:36:28 +0000
@@ -351,6 +351,17 @@
         from lp.registry.model.milestonetag import MilestoneTag
         return list(self.getTagsData().values(MilestoneTag.tag))
 
+    def userCanView(self, user):
+        """See `IMilestone`."""
+        # A database constraint ensures that either self.product
+        # or self.distribution is not None.
+        if self.product is None:
+            # Distributions are always public, and so are their
+            # milestones.
+            return True
+        # Delegate the permission check
+        return self.product.userCanView(user)
+
 
 class MilestoneSet:
     implements(IMilestoneSet)

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2012-10-15 09:15:29 +0000
+++ lib/lp/registry/model/product.py	2012-10-16 14:36:28 +0000
@@ -68,8 +68,12 @@
     FREE_INFORMATION_TYPES,
     InformationType,
     PRIVATE_INFORMATION_TYPES,
+<<<<<<< TREE
     PROPRIETARY_INFORMATION_TYPES,
     PUBLIC_INFORMATION_TYPES,
+=======
+    PUBLIC_INFORMATION_TYPES,
+>>>>>>> MERGE-SOURCE
     PUBLIC_PROPRIETARY_INFORMATION_TYPES,
     service_uses_launchpad,
     ServiceUsage,
@@ -432,6 +436,7 @@
         # Proprietary check works only after creation, because during
         # creation, has_commercial_subscription cannot give the right value
         # and triggers an inappropriate DB flush.
+<<<<<<< TREE
 
         # If you're changing the license, and setting a PROPRIETARY
         # information type, yet you don't have a subscription you get one when
@@ -450,11 +455,33 @@
                     'A valid commercial subscription is required for private'
                     ' Projects.')
 
+=======
+
+        # If you're changing the license, and setting a PROPRIETARY
+        # information type, yet you don't have a subscription you get one when
+        # the license is set.
+
+        # If you have a commercial subscription, but it's not current, you
+        # cannot set the information type to a PROPRIETARY type.
+        if not self._SO_creating and value in PROPRIETARY_INFORMATION_TYPES:
+            # Create the complimentary commercial subscription for the product.
+            self._ensure_complimentary_subscription()
+
+            if not self.has_current_commercial_subscription:
+                raise CommercialSubscribersOnly(
+                    'A valid commercial subscription is required for private'
+                    ' Projects.')
+
+>>>>>>> MERGE-SOURCE
         return value
 
     _information_type = EnumCol(
         enum=InformationType, default=InformationType.PUBLIC,
+<<<<<<< TREE
         dbName="information_type",
+=======
+        dbName='information_type',
+>>>>>>> MERGE-SOURCE
         storm_validator=_valid_product_information_type)
 
     def _get_information_type(self):
@@ -1521,6 +1548,7 @@
 
         return weight_function
 
+<<<<<<< TREE
     @cachedproperty
     def _known_viewers(self):
         """A set of known persons able to view this product."""
@@ -1554,6 +1582,33 @@
         self._known_viewers.add(user.id)
         return True
 
+=======
+    @cachedproperty
+    def _known_viewers(self):
+        """A set of known persons able to view this product."""
+        return set()
+
+    def userCanView(self, user):
+        """See `IProductPublic`."""
+        if self.information_type in PUBLIC_INFORMATION_TYPES:
+            return True
+        if user is None:
+            return False
+        if user.id in self._known_viewers:
+            return True
+        # We want an actual Storm Person.
+        if IPersonRoles.providedBy(user):
+            user = user.person
+        policy = getUtility(IAccessPolicySource).find(
+            [(self, self.information_type)]).one()
+        grants_for_user = getUtility(IAccessPolicyGrantSource).find(
+            [(policy, user)])
+        if grants_for_user.is_empty():
+            return False
+        self._known_viewers.add(user.id)
+        return True
+
+>>>>>>> MERGE-SOURCE
 
 def get_precached_products(products, need_licences=False, need_projects=False,
                         need_series=False, need_releases=False,

=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
=== modified file 'lib/lp/registry/tests/test_milestone.py'
--- lib/lp/registry/tests/test_milestone.py	2012-10-02 02:56:32 +0000
+++ lib/lp/registry/tests/test_milestone.py	2012-10-16 14:36:28 +0000
@@ -6,11 +6,20 @@
 __metaclass__ = type
 
 from operator import attrgetter
+from storm.exceptions import NoneError
 import unittest
 
 from zope.component import getUtility
+from zope.security.checker import (
+    CheckerPublic,
+    getChecker,
+    )
+from zope.security.interfaces import Unauthorized
 
+from lp.app.enums import InformationType
+from lp.app.interfaces.services import IService
 from lp.app.errors import NotFoundError
+from lp.registry.enums import SharingPermission
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.milestone import (
     IHasMilestones,
@@ -101,6 +110,244 @@
             [1, 2, 3])
 
 
+class MilestoneSecurityAdaperTestCase(TestCaseWithFactory):
+    """A TestCase for the security adapter of milestones."""
+
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        super(MilestoneSecurityAdaperTestCase, self).setUp()
+        self.public_product = self.factory.makeProduct()
+        self.public_milestone = self.factory.makeMilestone(
+            product=self.public_product)
+        self.proprietary_product_owner = self.factory.makePerson()
+        self.proprietary_product = self.factory.makeProduct(
+            owner=self.proprietary_product_owner,
+            information_type=InformationType.PROPRIETARY)
+        self.proprietary_milestone = self.factory.makeMilestone(
+            product=self.proprietary_product)
+
+    expected_get_permissions = {
+        CheckerPublic: set((
+            'id', 'checkAuthenticated', 'checkUnauthenticated',
+            'userCanView',
+            )),
+        'launchpad.View': set((
+            'active', 'bug_subscriptions', 'bugtasks', 'code_name',
+            'dateexpected', 'displayname', 'distribution', 'distroseries',
+            '_getOfficialTagClause', 'getBugSummaryContextWhereClause',
+            'getBugTaskWeightFunction', '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',
+            )),
+        'launchpad.AnyAllowedPerson': set((
+            'addBugSubscription', 'addBugSubscriptionFilter',
+            'addSubscription', 'removeBugSubscription',
+            )),
+        'launchpad.Edit': set((
+            'closeBugsAndBlueprints', 'createProductRelease',
+            'destroySelf', 'setTags',
+            )),
+        }
+
+    def test_get_permissions(self):
+        milestone = self.factory.makeMilestone()
+        checker = getChecker(milestone)
+        self.checkPermissions(
+            self.expected_get_permissions, checker.get_permissions, 'get')
+
+    expected_set_permissions = {
+        'launchpad.Edit': set((
+            'active', 'code_name', 'dateexpected', 'distroseries', 'name',
+            'product_release', 'productseries', 'summary',
+            )),
+        }
+
+    def test_set_permissions(self):
+        milestone = self.factory.makeMilestone()
+        checker = getChecker(milestone)
+        self.checkPermissions(
+            self.expected_set_permissions, checker.set_permissions, 'set')
+
+    def assertAccessAuthorzized(self, attribute_names, obj):
+        # Try to access the given attributes of obj. No exception
+        # should be raised.
+        for name in attribute_names:
+            # class Milestone does not implenet all attributes defined by
+            # class IMilestone. AttributeErrors caused by attempts to
+            # access these attribues are not relevant here: We simply
+            # want to be sure that no Unauthorized error is raised.
+            try:
+                getattr(obj, name)
+            except AttributeError:
+                pass
+
+    def assertAccessUnauthorzized(self, attribute_names, obj):
+        # Try to access the given attributes of obj. Unauthorized
+        # should be raised.
+        for name in attribute_names:
+            self.assertRaises(Unauthorized, getattr, obj, name)
+
+    def assertChangeAuthorzized(self, attribute_names, obj):
+        # Try to changes the given attributes of obj. Unauthorized
+        # should be raised.
+        for name in attribute_names:
+            # Not all attributes declared in configure.zcml to be
+            # settable actually exist. Attempts to set them raises
+            # an AttributeError. Setting an Attribute to None may no
+            # be allowed.
+            #
+            # Both errors can be ignored here: This method intends only
+            # to prove that Unauthorized is not raised.
+            try:
+                setattr(obj, name, None)
+            except (AttributeError, NoneError):
+                pass
+
+    def assertChangeUnauthorzized(self, attribute_names, obj):
+        # Try to changes the given attributes of obj. Unauthorized
+        # should be raised.
+        for name in attribute_names:
+            self.assertRaises(Unauthorized, setattr, obj, name, None)
+
+    def test_access_for_anonymous(self):
+        # Anonymous users have to public attributes of milestones for
+        # private and public products.
+        with person_logged_in(ANONYMOUS):
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions[CheckerPublic],
+                self.public_milestone)
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions[CheckerPublic],
+                self.proprietary_milestone)
+
+            # They have access to attributes requiring the permission
+            # launchpad.View of milestones for public products...
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions['launchpad.View'],
+                self.public_milestone)
+
+            # ...but not to the same attributes of milestones for private
+            # products.
+            self.assertAccessUnauthorzized(
+                self.expected_get_permissions['launchpad.View'],
+                self.proprietary_milestone)
+
+            # They cannot access other attributes.
+            for permission, names in self.expected_get_permissions.items():
+                if permission in (CheckerPublic, 'launchpad.View'):
+                    continue
+                self.assertAccessUnauthorzized(names, self.public_milestone)
+                self.assertAccessUnauthorzized(
+                    names, self.proprietary_milestone)
+
+            # They cannot change any attributes.
+            for permission, names in self.expected_set_permissions.items():
+                self.assertChangeUnauthorzized(names, self.public_milestone)
+                self.assertChangeUnauthorzized(
+                    names, self.proprietary_milestone)
+
+    def test_access_for_ordinary_user(self):
+        # Regular users have to public attributes of milestones for
+        # private and public products.
+        user = self.factory.makePerson()
+        with person_logged_in(user):
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions[CheckerPublic],
+                self.public_milestone)
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions[CheckerPublic],
+                self.proprietary_milestone)
+
+            # They have access to attributes requiring the permission
+            # launchpad.View or launchpad.AnyAllowedPerson of milestones
+            # for public products...
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions['launchpad.View'],
+                self.public_milestone)
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions['launchpad.AnyAllowedPerson'],
+                self.public_milestone)
+
+            # ...but not to the same attributes of milestones for private
+            # products.
+            self.assertAccessUnauthorzized(
+                self.expected_get_permissions['launchpad.View'],
+                self.proprietary_milestone)
+            self.assertAccessUnauthorzized(
+                self.expected_get_permissions['launchpad.AnyAllowedPerson'],
+                self.proprietary_milestone)
+
+            # They cannot access other attributes.
+            for permission, names in self.expected_get_permissions.items():
+                if permission in (
+                    CheckerPublic, 'launchpad.View',
+                    'launchpad.AnyAllowedPerson'):
+                    continue
+                self.assertAccessUnauthorzized(names, self.public_milestone)
+                self.assertAccessUnauthorzized(
+                    names, self.proprietary_milestone)
+
+            # They cannot change attributes.
+            for permission, names in self.expected_set_permissions.items():
+                self.assertChangeUnauthorzized(names, self.public_milestone)
+                self.assertChangeUnauthorzized(
+                    names, self.proprietary_milestone)
+
+    def test_access_for_user_with_grant_for_private_product(self):
+        # Users with a policy grant for a private product have access
+        # to most attributes of the private product.
+        user = self.factory.makePerson()
+        with person_logged_in(self.proprietary_product_owner):
+            getUtility(IService, 'sharing').sharePillarInformation(
+                self.proprietary_product, user, self.proprietary_product_owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+
+        with person_logged_in(user):
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions[CheckerPublic],
+                self.proprietary_milestone)
+
+            # They have access to attributes requiring the permission
+            # launchpad.View or launchpad.AnyAllowedPerson of milestones
+            # for the private product.
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions['launchpad.View'],
+                self.proprietary_milestone)
+            self.assertAccessAuthorzized(
+                self.expected_get_permissions['launchpad.AnyAllowedPerson'],
+                self.proprietary_milestone)
+
+            # They cannot access other attributes.
+            for permission, names in self.expected_get_permissions.items():
+                if permission in (
+                    CheckerPublic, 'launchpad.View',
+                    'launchpad.AnyAllowedPerson'):
+                    continue
+                self.assertAccessUnauthorzized(
+                    names, self.proprietary_milestone)
+
+            # They cannot change attributes.
+            for names in self.expected_set_permissions.values():
+                self.assertChangeUnauthorzized(
+                    names, self.proprietary_milestone)
+
+    def test_access_for_product_owner(self):
+        # The owner of a private product can access all attributes.
+        with person_logged_in(self.proprietary_product_owner):
+            for names in self.expected_get_permissions.values():
+                self.assertAccessAuthorzized(names, self.proprietary_milestone)
+
+            # They can change attributes.
+            for permission, names in self.expected_set_permissions.items():
+                self.assertChangeAuthorzized(names, self.proprietary_milestone)
+
+
 class HasMilestonesSnapshotTestCase(TestCaseWithFactory):
     """A TestCase for snapshots of pillars with milestones."""
 

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py	2012-10-15 09:15:29 +0000
+++ lib/lp/registry/tests/test_product.py	2012-10-16 14:36:28 +0000
@@ -523,6 +523,7 @@
                 CannotChangeInformationType, 'Answers is enabled.'):
                 product.information_type = InformationType.PROPRIETARY
 
+<<<<<<< TREE
     def test_no_proprietary_if_packaging(self):
         # information_type cannot be set to proprietary while any
         # productseries are packaged.
@@ -878,6 +879,316 @@
         product.userCanView(user)
         product.userCanView(IPersonRoles(user))
 
+=======
+    expected_get_permissions = {
+        # bug=1065162 Abel Deuring 2012-10-10:
+        # name, displayname
+        # should be defined in IProductLimitedView
+        CheckerPublic: set((
+            'id', 'information_type', 'userCanView', 'private',
+            'name', 'displayname', 'bug_subscriptions', 'getSubscription',
+            'getSubscriptions', 'parent_subscription_target',
+            'target_type_display', 'userCanAlterBugSubscription',
+            'userCanAlterSubscription', 'userHasBugSubscriptions'
+            )),
+        'launchpad.View': set((
+            '_getOfficialTagClause', '_all_specifications',
+            '_valid_specifications', 'active', 'active_or_packaged_series',
+            'aliases', 'all_milestones',
+            'allowsTranslationEdits', 'allowsTranslationSuggestions',
+            'announce', 'answer_contacts', 'answers_usage', 'autoupdate',
+            'blueprints_usage', 'branch_sharing_policy',
+            'bug_reported_acknowledgement', 'bug_reporting_guidelines',
+            'bug_sharing_policy', 'bug_supervisor',
+            'bug_tracking_usage', 'bugtargetdisplayname', 'bugtargetname',
+            'bugtracker', 'canUserAlterAnswerContact',
+            'checkPrivateBugsTransitionAllowed', 'codehosting_usage',
+            'coming_sprints', 'commercial_subscription',
+            'commercial_subscription_is_due', 'createBug',
+            'createCustomLanguageCode', 'custom_language_codes',
+            'date_next_suggest_packaging', 'datecreated', 'description',
+            'development_focus', 'development_focusID',
+            'direct_answer_contacts', 'distrosourcepackages',
+            'downloadurl', 'driver', 'drivers', 'enable_bug_expiration',
+            'enable_bugfiling_duplicate_search', 'findReferencedOOPS',
+            'findSimilarFAQs', 'findSimilarQuestions', 'freshmeatproject',
+            'getAllowedBugInformationTypes',
+            'getAllowedSpecificationInformationTypes', 'getAnnouncement',
+            'getAnnouncements', 'getAnswerContactsForLanguage',
+            'getAnswerContactRecipients', 'getBaseBranchVisibilityRule',
+            'getBranchVisibilityRuleForBranch',
+            'getBranchVisibilityRuleForTeam',
+            'getBranchVisibilityTeamPolicies', 'getBranches',
+            'getBugSummaryContextWhereClause', 'getBugTaskWeightFunction',
+            'getCustomLanguageCode', 'getDefaultBugInformationType',
+            'getDefaultSpecificationInformationType',
+            'getEffectiveTranslationPermission', 'getExternalBugTracker',
+            'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',
+            'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',
+            'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',
+            'getSeries', 'getSpecification',
+            'getSupportedLanguages', 'getTimeline',
+            'getTopContributors', 'getTopContributorsGroupedByCategory',
+            'getTranslationGroups', 'getTranslationImportQueueEntries',
+            'getTranslators', 'getUsedBugTagsWithOpenCounts',
+            'getVersionSortedSeries',
+            'has_current_commercial_subscription',
+            'has_custom_language_codes', 'has_milestones', 'homepage_content',
+            'homepageurl', 'icon', 'invitesTranslationEdits',
+            'invitesTranslationSuggestions',
+            'isUsingInheritedBranchVisibilityPolicy',
+            'license_info', 'license_status', 'licenses', 'logo', 'milestones',
+            'mugshot', 'name_with_project', 'newCodeImport',
+            'obsolete_translatable_series', 'official_answers',
+            'official_anything', 'official_blueprints', 'official_bug_tags',
+            'official_codehosting', 'official_malone', 'owner',
+            'packagedInDistros', 'packagings',
+            'past_sprints', 'personHasDriverRights', 'pillar',
+            'pillar_category', 'primary_translatable', 'private_bugs',
+            'programminglang', 'project', 'qualifies_for_free_hosting',
+            'recipes', 'redeemSubscriptionVoucher', 'registrant', 'releases',
+            'remote_product', 'removeCustomLanguageCode',
+            'removeTeamFromBranchVisibilityPolicy', 'screenshotsurl',
+            'searchFAQs', 'searchQuestions', 'searchTasks', 'security_contact',
+            'series', 'setBranchVisibilityTeamPolicy', 'setPrivateBugs',
+            'sharesTranslationsWithOtherSide', 'sourceforgeproject',
+            'sourcepackages', 'specification_sharing_policy', 'specifications',
+            'sprints', 'summary', 'title',
+            'translatable_packages', 'translatable_series',
+            'translation_focus', 'translationgroup', 'translationgroups',
+            'translationpermission', 'translations_usage', 'ubuntu_packages',
+            'userCanEdit', 'uses_launchpad',
+            'wikiurl')),
+        'launchpad.AnyAllowedPerson': set((
+            'addAnswerContact', 'addBugSubscription',
+            'addBugSubscriptionFilter', 'addSubscription',
+            'createQuestionFromBug', 'newQuestion', 'removeAnswerContact',
+            'removeBugSubscription')),
+        'launchpad.Append': set(('newFAQ', )),
+        'launchpad.Driver': set(('newSeries', )),
+        'launchpad.Edit': set((
+            'addOfficialBugTag', 'removeOfficialBugTag',
+            'setBranchSharingPolicy', 'setBugSharingPolicy',
+            'setSpecificationSharingPolicy')),
+        'launchpad.Moderate': set((
+            'is_permitted', 'license_approved', 'project_reviewed',
+            'reviewer_whiteboard', 'setAliases')),
+        }
+
+    def test_get_permissions(self):
+        product = self.factory.makeProduct()
+        checker = getChecker(product)
+        self.checkPermissions(
+            self.expected_get_permissions, checker.get_permissions, 'get')
+
+    def test_set_permissions(self):
+        expected_set_permissions = {
+            'launchpad.BugSupervisor': set((
+                'bug_reported_acknowledgement', 'bug_reporting_guidelines',
+                'bugtracker', 'enable_bug_expiration',
+                'enable_bugfiling_duplicate_search', 'official_bug_tags',
+                'official_malone', 'remote_product')),
+            'launchpad.Edit': set((
+                'answers_usage', 'blueprints_usage', 'bug_supervisor',
+                'bug_tracking_usage', 'codehosting_usage',
+                'commercial_subscription', 'description', 'development_focus',
+                'displayname', 'downloadurl', 'driver', 'freshmeatproject',
+                'homepage_content', 'homepageurl', 'icon', 'information_type',
+                'license_info', 'licenses', 'logo', 'mugshot',
+                'official_answers', 'official_blueprints',
+                'official_codehosting', 'owner', 'private',
+                'programminglang', 'project', 'redeemSubscriptionVoucher',
+                'releaseroot', 'screenshotsurl', 'sourceforgeproject',
+                'summary', 'title', 'uses_launchpad', 'wikiurl')),
+            'launchpad.Moderate': set((
+                'active', 'autoupdate', 'license_approved', 'name',
+                'project_reviewed', 'registrant', 'reviewer_whiteboard')),
+            'launchpad.TranslationsAdmin': set((
+                'translation_focus', 'translationgroup',
+                'translationpermission', 'translations_usage')),
+            'launchpad.AnyAllowedPerson': set((
+                'date_next_suggest_packaging', )),
+            }
+        product = self.factory.makeProduct()
+        checker = getChecker(product)
+        self.checkPermissions(
+            expected_set_permissions, checker.set_permissions, 'set')
+
+    def test_access_launchpad_View_public_product(self):
+        # Everybody, including anonymous users, has access to
+        # properties of public products that require the permission
+        # launchpad.View
+        product = self.factory.makeProduct()
+        names = self.expected_get_permissions['launchpad.View']
+        with person_logged_in(None):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+        ordinary_user = self.factory.makePerson()
+        with person_logged_in(ordinary_user):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+        with person_logged_in(product.owner):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+
+    def test_access_launchpad_View_proprietary_product(self):
+        # Only people with grants for a private product can access
+        # attributes protected by the permission launchpad.View.
+        product = self.createProduct(
+            information_type=InformationType.PROPRIETARY,
+            license=License.OTHER_PROPRIETARY)
+        owner = removeSecurityProxy(product).owner
+        names = self.expected_get_permissions['launchpad.View']
+        with person_logged_in(None):
+            for attribute_name in names:
+                self.assertRaises(
+                    Unauthorized, getattr, product, attribute_name)
+        ordinary_user = self.factory.makePerson()
+        with person_logged_in(ordinary_user):
+            for attribute_name in names:
+                self.assertRaises(
+                    Unauthorized, getattr, product, attribute_name)
+        with person_logged_in(owner):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+        # A user with a policy grant for the product can access attributes
+        # of a private product.
+        with person_logged_in(owner):
+            getUtility(IService, 'sharing').sharePillarInformation(
+                product, ordinary_user, owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        with person_logged_in(ordinary_user):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+
+    def test_access_launchpad_AnyAllowedPerson_public_product(self):
+        # Only logged in persons have access to properties of public products
+        # that require the permission launchpad.AnyAllowedPerson.
+        product = self.factory.makeProduct()
+        names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
+        with person_logged_in(None):
+            for attribute_name in names:
+                self.assertRaises(
+                    Unauthorized, getattr, product, attribute_name)
+        ordinary_user = self.factory.makePerson()
+        with person_logged_in(ordinary_user):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+        with person_logged_in(product.owner):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+
+    def test_access_launchpad_AnyAllowedPerson_proprietary_product(self):
+        # Only people with grants for a private product can access
+        # attributes protected by the permission launchpad.AnyAllowedPerson.
+        product = self.createProduct(
+            information_type=InformationType.PROPRIETARY,
+            license=License.OTHER_PROPRIETARY)
+        owner = removeSecurityProxy(product).owner
+        names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
+        with person_logged_in(None):
+            for attribute_name in names:
+                self.assertRaises(
+                    Unauthorized, getattr, product, attribute_name)
+        ordinary_user = self.factory.makePerson()
+        with person_logged_in(ordinary_user):
+            for attribute_name in names:
+                self.assertRaises(
+                    Unauthorized, getattr, product, attribute_name)
+        with person_logged_in(owner):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+        # A user with a policy grant for the product can access attributes
+        # of a private product.
+        with person_logged_in(owner):
+            getUtility(IService, 'sharing').sharePillarInformation(
+                product, ordinary_user, owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        with person_logged_in(ordinary_user):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+
+    def test_set_launchpad_AnyAllowedPerson_public_product(self):
+        # Only logged in users can set attributes protected by the
+        # permission launchpad.AnyAllowedPerson.
+        product = self.factory.makeProduct()
+        with person_logged_in(None):
+            self.assertRaises(
+                Unauthorized, setattr, product, 'date_next_suggest_packaging',
+                'foo')
+        ordinary_user = self.factory.makePerson()
+        with person_logged_in(ordinary_user):
+            setattr(product, 'date_next_suggest_packaging', 'foo')
+        with person_logged_in(product.owner):
+            setattr(product, 'date_next_suggest_packaging', 'foo')
+
+    def test_set_launchpad_AnyAllowedPerson_proprietary_product(self):
+        # Only people with grants for a private product can set
+        # attributes protected by the permission launchpad.AnyAllowedPerson.
+        product = self.createProduct(
+            information_type=InformationType.PROPRIETARY,
+            license=License.OTHER_PROPRIETARY)
+        owner = removeSecurityProxy(product).owner
+        with person_logged_in(None):
+            self.assertRaises(
+                Unauthorized, setattr, product, 'date_next_suggest_packaging',
+                'foo')
+        ordinary_user = self.factory.makePerson()
+        with person_logged_in(ordinary_user):
+            self.assertRaises(
+                Unauthorized, setattr, product, 'date_next_suggest_packaging',
+                'foo')
+        with person_logged_in(owner):
+            setattr(product, 'date_next_suggest_packaging', 'foo')
+        # A user with a policy grant for the product can access attributes
+        # of a private product.
+        with person_logged_in(owner):
+            getUtility(IService, 'sharing').sharePillarInformation(
+                product, ordinary_user, owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        with person_logged_in(ordinary_user):
+            setattr(product, 'date_next_suggest_packaging', 'foo')
+
+    def test_userCanView_caches_known_users(self):
+        # userCanView() maintains a cache of users known to have the
+        # permission to access a product.
+        product = self.createProduct(
+            information_type=InformationType.PROPRIETARY,
+            license=License.OTHER_PROPRIETARY)
+        owner = removeSecurityProxy(product).owner
+        user = self.factory.makePerson()
+        with person_logged_in(owner):
+            getUtility(IService, 'sharing').sharePillarInformation(
+                product, user, owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        with person_logged_in(user):
+            with StormStatementRecorder() as recorder:
+                # The first access to a property of the product from
+                # a user requires a DB query.
+                product.homepageurl
+                queries_for_first_user_access = len(recorder.queries)
+                # The second access does not require another query.
+                product.description
+                self.assertEqual(
+                queries_for_first_user_access, len(recorder.queries))
+
+    def test_access_to_deactivated_product(self):
+        # XXX bug=1065162 Abel Deuring 2012-10-10:
+        # The properties name, displayname, parent_subscription_target must
+        # be visible for inactive products.
+        product = self.factory.makeProduct()
+        removeSecurityProxy(product).active = False
+        user = self.factory.makePerson()
+        with person_logged_in(user):
+            product.name
+            product.displayname
+            product.parent_subscription_target
+            # Access to other attributes, including those defined in
+            # IPillar, is not possible.
+            self.assertRaises(Unauthorized, getattr, product, 'active')
+            self.assertRaises(Unauthorized, getattr, product, 'title')
+
+>>>>>>> MERGE-SOURCE
 
 class TestProductBugInformationTypes(TestCaseWithFactory):
 
@@ -1492,6 +1803,7 @@
                 self.product.owner))
         self.assertTrue(
             getUtility(IService, 'sharing').checkPillarAccess(
+<<<<<<< TREE
                 [self.product], InformationType.EMBARGOED,
                 self.product.owner))
 
@@ -1518,6 +1830,10 @@
 
     def getSharingPolicy(self):
         return self.product.specification_sharing_policy
+=======
+                [self.product], InformationType.EMBARGOED,
+                self.product.owner))
+>>>>>>> MERGE-SOURCE
 
 
 class ProductSnapshotTestCase(TestCaseWithFactory):

=== modified file 'lib/lp/registry/tests/test_product_webservice.py'
--- lib/lp/registry/tests/test_product_webservice.py	2012-10-12 14:53:10 +0000
+++ lib/lp/registry/tests/test_product_webservice.py	2012-10-16 14:36:28 +0000
@@ -70,8 +70,13 @@
     def test_branch_sharing_policy_non_commercial(self):
         # An API attempt to set a commercial-only branch_sharing_policy
         # on a non-commercial project returns Forbidden.
+<<<<<<< TREE
         product = self.factory.makeProduct()
         owner = product.owner
+=======
+        product = self.factory.makeLegacyProduct()
+        owner = product.owner
+>>>>>>> MERGE-SOURCE
         webservice = webservice_for_person(
             product.owner, permission=OAuthPermission.WRITE_PRIVATE)
         response = self.patch(
@@ -80,9 +85,14 @@
                 status=403,
                 body=('A current commercial subscription is required to use '
                       'proprietary branches.')))
+<<<<<<< TREE
         with person_logged_in(owner):
             self.assertEqual(
                 BranchSharingPolicy.PUBLIC, product.branch_sharing_policy)
+=======
+        with person_logged_in(owner):
+            self.assertIs(None, product.branch_sharing_policy)
+>>>>>>> MERGE-SOURCE
 
     def test_bug_sharing_policy_can_be_set(self):
         # bug_sharing_policy can be set via the API.
@@ -101,8 +111,13 @@
     def test_bug_sharing_policy_non_commercial(self):
         # An API attempt to set a commercial-only bug_sharing_policy
         # on a non-commercial project returns Forbidden.
+<<<<<<< TREE
         product = self.factory.makeProduct()
         owner = product.owner
+=======
+        product = self.factory.makeLegacyProduct()
+        owner = product.owner
+>>>>>>> MERGE-SOURCE
         webservice = webservice_for_person(
             product.owner, permission=OAuthPermission.WRITE_PRIVATE)
         response = self.patch(
@@ -111,9 +126,14 @@
                 status=403,
                 body=('A current commercial subscription is required to use '
                       'proprietary bugs.')))
+<<<<<<< TREE
         with person_logged_in(owner):
             self.assertEqual(
                 BugSharingPolicy.PUBLIC, product.bug_sharing_policy)
+=======
+        with person_logged_in(owner):
+            self.assertIs(None, product.bug_sharing_policy)
+>>>>>>> MERGE-SOURCE
 
     def fetch_product(self, webservice, product, api_version):
         with person_logged_in(webservice.user):

=== modified file 'lib/lp/security.py'
--- lib/lp/security.py	2012-10-12 14:53:10 +0000
+++ lib/lp/security.py	2012-10-16 14:36:28 +0000
@@ -425,6 +425,7 @@
         return user.isOwner(self.obj) or user.in_admin
 
 
+<<<<<<< TREE
 class ViewProduct(AuthorizationBase):
     permission = 'launchpad.View'
     usedfor = IProduct
@@ -449,6 +450,36 @@
         return False
 
 
+=======
+class ViewProduct(ViewPillar):
+    permission = 'launchpad.View'
+    usedfor = IProduct
+
+    def checkAuthenticated(self, user):
+        return (
+            super(ViewProduct, self).checkAuthenticated(user) and
+            self.obj.userCanView(user))
+
+    def checkUnauthenticated(self):
+        return (
+            self.obj.information_type in PUBLIC_INFORMATION_TYPES and
+            super(ViewProduct, self).checkUnauthenticated())
+
+
+class ChangeProduct(ViewProduct):
+    """Used for attributes of IProduct that are accessible to any logged
+    in user for public product but only to persons with access grants
+    for private products.
+    """
+
+    permission = 'launchpad.AnyAllowedPerson'
+    usedfor = IProduct
+
+    def checkUnauthenticated(self):
+        return False
+
+
+>>>>>>> MERGE-SOURCE
 class EditProduct(EditByOwnersOrAdmins):
     usedfor = IProduct
 
@@ -727,6 +758,25 @@
         return False
 
 
+class ViewMilestone(AuthorizationBase):
+    permission = 'launchpad.View'
+    usedfor = IMilestone
+
+    def checkAuthenticated(self, user):
+        return self.obj.userCanView(user)
+
+    def checkUnauthenticated(self):
+        return self.obj.userCanView(user=None)
+
+
+class EditMilestone(ViewMilestone):
+    permission = 'launchpad.AnyAllowedPerson'
+    usedfor = IMilestone
+
+    def checkUnauthenticated(self):
+        return False
+
+
 class EditMilestoneByTargetOwnerOrAdmins(AuthorizationBase):
     permission = 'launchpad.Edit'
     usedfor = IMilestone

=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2012-09-05 14:44:17 +0000
+++ lib/lp/testing/__init__.py	2012-10-16 14:36:28 +0000
@@ -704,6 +704,36 @@
             raise AssertionError(
                 'string %r does not end with %r' % (s, suffix))
 
+    def checkPermissions(self, expected_permissions, used_permissions,
+                          type_):
+        """Check if the used_permissions match expected_permissions.
+
+        :param expected_permissions: A dictionary mapping a permission
+            to a set of attribute names.
+        :param used_permissions: The property get_permissions or
+            set_permissions of getChecker(security_proxied_object).
+        :param type_: The string "set" or "get".
+        """
+        expected = set(expected_permissions.keys())
+        self.assertEqual(
+            expected, set(used_permissions.values()),
+            'Unexpected %s permissions' % type_)
+        for permission in expected_permissions:
+            attribute_names = set(
+                name for name, value in used_permissions.items()
+                if value == permission)
+            self.assertEqual(
+                expected_permissions[permission], attribute_names,
+                'Unexpected set of attributes with %s permission %s:\n'
+                'Defined but not expected: %s\n'
+                'Expected but not defined: %s'
+                % (
+                    type_, permission,
+                    sorted(
+                        attribute_names - expected_permissions[permission]),
+                    sorted(
+                        expected_permissions[permission] - attribute_names)))
+
 
 class TestCaseWithFactory(TestCase):
 

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2012-10-15 23:20:25 +0000
+++ lib/lp/testing/factory.py	2012-10-16 14:36:28 +0000
@@ -1025,6 +1025,23 @@
                 specification_sharing_policy)
         if information_type is not None:
             naked_product.information_type = information_type
+<<<<<<< TREE
+=======
+        return product
+
+    def makeLegacyProduct(self, **kwargs):
+        # Create a product which does not have any of the new bug and branch
+        # sharing policies set. New products have these set to default values
+        # but we need to test for existing products which have not yet been
+        # migrated.
+        # XXX This method can be removed when branch visibility policy dies.
+        product = self.makeProduct(**kwargs)
+        # Since createProduct() doesn't create PRIVATESECURITY/USERDATA.
+        removeSecurityProxy(product)._ensurePolicies([
+            InformationType.PRIVATESECURITY, InformationType.USERDATA])
+        removeSecurityProxy(product).bug_sharing_policy = None
+        removeSecurityProxy(product).branch_sharing_policy = None
+>>>>>>> MERGE-SOURCE
         return product
 
     def makeProductSeries(self, product=None, name=None, owner=None,