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

Proposed by Abel Deuring on 2012-10-10
Status: Rejected
Rejected by: Abel Deuring on 2012-10-12
Proposed branch: lp:~adeuring/launchpad/authentication-for-private-products
Merge into: lp:launchpad
Diff against target: 1561 lines (+580/-109)
34 files modified
lib/lp/answers/tests/test_question_webservice.py (+5/-3)
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 (+11/-6)
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/configure.zcml (+27/-11)
lib/lp/registry/interfaces/product.py (+36/-28)
lib/lp/registry/model/product.py (+27/-1)
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 (+340/-2)
lib/lp/registry/tests/test_product_webservice.py (+22/-13)
lib/lp/registry/tests/test_subscribers.py (+4/-1)
lib/lp/security.py (+29/-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
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code 2012-10-10 Needs Fixing on 2012-10-10
Review via email: mp+129014@code.launchpad.net

Description of the Change

This branch is my second attempt to activate permission checks for private products. The first attempt caused numerous OOPSes when it was deployed last monday. The reason was that the security adapter also refused access to most IProduct properties for inactive products, but in some cases details about inactive products are needed during page rendering, even if they are not visible for unprivileged users.

I discussed several approaches to fix this problem with sinzui; it turned out that we should for now simply allow access to the properties that are needed. Better fixes would need to long to implement.

The diff against trunk is pretty long. But r16117 simply adds the the changed from r16090 that were reverted in r16112.

tests:

./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_to_deactivated_product
./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_get_permissions
./bin/test -vvt lp.registry.tests.test_product.TestProduct.test_access_launchpad_View_proprietary_product

no lint

To post a comment you must log in.
Abel Deuring (adeuring) wrote :
Download full text (8.9 KiB)

the "real" changes:

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml 2012-10-09 10:28:02 +0000
+++ lib/lp/registry/configure.zcml 2012-10-10 18:04:36 +0000
@@ -1229,8 +1229,9 @@
         class="lp.registry.model.product.Product">
         <allow
             interface="lp.registry.interfaces.product.IProductPublic"/>
- <allow
- interface="lp.registry.interfaces.pillar.IPillar"/>
+ <require
+ permission="launchpad.View"
+ interface="lp.registry.interfaces.product.IPillar"/>
         <require
             permission="launchpad.View"
             interface="lp.registry.interfaces.product.IProductLimitedView"/>
@@ -1371,8 +1372,9 @@

         <!-- IStructuralSubscriptionTarget -->

- <require
- permission="launchpad.View"
+ <!-- XXX bug=1065162 Abel Deuring 2012-10-10:
+ This interface should require the permission launchpad.View. -->
+ <allow
             interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
         <require
             permission="launchpad.AnyAllowedPerson"

=== modified file 'lib/lp/registry/interfaces/product.py'
--- lib/lp/registry/interfaces/product.py 2012-10-09 10:28:02 +0000
+++ lib/lp/registry/interfaces/product.py 2012-10-10 18:04:36 +0000
@@ -430,6 +430,24 @@
     def userCanView(user):
         """True if the given user has access to this product."""

+ # bug=1065162 Abel Deuring 2012-10-10:
+ # name and displayname should be defined in IProductLimitedView
+ name = exported(
+ ProductNameField(
+ title=_('Name'),
+ constraint=name_validator,
+ description=_(
+ "At least one lowercase letter or number, followed by "
+ "letters, numbers, dots, hyphens or pluses. "
+ "Keep this name short; it is used in URLs as shown above.")))
+
+ displayname = exported(
+ TextLine(
+ title=_('Display Name'),
+ description=_("""The name of the project as it would appear in a
+ paragraph.""")),
+ exported_as='display_name')
+

 class IProductLimitedView(
     IBugTarget, ICanGetMilestonesDirectly, IHasAppointedDriver, IHasBranches,
@@ -491,22 +509,6 @@
         "required because there might be a project driver and also a "
         "driver appointed in the overarching project group.")

- name = exported(
- ProductNameField(
- title=_('Name'),
- constraint=name_validator,
- description=_(
- "At least one lowercase letter or number, followed by "
- "letters, numbers, dots, hyphens or pluses. "
- "Keep this name short; it is used in URLs as shown above.")))
-
- displayname = exported(
- TextLine(
- title=_('Display Name'),
- description=_("""The name of the project as it would appear in a
- paragraph.""")),
- exported_as='display_name')
-
     title = exported(
         Title(
             title=_('Title'),

=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry...

Read more...

Curtis Hovey (sinzui) wrote :

The comment format is
XXX: Abel Deuring 2012-10-10 bug=1065162

There is still something wrong and we think are missing tests. non-registry users and anonymous users get 403 on pages that that are working with deactivated projects.
1. Start an Lp instance
2. As admin, https://bugs.launchpad.dev/redfish/+bug/15
   and verify redfish and thunderbird are in the affected table.
3. Visit https://launchpad.dev/ and verify that thunderbird is listed
4. Visit https://launchpad.dev/thunderbird/+admin
   and deactivate thunderbird
5. Visit https://launchpad.dev/
   and verify thunderbird is listed.
6. Follow the link to https://launchpad.dev/thunderbird
   and verify it says it is deactivated
7. Visit https://bugs.launchpad.dev/redfish/+bug/15
   and verify that only redfish is shown in the tasks table.

8. As no-priv, visit https://launchpad.dev/thunderbird
   and verify the page is a 404.
9. Visit https://bugs.launchpad.dev/redfish/+bug/15
   and verify that only redfish is shown in the tasks table.
   BAD: the user gets a 403!
   OH, this loads after a while...we have a async issue?

Module lp.bugs.model.structuralsubscription, line 275, in __init__
self.target_parent = target.project
Unauthorized: (<Product at 0x2b15b8d98410>, 'project', 'launchpad.View'

10. Visit https://launchpad.dev/
    and verify thunderbird is listed.
    BAD: the user gets a 403!

if IHasIcon.providedBy(context) and context.icon is not None:
Unauthorized: (<Product at 0x2b15bca1fd10>, 'icon', 'launchpad.View')

11. As anonymous, visit https://launchpad.dev/thunderbird
    and verify the page is a 404.
12. Visit https://bugs.launchpad.dev/redfish/+bug/15
    and verify that only redfish is shown in the tasks table.
    BAD: the user gets a 403!
    OH, this loads after a while...we have a async issue?

Module lp.bugs.model.structuralsubscription, line 275, in __init__
self.target_parent = target.project
Unauthorized: (<Product at 0x2b15b8d98410>, 'project', 'launchpad.View'

13. Visit https://launchpad.dev/
    ...I cannot. Lp requires me to login to see a page intended for bots and anonymous users.

This kind of error can be seen elsewhere where deactivated projects can appear in Lp. In qastaging for example we can deactivate a project listed on Lp's front page ans the page still displays for anonymous and non-registry users, but devel breaks with your branch.

I think we want to make the security checker smarter so we can land this branch to maintain the current behaviour. We can revise the checker and the interfaces in future branches. Maybe both checkAuthenticated() and checkUnauthenticated() can return true if the project is active and public. They can return true if the project is inactive and public and the user is in A, CA, R, otherwise if the project is private and user in A, CA return true

review: Needs Fixing (code)
Abel Deuring (adeuring) wrote :

I think we should not return True for inactive products. In that case, oridnary users will be able to see these inactive products. Or we separate the checkers for IPillar and IProductView. But as we discussed in a hangout, I had a hard time to implement this when the permission lp.View is used both for IPillar and IProductView.

Curtis Hovey (sinzui) wrote :

Members of ~registry, ~admins, and ~commercial-admins *do* have lp.view on deactivated projects now. They also have lp.moderate and some have lp.admin and lp.edit. So we will return true for deactivated project for about 100 people.

Abel Deuring (adeuring) wrote :

On 10.10.2012 21:52, Curtis Hovey wrote:
> Members of ~registry, ~admins, and ~commercial-admins *do* have lp.view on deactivated projects now. They also have lp.moderate and some have lp.admin and lp.edit. So we will return true for deactivated project for about 100 people.

yes, but my point are the permissions for ordinary users: I could simply
not find a way to use lp.View both for IPillar and for IProductView so
that an oridnary users sees a 404 error when visiting the main page of a
deactivated product

I'd be grateful for a hint how to do this.

Curtis Hovey (sinzui) wrote :

This is the checker used in production:

class ViewPillar(AuthorizationBase):
    usedfor = IPillar
    permission = 'launchpad.View'

    def checkUnauthenticated(self):
        return self.obj.active

    def checkAuthenticated(self, user):
        """The Admins & Commercial Admins can see inactive pillars."""
        if self.obj.active:
            return True
        else:
            return (user.in_commercial_admin or
                    user.in_admin or
                    user.in_registry_experts)

You introduced a new checker that is specific to IProduct, but is does not ever consider .active. As is said in the hangout, IPillar is not properly implemented. IDistribution.active cannot ever be false, so we know the .active rule is mostly for IProduct. I think this phrasing of rules always defers to ViewPillar for the current case that all projects are public. We only do new rule checking for private types. Unauthenticated is always false, and authenticated has to exempt A and CA from the data drive rules in userCanView()

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

    def checkAuthenticated(self, user):
        if self.obj.information_type in PUBLIC_INFORMATION_TYPES:
            return super(ViewProduct, self).checkAuthenticated(user)
        return (user.in_commercial_admin
                or user.in_admin
                or self.obj.userCanView(user))

    def checkUnauthenticated(self):
        if self.obj.information_type in PUBLIC_INFORMATION_TYPES:
            return super(ViewProduct, self).checkUnauthenticated()
        return False

Abel Deuring (adeuring) wrote :

On 10.10.2012 22:30, Curtis Hovey wrote:
> This is the checker used in production:
>
> class ViewPillar(AuthorizationBase):
> usedfor = IPillar
> permission = 'launchpad.View'
>
> def checkUnauthenticated(self):
> return self.obj.active
>
> def checkAuthenticated(self, user):
> """The Admins & Commercial Admins can see inactive pillars."""
> if self.obj.active:
> return True
> else:
> return (user.in_commercial_admin or
> user.in_admin or
> user.in_registry_experts)
>
> You introduced a new checker that is specific to IProduct, but is does not ever consider .active.

The one in r16090 did.

> As is said in the hangout, IPillar is not properly implemented. IDistribution.active cannot ever be false, so we know the .active rule is mostly for IProduct. I think this phrasing of rules always defers to ViewPillar for the current case that all projects are public. We only do new rule checking for private types. Unauthenticated is always false, and authenticated has to exempt A and CA from the data drive rules in userCanView()
>
> class ViewProduct(ViewPillar):
> permission = 'launchpad.View'
> usedfor = IProduct
>
> def checkAuthenticated(self, user):
> if self.obj.information_type in PUBLIC_INFORMATION_TYPES:
> return super(ViewProduct, self).checkAuthenticated(user)

...this would deny access to properties like name, displayname etc which
we need for deactivated products, so the same problem we had with r16090.

> return (user.in_commercial_admin
> or user.in_admin
> or self.obj.userCanView(user))
>
> def checkUnauthenticated(self):
> if self.obj.information_type in PUBLIC_INFORMATION_TYPES:
> return super(ViewProduct, self).checkUnauthenticated()
> return False
>

William Grant (wgrant) wrote :

Have you considered feature-flagging this so we can revert easily if necessary? It may also be helpful to later make projects show up as <redacted> and raise a soft OOPS as private teams do today, although that will be less helpful as a lot of artifact URLs contain the project name.

Abel Deuring (adeuring) wrote :

On 10.10.2012 23:53, William Grant wrote:
> Have you considered feature-flagging this so we can revert easily if necessary? It may also be helpful to later make projects show up as <redacted> and raise a soft OOPS as private teams do today, although that will be less helpful as a lot of artifact URLs contain the project name.
>

Do we have means to change ZCML configurations via feature flags? The
main issue are new permissions and a changed inheritance hierarchy for
IProduct.

William Grant (wgrant) wrote :

AIUI the main thing launchpad.View on IProduct is used for today is to prevent traversal to deactivated products. If you add an extra guard to prevent such traversal even if launchpad.View is held, then the launchpad.View adapter can have an emergency feature flag which grants it to everyone.

Abel Deuring (adeuring) wrote :
Download full text (4.3 KiB)

On 11.10.2012 10:40, William Grant wrote:
> AIUI the main thing launchpad.View on IProduct is used for today is to prevent traversal to deactivated products. If you add an extra guard to prevent such traversal even if launchpad.View is held, then the launchpad.View adapter can have an emergency feature flag which grants it to everyone.
>

A feature flag alone would not help much. I think the main issue is
that the same checker is used for IPillar and IProductLimitedView
(should be renamed to IProductView, but this is different issue).

Let me try to explain the problem with this branch:
lp:~adeuring/launchpad/authentication-for-private-products-test

It has my core changes: most of attributes of IProduct are defined in
IProductLimitedView and require the permission launchpad.View.

The relevant security adapter is ViewProduct.

First, the variant r16120:

The section for Product in registry/configure.zcml does not contain
any directive for IPillar.

Now do "make run", log in as <email address hidden> and try to view any
product related page. You'll get "LocationError:
(<Product at 0xba970d0>, 'active')<br />"

Or run iharness:

from lp.registry.interfaces.product import IProductSet
p = getUtility(IProductSet).getByName('firefox')
p.active

You'll get a ForbiddenAttribute exception.

So we need a directive for IPillar in registry/configure.zcml.

r16121 has

        <allow
            interface="lp.registry.interfaces.product.IPillar"/>

The iharness test now works.

Now "make run", log in as an admin and make thunderbird private, as
suggested by Curtis.

Visit https://launchpad.dev/thunderbird as an admin, as an ordinary user
and without being logged in. This works as expected: The admin sees
the regular page with the notice "this project is currently inactive."

Anonymous and <email address hidden> see a 404 page.

Now visit https://bugs.launchpad.dev/redfish/+bug/15 (this bug has a
task for thunderbird.)

Anonymous and the admin see the page just fine, but <email address hidden>
gets a "Not allowed here" error:
Unauthorized: (<Product at 0x2b0588b8d9d0>, 'project',
'launchpad.View')<br />

as noticed by Curtis.

Now let us try to remove the check for inactive products in the
security adapter, as suggested by William (out of lazyness, without a
feature flag, i just removed the check for inactive products): r16122

The bug page works fine for all users, but anonymous and
test@canoncial,com no longer get the 404 error for
https://launchpad.dev/thunderbird . Instead, they see the same page as
the admin: "This project is currently inactive."

So, no luck either.

Let us now require the permission lp.View for IPillar: r16123

Both pages, https://bugs.launchpad.dev/redfish/+bug/15 (good) and
https://launchpad.dev/thunderbird (bad) are still visible for anonymous
and <email address hidden> .

Let's reenable the check for inactive products in ViewProduct: r16124

Now anonymous and <email address hidden> get again the 404 error for
https://launchpad.dev/thunderbird -- but <email address hidden> gets
a 403 for https://bugs.launchpad.dev/redfish/+bug/15. So we are back
at the situation that required a rollback of r16090 of trunk.

I think the main problem is that the inte...

Read more...

Unmerged revisions

16119. By Abel Deuring on 2012-10-10

trunk merged.

16118. By Abel Deuring on 2012-10-10

avoid OOPSes when data of inactive products is needed for users without special permissions.

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