← Back to team overview

launchpad-reviewers team mailing list archive

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

 

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

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~adeuring/launchpad/product-sharing-sec-adapter/+merge/127473

This branch changes the security configuration for IProduct.

Access to most properties of IProduct now requires the permission
launchpad.LimitedView or launchpad.AnyAllowedPerson. The only still
publicly available properties are id, information_type and a new method
userCanView()

Details:

- A new interface class IProductLimitedView, which defines most
  properties that were previously defined in IProductPublic

- IProductPublic now defines only the properties id, information_type
  and userCanView()

- registry/configure.zcml now requires the permissions launchpad.LimitedView
  or launchpad.AnyAllowedPerson for most "partial interfaces" of IProduct.

- new security adapters ViewProduct (for the permission lp.LimitedView)
  and ChangeProduct (for the permission lp.AnyAllowedPerson).
  These security adapters check if a project is public; if so, they allow
  access for any logged in person; ViewProduct also allws access for
  anonymous users in this case. For non-public projects, these adapters
  use the new method IProduct.userCanView() to check if the current user
  may be given access.

- the implementation of userCanView() is obviously incomplete: Only
  a project owner can currently access data of a private project.
  I will change this in a follow-up branch so that userCanView() will
  check if the user has an access policy grant for the given product.
  (The implementation of this change would have made the diff of this
  branch a bit too large ;)

  The implementation of IProduct.information_type is also still
  incomplete (the DB patch that adds a column Product.information_type
  has not yet been applied). Products are by default public; only some
  new tests set information_type to a non-public state, but this
  change is obviously not permanent. Hence there on risk that properties
  of any existing product become accidentally inaccessible.

- the tests test_get_permissions() and test_set_permissions() may look a
  bit excessive, but having the permissions neede for all properties
  documented in the dictionaries expected_[gs]et_permissions helped
  me to check that I did not leave any property publicly accessible.

- I had to change some websevice tests: webservice_for_person() calls
  endInteraction(), so we don't have any user/principal defined when
  this function finishes. This mean that access to an attribute of a
  product will fail, if no new interaction is created. This is why the
  changes store for example product.owner in alocal variable right after
  the product has been  generated. For assertions that access
  product attributes, I used the context manager person_logged_in().

tests:

bin/test -vvt lp.registry.tests.test_product.TestProduct.test_.et_permissions
bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_LimitedView_proprietary_product
bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_LimitedView_public_product
bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_AnyAllowedPerson_proprietary_product
bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_AnyAllowedPerson_public_product

no lint.

-- 
https://code.launchpad.net/~adeuring/launchpad/product-sharing-sec-adapter/+merge/127473
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~adeuring/launchpad/product-sharing-sec-adapter into lp:launchpad.
=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2012-09-25 04:29:34 +0000
+++ lib/lp/registry/configure.zcml	2012-10-02 12:37:21 +0000
@@ -1229,11 +1229,17 @@
         class="lp.registry.model.product.Product">
         <allow
             interface="lp.registry.interfaces.product.IProductPublic"/>
-        <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
-        <allow
+        <require
+            permission="launchpad.LimitedView"
+            interface="lp.registry.interfaces.product.IProductLimitedView"/>
+        <require
+            permission="launchpad.LimitedView"
+            interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
+        <require
+            permission="launchpad.LimitedView"
             interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/>
         <require
-            permission="launchpad.View"
+            permission="launchpad.AnyAllowedPerson"
             set_attributes="date_next_suggest_packaging"/>
         <require
             permission="launchpad.Driver"
@@ -1311,7 +1317,7 @@
                 translationpermission
                 translations_usage"/>
         <require
-            permission="zope.Public"
+            permission="launchpad.LimitedView"
             attributes="
                 qualifies_for_free_hosting"/>
         <require
@@ -1326,7 +1332,8 @@
 
         <!-- IHasAliases -->
 
-        <allow
+        <require
+            permission="launchpad.LimitedView"
             attributes="
                 aliases"/>
         <require
@@ -1336,14 +1343,17 @@
 
         <!-- IQuestionTarget -->
 
-        <allow interface="lp.answers.interfaces.questiontarget.IQuestionTargetPublic"/>
-        <require
-          permission="launchpad.AnyPerson"
+        <require
+            permission="launchpad.LimitedView"
+            interface="lp.answers.interfaces.questiontarget.IQuestionTargetPublic"/>
+        <require
+          permission="launchpad.AnyAllowedPerson"
           interface="lp.answers.interfaces.questiontarget.IQuestionTargetView"/>
 
         <!-- IFAQTarget -->
 
-        <allow
+        <require
+            permission="launchpad.LimitedView"
             interface="lp.answers.interfaces.faqcollection.IFAQCollection"
             attributes="
                 findSimilarFAQs"/>
@@ -1354,15 +1364,17 @@
 
         <!-- IStructuralSubscriptionTarget -->
 
-        <allow
+        <require
+            permission="launchpad.LimitedView"
             interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
         <require
-            permission="launchpad.AnyPerson"
+            permission="launchpad.AnyAllowedPerson"
             interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />
 
         <!-- IHasBugSupervisor -->
 
-        <allow
+        <require
+            permission="launchpad.LimitedView"
             attributes="
                 bug_supervisor"/>
         <require

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py	2012-09-28 06:15:58 +0000
+++ lib/lp/registry/interfaces/product.py	2012-10-02 12:37:21 +0000
@@ -422,7 +422,22 @@
                 "Not applicable to 'Other/Proprietary'.")))
 
 
-class IProductPublic(
+class IProductPublic(Interface):
+
+    id = Int(title=_('The Project ID'))
+
+    information_type = exported(
+        Choice(
+            title=_('Information Type'), vocabulary=InformationType,
+            required=True, readonly=True,
+            description=_(
+                'The type of of data contained in this project.')))
+
+    def userCanView(user):
+        """True if the given user has access to this product."""
+
+
+class IProductLimitedView(
     IBugTarget, ICanGetMilestonesDirectly, IHasAppointedDriver, IHasBranches,
     IHasBranchVisibilityPolicy, IHasDrivers, IHasExternalBugTracker, IHasIcon,
     IHasLogo, IHasMergeProposals, IHasMilestones,
@@ -432,8 +447,6 @@
     ISpecificationTarget, IHasRecipes, IHasCodeImports, IServiceUsage):
     """Public IProduct properties."""
 
-    id = Int(title=_('The Project ID'))
-
     project = exported(
         ReferenceChoice(
             title=_('Part of'),
@@ -450,13 +463,6 @@
                 'and security policy will apply to this project.')),
         exported_as='project_group')
 
-    information_type = exported(
-        Choice(
-            title=_('Information Type'), vocabulary=InformationType,
-            required=True, readonly=True,
-            description=_(
-                'The type of of data contained in this project.')))
-
     owner = exported(
         PersonChoice(
             title=_('Maintainer'),
@@ -893,9 +899,9 @@
 class IProductEditRestricted(IOfficialBugTagTargetRestricted):
     """`IProduct` properties which require launchpad.Edit permission."""
 
-    @mutator_for(IProductPublic['bug_sharing_policy'])
+    @mutator_for(IProductLimitedView['bug_sharing_policy'])
     @operation_parameters(bug_sharing_policy=copy_field(
-        IProductPublic['bug_sharing_policy']))
+        IProductLimitedView['bug_sharing_policy']))
     @export_write_operation()
     @operation_for_version("devel")
     def setBugSharingPolicy(bug_sharing_policy):
@@ -904,10 +910,10 @@
         Checks authorization and entitlement.
         """
 
-    @mutator_for(IProductPublic['branch_sharing_policy'])
+    @mutator_for(IProductLimitedView['branch_sharing_policy'])
     @operation_parameters(
         branch_sharing_policy=copy_field(
-            IProductPublic['branch_sharing_policy']))
+            IProductLimitedView['branch_sharing_policy']))
     @export_write_operation()
     @operation_for_version("devel")
     def setBranchSharingPolicy(branch_sharing_policy):
@@ -916,10 +922,10 @@
         Checks authorization and entitlement.
         """
 
-    @mutator_for(IProductPublic['specification_sharing_policy'])
+    @mutator_for(IProductLimitedView['specification_sharing_policy'])
     @operation_parameters(
         specification_sharing_policy=copy_field(
-            IProductPublic['specification_sharing_policy']))
+            IProductLimitedView['specification_sharing_policy']))
     @export_write_operation()
     @operation_for_version("devel")
     def setSpecificationSharingPolicy(specification_sharing_policy):
@@ -932,7 +938,7 @@
 class IProduct(
     IHasBugSupervisor, IProductEditRestricted,
     IProductModerateRestricted, IProductDriverRestricted,
-    IProductPublic, IQuestionTarget, IRootContext,
+    IProductLimitedView, IProductPublic, IQuestionTarget, IRootContext,
     IStructuralSubscriptionTarget):
     """A Product.
 

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py	2012-09-28 19:59:35 +0000
+++ lib/lp/registry/model/product.py	2012-10-02 12:37:21 +0000
@@ -69,6 +69,7 @@
     FREE_INFORMATION_TYPES,
     InformationType,
     PRIVATE_INFORMATION_TYPES,
+    PUBLIC_INFORMATION_TYPES,
     service_uses_launchpad,
     ServiceUsage,
     )
@@ -413,15 +414,17 @@
         """
         pass
 
-    @property
-    def information_type(self):
-        """See `IProduct`
-
-        Place holder for a db column.
-        XXX: rharding 2012-09-10 bug=1048720: Waiting on db patch to connect
-        into place.
-        """
-        pass
+    # Place holder for a db column.
+    # XXX: rharding 2012-09-10 bug=1048720: Waiting on db patch to connect
+    # into place.
+    def _get_information_type(self):
+        return getattr(
+            self, '_information_type', InformationType.PUBLIC)
+
+    def _set_information_type(self, value):
+        self._information_type = value
+
+    information_type = property(_get_information_type, _set_information_type)
 
     security_contact = None
 
@@ -1518,6 +1521,12 @@
 
         return weight_function
 
+    def userCanView(self, user):
+        """See `IProductPublic`."""
+        if self.information_type in PUBLIC_INFORMATION_TYPES:
+            return True
+        return user.inTeam(self.owner)
+
 
 class ProductSet:
     implements(IProductSet)

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py	2012-09-28 06:15:58 +0000
+++ lib/lp/registry/tests/test_product.py	2012-10-02 12:37:21 +0000
@@ -11,6 +11,10 @@
 import transaction
 from zope.component import getUtility
 from zope.lifecycleevent.interfaces import IObjectModifiedEvent
+from zope.security.checker import (
+    CheckerPublic,
+    getChecker,
+    )
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
@@ -390,6 +394,256 @@
         expected = [InformationType.PROPRIETARY]
         self.assertContentEqual(expected, [policy.type for policy in aps])
 
+    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,
+                    sorted(
+                        attribute_names - expected_permissions[permission]),
+                    sorted(
+                        expected_permissions[permission] - attribute_names)))
+
+    expected_get_permissions = {
+        CheckerPublic: set(('id', 'information_type', 'userCanView',)),
+        'launchpad.LimitedView': set((
+            '_getOfficialTagClause', 'active', 'active_or_packaged_series',
+            'aliases', 'all_milestones', 'all_specifications',
+            'allowsTranslationEdits', 'allowsTranslationSuggestions',
+            'announce', 'answer_contacts', 'answers_usage', 'autoupdate',
+            'blueprints_usage', 'branch_sharing_policy',
+            'bug_reported_acknowledgement', 'bug_reporting_guidelines',
+            'bug_sharing_policy', 'bug_subscriptions', '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', 'displayname', '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', 'getSubscription',
+            'getSubscriptions', 'getSupportedLanguages', 'getTimeline',
+            'getTopContributors', 'getTopContributorsGroupedByCategory',
+            'getTranslationGroups', 'getTranslationImportQueueEntries',
+            'getTranslators', 'getUsedBugTagsWithOpenCounts',
+            'getVersionSortedSeries', 'has_any_specifications',
+            'has_current_commercial_subscription',
+            'has_custom_language_codes', 'has_milestones', 'homepage_content',
+            'homepageurl', 'icon', 'invitesTranslationEdits',
+            'invitesTranslationSuggestions',
+            'isUsingInheritedBranchVisibilityPolicy',
+            'latest_completed_specifications', 'latest_specifications',
+            'license_info', 'license_status', 'licenses', 'logo', 'milestones',
+            'mugshot', 'name', 'name_with_project', 'newCodeImport',
+            'obsolete_translatable_series', 'official_answers',
+            'official_anything', 'official_blueprints', 'official_bug_tags',
+            'official_codehosting', 'official_malone', 'owner',
+            'parent_subscription_target', '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', 'target_type_display', 'title',
+            'translatable_packages', 'translatable_series',
+            'translation_focus', 'translationgroup', 'translationgroups',
+            'translationpermission', 'translations_usage', 'ubuntu_packages',
+            'userCanAlterBugSubscription', 'userCanAlterSubscription',
+            'userCanEdit', 'userHasBugSubscriptions', 'uses_launchpad',
+            'valid_specifications', '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.check_permissions(
+            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', 'license_info',
+                'licenses', 'logo', 'mugshot', 'official_answers',
+                'official_blueprints', 'official_codehosting', 'owner',
+                '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.check_permissions(
+            expected_set_permissions, checker.set_permissions, 'set')
+
+    def test_access_launchpad_LimitedView_public_product(self):
+        # Everybody, including anonymous users, has access to
+        # properties of public products that require the permission
+        # launchpad.LimitedView
+        product = self.factory.makeProduct()
+        names = self.expected_get_permissions['launchpad.LimitedView']
+        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_LimitedView_proprietary_product(self):
+        # Only people with grants for a prviate product can access
+        # attributes protected by the permission launchapd.LimitedView.
+        product = self.factory.makeProduct(
+            information_type=InformationType.PROPRIETARY)
+        names = self.expected_get_permissions['launchpad.LimitedView']
+        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(removeSecurityProxy(product).owner):
+            for attribute_name in names:
+                getattr(product, attribute_name)
+
+    def test_access_launchpad_AnyAllowedPerson_public_product(self):
+        # Only logged in persons hav 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 prviate product can access
+        # attributes protected by the permission launchapd.AnyAllowedPerson.
+        product = self.factory.makeProduct(
+            information_type=InformationType.PROPRIETARY)
+        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(removeSecurityProxy(product).owner):
+            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 launchapd.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 prviate product can set
+        # attributes protected by the permission launchapd.AnyAllowedPerson.
+        product = self.factory.makeProduct(
+            information_type=InformationType.PROPRIETARY)
+        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(removeSecurityProxy(product).owner):
+            setattr(product, 'date_next_suggest_packaging', 'foo')
+
 
 class TestProductBugInformationTypes(TestCaseWithFactory):
 

=== modified file 'lib/lp/registry/tests/test_product_webservice.py'
--- lib/lp/registry/tests/test_product_webservice.py	2012-09-19 05:15:39 +0000
+++ lib/lp/registry/tests/test_product_webservice.py	2012-10-02 12:37:21 +0000
@@ -17,6 +17,7 @@
 from lp.services.webapp.publisher import canonical_url
 from lp.testing import TestCaseWithFactory
 from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing import person_logged_in
 from lp.testing.pages import (
     LaunchpadWebServiceCaller,
     webservice_for_person,
@@ -47,27 +48,30 @@
     layer = DatabaseFunctionalLayer
 
     def patch(self, webservice, obj, **data):
+        with person_logged_in(webservice.user):
+            path = URI(canonical_url(obj)).path
         return webservice.patch(
-            URI(canonical_url(obj)).path,
-            'application/json', json.dumps(data),
-            api_version='devel')
+            path, 'application/json', json.dumps(data), api_version='devel')
 
     def test_branch_sharing_policy_can_be_set(self):
         # branch_sharing_policy can be set via the API.
         product = self.factory.makeProduct()
+        owner = product.owner
         self.factory.makeCommercialSubscription(product=product)
         webservice = webservice_for_person(
-            product.owner, permission=OAuthPermission.WRITE_PRIVATE)
+            owner, permission=OAuthPermission.WRITE_PRIVATE)
         response = self.patch(
             webservice, product, branch_sharing_policy='Proprietary')
         self.assertEqual(209, response.status)
-        self.assertEqual(
-            BranchSharingPolicy.PROPRIETARY, product.branch_sharing_policy)
+        with person_logged_in(owner):
+            self.assertEqual(
+                BranchSharingPolicy.PROPRIETARY, product.branch_sharing_policy)
 
     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.
         product = self.factory.makeLegacyProduct()
+        owner = product.owner
         webservice = webservice_for_person(
             product.owner, permission=OAuthPermission.WRITE_PRIVATE)
         response = self.patch(
@@ -76,24 +80,28 @@
                 status=403,
                 body=('A current commercial subscription is required to use '
                       'proprietary branches.')))
-        self.assertIs(None, product.branch_sharing_policy)
+        with person_logged_in(owner):
+            self.assertIs(None, product.branch_sharing_policy)
 
     def test_bug_sharing_policy_can_be_set(self):
         # bug_sharing_policy can be set via the API.
         product = self.factory.makeProduct()
+        owner = product.owner
         self.factory.makeCommercialSubscription(product=product)
         webservice = webservice_for_person(
             product.owner, permission=OAuthPermission.WRITE_PRIVATE)
         response = self.patch(
             webservice, product, bug_sharing_policy='Proprietary')
         self.assertEqual(209, response.status)
-        self.assertEqual(
-            BugSharingPolicy.PROPRIETARY, product.bug_sharing_policy)
+        with person_logged_in(owner):
+            self.assertEqual(
+                BugSharingPolicy.PROPRIETARY, product.bug_sharing_policy)
 
     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.
         product = self.factory.makeLegacyProduct()
+        owner = product.owner
         webservice = webservice_for_person(
             product.owner, permission=OAuthPermission.WRITE_PRIVATE)
         response = self.patch(
@@ -102,12 +110,13 @@
                 status=403,
                 body=('A current commercial subscription is required to use '
                       'proprietary bugs.')))
-        self.assertIs(None, product.bug_sharing_policy)
+        with person_logged_in(owner):
+            self.assertIs(None, product.bug_sharing_policy)
 
     def fetch_product(self, webservice, product, api_version):
-        return webservice.get(
-            canonical_url(product, force_local_path=True),
-            api_version=api_version).jsonBody()
+        with person_logged_in(webservice.user):
+            url = canonical_url(product, force_local_path=True)
+        return webservice.get(url, api_version=api_version).jsonBody()
 
     def test_security_contact_exported(self):
         # security_contact is exported for 1.0, but not for other versions.

=== modified file 'lib/lp/security.py'
--- lib/lp/security.py	2012-09-19 04:09:06 +0000
+++ lib/lp/security.py	2012-10-02 12:37:21 +0000
@@ -34,6 +34,7 @@
 from lp.answers.interfaces.questionmessage import IQuestionMessage
 from lp.answers.interfaces.questionsperson import IQuestionsPerson
 from lp.answers.interfaces.questiontarget import IQuestionTarget
+from lp.app.enums import PUBLIC_INFORMATION_TYPES
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.interfaces.security import IAuthorization
 from lp.app.security import (
@@ -430,6 +431,30 @@
         return user.isOwner(self.obj) or user.in_admin
 
 
+class ViewProduct(AuthorizationBase):
+    permission = 'launchpad.LimitedView'
+    usedfor = IProduct
+
+    def checkAuthenticated(self, user):
+        return self.obj.userCanView(user)
+
+    def checkUnauthenticated(self):
+        return self.obj.information_type in PUBLIC_INFORMATION_TYPES
+
+
+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
+
+
 class EditProduct(EditByOwnersOrAdmins):
     usedfor = IProduct
 

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2012-10-02 07:08:13 +0000
+++ lib/lp/testing/factory.py	2012-10-02 12:37:21 +0000
@@ -964,7 +964,8 @@
         title=None, summary=None, official_malone=None,
         translations_usage=None, bug_supervisor=None, private_bugs=False,
         driver=None, icon=None, bug_sharing_policy=None,
-        branch_sharing_policy=None, specification_sharing_policy=None):
+        branch_sharing_policy=None, specification_sharing_policy=None,
+        information_type=None):
         """Create and return a new, arbitrary Product."""
         if owner is None:
             owner = self.makePerson()
@@ -1020,6 +1021,8 @@
         if specification_sharing_policy:
             naked_product.setSpecificationSharingPolicy(
                 specification_sharing_policy)
+        if information_type is not None:
+            naked_product.information_type = information_type
 
         return product
 

=== modified file 'lib/lp/testing/pages.py'
--- lib/lp/testing/pages.py	2012-08-14 23:27:07 +0000
+++ lib/lp/testing/pages.py	2012-10-02 12:37:21 +0000
@@ -730,7 +730,9 @@
     request_token.review(person, permission, context)
     access_token = request_token.createAccessToken()
     logout()
-    return LaunchpadWebServiceCaller(consumer_key, access_token.key)
+    service = LaunchpadWebServiceCaller(consumer_key, access_token.key)
+    service.user = person
+    return service
 
 
 def setupDTCBrowser():


Follow ups