Merge lp:~adeuring/launchpad/authentication-for-private-products-2 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 16148
Proposed branch: lp:~adeuring/launchpad/authentication-for-private-products-2
Merge into: lp:launchpad
Diff against target: 1795 lines (+733/-105)
38 files modified
lib/lp/answers/tests/test_question_webservice.py (+5/-3)
lib/lp/app/browser/launchpad.py (+25/-0)
lib/lp/app/browser/tests/test_launchpad.py (+104/-1)
lib/lp/app/model/launchpad.py (+1/-1)
lib/lp/blueprints/browser/tests/test_specification.py (+11/-4)
lib/lp/blueprints/browser/tests/test_views.py (+1/-1)
lib/lp/blueprints/tests/test_webservice.py (+6/-4)
lib/lp/bugs/browser/tests/test_bugtask.py (+2/-2)
lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt (+2/-1)
lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt (+2/-1)
lib/lp/bugs/stories/patches-view/patches-view.txt (+2/-0)
lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt (+2/-2)
lib/lp/bugs/stories/webservice/xx-bug.txt (+5/-3)
lib/lp/bugs/tests/test_bugs_webservice.py (+7/-4)
lib/lp/bugs/tests/test_searchtasks_webservice.py (+3/-2)
lib/lp/code/browser/tests/test_branch.py (+1/-1)
lib/lp/code/browser/tests/test_product.py (+4/-2)
lib/lp/code/stories/branches/xx-product-branches.txt (+5/-2)
lib/lp/code/stories/webservice/xx-code-import.txt (+3/-2)
lib/lp/registry/browser/tests/test_milestone.py (+4/-2)
lib/lp/registry/browser/tests/test_pillar_sharing.py (+3/-3)
lib/lp/registry/browser/tests/test_product.py (+1/-1)
lib/lp/registry/browser/tests/test_productseries_views.py (+9/-2)
lib/lp/registry/browser/tests/test_sourcepackage_views.py (+14/-6)
lib/lp/registry/configure.zcml (+25/-11)
lib/lp/registry/interfaces/product.py (+22/-16)
lib/lp/registry/model/product.py (+35/-0)
lib/lp/registry/services/tests/test_sharingservice.py (+4/-2)
lib/lp/registry/tests/test_pillaraffiliation.py (+1/-1)
lib/lp/registry/tests/test_product.py (+355/-2)
lib/lp/registry/tests/test_product_webservice.py (+24/-15)
lib/lp/registry/tests/test_subscribers.py (+4/-1)
lib/lp/security.py (+25/-0)
lib/lp/testing/factory.py (+3/-3)
lib/lp/testing/pages.py (+3/-1)
lib/lp/translations/browser/tests/test_noindex.py (+6/-1)
lib/lp/translations/stories/importqueue/xx-entry-details.txt (+2/-1)
lib/lp/translations/stories/webservice/xx-potemplate.txt (+2/-1)
To merge this branch: bzr merge lp:~adeuring/launchpad/authentication-for-private-products-2
Reviewer Review Type Date Requested Status
William Grant code Approve
Curtis Hovey (community) code Approve
Review via email: mp+129459@code.launchpad.net

Commit message

policy grant based access checks for private products.

Description of the change

Another attempt to activate permission checks for private products.

pre-imp calls with suínzui and flacoste

The first attempt, merged in r16090, reviewed here
https://code.launchpad.net/~adeuring/launchpad/correct-permission-check-for-iproduct/+merge/127518
and here
https://code.launchpad.net/~adeuring/launchpad/product-sharing-sec-adapter/+merge/127473
had to be reverted becaused is caused lots of "permission denied"
errors.

The causes of these error:

(1) Nearly all properties of IProduct required the permission
launchpad.View . Before, only IPillar was guarded by this
permission, most other attributes were public.

(2) The security adapter, class ViewProduct, inherited from
the checker for IPillar, class ViewPillar. And ViewProduct
further limited the access so that only people with policy
grants for private products get acces to those product. But
it also denided access to most properties of inactive public
products, by also calling ViewPillar.check[Un]Authenticated()

(3) Lots of pages retrieve "related products", for example
person pages, or bug pages. It often happens that attributes
like product.name, product.displayname or subscription related
stuff, were accessed -- even for inactive products. Data for
inactive products is later dropped, but obviously at a very
late stage during rendering. It seems that inactive products
are quite often filtered out at a very late stage of page
rendering.

This branch fixes these problems by again allowing access to
all attributes that require the permisison launchpad.View
for inactive public products, by not checking the flag
product.active flag in ProductView.check[Un]authenticated()

This has again another unwanted side effect:

All users can see inactive public products, while we want to
present a 404 page in this case. The reason:

lp.app.browser.launchpad.Launchpad.traverse() checks if
the user has the permission launchpad.View for a given pillar.

This check worked before because launchpad.View was only
required for IPillar (see class ViewPillar), so all these
eventually unnecessary accesses to product properties were
possible even if a user did not have the permission lp.View
-- but with the new permission checker ViewProduct in place,
users would see inactive products like admins, just with a
warning "this project ic inactive".

So we simply can't call check_permission('lp.View', product)
in Launchpad.traverse() any longer but need to explicitly check
for inactive products.

Note that this "special rule" for products will be needed anyway
once we allow artifact grants for private products: This will
require another permission, launchpad.LimitedView, so that users
with grants for artifacts can traverse to branches or bugs.
Aand the traverse() method should then check if users

tests:

./bin/test -vvt lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt
./bin/test -vvt lp.app.browser.tests.test_launchpad.TestProductTraversal
./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_View_proprietary_product
./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_View_public_inactive_product

no lint.

The complete diff against trunk is quite large.

r16117 is just a "rollback of the rollback" from r16112.

The relevant changes are in r16118. The diff r16117..16118:

=== modified file 'lib/lp/app/browser/launchpad.py'
--- lib/lp/app/browser/launchpad.py 2012-07-09 13:18:34 +0000
+++ lib/lp/app/browser/launchpad.py 2012-10-12 13:02:05 +0000
@@ -105,9 +105,11 @@
 from lp.registry.interfaces.pillar import IPillarNameSet
 from lp.registry.interfaces.product import (
     InvalidProductName,
+ IProduct,
     IProductSet,
     )
 from lp.registry.interfaces.projectgroup import IProjectGroupSet
+from lp.registry.interfaces.role import IPersonRoles
 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
 from lp.services.config import config
 from lp.services.helpers import intOrZero
@@ -763,6 +765,29 @@

         pillar = getUtility(IPillarNameSet).getByName(
             name, ignore_inactive=False)
+
+ if (pillar is not None and IProduct.providedBy(pillar) and
+ not pillar.active):
+ # Emergency brake for public but inactive products:
+ # These products should not be shown to ordinary users.
+ # The root problem is that many views iterate over products,
+ # inactive products included, and access attributes like
+ # name, displayname or call canonical_url(product) --
+ # and finally throw the data away, if the product is
+ # inactive. So we cannot make these attributes inaccessible
+ # for inactive public products. On the other hand, we
+ # require the permission launchpad.View to protect private
+ # products.
+ # This means that we cannot simply check if the current
+ # user has the permission launchpad.View for an inactive
+ # product.
+ user = getUtility(ILaunchBag).user
+ if user is None:
+ return None
+ user = IPersonRoles(user)
+ if (not user.in_commercial_admin and not user.in_admin and
+ not user.in_registry_experts):
+ return None
         if pillar is not None and check_permission('launchpad.View', pillar):
             if pillar.name != name:
                 # This pillar was accessed through one of its aliases, so we

=== modified file 'lib/lp/app/browser/tests/test_launchpad.py'
--- lib/lp/app/browser/tests/test_launchpad.py 2012-09-17 15:19:10 +0000
+++ lib/lp/app/browser/tests/test_launchpad.py 2012-10-12 13:02:05 +0000
@@ -20,8 +20,12 @@
 from lp.app.enums import InformationType
 from lp.app.errors import GoneError
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.app.interfaces.services import IService
 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
-from lp.registry.enums import PersonVisibility
+from lp.registry.enums import (
+ PersonVisibility,
+ SharingPermission,
+ )
 from lp.registry.interfaces.person import IPersonSet
 from lp.services.identity.interfaces.account import AccountStatus
 from lp.services.webapp import canonical_url
@@ -468,3 +472,108 @@
             reg.name for reg in iter_view_registrations(macros.__class__))
         self.assertIn('+base-layout-macros', names)
         self.assertNotIn('+related-pages', names)
+
+
+class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestProductTraversal, self).setUp()
+ self.active_public_product = self.factory.makeProduct()
+ self.inactive_public_product = self.factory.makeProduct()
+ removeSecurityProxy(self.inactive_public_product).active = False
+ self.proprietary_product_owner = self.factory.makePerson()
+ self.active_proprietary_product = self.factory.makeProduct(
+ owner=self.proprietary_product_owner,
+ information_type=InformationType.PROPRIETARY)
+ self.inactive_proprietary_product = self.factory.makeProduct(
+ owner=self.proprietary_product_owner,
+ information_type=InformationType.PROPRIETARY)
+ removeSecurityProxy(self.inactive_proprietary_product).active = False
+
+ def traverse_to_active_public_product(self):
+ segment = self.active_public_product.name
+ self.traverse(segment, segment)
+
+ def traverse_to_inactive_public_product(self):
+ segment = removeSecurityProxy(self.inactive_public_product).name
+ self.traverse(segment, segment)
+
+ def traverse_to_active_proprietary_product(self):
+ segment = removeSecurityProxy(self.active_proprietary_product).name
+ self.traverse(segment, segment)
+
+ def traverse_to_inactive_proprietary_product(self):
+ segment = removeSecurityProxy(self.inactive_proprietary_product).name
+ self.traverse(segment, segment)
+
+ def test_access_for_anon(self):
+ # Anonymous users can see only public active products.
+ with person_logged_in(ANONYMOUS):
+ self.traverse_to_active_public_product()
+ # Access to other products raises a NotFound error.
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_public_product)
+ self.assertRaises(
+ NotFound, self.traverse_to_active_proprietary_product)
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_proprietary_product)
+
+ def test_access_for_ordinary_users(self):
+ # Ordinary logged in users can see only public active products.
+ with person_logged_in(self.factory.makePerson()):
+ self.traverse_to_active_public_product()
+ # Access to other products raises a NotFound error.
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_public_product)
+ self.assertRaises(
+ NotFound, self.traverse_to_active_proprietary_product)
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_proprietary_product)
+
+ def test_access_for_person_with_pillar_grant(self):
+ # Persons with a policy grant for a proprietary product can
+ # access this product, if it is active.
+ user = self.factory.makePerson()
+ with person_logged_in(self.proprietary_product_owner):
+ getUtility(IService, 'sharing').sharePillarInformation(
+ self.active_proprietary_product, user,
+ self.proprietary_product_owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ getUtility(IService, 'sharing').sharePillarInformation(
+ self.inactive_proprietary_product, user,
+ self.proprietary_product_owner,
+ {InformationType.PROPRIETARY: SharingPermission.ALL})
+ with person_logged_in(user):
+ self.traverse_to_active_public_product()
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_public_product)
+ self.traverse_to_active_proprietary_product()
+ self.assertRaises(
+ NotFound, self.traverse_to_inactive_proprietary_product)
+
+ def check_admin_access(self, user):
+ with person_logged_in(user):
+ self.traverse_to_active_public_product()
+ self.traverse_to_inactive_public_product()
+ self.traverse_to_active_proprietary_product()
+ self.traverse_to_inactive_proprietary_product()
+
+ def test_access_for_persons_with_special_permissions(self):
+ # Admins have access all products, including inactive and propretary
+ # products.
+ self.check_admin_access(getUtility(IPersonSet).getByName('name16'))
+ # Registry experts can access to all products.
+ registry_expert = self.factory.makePerson()
+ registry = getUtility(ILaunchpadCelebrities).registry_experts
+ with person_logged_in(registry.teamowner):
+ registry.addMember(registry_expert, registry.teamowner)
+ self.check_admin_access(registry_expert)
+ # Commercial admins have access to all products.
+ commercial_admin = self.factory.makePerson()
+ commercial_admins = getUtility(ILaunchpadCelebrities).commercial_admin
+ with person_logged_in(commercial_admins.teamowner):
+ commercial_admins.addMember(
+ commercial_admin, commercial_admins.teamowner)
+ self.check_admin_access(registry_expert)

=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2012-10-09 10:28:02 +0000
+++ lib/lp/registry/model/product.py 2012-10-12 13:02:05 +0000
@@ -1548,13 +1548,21 @@
             return False
         if user.id in self._known_viewers:
             return True
- # We want an actual Storm Person.
+ # We need the plain Storm Person object for the SQL query below
+ # but an IPersonRoles object for the team membership checks.
         if IPersonRoles.providedBy(user):
- user = user.person
+ plain_user = user.person
+ else:
+ plain_user = user
+ user = IPersonRoles(user)
+ if (user.in_commercial_admin or user.in_admin or
+ user.in_registry_experts):
+ self._known_viewers.add(user.id)
+ return True
         policy = getUtility(IAccessPolicySource).find(
             [(self, self.information_type)]).one()
         grants_for_user = getUtility(IAccessPolicyGrantSource).find(
- [(policy, user)])
+ [(policy, plain_user)])
         if grants_for_user.is_empty():
             return False
         self._known_viewers.add(user.id)

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2012-10-09 10:28:02 +0000
+++ lib/lp/registry/tests/test_product.py 2012-10-12 13:02:05 +0000
@@ -59,11 +59,13 @@
     IAccessPolicySource,
     )
 from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
+from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.product import (
     IProduct,
     IProductSet,
     License,
     )
+from lp.registry.interfaces.role import IPersonRoles
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.model.product import (
     Product,
@@ -741,6 +743,24 @@
             for attribute_name in names:
                 getattr(product, attribute_name)

+ def test_access_launchpad_View_public_inactive_product(self):
+ # Everybody, including anonymous users, has access to
+ # properties of public but inactvie products that require
+ # the permission launchpad.View.
+ product = self.factory.makeProduct()
+ removeSecurityProxy(product).active = False
+ 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.
@@ -770,6 +790,26 @@
         with person_logged_in(ordinary_user):
             for attribute_name in names:
                 getattr(product, attribute_name)
+ # Admins can access proprietary products.
+ with person_logged_in(getUtility(IPersonSet).getByName('name16')):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ registry_expert = self.factory.makePerson()
+ registry = getUtility(ILaunchpadCelebrities).registry_experts
+ with person_logged_in(registry.teamowner):
+ registry.addMember(registry_expert, registry.teamowner)
+ with person_logged_in(registry_expert):
+ for attribute_name in names:
+ getattr(product, attribute_name)
+ # Commercial admins have access to all products.
+ commercial_admin = self.factory.makePerson()
+ commercial_admins = getUtility(ILaunchpadCelebrities).commercial_admin
+ with person_logged_in(commercial_admins.teamowner):
+ commercial_admins.addMember(
+ commercial_admin, commercial_admins.teamowner)
+ with person_logged_in(commercial_admin):
+ 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
@@ -882,6 +922,16 @@
                 self.assertEqual(
                 queries_for_first_user_access, len(recorder.queries))

+ def test_userCanView_works_with_IPersonRoles(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)
+ user = self.factory.makePerson()
+ product.userCanView(user)
+ product.userCanView(IPersonRoles(user))
+

 class TestProductBugInformationTypes(TestCaseWithFactory):

=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2012-10-09 10:28:02 +0000
+++ lib/lp/security.py 2012-10-12 13:02:05 +0000
@@ -14,13 +14,8 @@
 from operator import methodcaller

 from storm.expr import (
- And,
- Exists,
- Or,
     Select,
- SQL,
     Union,
- With,
     )
 from zope.component import (
     getUtility,
@@ -177,7 +172,6 @@
     )
 from lp.registry.interfaces.wikiname import IWikiName
 from lp.registry.model.person import Person
-from lp.registry.model.teammembership import TeamParticipation
 from lp.services.config import config
 from lp.services.database.lpstorm import IStore
 from lp.services.identity.interfaces.account import IAccount
@@ -431,19 +425,15 @@
         return user.isOwner(self.obj) or user.in_admin

-class ViewProduct(ViewPillar):
+class ViewProduct(AuthorizationBase):
     permission = 'launchpad.View'
     usedfor = IProduct

     def checkAuthenticated(self, user):
- return (
- super(ViewProduct, self).checkAuthenticated(user) and
- self.obj.userCanView(user))
+ return self.obj.userCanView(user)

     def checkUnauthenticated(self):
- return (
- self.obj.information_type in PUBLIC_INFORMATION_TYPES and
- super(ViewProduct, self).checkUnauthenticated())
+ return self.obj.information_type in PUBLIC_INFORMATION_TYPES

 class ChangeProduct(ViewProduct):

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

This looks good. Thank you. I think one test can be simplified (and it looks like it is testing the wrong user at the end). this test duplicates code performed by 'celebrity_logged_in'. We also want to avoid sampledata since Foo Bar is more than just an Admin.

203 + def test_access_for_persons_with_special_permissions(self):
204 + # Admins have access all products, including inactive and propretary
205 + # products.
206 + self.check_admin_access(getUtility(IPersonSet).getByName('name16'))
207 + # Registry experts can access to all products.
208 + registry_expert = self.factory.makePerson()
209 + registry = getUtility(ILaunchpadCelebrities).registry_experts
210 + with person_logged_in(registry.teamowner):
211 + registry.addMember(registry_expert, registry.teamowner)
212 + self.check_admin_access(registry_expert)
213 + # Commercial admins have access to all products.
214 + commercial_admin = self.factory.makePerson()
215 + commercial_admins = getUtility(ILaunchpadCelebrities).commercial_admin
216 + with person_logged_in(commercial_admins.teamowner):
217 + commercial_admins.addMember(
218 + commercial_admin, commercial_admins.teamowner)
219 + self.check_admin_access(registry_expert)

Could be
    def test_access_for_persons_with_special_permissions(self):
        # Admins have access all products, including inactive and propretary
        # products.
        with celebrity_logged_in('admin') as admin:
            self.check_admin_access(admin)
        with celebrity_logged_in('registry_experts') as registry_expert:
            self.check_admin_access(registry_expert)
        with celebrity_logged_in('commercial_admins') as commercial_admin:
            self.check_admin_access(commercial_admin)

review: Approve (code)
Revision history for this message
William Grant (wgrant) wrote :

I'd still like to see an emergency feature flag in userCanView, in case we end up with the initial private projects on production breaking something unexpected. But otherwise this looks pretty good, particularly the improvements around handling deactivated products.

969 + if (user.in_commercial_admin or user.in_admin or
970 + user.in_registry_experts):

There are registry admins from outside Canonical, so ~registry must not be able to see all private projects.

Also, you can also easily fix bug #1061933 while you're here.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/answers/tests/test_question_webservice.py'
2--- lib/lp/answers/tests/test_question_webservice.py 2012-10-08 10:07:11 +0000
3+++ lib/lp/answers/tests/test_question_webservice.py 2012-10-15 15:39:22 +0000
4@@ -10,6 +10,7 @@
5 from simplejson import dumps
6 import transaction
7 from zope.component import getUtility
8+from zope.security.proxy import removeSecurityProxy
9
10 from lp.answers.errors import (
11 AddAnswerContactError,
12@@ -83,6 +84,7 @@
13 with celebrity_logged_in('admin'):
14 self.question = self.factory.makeQuestion(
15 title="This is a question")
16+ self.target_name = self.question.target.name
17
18 self.webservice = LaunchpadWebServiceCaller(
19 'launchpad-library', 'salgado-change-anything')
20@@ -105,7 +107,7 @@
21 def test_GET_xhtml_representation(self):
22 # A question's xhtml representation is available on the api.
23 response = self.webservice.get(
24- '/%s/+question/%d' % (self.question.target.name,
25+ '/%s/+question/%d' % (self.target_name,
26 self.question.id),
27 'application/xhtml+xml')
28 self.assertEqual(response.status, 200)
29@@ -119,7 +121,7 @@
30 new_title = "No, this is a question"
31
32 question_json = self.webservice.get(
33- '/%s/+question/%d' % (self.question.target.name,
34+ '/%s/+question/%d' % (self.target_name,
35 self.question.id)).jsonBody()
36
37 response = self.webservice.patch(
38@@ -156,7 +158,7 @@
39 # End any open lplib instance.
40 logout()
41 lp = launchpadlib_for("test", user)
42- return ws_object(lp, self.question)
43+ return ws_object(lp, removeSecurityProxy(self.question))
44
45 def _set_visibility(self, question):
46 """Method to set visibility; needed for assertRaises."""
47
48=== modified file 'lib/lp/app/browser/launchpad.py'
49--- lib/lp/app/browser/launchpad.py 2012-07-09 13:18:34 +0000
50+++ lib/lp/app/browser/launchpad.py 2012-10-15 15:39:22 +0000
51@@ -105,9 +105,11 @@
52 from lp.registry.interfaces.pillar import IPillarNameSet
53 from lp.registry.interfaces.product import (
54 InvalidProductName,
55+ IProduct,
56 IProductSet,
57 )
58 from lp.registry.interfaces.projectgroup import IProjectGroupSet
59+from lp.registry.interfaces.role import IPersonRoles
60 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
61 from lp.services.config import config
62 from lp.services.helpers import intOrZero
63@@ -763,6 +765,29 @@
64
65 pillar = getUtility(IPillarNameSet).getByName(
66 name, ignore_inactive=False)
67+
68+ if (pillar is not None and IProduct.providedBy(pillar) and
69+ not pillar.active):
70+ # Emergency brake for public but inactive products:
71+ # These products should not be shown to ordinary users.
72+ # The root problem is that many views iterate over products,
73+ # inactive products included, and access attributes like
74+ # name, displayname or call canonical_url(product) --
75+ # and finally throw the data away, if the product is
76+ # inactive. So we cannot make these attributes inaccessible
77+ # for inactive public products. On the other hand, we
78+ # require the permission launchpad.View to protect private
79+ # products.
80+ # This means that we cannot simply check if the current
81+ # user has the permission launchpad.View for an inactive
82+ # product.
83+ user = getUtility(ILaunchBag).user
84+ if user is None:
85+ return None
86+ user = IPersonRoles(user)
87+ if (not user.in_commercial_admin and not user.in_admin and
88+ not user.in_registry_experts):
89+ return None
90 if pillar is not None and check_permission('launchpad.View', pillar):
91 if pillar.name != name:
92 # This pillar was accessed through one of its aliases, so we
93
94=== modified file 'lib/lp/app/browser/tests/test_launchpad.py'
95--- lib/lp/app/browser/tests/test_launchpad.py 2012-09-17 15:19:10 +0000
96+++ lib/lp/app/browser/tests/test_launchpad.py 2012-10-15 15:39:22 +0000
97@@ -20,8 +20,12 @@
98 from lp.app.enums import InformationType
99 from lp.app.errors import GoneError
100 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
101+from lp.app.interfaces.services import IService
102 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
103-from lp.registry.enums import PersonVisibility
104+from lp.registry.enums import (
105+ PersonVisibility,
106+ SharingPermission,
107+ )
108 from lp.registry.interfaces.person import IPersonSet
109 from lp.services.identity.interfaces.account import AccountStatus
110 from lp.services.webapp import canonical_url
111@@ -33,6 +37,7 @@
112 from lp.services.webapp.url import urlappend
113 from lp.testing import (
114 ANONYMOUS,
115+ celebrity_logged_in,
116 login,
117 login_person,
118 person_logged_in,
119@@ -468,3 +473,101 @@
120 reg.name for reg in iter_view_registrations(macros.__class__))
121 self.assertIn('+base-layout-macros', names)
122 self.assertNotIn('+related-pages', names)
123+
124+
125+class TestProductTraversal(TestCaseWithFactory, TraversalMixin):
126+
127+ layer = DatabaseFunctionalLayer
128+
129+ def setUp(self):
130+ super(TestProductTraversal, self).setUp()
131+ self.active_public_product = self.factory.makeProduct()
132+ self.inactive_public_product = self.factory.makeProduct()
133+ removeSecurityProxy(self.inactive_public_product).active = False
134+ self.proprietary_product_owner = self.factory.makePerson()
135+ self.active_proprietary_product = self.factory.makeProduct(
136+ owner=self.proprietary_product_owner,
137+ information_type=InformationType.PROPRIETARY)
138+ self.inactive_proprietary_product = self.factory.makeProduct(
139+ owner=self.proprietary_product_owner,
140+ information_type=InformationType.PROPRIETARY)
141+ removeSecurityProxy(self.inactive_proprietary_product).active = False
142+
143+ def traverse_to_active_public_product(self):
144+ segment = self.active_public_product.name
145+ self.traverse(segment, segment)
146+
147+ def traverse_to_inactive_public_product(self):
148+ segment = removeSecurityProxy(self.inactive_public_product).name
149+ self.traverse(segment, segment)
150+
151+ def traverse_to_active_proprietary_product(self):
152+ segment = removeSecurityProxy(self.active_proprietary_product).name
153+ self.traverse(segment, segment)
154+
155+ def traverse_to_inactive_proprietary_product(self):
156+ segment = removeSecurityProxy(self.inactive_proprietary_product).name
157+ self.traverse(segment, segment)
158+
159+ def test_access_for_anon(self):
160+ # Anonymous users can see only public active products.
161+ with person_logged_in(ANONYMOUS):
162+ self.traverse_to_active_public_product()
163+ # Access to other products raises a NotFound error.
164+ self.assertRaises(
165+ NotFound, self.traverse_to_inactive_public_product)
166+ self.assertRaises(
167+ NotFound, self.traverse_to_active_proprietary_product)
168+ self.assertRaises(
169+ NotFound, self.traverse_to_inactive_proprietary_product)
170+
171+ def test_access_for_ordinary_users(self):
172+ # Ordinary logged in users can see only public active products.
173+ with person_logged_in(self.factory.makePerson()):
174+ self.traverse_to_active_public_product()
175+ # Access to other products raises a NotFound error.
176+ self.assertRaises(
177+ NotFound, self.traverse_to_inactive_public_product)
178+ self.assertRaises(
179+ NotFound, self.traverse_to_active_proprietary_product)
180+ self.assertRaises(
181+ NotFound, self.traverse_to_inactive_proprietary_product)
182+
183+ def test_access_for_person_with_pillar_grant(self):
184+ # Persons with a policy grant for a proprietary product can
185+ # access this product, if it is active.
186+ user = self.factory.makePerson()
187+ with person_logged_in(self.proprietary_product_owner):
188+ getUtility(IService, 'sharing').sharePillarInformation(
189+ self.active_proprietary_product, user,
190+ self.proprietary_product_owner,
191+ {InformationType.PROPRIETARY: SharingPermission.ALL})
192+ getUtility(IService, 'sharing').sharePillarInformation(
193+ self.inactive_proprietary_product, user,
194+ self.proprietary_product_owner,
195+ {InformationType.PROPRIETARY: SharingPermission.ALL})
196+ with person_logged_in(user):
197+ self.traverse_to_active_public_product()
198+ self.assertRaises(
199+ NotFound, self.traverse_to_inactive_public_product)
200+ self.traverse_to_active_proprietary_product()
201+ self.assertRaises(
202+ NotFound, self.traverse_to_inactive_proprietary_product)
203+
204+ def check_admin_access(self):
205+ self.traverse_to_active_public_product()
206+ self.traverse_to_inactive_public_product()
207+ self.traverse_to_active_proprietary_product()
208+ self.traverse_to_inactive_proprietary_product()
209+
210+ def test_access_for_persons_with_special_permissions(self):
211+ # Admins have access all products, including inactive and propretary
212+ # products.
213+ with celebrity_logged_in('admin'):
214+ self.check_admin_access()
215+ # Registry experts can access to all products.
216+ with celebrity_logged_in('registry_experts'):
217+ self.check_admin_access()
218+ # Commercial admins have access to all products.
219+ with celebrity_logged_in('commercial_admin'):
220+ self.check_admin_access()
221
222=== modified file 'lib/lp/app/model/launchpad.py'
223--- lib/lp/app/model/launchpad.py 2012-10-08 10:07:11 +0000
224+++ lib/lp/app/model/launchpad.py 2012-10-15 15:39:22 +0000
225@@ -43,7 +43,7 @@
226
227
228 class InformationTypeMixin:
229- """"Common functionality for classes implementing IInformationType."""
230+ """Common functionality for classes implementing IInformationType."""
231
232 @property
233 def private(self):
234
235=== modified file 'lib/lp/blueprints/browser/tests/test_specification.py'
236--- lib/lp/blueprints/browser/tests/test_specification.py 2012-10-11 12:41:43 +0000
237+++ lib/lp/blueprints/browser/tests/test_specification.py 2012-10-15 15:39:22 +0000
238@@ -40,6 +40,7 @@
239 IProductSeries,
240 )
241 from lp.services.features.testing import FeatureFixture
242+from lp.services.webapp.interaction import ANONYMOUS
243 from lp.services.webapp.interfaces import BrowserNotificationLevel
244 from lp.services.webapp.publisher import canonical_url
245 from lp.testing import (
246@@ -480,7 +481,11 @@
247 control = browser.getControl(information_type.title)
248 if not control.selected:
249 control.click()
250- return product.getSpecification(self.submitSpec(browser))
251+ specification_name = self.submitSpec(browser)
252+ # Using the browser terminated the interaction, but we need
253+ # an interaction in order to access a product.
254+ with person_logged_in(ANONYMOUS):
255+ return product.getSpecification(specification_name)
256
257 def test_supplied_information_types(self):
258 """Creating honours information types."""
259@@ -564,9 +569,11 @@
260 Useful because we need to follow to product from a
261 ProductSeries.
262 """
263- if IProductSeries.providedBy(target):
264- return target.product.getSpecification(name)
265- return target.getSpecification(name)
266+ # We need an interaction in order to access a product.
267+ with person_logged_in(ANONYMOUS):
268+ if IProductSeries.providedBy(target):
269+ return target.product.getSpecification(name)
270+ return target.getSpecification(name)
271
272 def submitSpec(self, browser):
273 """Submit a Specification via a browser."""
274
275=== modified file 'lib/lp/blueprints/browser/tests/test_views.py'
276--- lib/lp/blueprints/browser/tests/test_views.py 2012-10-08 10:07:11 +0000
277+++ lib/lp/blueprints/browser/tests/test_views.py 2012-10-15 15:39:22 +0000
278@@ -66,9 +66,9 @@
279 collector = QueryCollector()
280 collector.register()
281 self.addCleanup(collector.unregister)
282+ url = canonical_url(target) + "/+assignments"
283 viewer = self.factory.makePerson()
284 browser = self.getUserBrowser(user=viewer)
285- url = canonical_url(target) + "/+assignments"
286 # Seed the cookie cache and any other cross-request state we may gain
287 # in future. See lp.services.webapp.serssion: _get_secret.
288 browser.open(url)
289
290=== modified file 'lib/lp/blueprints/tests/test_webservice.py'
291--- lib/lp/blueprints/tests/test_webservice.py 2012-10-08 10:07:11 +0000
292+++ lib/lp/blueprints/tests/test_webservice.py 2012-10-15 15:39:22 +0000
293@@ -7,6 +7,7 @@
294
295 import transaction
296 from zope.security.management import endInteraction
297+from zope.security.proxy import removeSecurityProxy
298
299 from lp.blueprints.enums import SpecificationDefinitionStatus
300 from lp.services.webapp.interaction import ANONYMOUS
301@@ -42,8 +43,9 @@
302 return result
303
304 def getPillarOnWebservice(self, pillar_obj):
305+ pillar_name = pillar_obj.name
306 launchpadlib = self.getLaunchpadlib()
307- return launchpadlib.load(pillar_obj.name)
308+ return launchpadlib.load(pillar_name)
309
310
311 class SpecificationAttributeWebserviceTests(SpecificationWebserviceTestCase):
312@@ -74,9 +76,9 @@
313 def test_representation_contains_target(self):
314 spec = self.factory.makeSpecification(
315 product=self.factory.makeProduct())
316- spec_target = spec.target
317+ spec_target_name = spec.target.name
318 spec_webservice = self.getSpecOnWebservice(spec)
319- self.assertEqual(spec_target.name, spec_webservice.target.name)
320+ self.assertEqual(spec_target_name, spec_webservice.target.name)
321
322 def test_representation_contains_title(self):
323 spec = self.factory.makeSpecification(title='Foo')
324@@ -271,7 +273,7 @@
325 # setup a new one.
326 endInteraction()
327 lplib = launchpadlib_for('lplib-test', person=None, version='devel')
328- ws_product = ws_object(lplib, product)
329+ ws_product = ws_object(lplib, removeSecurityProxy(product))
330 self.assertNamesOfSpecificationsAre(
331 ["spec1", "spec2"], ws_product.all_specifications)
332
333
334=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
335--- lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-15 02:32:30 +0000
336+++ lib/lp/bugs/browser/tests/test_bugtask.py 2012-10-15 15:39:22 +0000
337@@ -2024,8 +2024,10 @@
338
339 def test_rendered_query_counts_constant_with_many_bugtasks(self):
340 product = self.factory.makeProduct()
341+ url = canonical_url(product, view_name='+bugs')
342 bug = self.factory.makeBug(target=product)
343 buggy_product = self.factory.makeProduct()
344+ buggy_url = canonical_url(buggy_product, view_name='+bugs')
345 for _ in range(10):
346 self.factory.makeBug(target=buggy_product)
347 recorder = QueryCollector()
348@@ -2033,11 +2035,9 @@
349 self.addCleanup(recorder.unregister)
350 self.invalidate_caches(bug)
351 # count with single task
352- url = canonical_url(product, view_name='+bugs')
353 self.getUserBrowser(url)
354 self.assertThat(recorder, HasQueryCount(LessThan(35)))
355 # count with many tasks
356- buggy_url = canonical_url(buggy_product, view_name='+bugs')
357 self.getUserBrowser(buggy_url)
358 self.assertThat(recorder, HasQueryCount(LessThan(35)))
359
360
361=== modified file 'lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt'
362--- lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2012-10-08 10:07:11 +0000
363+++ lib/lp/bugs/stories/bug-also-affects/xx-bug-also-affects.txt 2012-10-15 15:39:22 +0000
364@@ -219,6 +219,7 @@
365 >>> other_product = factory.makeProduct(
366 ... official_malone=True,
367 ... bug_sharing_policy=BugSharingPolicy.PROPRIETARY)
368+ >>> other_product_name = other_product.name
369 >>> params = CreateBugParams(
370 ... title="a test private bug",
371 ... comment="a description of the bug",
372@@ -229,7 +230,7 @@
373
374 >>> browser.open(canonical_url(private_bug, rootsite='bugs'))
375 >>> browser.getLink(url='+choose-affected-product').click()
376- >>> browser.getControl(name='field.product').value = other_product.name
377+ >>> browser.getControl(name='field.product').value = other_product_name
378 >>> browser.getControl('Continue').click()
379 >>> print browser.url
380 http://bugs.launchpad.dev/proprietary-product/+bug/16/+choose-affected-product
381
382=== modified file 'lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt'
383--- lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt 2012-10-08 10:07:11 +0000
384+++ lib/lp/bugs/stories/bugs/xx-bug-personal-subscriptions-advanced-features.txt 2012-10-15 15:39:22 +0000
385@@ -9,8 +9,9 @@
386 >>> login(USER_EMAIL)
387 >>> bug = factory.makeBug()
388 >>> task = bug.default_bugtask
389+ >>> url = canonical_url(task, view_name='+subscribe')
390 >>> logout()
391- >>> user_browser.open(canonical_url(task, view_name='+subscribe'))
392+ >>> user_browser.open(url)
393 >>> bug_notification_level_control = user_browser.getControl(
394 ... name='field.bug_notification_level')
395 >>> for control in bug_notification_level_control.controls:
396
397=== modified file 'lib/lp/bugs/stories/patches-view/patches-view.txt'
398--- lib/lp/bugs/stories/patches-view/patches-view.txt 2012-10-08 10:07:11 +0000
399+++ lib/lp/bugs/stories/patches-view/patches-view.txt 2012-10-15 15:39:22 +0000
400@@ -255,9 +255,11 @@
401 ... bugtask.transitionToImportance(importance, ubuntu_distro.owner)
402 ... if status is not None:
403 ... bugtask.transitionToStatus(status, ubuntu_distro.owner)
404+ >>> login(ANONYMOUS)
405 >>> patchy_product_series = patchy_product.getSeries('trunk')
406 >>> make_bugtask(bug=bug_a, target=patchy_product_series)
407 >>> make_bugtask(bug=bug_c, target=patchy_product_series)
408+ >>> logout()
409 >>> anon_browser.open(
410 ... 'https://bugs.launchpad.dev/patchy-product-1/trunk/+patches')
411 >>> show_patches_view(anon_browser.contents)
412
413=== modified file 'lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt'
414--- lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt 2012-10-08 10:07:11 +0000
415+++ lib/lp/bugs/stories/structural-subscriptions/xx-advanced-features.txt 2012-10-15 15:39:22 +0000
416@@ -8,8 +8,8 @@
417 >>> from lp.testing.sampledata import USER_EMAIL
418 >>> login(USER_EMAIL)
419 >>> product = factory.makeProduct()
420+ >>> url = canonical_url(product, view_name='+subscriptions')
421 >>> logout()
422- >>> user_browser.open(
423- ... canonical_url(product, view_name='+subscriptions'))
424+ >>> user_browser.open(url)
425 >>> user_browser.getLink("Add a subscription")
426 <Link text='Add a subscription' url='.../+subscriptions#'>
427
428=== modified file 'lib/lp/bugs/stories/webservice/xx-bug.txt'
429--- lib/lp/bugs/stories/webservice/xx-bug.txt 2012-10-08 10:07:11 +0000
430+++ lib/lp/bugs/stories/webservice/xx-bug.txt 2012-10-15 15:39:22 +0000
431@@ -1463,13 +1463,14 @@
432 >>> from lp.testing.sampledata import ADMIN_EMAIL
433 >>> login(ADMIN_EMAIL)
434 >>> target = factory.makeProduct()
435+ >>> target_name = target.name
436 >>> bug = factory.makeBug(target=target)
437 >>> bug = removeSecurityProxy(bug)
438 >>> date = bug.date_last_updated - timedelta(days=6)
439 >>> logout()
440
441 >>> pprint_collection(webservice.named_get(
442- ... '/%s' % target.name, 'searchTasks',
443+ ... '/%s' % target_name, 'searchTasks',
444 ... modified_since=u'%s' % date ).jsonBody())
445 start: 0
446 total_size: 1
447@@ -1481,12 +1482,13 @@
448 >>> from lp.bugs.interfaces.bugtarget import IBugTarget
449 >>> login(ADMIN_EMAIL)
450 >>> target = IBugTarget(factory.makeProduct())
451+ >>> target_name = target.name
452 >>> task = factory.makeBugTask(target=target)
453 >>> date = task.datecreated - timedelta(days=8)
454 >>> logout()
455
456 >>> pprint_collection(webservice.named_get(
457- ... '/%s' % target.name, 'searchTasks',
458+ ... '/%s' % target_name, 'searchTasks',
459 ... created_since=u'%s' % date).jsonBody())
460 start: 0
461 total_size: 1
462@@ -1497,7 +1499,7 @@
463
464 >>> before_date = task.datecreated + timedelta(days=8)
465 >>> pprint_collection(webservice.named_get(
466- ... '/%s' % target.name, 'searchTasks',
467+ ... '/%s' % target_name, 'searchTasks',
468 ... created_before=u'%s' % before_date).jsonBody())
469 start: 0
470 total_size: 1
471
472=== modified file 'lib/lp/bugs/tests/test_bugs_webservice.py'
473--- lib/lp/bugs/tests/test_bugs_webservice.py 2012-10-09 08:39:54 +0000
474+++ lib/lp/bugs/tests/test_bugs_webservice.py 2012-10-15 15:39:22 +0000
475@@ -355,12 +355,13 @@
476 def test_add_duplicate_bugtask_for_project_gives_bad_request(self):
477 bug = self.factory.makeBug()
478 product = self.factory.makeProduct()
479+ product_url = api_url(product)
480 self.factory.makeBugTask(bug=bug, target=product)
481
482 launchpad = launchpadlib_for('test', bug.owner)
483 lp_bug = launchpad.load(api_url(bug))
484 self.assertRaises(
485- BadRequest, lp_bug.addTask, target=api_url(product))
486+ BadRequest, lp_bug.addTask, target=product_url)
487
488 def test_add_invalid_bugtask_to_proprietary_bug_gives_bad_request(self):
489 # Test we get an error when we attempt to invalidly add a bug task to
490@@ -371,6 +372,7 @@
491 bug_sharing_policy=BugSharingPolicy.PROPRIETARY)
492 product2 = self.factory.makeProduct(
493 bug_sharing_policy=BugSharingPolicy.PROPRIETARY)
494+ product2_url = api_url(product2)
495 bug = self.factory.makeBug(
496 target=product1, owner=owner,
497 information_type=InformationType.PROPRIETARY)
498@@ -379,7 +381,7 @@
499 launchpad = launchpadlib_for('test', owner)
500 lp_bug = launchpad.load(api_url(bug))
501 self.assertRaises(
502- BadRequest, lp_bug.addTask, target=api_url(product2))
503+ BadRequest, lp_bug.addTask, target=product2_url)
504
505 def test_add_attachment_with_bad_filename_raises_exception(self):
506 # Test that addAttachment raises BadRequest when the filename given
507@@ -404,13 +406,14 @@
508 # to the project's bug sharing policy.
509 project = self.factory.makeProduct(
510 licenses=[License.OTHER_PROPRIETARY])
511+ target_url = api_url(project)
512 with person_logged_in(project.owner):
513 project.setBugSharingPolicy(
514 BugSharingPolicy.PROPRIETARY_OR_PUBLIC)
515 webservice = launchpadlib_for('test', 'salgado')
516 bugs_collection = webservice.load('/bugs')
517 bug = bugs_collection.createBug(
518- target=api_url(project), title='title', description='desc')
519+ target=target_url, title='title', description='desc')
520 self.assertEqual('Proprietary', bug.information_type)
521
522
523@@ -430,7 +433,7 @@
524
525 def test_subscribe_does_not_update(self):
526 # Calling subscribe over the API does not update date_last_updated.
527- (bug, owner, webservice) = self.make_old_bug()
528+ (bug, owner, webservice) = self.make_old_bug()
529 subscriber = self.factory.makePerson()
530 date_last_updated = bug.date_last_updated
531 api_sub = api_url(subscriber)
532
533=== modified file 'lib/lp/bugs/tests/test_searchtasks_webservice.py'
534--- lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-10-08 10:07:11 +0000
535+++ lib/lp/bugs/tests/test_searchtasks_webservice.py 2012-10-15 15:39:22 +0000
536@@ -54,6 +54,7 @@
537 self.owner = self.factory.makePerson()
538 with person_logged_in(self.owner):
539 self.product = self.factory.makeProduct()
540+ self.product_name = self.product.name
541 self.bug = self.factory.makeBug(
542 target=self.product,
543 information_type=InformationType.PRIVATESECURITY)
544@@ -62,7 +63,7 @@
545
546 def search(self, api_version, **kwargs):
547 return self.webservice.named_get(
548- '/%s' % self.product.name, 'searchTasks',
549+ '/%s' % self.product_name, 'searchTasks',
550 api_version=api_version, **kwargs).jsonBody()
551
552 def test_linked_blueprints_in_devel(self):
553@@ -102,7 +103,7 @@
554 def test_search_with_wrong_orderby(self):
555 # Calling searchTasks() with a wrong order_by is a Bad Request.
556 response = self.webservice.named_get(
557- '/%s' % self.product.name, 'searchTasks',
558+ '/%s' % self.product_name, 'searchTasks',
559 api_version='devel', order_by='date_created')
560 self.assertEqual(400, response.status)
561 self.assertRaisesWithContent(
562
563=== modified file 'lib/lp/code/browser/tests/test_branch.py'
564--- lib/lp/code/browser/tests/test_branch.py 2012-10-09 00:05:04 +0000
565+++ lib/lp/code/browser/tests/test_branch.py 2012-10-15 15:39:22 +0000
566@@ -694,11 +694,11 @@
567 base_url = canonical_url(branch, rootsite='code')
568 product_url = canonical_url(product, rootsite='code')
569 url = '%s/+subscription/%s' % (base_url, subscriber.name)
570+ expected_title = "Code : %s" % product.displayname
571 browser = self._getBrowser(user=subscriber)
572 browser.open(url)
573 browser.getControl('Unsubscribe').click()
574 self.assertEqual(product_url, browser.url)
575- expected_title = "Code : %s" % product.displayname
576 self.assertEqual(expected_title, browser.title)
577
578
579
580=== modified file 'lib/lp/code/browser/tests/test_product.py'
581--- lib/lp/code/browser/tests/test_product.py 2012-10-08 14:27:31 +0000
582+++ lib/lp/code/browser/tests/test_product.py 2012-10-15 15:39:22 +0000
583@@ -222,8 +222,9 @@
584 login_person(product.owner)
585 product.development_focus.branch = code_import.branch
586 self.assertEqual(ServiceUsage.EXTERNAL, product.codehosting_usage)
587+ product_url = canonical_url(product, rootsite='code')
588 logout()
589- browser = self.getUserBrowser(canonical_url(product, rootsite='code'))
590+ browser = self.getUserBrowser(product_url)
591 login(ANONYMOUS)
592 content = find_tag_by_id(browser.contents, 'external')
593 text = extract_text(content)
594@@ -379,6 +380,7 @@
595
596 def test_is_public(self):
597 product = self.factory.makeProduct()
598+ product_displayname = product.displayname
599 branch = self.factory.makeProductBranch(product=product)
600 login_person(product.owner)
601 product.development_focus.branch = branch
602@@ -386,7 +388,7 @@
603 text = extract_text(find_tag_by_id(browser.contents, 'privacy'))
604 expected = (
605 "New branches for %(name)s are Public.*"
606- % dict(name=product.displayname))
607+ % dict(name=product_displayname))
608 self.assertTextMatchesExpressionIgnoreWhitespace(expected, text)
609
610
611
612=== modified file 'lib/lp/code/stories/branches/xx-product-branches.txt'
613--- lib/lp/code/stories/branches/xx-product-branches.txt 2012-10-11 04:14:37 +0000
614+++ lib/lp/code/stories/branches/xx-product-branches.txt 2012-10-15 15:39:22 +0000
615@@ -180,10 +180,10 @@
616 >>> factory = LaunchpadObjectFactory()
617 >>> login(ANONYMOUS)
618 >>> product = getUtility(IProductSet).getByName('firefox')
619+ >>> owner = product.owner
620 >>> old_branch = product.development_focus.branch
621 >>> ignored = login_person(product.owner)
622 >>> product.development_focus.branch = None
623- >>> logout()
624 >>> def print_links(browser):
625 ... links = find_tag_by_id(browser.contents, 'involvement')
626 ... if links is None:
627@@ -203,13 +203,16 @@
628
629 >>> print product.codehosting_usage.name
630 UNKNOWN
631+ >>> logout()
632 >>> admin_browser.open('http://code.launchpad.dev/firefox')
633 >>> print_links(admin_browser)
634 None
635
636 >>> setup_code_hosting('firefox')
637+ >>> login(ANONYMOUS)
638 >>> print product.codehosting_usage.name
639 LAUNCHPAD
640+ >>> logout()
641 >>> admin_browser.open('http://code.launchpad.dev/firefox')
642 >>> print_links(admin_browser)
643 Import a branch
644@@ -233,7 +236,7 @@
645 If the product specifies that it officially uses Launchpad code, then
646 the 'Import a branch' button is still shown.
647
648- >>> ignored = login_person(product.owner)
649+ >>> ignored = login_person(owner)
650 >>> product.development_focus.branch = old_branch
651 >>> logout()
652 >>> browser.open('http://code.launchpad.dev/firefox')
653
654=== modified file 'lib/lp/code/stories/webservice/xx-code-import.txt'
655--- lib/lp/code/stories/webservice/xx-code-import.txt 2012-10-09 00:10:04 +0000
656+++ lib/lp/code/stories/webservice/xx-code-import.txt 2012-10-15 15:39:22 +0000
657@@ -17,6 +17,7 @@
658 >>> other_person = factory.makePerson(name='other-person')
659 >>> removeSecurityProxy(person).join(team)
660 >>> product = factory.makeProduct(name='scruff')
661+ >>> product_name = product.name
662 >>> svn_branch_url = "http://svn.domain.com/source"
663 >>> code_import = removeSecurityProxy(factory.makeProductCodeImport(
664 ... registrant=person, product=product, branch_name='import',
665@@ -122,7 +123,7 @@
666
667 We can create an import using the API by calling a method on the project.
668
669- >>> product_url = '/' + product.name
670+ >>> product_url = '/' + product_name
671 >>> new_remote_url = factory.getUniqueURL()
672 >>> response = import_webservice.named_post(product_url, 'newCodeImport',
673 ... branch_name='new-import', rcs_type='Git',
674@@ -149,7 +150,7 @@
675
676 If we must we can create a CVS import.
677
678- >>> product_url = '/' + product.name
679+ >>> product_url = '/' + product_name
680 >>> new_remote_url = factory.getUniqueURL()
681 >>> response = import_webservice.named_post(product_url, 'newCodeImport',
682 ... branch_name='cvs-import', rcs_type='Concurrent Versions System',
683
684=== modified file 'lib/lp/registry/browser/tests/test_milestone.py'
685--- lib/lp/registry/browser/tests/test_milestone.py 2012-10-08 10:07:11 +0000
686+++ lib/lp/registry/browser/tests/test_milestone.py 2012-10-15 15:39:22 +0000
687@@ -234,12 +234,13 @@
688 super(TestProjectMilestoneIndexQueryCount, self).setUp()
689 self.owner = self.factory.makePerson(name='product-owner')
690 self.product = self.factory.makeProduct(owner=self.owner)
691+ self.product_owner = self.product.owner
692 login_person(self.product.owner)
693 self.milestone = self.factory.makeMilestone(
694 productseries=self.product.development_focus)
695
696 def add_bug(self, count):
697- login_person(self.product.owner)
698+ login_person(self.product_owner)
699 for i in range(count):
700 bug = self.factory.makeBug(target=self.product)
701 bug.bugtasks[0].transitionToMilestone(
702@@ -284,6 +285,7 @@
703 # increasing the cap.
704 page_query_limit = 37
705 product = self.factory.makeProduct()
706+ product_owner = product.owner
707 login_person(product.owner)
708 milestone = self.factory.makeMilestone(
709 productseries=product.development_focus)
710@@ -316,7 +318,7 @@
711 with_1_private_bug = collector.count
712 with_1_queries = ["%s: %s" % (pos, stmt[3]) for (pos, stmt) in
713 enumerate(collector.queries)]
714- login_person(product.owner)
715+ login_person(product_owner)
716 bug2 = self.factory.makeBug(
717 target=product, information_type=InformationType.USERDATA,
718 owner=product.owner)
719
720=== modified file 'lib/lp/registry/browser/tests/test_pillar_sharing.py'
721--- lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-10-08 10:07:11 +0000
722+++ lib/lp/registry/browser/tests/test_pillar_sharing.py 2012-10-15 15:39:22 +0000
723@@ -243,10 +243,10 @@
724
725 def test_sharing_menu(self):
726 url = canonical_url(self.pillar)
727- browser = setupBrowserForUser(user=self.driver)
728- browser.open(url)
729- soup = BeautifulSoup(browser.contents)
730 sharing_url = canonical_url(self.pillar, view_name='+sharing')
731+ browser = setupBrowserForUser(user=self.driver)
732+ browser.open(url)
733+ soup = BeautifulSoup(browser.contents)
734 sharing_menu = soup.find('a', {'href': sharing_url})
735 self.assertIsNotNone(sharing_menu)
736
737
738=== modified file 'lib/lp/registry/browser/tests/test_product.py'
739--- lib/lp/registry/browser/tests/test_product.py 2012-10-12 20:17:12 +0000
740+++ lib/lp/registry/browser/tests/test_product.py 2012-10-15 15:39:22 +0000
741@@ -631,8 +631,8 @@
742 def test_headers(self):
743 """The headers for the RDF view of a product should be as expected."""
744 product = self.factory.makeProduct()
745+ content_disposition = 'attachment; filename="%s.rdf"' % product.name
746 browser = self.getViewBrowser(product, view_name='+rdf')
747- content_disposition = 'attachment; filename="%s.rdf"' % product.name
748 self.assertEqual(
749 content_disposition, browser.headers['Content-disposition'])
750 self.assertEqual(
751
752=== modified file 'lib/lp/registry/browser/tests/test_productseries_views.py'
753--- lib/lp/registry/browser/tests/test_productseries_views.py 2012-10-11 16:27:29 +0000
754+++ lib/lp/registry/browser/tests/test_productseries_views.py 2012-10-15 15:39:22 +0000
755@@ -8,12 +8,14 @@
756
757 import soupmatchers
758 from testtools.matchers import Not
759+from zope.security.proxy import removeSecurityProxy
760
761 from lp.app.enums import InformationType
762 from lp.bugs.interfaces.bugtask import (
763 BugTaskStatus,
764 BugTaskStatusSearch,
765 )
766+from lp.services.webapp import canonical_url
767 from lp.testing import (
768 BrowserTestCase,
769 person_logged_in,
770@@ -60,6 +62,11 @@
771 browser = self.getViewBrowser(series)
772 self.assertThat(browser.contents, soupmatchers.HTMLContains(tag))
773
774+ def getBrowser(self, series, view_name=None):
775+ series = removeSecurityProxy(series)
776+ url = canonical_url(series, view_name=view_name)
777+ return self.getUserBrowser(url, series.product.owner)
778+
779 def test_package_proprietary_error(self):
780 """Packaging a proprietary product produces an error."""
781 product = self.factory.makeProduct(
782@@ -68,7 +75,7 @@
783 ubuntu_series = self.factory.makeUbuntuDistroSeries()
784 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series,
785 publish=True)
786- browser = self.getViewBrowser(productseries, '+ubuntupkg')
787+ browser = self.getBrowser(productseries, '+ubuntupkg')
788 browser.getControl('Source Package Name').value = (
789 sp.sourcepackagename.name)
790 browser.getControl(ubuntu_series.displayname).selected = True
791@@ -84,7 +91,7 @@
792 product = self.factory.makeProduct(
793 information_type=InformationType.PROPRIETARY)
794 series = self.factory.makeProductSeries(product=product)
795- browser = self.getViewBrowser(series)
796+ browser = self.getBrowser(series)
797 tag = soupmatchers.Tag(
798 'portlet-packages', True, attrs={'id': 'portlet-packages'})
799 self.assertThat(browser.contents, Not(soupmatchers.HTMLContains(tag)))
800
801=== modified file 'lib/lp/registry/browser/tests/test_sourcepackage_views.py'
802--- lib/lp/registry/browser/tests/test_sourcepackage_views.py 2012-10-11 20:33:03 +0000
803+++ lib/lp/registry/browser/tests/test_sourcepackage_views.py 2012-10-15 15:39:22 +0000
804@@ -297,12 +297,16 @@
805
806 def test_error_on_proprietary_product(self):
807 """Packaging cannot be created for PROPRIETARY products"""
808+ product_owner = self.factory.makePerson()
809+ product_name = 'proprietary-product'
810 product = self.factory.makeProduct(
811+ name=product_name, owner=product_owner,
812 information_type=InformationType.PROPRIETARY)
813 ubuntu_series = self.factory.makeUbuntuDistroSeries()
814 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
815- browser = self.getViewBrowser(sp, '+edit-packaging')
816- browser.getControl('Project').value = product.name
817+ browser = self.getViewBrowser(
818+ sp, '+edit-packaging', user=product_owner)
819+ browser.getControl('Project').value = product_name
820 browser.getControl('Continue').click()
821 self.assertIn(
822 'Only Public projects can be packaged, not Proprietary.',
823@@ -310,14 +314,18 @@
824
825 def test_error_on_proprietary_productseries(self):
826 """Packaging cannot be created for PROPRIETARY productseries"""
827- product = self.factory.makeProduct()
828+ product_owner = self.factory.makePerson()
829+ product_name = 'proprietary-product'
830+ product = self.factory.makeProduct(
831+ name=product_name, owner=product_owner)
832 series = self.factory.makeProductSeries(product=product)
833 ubuntu_series = self.factory.makeUbuntuDistroSeries()
834 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
835- browser = self.getViewBrowser(sp, '+edit-packaging')
836- browser.getControl('Project').value = product.name
837+ browser = self.getViewBrowser(
838+ sp, '+edit-packaging', user=product_owner)
839+ browser.getControl('Project').value = product_name
840 browser.getControl('Continue').click()
841- with person_logged_in(product.owner):
842+ with person_logged_in(product_owner):
843 product.information_type = InformationType.PROPRIETARY
844 browser.getControl(series.displayname).selected = True
845 browser.getControl('Change').click()
846
847=== modified file 'lib/lp/registry/configure.zcml'
848--- lib/lp/registry/configure.zcml 2012-10-08 10:07:11 +0000
849+++ lib/lp/registry/configure.zcml 2012-10-15 15:39:22 +0000
850@@ -1229,11 +1229,19 @@
851 class="lp.registry.model.product.Product">
852 <allow
853 interface="lp.registry.interfaces.product.IProductPublic"/>
854- <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
855 <allow
856+ interface="lp.registry.interfaces.pillar.IPillar"/>
857+ <require
858+ permission="launchpad.View"
859+ interface="lp.registry.interfaces.product.IProductLimitedView"/>
860+ <require
861+ permission="launchpad.View"
862+ interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
863+ <require
864+ permission="launchpad.View"
865 interface="lp.translations.interfaces.customlanguagecode.IHasCustomLanguageCodes"/>
866 <require
867- permission="launchpad.View"
868+ permission="launchpad.AnyAllowedPerson"
869 set_attributes="date_next_suggest_packaging"/>
870 <require
871 permission="launchpad.Driver"
872@@ -1316,7 +1324,7 @@
873 translationpermission
874 translations_usage"/>
875 <require
876- permission="zope.Public"
877+ permission="launchpad.View"
878 attributes="
879 qualifies_for_free_hosting"/>
880 <require
881@@ -1331,7 +1339,8 @@
882
883 <!-- IHasAliases -->
884
885- <allow
886+ <require
887+ permission="launchpad.View"
888 attributes="
889 aliases"/>
890 <require
891@@ -1341,14 +1350,17 @@
892
893 <!-- IQuestionTarget -->
894
895- <allow interface="lp.answers.interfaces.questiontarget.IQuestionTargetPublic"/>
896- <require
897- permission="launchpad.AnyPerson"
898+ <require
899+ permission="launchpad.View"
900+ interface="lp.answers.interfaces.questiontarget.IQuestionTargetPublic"/>
901+ <require
902+ permission="launchpad.AnyAllowedPerson"
903 interface="lp.answers.interfaces.questiontarget.IQuestionTargetView"/>
904
905 <!-- IFAQTarget -->
906
907- <allow
908+ <require
909+ permission="launchpad.View"
910 interface="lp.answers.interfaces.faqcollection.IFAQCollection"
911 attributes="
912 findSimilarFAQs"/>
913@@ -1359,15 +1371,17 @@
914
915 <!-- IStructuralSubscriptionTarget -->
916
917- <allow
918+ <require
919+ permission="launchpad.View"
920 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
921 <require
922- permission="launchpad.AnyPerson"
923+ permission="launchpad.AnyAllowedPerson"
924 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />
925
926 <!-- IHasBugSupervisor -->
927
928- <allow
929+ <require
930+ permission="launchpad.View"
931 attributes="
932 bug_supervisor"/>
933 <require
934
935=== modified file 'lib/lp/registry/interfaces/product.py'
936--- lib/lp/registry/interfaces/product.py 2012-10-11 07:45:33 +0000
937+++ lib/lp/registry/interfaces/product.py 2012-10-15 15:39:22 +0000
938@@ -420,18 +420,24 @@
939 "Not applicable to 'Other/Proprietary'.")))
940
941
942-class IProductPublic(
943+class IProductPublic(Interface):
944+
945+ id = Int(title=_('The Project ID'))
946+
947+ def userCanView(user):
948+ """True if the given user has access to this product."""
949+
950+
951+class IProductLimitedView(
952 IBugTarget, ICanGetMilestonesDirectly, IHasAppointedDriver, IHasBranches,
953- IHasDrivers, IHasExternalBugTracker, IHasIcon, IHasLogo,
954- IHasMergeProposals, IHasMilestones, IHasExpirableBugs, IHasMugshot,
955- IHasOwner, IHasSprints, IHasTranslationImports, ITranslationPolicy,
956- IKarmaContext, ILaunchpadUsage, IMakesAnnouncements,
957- IOfficialBugTagTargetPublic, IHasOOPSReferences, IPillar,
958+ IHasDrivers, IHasExternalBugTracker, IHasIcon,
959+ IHasLogo, IHasMergeProposals, IHasMilestones, IHasExpirableBugs,
960+ IHasMugshot, IHasOwner, IHasSprints, IHasTranslationImports,
961+ ITranslationPolicy, IKarmaContext, ILaunchpadUsage, IMakesAnnouncements,
962+ IOfficialBugTagTargetPublic, IHasOOPSReferences,
963 ISpecificationTarget, IHasRecipes, IHasCodeImports, IServiceUsage):
964 """Public IProduct properties."""
965
966- id = Int(title=_('The Project ID'))
967-
968 project = exported(
969 ReferenceChoice(
970 title=_('Part of'),
971@@ -868,9 +874,9 @@
972 class IProductEditRestricted(IOfficialBugTagTargetRestricted):
973 """`IProduct` properties which require launchpad.Edit permission."""
974
975- @mutator_for(IProductPublic['bug_sharing_policy'])
976+ @mutator_for(IProductLimitedView['bug_sharing_policy'])
977 @operation_parameters(bug_sharing_policy=copy_field(
978- IProductPublic['bug_sharing_policy']))
979+ IProductLimitedView['bug_sharing_policy']))
980 @export_write_operation()
981 @operation_for_version("devel")
982 def setBugSharingPolicy(bug_sharing_policy):
983@@ -879,10 +885,10 @@
984 Checks authorization and entitlement.
985 """
986
987- @mutator_for(IProductPublic['branch_sharing_policy'])
988+ @mutator_for(IProductLimitedView['branch_sharing_policy'])
989 @operation_parameters(
990 branch_sharing_policy=copy_field(
991- IProductPublic['branch_sharing_policy']))
992+ IProductLimitedView['branch_sharing_policy']))
993 @export_write_operation()
994 @operation_for_version("devel")
995 def setBranchSharingPolicy(branch_sharing_policy):
996@@ -891,10 +897,10 @@
997 Checks authorization and entitlement.
998 """
999
1000- @mutator_for(IProductPublic['specification_sharing_policy'])
1001+ @mutator_for(IProductLimitedView['specification_sharing_policy'])
1002 @operation_parameters(
1003 specification_sharing_policy=copy_field(
1004- IProductPublic['specification_sharing_policy']))
1005+ IProductLimitedView['specification_sharing_policy']))
1006 @export_write_operation()
1007 @operation_for_version("devel")
1008 def setSpecificationSharingPolicy(specification_sharing_policy):
1009@@ -907,8 +913,8 @@
1010 class IProduct(
1011 IHasBugSupervisor, IProductEditRestricted,
1012 IProductModerateRestricted, IProductDriverRestricted,
1013- IProductPublic, IQuestionTarget, IRootContext,
1014- IStructuralSubscriptionTarget, IInformationType):
1015+ IProductLimitedView, IProductPublic, IQuestionTarget, IRootContext,
1016+ IStructuralSubscriptionTarget, IInformationType, IPillar):
1017 """A Product.
1018
1019 The Launchpad Registry describes the open source world as ProjectGroups
1020
1021=== modified file 'lib/lp/registry/model/product.py'
1022--- lib/lp/registry/model/product.py 2012-10-12 18:23:18 +0000
1023+++ lib/lp/registry/model/product.py 2012-10-15 15:39:22 +0000
1024@@ -69,6 +69,7 @@
1025 InformationType,
1026 PRIVATE_INFORMATION_TYPES,
1027 PROPRIETARY_INFORMATION_TYPES,
1028+ PUBLIC_INFORMATION_TYPES,
1029 PUBLIC_PROPRIETARY_INFORMATION_TYPES,
1030 service_uses_launchpad,
1031 ServiceUsage,
1032@@ -155,6 +156,7 @@
1033 LicenseStatus,
1034 )
1035 from lp.registry.interfaces.productrelease import IProductReleaseSet
1036+from lp.registry.interfaces.role import IPersonRoles
1037 from lp.registry.model.announcement import MakesAnnouncements
1038 from lp.registry.model.commercialsubscription import CommercialSubscription
1039 from lp.registry.model.distribution import Distribution
1040@@ -1519,6 +1521,39 @@
1041
1042 return weight_function
1043
1044+ @cachedproperty
1045+ def _known_viewers(self):
1046+ """A set of known persons able to view this product."""
1047+ return set()
1048+
1049+ def userCanView(self, user):
1050+ """See `IProductPublic`."""
1051+ if self.information_type in PUBLIC_INFORMATION_TYPES:
1052+ return True
1053+ if user is None:
1054+ return False
1055+ if user.id in self._known_viewers:
1056+ return True
1057+ # We need the plain Storm Person object for the SQL query below
1058+ # but an IPersonRoles object for the team membership checks.
1059+ if IPersonRoles.providedBy(user):
1060+ plain_user = user.person
1061+ else:
1062+ plain_user = user
1063+ user = IPersonRoles(user)
1064+ if (user.in_commercial_admin or user.in_admin or
1065+ user.in_registry_experts):
1066+ self._known_viewers.add(user.id)
1067+ return True
1068+ policy = getUtility(IAccessPolicySource).find(
1069+ [(self, self.information_type)]).one()
1070+ grants_for_user = getUtility(IAccessPolicyGrantSource).find(
1071+ [(policy, plain_user)])
1072+ if grants_for_user.is_empty():
1073+ return False
1074+ self._known_viewers.add(user.id)
1075+ return True
1076+
1077
1078 def get_precached_products(products, need_licences=False, need_projects=False,
1079 need_series=False, need_releases=False,
1080
1081=== modified file 'lib/lp/registry/services/tests/test_sharingservice.py'
1082--- lib/lp/registry/services/tests/test_sharingservice.py 2012-10-11 04:14:37 +0000
1083+++ lib/lp/registry/services/tests/test_sharingservice.py 2012-10-15 15:39:22 +0000
1084@@ -1837,12 +1837,14 @@
1085 api_method, api_version='devel', **kwargs).jsonBody()
1086
1087 def _getPillarGranteeData(self):
1088- pillar_uri = canonical_url(self.pillar, force_local_path=True)
1089+ pillar_uri = canonical_url(
1090+ removeSecurityProxy(self.pillar), force_local_path=True)
1091 return self._named_get(
1092 'getPillarGranteeData', pillar=pillar_uri)
1093
1094 def _sharePillarInformation(self, pillar):
1095- pillar_uri = canonical_url(pillar, force_local_path=True)
1096+ pillar_uri = canonical_url(
1097+ removeSecurityProxy(pillar), force_local_path=True)
1098 return self._named_post(
1099 'sharePillarInformation', pillar=pillar_uri,
1100 grantee=self.grantee_uri,
1101
1102=== modified file 'lib/lp/registry/tests/test_pillaraffiliation.py'
1103--- lib/lp/registry/tests/test_pillaraffiliation.py 2012-10-08 10:07:11 +0000
1104+++ lib/lp/registry/tests/test_pillaraffiliation.py 2012-10-15 15:39:22 +0000
1105@@ -150,7 +150,7 @@
1106 Store.of(product).invalidate()
1107 with StormStatementRecorder() as recorder:
1108 IHasAffiliation(product).getAffiliationBadges([person])
1109- self.assertThat(recorder, HasQueryCount(Equals(5)))
1110+ self.assertThat(recorder, HasQueryCount(Equals(4)))
1111
1112 def test_distro_affiliation_query_count(self):
1113 # Only 2 business queries are expected, selects from:
1114
1115=== modified file 'lib/lp/registry/tests/test_product.py'
1116--- lib/lp/registry/tests/test_product.py 2012-10-11 14:05:29 +0000
1117+++ lib/lp/registry/tests/test_product.py 2012-10-15 15:39:22 +0000
1118@@ -15,6 +15,10 @@
1119 import transaction
1120 from zope.component import getUtility
1121 from zope.lifecycleevent.interfaces import IObjectModifiedEvent
1122+from zope.security.checker import (
1123+ CheckerPublic,
1124+ getChecker,
1125+ )
1126 from zope.security.interfaces import Unauthorized
1127 from zope.security.proxy import removeSecurityProxy
1128
1129@@ -44,6 +48,7 @@
1130 BugSharingPolicy,
1131 EXCLUSIVE_TEAM_POLICY,
1132 INCLUSIVE_TEAM_POLICY,
1133+ SharingPermission,
1134 SpecificationSharingPolicy,
1135 )
1136 from lp.registry.errors import (
1137@@ -56,11 +61,13 @@
1138 IAccessPolicySource,
1139 )
1140 from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
1141+from lp.registry.interfaces.person import IPersonSet
1142 from lp.registry.interfaces.product import (
1143 IProduct,
1144 IProductSet,
1145 License,
1146 )
1147+from lp.registry.interfaces.role import IPersonRoles
1148 from lp.registry.interfaces.series import SeriesStatus
1149 from lp.registry.model.product import (
1150 Product,
1151@@ -72,6 +79,7 @@
1152 celebrity_logged_in,
1153 login,
1154 person_logged_in,
1155+ StormStatementRecorder,
1156 TestCase,
1157 TestCaseWithFactory,
1158 WebServiceTestCase,
1159@@ -387,7 +395,7 @@
1160 license=License.OTHER_PROPRIETARY)
1161 self.assertEqual(InformationType.EMBARGOED, product.information_type)
1162 # Owner can set information_type
1163- with person_logged_in(product.owner):
1164+ with person_logged_in(removeSecurityProxy(product).owner):
1165 product.information_type = InformationType.PROPRIETARY
1166 self.assertEqual(InformationType.PROPRIETARY, product.information_type)
1167 # Database persists information_type value
1168@@ -399,7 +407,9 @@
1169
1170 def test_product_information_type_default(self):
1171 # Default information_type is PUBLIC
1172- product = self.createProduct()
1173+ owner = self.factory.makePerson()
1174+ product = getUtility(IProductSet).createProduct(
1175+ owner, 'fnord', 'Fnord', 'Fnord', 'test 1', 'test 2')
1176 self.assertEqual(InformationType.PUBLIC, product.information_type)
1177
1178 invalid_information_types = [info_type for info_type in
1179@@ -525,6 +535,349 @@
1180 CannotChangeInformationType, 'Some series are packaged.'):
1181 product.information_type = InformationType.PROPRIETARY
1182
1183+ def check_permissions(self, expected_permissions, used_permissions,
1184+ type_):
1185+ expected = set(expected_permissions.keys())
1186+ self.assertEqual(
1187+ expected, set(used_permissions.values()),
1188+ 'Unexpected %s permissions' % type_)
1189+ for permission in expected_permissions:
1190+ attribute_names = set(
1191+ name for name, value in used_permissions.items()
1192+ if value == permission)
1193+ self.assertEqual(
1194+ expected_permissions[permission], attribute_names,
1195+ 'Unexpected set of attributes with %s permission %s:\n'
1196+ 'Defined but not expected: %s\n'
1197+ 'Expected but not defined: %s'
1198+ % (
1199+ type_, permission,
1200+ sorted(
1201+ attribute_names - expected_permissions[permission]),
1202+ sorted(
1203+ expected_permissions[permission] - attribute_names)))
1204+
1205+ expected_get_permissions = {
1206+ CheckerPublic: set((
1207+ 'active', 'id', 'information_type', 'pillar_category', 'private',
1208+ 'userCanView',)),
1209+ 'launchpad.View': set((
1210+ '_getOfficialTagClause', '_all_specifications',
1211+ '_valid_specifications', 'active_or_packaged_series',
1212+ 'aliases', 'all_milestones',
1213+ 'allowsTranslationEdits', 'allowsTranslationSuggestions',
1214+ 'announce', 'answer_contacts', 'answers_usage', 'autoupdate',
1215+ 'blueprints_usage', 'branch_sharing_policy',
1216+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
1217+ 'bug_sharing_policy', 'bug_subscriptions', 'bug_supervisor',
1218+ 'bug_tracking_usage', 'bugtargetdisplayname', 'bugtargetname',
1219+ 'bugtracker', 'canUserAlterAnswerContact',
1220+ 'codehosting_usage',
1221+ 'coming_sprints', 'commercial_subscription',
1222+ 'commercial_subscription_is_due', 'createBug',
1223+ 'createCustomLanguageCode', 'custom_language_codes',
1224+ 'date_next_suggest_packaging', 'datecreated', 'description',
1225+ 'development_focus', 'development_focusID',
1226+ 'direct_answer_contacts', 'displayname', 'distrosourcepackages',
1227+ 'downloadurl', 'driver', 'drivers', 'enable_bug_expiration',
1228+ 'enable_bugfiling_duplicate_search', 'findReferencedOOPS',
1229+ 'findSimilarFAQs', 'findSimilarQuestions', 'freshmeatproject',
1230+ 'getAllowedBugInformationTypes',
1231+ 'getAllowedSpecificationInformationTypes', 'getAnnouncement',
1232+ 'getAnnouncements', 'getAnswerContactsForLanguage',
1233+ 'getAnswerContactRecipients', 'getBranches',
1234+ 'getBugSummaryContextWhereClause', 'getBugTaskWeightFunction',
1235+ 'getCustomLanguageCode', 'getDefaultBugInformationType',
1236+ 'getDefaultSpecificationInformationType',
1237+ 'getEffectiveTranslationPermission', 'getExternalBugTracker',
1238+ 'getFAQ', 'getFirstEntryToImport', 'getLinkedBugWatches',
1239+ 'getMergeProposals', 'getMilestone', 'getMilestonesAndReleases',
1240+ 'getQuestion', 'getQuestionLanguages', 'getPackage', 'getRelease',
1241+ 'getSeries', 'getSpecification', 'getSubscription',
1242+ 'getSubscriptions', 'getSupportedLanguages', 'getTimeline',
1243+ 'getTopContributors', 'getTopContributorsGroupedByCategory',
1244+ 'getTranslationGroups', 'getTranslationImportQueueEntries',
1245+ 'getTranslators', 'getUsedBugTagsWithOpenCounts',
1246+ 'getVersionSortedSeries',
1247+ 'has_current_commercial_subscription',
1248+ 'has_custom_language_codes', 'has_milestones', 'homepage_content',
1249+ 'homepageurl', 'icon', 'invitesTranslationEdits',
1250+ 'invitesTranslationSuggestions',
1251+ 'license_info', 'license_status', 'licenses', 'logo', 'milestones',
1252+ 'mugshot', 'name', 'name_with_project', 'newCodeImport',
1253+ 'obsolete_translatable_series', 'official_answers',
1254+ 'official_anything', 'official_blueprints', 'official_bug_tags',
1255+ 'official_codehosting', 'official_malone', 'owner',
1256+ 'parent_subscription_target', 'packagedInDistros', 'packagings',
1257+ 'past_sprints', 'personHasDriverRights', 'pillar',
1258+ 'primary_translatable', 'private_bugs',
1259+ 'programminglang', 'project', 'qualifies_for_free_hosting',
1260+ 'recipes', 'redeemSubscriptionVoucher', 'registrant', 'releases',
1261+ 'remote_product', 'removeCustomLanguageCode',
1262+ 'screenshotsurl',
1263+ 'searchFAQs', 'searchQuestions', 'searchTasks', 'security_contact',
1264+ 'series',
1265+ 'sharesTranslationsWithOtherSide', 'sourceforgeproject',
1266+ 'sourcepackages', 'specification_sharing_policy', 'specifications',
1267+ 'sprints', 'summary', 'target_type_display', 'title',
1268+ 'translatable_packages', 'translatable_series',
1269+ 'translation_focus', 'translationgroup', 'translationgroups',
1270+ 'translationpermission', 'translations_usage', 'ubuntu_packages',
1271+ 'userCanAlterBugSubscription', 'userCanAlterSubscription',
1272+ 'userCanEdit', 'userHasBugSubscriptions', 'uses_launchpad',
1273+ 'wikiurl')),
1274+ 'launchpad.AnyAllowedPerson': set((
1275+ 'addAnswerContact', 'addBugSubscription',
1276+ 'addBugSubscriptionFilter', 'addSubscription',
1277+ 'createQuestionFromBug', 'newQuestion', 'removeAnswerContact',
1278+ 'removeBugSubscription')),
1279+ 'launchpad.Append': set(('newFAQ', )),
1280+ 'launchpad.Driver': set(('newSeries', )),
1281+ 'launchpad.Edit': set((
1282+ 'addOfficialBugTag', 'removeOfficialBugTag',
1283+ 'setBranchSharingPolicy', 'setBugSharingPolicy',
1284+ 'setSpecificationSharingPolicy')),
1285+ 'launchpad.Moderate': set((
1286+ 'is_permitted', 'license_approved', 'project_reviewed',
1287+ 'reviewer_whiteboard', 'setAliases')),
1288+ }
1289+
1290+ def test_get_permissions(self):
1291+ product = self.factory.makeProduct()
1292+ checker = getChecker(product)
1293+ self.check_permissions(
1294+ self.expected_get_permissions, checker.get_permissions, 'get')
1295+
1296+ def test_set_permissions(self):
1297+ expected_set_permissions = {
1298+ 'launchpad.BugSupervisor': set((
1299+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
1300+ 'bugtracker', 'enable_bug_expiration',
1301+ 'enable_bugfiling_duplicate_search', 'official_bug_tags',
1302+ 'official_malone', 'remote_product')),
1303+ 'launchpad.Edit': set((
1304+ 'answers_usage', 'blueprints_usage', 'bug_supervisor',
1305+ 'bug_tracking_usage', 'codehosting_usage',
1306+ 'commercial_subscription', 'description', 'development_focus',
1307+ 'displayname', 'downloadurl', 'driver', 'freshmeatproject',
1308+ 'homepage_content', 'homepageurl', 'icon', 'information_type',
1309+ 'license_info', 'licenses', 'logo', 'mugshot',
1310+ 'official_answers', 'official_blueprints',
1311+ 'official_codehosting', 'owner', 'private',
1312+ 'programminglang', 'project', 'redeemSubscriptionVoucher',
1313+ 'releaseroot', 'screenshotsurl', 'sourceforgeproject',
1314+ 'summary', 'title', 'uses_launchpad', 'wikiurl')),
1315+ 'launchpad.Moderate': set((
1316+ 'active', 'autoupdate', 'license_approved', 'name',
1317+ 'project_reviewed', 'registrant', 'reviewer_whiteboard')),
1318+ 'launchpad.TranslationsAdmin': set((
1319+ 'translation_focus', 'translationgroup',
1320+ 'translationpermission', 'translations_usage')),
1321+ 'launchpad.AnyAllowedPerson': set((
1322+ 'date_next_suggest_packaging', )),
1323+ }
1324+ product = self.factory.makeProduct()
1325+ checker = getChecker(product)
1326+ self.check_permissions(
1327+ expected_set_permissions, checker.set_permissions, 'set')
1328+
1329+ def test_access_launchpad_View_public_product(self):
1330+ # Everybody, including anonymous users, has access to
1331+ # properties of public products that require the permission
1332+ # launchpad.View
1333+ product = self.factory.makeProduct()
1334+ names = self.expected_get_permissions['launchpad.View']
1335+ with person_logged_in(None):
1336+ for attribute_name in names:
1337+ getattr(product, attribute_name)
1338+ ordinary_user = self.factory.makePerson()
1339+ with person_logged_in(ordinary_user):
1340+ for attribute_name in names:
1341+ getattr(product, attribute_name)
1342+ with person_logged_in(product.owner):
1343+ for attribute_name in names:
1344+ getattr(product, attribute_name)
1345+
1346+ def test_access_launchpad_View_public_inactive_product(self):
1347+ # Everybody, including anonymous users, has access to
1348+ # properties of public but inactvie products that require
1349+ # the permission launchpad.View.
1350+ product = self.factory.makeProduct()
1351+ removeSecurityProxy(product).active = False
1352+ names = self.expected_get_permissions['launchpad.View']
1353+ with person_logged_in(None):
1354+ for attribute_name in names:
1355+ getattr(product, attribute_name)
1356+ ordinary_user = self.factory.makePerson()
1357+ with person_logged_in(ordinary_user):
1358+ for attribute_name in names:
1359+ getattr(product, attribute_name)
1360+ with person_logged_in(product.owner):
1361+ for attribute_name in names:
1362+ getattr(product, attribute_name)
1363+
1364+ def test_access_launchpad_View_proprietary_product(self):
1365+ # Only people with grants for a private product can access
1366+ # attributes protected by the permission launchpad.View.
1367+ product = self.createProduct(
1368+ information_type=InformationType.PROPRIETARY,
1369+ license=License.OTHER_PROPRIETARY)
1370+ owner = removeSecurityProxy(product).owner
1371+ names = self.expected_get_permissions['launchpad.View']
1372+ with person_logged_in(None):
1373+ for attribute_name in names:
1374+ self.assertRaises(
1375+ Unauthorized, getattr, product, attribute_name)
1376+ ordinary_user = self.factory.makePerson()
1377+ with person_logged_in(ordinary_user):
1378+ for attribute_name in names:
1379+ self.assertRaises(
1380+ Unauthorized, getattr, product, attribute_name)
1381+ with person_logged_in(owner):
1382+ for attribute_name in names:
1383+ getattr(product, attribute_name)
1384+ # A user with a policy grant for the product can access attributes
1385+ # of a private product.
1386+ with person_logged_in(owner):
1387+ getUtility(IService, 'sharing').sharePillarInformation(
1388+ product, ordinary_user, owner,
1389+ {InformationType.PROPRIETARY: SharingPermission.ALL})
1390+ with person_logged_in(ordinary_user):
1391+ for attribute_name in names:
1392+ getattr(product, attribute_name)
1393+ # Admins can access proprietary products.
1394+ with celebrity_logged_in('admin'):
1395+ for attribute_name in names:
1396+ getattr(product, attribute_name)
1397+ with celebrity_logged_in('registry_experts'):
1398+ for attribute_name in names:
1399+ getattr(product, attribute_name)
1400+ # Commercial admins have access to all products.
1401+ with celebrity_logged_in('commercial_admin'):
1402+ for attribute_name in names:
1403+ getattr(product, attribute_name)
1404+
1405+ def test_access_launchpad_AnyAllowedPerson_public_product(self):
1406+ # Only logged in persons have access to properties of public products
1407+ # that require the permission launchpad.AnyAllowedPerson.
1408+ product = self.factory.makeProduct()
1409+ names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
1410+ with person_logged_in(None):
1411+ for attribute_name in names:
1412+ self.assertRaises(
1413+ Unauthorized, getattr, product, attribute_name)
1414+ ordinary_user = self.factory.makePerson()
1415+ with person_logged_in(ordinary_user):
1416+ for attribute_name in names:
1417+ getattr(product, attribute_name)
1418+ with person_logged_in(product.owner):
1419+ for attribute_name in names:
1420+ getattr(product, attribute_name)
1421+
1422+ def test_access_launchpad_AnyAllowedPerson_proprietary_product(self):
1423+ # Only people with grants for a private product can access
1424+ # attributes protected by the permission launchpad.AnyAllowedPerson.
1425+ product = self.createProduct(
1426+ information_type=InformationType.PROPRIETARY,
1427+ license=License.OTHER_PROPRIETARY)
1428+ owner = removeSecurityProxy(product).owner
1429+ names = self.expected_get_permissions['launchpad.AnyAllowedPerson']
1430+ with person_logged_in(None):
1431+ for attribute_name in names:
1432+ self.assertRaises(
1433+ Unauthorized, getattr, product, attribute_name)
1434+ ordinary_user = self.factory.makePerson()
1435+ with person_logged_in(ordinary_user):
1436+ for attribute_name in names:
1437+ self.assertRaises(
1438+ Unauthorized, getattr, product, attribute_name)
1439+ with person_logged_in(owner):
1440+ for attribute_name in names:
1441+ getattr(product, attribute_name)
1442+ # A user with a policy grant for the product can access attributes
1443+ # of a private product.
1444+ with person_logged_in(owner):
1445+ getUtility(IService, 'sharing').sharePillarInformation(
1446+ product, ordinary_user, owner,
1447+ {InformationType.PROPRIETARY: SharingPermission.ALL})
1448+ with person_logged_in(ordinary_user):
1449+ for attribute_name in names:
1450+ getattr(product, attribute_name)
1451+
1452+ def test_set_launchpad_AnyAllowedPerson_public_product(self):
1453+ # Only logged in users can set attributes protected by the
1454+ # permission launchpad.AnyAllowedPerson.
1455+ product = self.factory.makeProduct()
1456+ with person_logged_in(None):
1457+ self.assertRaises(
1458+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
1459+ 'foo')
1460+ ordinary_user = self.factory.makePerson()
1461+ with person_logged_in(ordinary_user):
1462+ setattr(product, 'date_next_suggest_packaging', 'foo')
1463+ with person_logged_in(product.owner):
1464+ setattr(product, 'date_next_suggest_packaging', 'foo')
1465+
1466+ def test_set_launchpad_AnyAllowedPerson_proprietary_product(self):
1467+ # Only people with grants for a private product can set
1468+ # attributes protected by the permission launchpad.AnyAllowedPerson.
1469+ product = self.createProduct(
1470+ information_type=InformationType.PROPRIETARY,
1471+ license=License.OTHER_PROPRIETARY)
1472+ owner = removeSecurityProxy(product).owner
1473+ with person_logged_in(None):
1474+ self.assertRaises(
1475+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
1476+ 'foo')
1477+ ordinary_user = self.factory.makePerson()
1478+ with person_logged_in(ordinary_user):
1479+ self.assertRaises(
1480+ Unauthorized, setattr, product, 'date_next_suggest_packaging',
1481+ 'foo')
1482+ with person_logged_in(owner):
1483+ setattr(product, 'date_next_suggest_packaging', 'foo')
1484+ # A user with a policy grant for the product can access attributes
1485+ # of a private product.
1486+ with person_logged_in(owner):
1487+ getUtility(IService, 'sharing').sharePillarInformation(
1488+ product, ordinary_user, owner,
1489+ {InformationType.PROPRIETARY: SharingPermission.ALL})
1490+ with person_logged_in(ordinary_user):
1491+ setattr(product, 'date_next_suggest_packaging', 'foo')
1492+
1493+ def test_userCanView_caches_known_users(self):
1494+ # userCanView() maintains a cache of users known to have the
1495+ # permission to access a product.
1496+ product = self.createProduct(
1497+ information_type=InformationType.PROPRIETARY,
1498+ license=License.OTHER_PROPRIETARY)
1499+ owner = removeSecurityProxy(product).owner
1500+ user = self.factory.makePerson()
1501+ with person_logged_in(owner):
1502+ getUtility(IService, 'sharing').sharePillarInformation(
1503+ product, user, owner,
1504+ {InformationType.PROPRIETARY: SharingPermission.ALL})
1505+ with person_logged_in(user):
1506+ with StormStatementRecorder() as recorder:
1507+ # The first access to a property of the product from
1508+ # a user requires a DB query.
1509+ product.homepageurl
1510+ queries_for_first_user_access = len(recorder.queries)
1511+ # The second access does not require another query.
1512+ product.description
1513+ self.assertEqual(
1514+ queries_for_first_user_access, len(recorder.queries))
1515+
1516+ def test_userCanView_works_with_IPersonRoles(self):
1517+ # userCanView() maintains a cache of users known to have the
1518+ # permission to access a product.
1519+ product = self.createProduct(
1520+ information_type=InformationType.PROPRIETARY,
1521+ license=License.OTHER_PROPRIETARY)
1522+ user = self.factory.makePerson()
1523+ product.userCanView(user)
1524+ product.userCanView(IPersonRoles(user))
1525+
1526
1527 class TestProductBugInformationTypes(TestCaseWithFactory):
1528
1529
1530=== modified file 'lib/lp/registry/tests/test_product_webservice.py'
1531--- lib/lp/registry/tests/test_product_webservice.py 2012-10-11 04:18:37 +0000
1532+++ lib/lp/registry/tests/test_product_webservice.py 2012-10-15 15:39:22 +0000
1533@@ -17,6 +17,7 @@
1534 from lp.services.webapp.publisher import canonical_url
1535 from lp.testing import TestCaseWithFactory
1536 from lp.testing.layers import DatabaseFunctionalLayer
1537+from lp.testing import person_logged_in
1538 from lp.testing.pages import (
1539 LaunchpadWebServiceCaller,
1540 webservice_for_person,
1541@@ -47,27 +48,30 @@
1542 layer = DatabaseFunctionalLayer
1543
1544 def patch(self, webservice, obj, **data):
1545+ with person_logged_in(webservice.user):
1546+ path = URI(canonical_url(obj)).path
1547 return webservice.patch(
1548- URI(canonical_url(obj)).path,
1549- 'application/json', json.dumps(data),
1550- api_version='devel')
1551+ path, 'application/json', json.dumps(data), api_version='devel')
1552
1553 def test_branch_sharing_policy_can_be_set(self):
1554 # branch_sharing_policy can be set via the API.
1555 product = self.factory.makeProduct()
1556+ owner = product.owner
1557 self.factory.makeCommercialSubscription(product=product)
1558 webservice = webservice_for_person(
1559- product.owner, permission=OAuthPermission.WRITE_PRIVATE)
1560+ owner, permission=OAuthPermission.WRITE_PRIVATE)
1561 response = self.patch(
1562 webservice, product, branch_sharing_policy='Proprietary')
1563 self.assertEqual(209, response.status)
1564- self.assertEqual(
1565- BranchSharingPolicy.PROPRIETARY, product.branch_sharing_policy)
1566+ with person_logged_in(owner):
1567+ self.assertEqual(
1568+ BranchSharingPolicy.PROPRIETARY, product.branch_sharing_policy)
1569
1570 def test_branch_sharing_policy_non_commercial(self):
1571 # An API attempt to set a commercial-only branch_sharing_policy
1572 # on a non-commercial project returns Forbidden.
1573 product = self.factory.makeProduct()
1574+ owner = product.owner
1575 webservice = webservice_for_person(
1576 product.owner, permission=OAuthPermission.WRITE_PRIVATE)
1577 response = self.patch(
1578@@ -76,25 +80,29 @@
1579 status=403,
1580 body=('A current commercial subscription is required to use '
1581 'proprietary branches.')))
1582- self.assertEqual(
1583- BranchSharingPolicy.PUBLIC, product.branch_sharing_policy)
1584+ with person_logged_in(owner):
1585+ self.assertEqual(
1586+ BranchSharingPolicy.PUBLIC, product.branch_sharing_policy)
1587
1588 def test_bug_sharing_policy_can_be_set(self):
1589 # bug_sharing_policy can be set via the API.
1590 product = self.factory.makeProduct()
1591+ owner = product.owner
1592 self.factory.makeCommercialSubscription(product=product)
1593 webservice = webservice_for_person(
1594 product.owner, permission=OAuthPermission.WRITE_PRIVATE)
1595 response = self.patch(
1596 webservice, product, bug_sharing_policy='Proprietary')
1597 self.assertEqual(209, response.status)
1598- self.assertEqual(
1599- BugSharingPolicy.PROPRIETARY, product.bug_sharing_policy)
1600+ with person_logged_in(owner):
1601+ self.assertEqual(
1602+ BugSharingPolicy.PROPRIETARY, product.bug_sharing_policy)
1603
1604 def test_bug_sharing_policy_non_commercial(self):
1605 # An API attempt to set a commercial-only bug_sharing_policy
1606 # on a non-commercial project returns Forbidden.
1607 product = self.factory.makeProduct()
1608+ owner = product.owner
1609 webservice = webservice_for_person(
1610 product.owner, permission=OAuthPermission.WRITE_PRIVATE)
1611 response = self.patch(
1612@@ -103,13 +111,14 @@
1613 status=403,
1614 body=('A current commercial subscription is required to use '
1615 'proprietary bugs.')))
1616- self.assertEqual(
1617- BugSharingPolicy.PUBLIC, product.bug_sharing_policy)
1618+ with person_logged_in(owner):
1619+ self.assertEqual(
1620+ BugSharingPolicy.PUBLIC, product.bug_sharing_policy)
1621
1622 def fetch_product(self, webservice, product, api_version):
1623- return webservice.get(
1624- canonical_url(product, force_local_path=True),
1625- api_version=api_version).jsonBody()
1626+ with person_logged_in(webservice.user):
1627+ url = canonical_url(product, force_local_path=True)
1628+ return webservice.get(url, api_version=api_version).jsonBody()
1629
1630 def test_security_contact_exported(self):
1631 # security_contact is exported for 1.0, but not for other versions.
1632
1633=== modified file 'lib/lp/registry/tests/test_subscribers.py'
1634--- lib/lp/registry/tests/test_subscribers.py 2012-10-08 10:07:11 +0000
1635+++ lib/lp/registry/tests/test_subscribers.py 2012-10-15 15:39:22 +0000
1636@@ -195,7 +195,10 @@
1637 # If there is no request, there is no reason to show a message in
1638 # the browser.
1639 product, user = self.make_product_user([License.GNU_GPL_V2])
1640- notification = LicenseNotification(product)
1641+ # Using the proxied product leads to an exeception when
1642+ # notification.display() below is called because the permission
1643+ # checks product require an interaction.
1644+ notification = LicenseNotification(removeSecurityProxy(product))
1645 logout()
1646 result = notification.display()
1647 self.assertIs(False, result)
1648
1649=== modified file 'lib/lp/security.py'
1650--- lib/lp/security.py 2012-10-09 08:39:54 +0000
1651+++ lib/lp/security.py 2012-10-15 15:39:22 +0000
1652@@ -29,6 +29,7 @@
1653 from lp.answers.interfaces.questionmessage import IQuestionMessage
1654 from lp.answers.interfaces.questionsperson import IQuestionsPerson
1655 from lp.answers.interfaces.questiontarget import IQuestionTarget
1656+from lp.app.enums import PUBLIC_INFORMATION_TYPES
1657 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1658 from lp.app.interfaces.security import IAuthorization
1659 from lp.app.security import (
1660@@ -424,6 +425,30 @@
1661 return user.isOwner(self.obj) or user.in_admin
1662
1663
1664+class ViewProduct(AuthorizationBase):
1665+ permission = 'launchpad.View'
1666+ usedfor = IProduct
1667+
1668+ def checkAuthenticated(self, user):
1669+ return self.obj.userCanView(user)
1670+
1671+ def checkUnauthenticated(self):
1672+ return self.obj.information_type in PUBLIC_INFORMATION_TYPES
1673+
1674+
1675+class ChangeProduct(ViewProduct):
1676+ """Used for attributes of IProduct that are accessible to any logged
1677+ in user for public product but only to persons with access grants
1678+ for private products.
1679+ """
1680+
1681+ permission = 'launchpad.AnyAllowedPerson'
1682+ usedfor = IProduct
1683+
1684+ def checkUnauthenticated(self):
1685+ return False
1686+
1687+
1688 class EditProduct(EditByOwnersOrAdmins):
1689 usedfor = IProduct
1690
1691
1692=== modified file 'lib/lp/testing/factory.py'
1693--- lib/lp/testing/factory.py 2012-10-11 04:21:07 +0000
1694+++ lib/lp/testing/factory.py 2012-10-15 15:39:22 +0000
1695@@ -1025,7 +1025,6 @@
1696 specification_sharing_policy)
1697 if information_type is not None:
1698 naked_product.information_type = information_type
1699-
1700 return product
1701
1702 def makeProductSeries(self, product=None, name=None, owner=None,
1703@@ -1044,7 +1043,7 @@
1704 if product is None:
1705 product = self.makeProduct()
1706 if owner is None:
1707- owner = product.owner
1708+ owner = removeSecurityProxy(product).owner
1709 if name is None:
1710 name = self.getUniqueString()
1711 if summary is None:
1712@@ -1821,7 +1820,8 @@
1713
1714 if owner is None:
1715 owner = self.makePerson()
1716- return removeSecurityProxy(bug).addTask(owner, target)
1717+ return removeSecurityProxy(bug).addTask(
1718+ owner, removeSecurityProxy(target))
1719
1720 def makeBugNomination(self, bug=None, target=None):
1721 """Create and return a BugNomination.
1722
1723=== modified file 'lib/lp/testing/pages.py'
1724--- lib/lp/testing/pages.py 2012-10-08 10:07:11 +0000
1725+++ lib/lp/testing/pages.py 2012-10-15 15:39:22 +0000
1726@@ -730,7 +730,9 @@
1727 request_token.review(person, permission, context)
1728 access_token = request_token.createAccessToken()
1729 logout()
1730- return LaunchpadWebServiceCaller(consumer_key, access_token.key)
1731+ service = LaunchpadWebServiceCaller(consumer_key, access_token.key)
1732+ service.user = person
1733+ return service
1734
1735
1736 def setupDTCBrowser():
1737
1738=== modified file 'lib/lp/translations/browser/tests/test_noindex.py'
1739--- lib/lp/translations/browser/tests/test_noindex.py 2012-10-08 10:07:11 +0000
1740+++ lib/lp/translations/browser/tests/test_noindex.py 2012-10-15 15:39:22 +0000
1741@@ -46,7 +46,12 @@
1742 # Using create_initialized_view for distroseries causes an error when
1743 # rendering the view due to the way the view is registered and menus
1744 # are adapted. Getting the contents via a browser does work.
1745- self.user_browser.open(self.url)
1746+ #
1747+ # Retrieve the URL before the user_browser is created. Products
1748+ # can only be access with an active interaction, and getUserBrowser()
1749+ # closes the current interaction.
1750+ url = self.url
1751+ self.user_browser.open(url)
1752 return self.user_browser.contents
1753
1754 def getRobotsDirective(self):
1755
1756=== modified file 'lib/lp/translations/stories/importqueue/xx-entry-details.txt'
1757--- lib/lp/translations/stories/importqueue/xx-entry-details.txt 2012-10-08 10:07:11 +0000
1758+++ lib/lp/translations/stories/importqueue/xx-entry-details.txt 2012-10-15 15:39:22 +0000
1759@@ -15,6 +15,7 @@
1760 >>> queue = TranslationImportQueue()
1761 >>> product = factory.makeProduct(
1762 ... translations_usage=ServiceUsage.LAUNCHPAD)
1763+ >>> product_displayname = product.displayname
1764 >>> trunk = product.getSeries('trunk')
1765 >>> uploader = factory.makePerson()
1766 >>> entry = queue.addOrUpdateEntry(
1767@@ -34,7 +35,7 @@
1768
1769 The details include the project the entry is for, and who uploaded it.
1770
1771- >>> product.displayname in details_text
1772+ >>> product_displayname in details_text
1773 True
1774
1775 # Must remove the security proxy because IPerson.displayname is protected.
1776
1777=== modified file 'lib/lp/translations/stories/webservice/xx-potemplate.txt'
1778--- lib/lp/translations/stories/webservice/xx-potemplate.txt 2012-10-08 10:07:11 +0000
1779+++ lib/lp/translations/stories/webservice/xx-potemplate.txt 2012-10-15 15:39:22 +0000
1780@@ -71,13 +71,14 @@
1781
1782 >>> login('admin@canonical.com')
1783 >>> productseries = factory.makeProductSeries()
1784+ >>> product_name = productseries.product.name
1785 >>> potemplate_1 = factory.makePOTemplate(productseries=productseries)
1786 >>> potemplate_2 = factory.makePOTemplate(productseries=productseries)
1787 >>> potemplate_count = 2
1788 >>> logout()
1789 >>> all_translation_templates = anon_webservice.named_get(
1790 ... '/%s/%s' % (
1791- ... productseries.product.name,
1792+ ... product_name,
1793 ... productseries.name),
1794 ... 'getTranslationTemplates'
1795 ... ).jsonBody()