Merge lp:~adeuring/launchpad/productseries-sec-adapter into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: j.c.sackett
Approved revision: no longer in the source branch.
Merged at revision: 16174
Proposed branch: lp:~adeuring/launchpad/productseries-sec-adapter
Merge into: lp:launchpad
Diff against target: 735 lines (+462/-30) (has conflicts)
12 files modified
lib/lp/code/browser/tests/test_branchlisting.py (+2/-1)
lib/lp/registry/browser/tests/test_productseries_views.py (+51/-0)
lib/lp/registry/browser/tests/test_sourcepackage_views.py (+2/-1)
lib/lp/registry/configure.zcml (+12/-5)
lib/lp/registry/interfaces/productseries.py (+11/-5)
lib/lp/registry/model/packaging.py (+4/-0)
lib/lp/registry/model/productseries.py (+5/-0)
lib/lp/registry/tests/test_packaging.py (+18/-10)
lib/lp/registry/tests/test_productseries.py (+326/-1)
lib/lp/registry/tests/test_sourcepackage.py (+8/-3)
lib/lp/security.py (+21/-3)
lib/lp/translations/stories/webservice/xx-potemplate.txt (+2/-1)
Text conflict in lib/lp/registry/browser/tests/test_productseries_views.py
Text conflict in lib/lp/registry/tests/test_productseries.py
To merge this branch: bzr merge lp:~adeuring/launchpad/productseries-sec-adapter
Reviewer Review Type Date Requested Status
Deryck Hodge (community) Approve
j.c.sackett (community) Approve
Review via email: mp+130305@code.launchpad.net

Commit message

privacy aware security adapter for IProductSeries

Description of the change

This branch adds a "sharing aware" security adapter for IProductSeries.

Serieses of private products are now only shown to persons
with policy grants for the product; the only exception are some
attributes that do not leak any "real" information: the database ID,
and the method userCanView().

Details of the change:

lp/registry/configure.zcml:

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

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

lp/security.py:

The existing class ViewProductSeries derived AnonymousAuthorization.
This does not make sense anymore, instead the class now derives
AuthorizationBase and calls the new method ProductSeries,userCanView()
for real authorization check.

The new class ChangeProductSeries does the authorization check for
the permission launchpad.AnyAllowedPerson.

lp/registry/interfaces/productseries.py

The existing interface IProductSeriesPublic now defines only the DB ID
and the method userCanView(), all other attributes are defined in
the new class IProductSeriesView.

lp/registry/model/productseries.py:

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

lp/registry/tests/test_productseries.py:

Tests for the permissions.

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

test:

./bin/test registry -vvt lp.registry.tests.test_productseries.ProductSeriesSecurityAdaperTestCase

no lint

Update:

I'm running an EC2 test for this branch; several failures are
fixed in r 10250.

The tests lp.registry.tests.test_packaging.TestCreatePackaging.test_createPackaging_refuses_EMBARGOED
and lp.registry.tests.test_packaging.TestCreatePackaging.test_createPackaging_refuses_PROPRIETARY
failed in lp.registry.packaging.PackagingUtil.createPackaging() because
productseries.product is no longer available. To fix these tests, I added
the interfaces IInformationType to IProduct.

The other failures were either Unauthorized execption, or, in browser
tests, "AttributeError: 'thread._local' object has no attribute 'interaction'".

WHen a borwser instance is created, an existing interaction in terminated,
so that thread._local.interaction is no longer available outside a
a browser call -- but the current user is stored as an attribute of
interaction, and the new security tests require a check if this user has
access rights. The most easy fix is to access some required attribute
the broswer object is created.

additional tests:

lp.registry.tests.test_productseries.TestProductSeriesInformationType
lp.code.browser.tests.test_branchlisting.TestProductSeriesTemplate.test_product_series_link
lp.registry.browser.tests.test_sourcepackage_views.TestSourcePackageChangeUpstreamView.test_error_on_proprietary_productseries
lp.registry.tests.test_packaging.TestCreatePackaging.test_createPackaging_refuses_EMBARGOED
lp.registry.tests.test_packaging.TestCreatePackaging.test_createPackaging_refuses_PROPRIETARY
lp.registry.tests.test_sourcepackage.TestSourcePackage.test_refuses_PROPRIETARY

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

Looks alright. Thanks Abel.

review: Approve
Revision history for this message
Deryck Hodge (deryck) wrote :

Thanks for merging my work and using adaptation here. Looks good to me now!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/model/bug.py'
2=== modified file 'lib/lp/code/browser/tests/test_branchlisting.py'
3--- lib/lp/code/browser/tests/test_branchlisting.py 2012-10-08 07:15:07 +0000
4+++ lib/lp/code/browser/tests/test_branchlisting.py 2012-10-19 08:28:25 +0000
5@@ -596,10 +596,11 @@
6 # series on the main site, not the code site.
7 branch = self.factory.makeProductBranch()
8 series = self.factory.makeProductSeries(product=branch.product)
9+ series_name = series.name
10 remove_security_proxy_and_shout_at_engineer(series).branch = branch
11 browser = self.getUserBrowser(
12 canonical_url(branch.product, rootsite='code'))
13- link = browser.getLink(re.compile('^' + series.name + '$'))
14+ link = browser.getLink(re.compile('^' + series_name + '$'))
15 self.assertEqual('launchpad.dev', URI(link.url).host)
16
17
18
19=== modified file 'lib/lp/registry/browser/tests/test_productseries_views.py'
20--- lib/lp/registry/browser/tests/test_productseries_views.py 2012-10-18 16:56:54 +0000
21+++ lib/lp/registry/browser/tests/test_productseries_views.py 2012-10-19 08:28:25 +0000
22@@ -26,6 +26,7 @@
23 from lp.testing.views import create_initialized_view
24
25
26+<<<<<<< TREE
27 class TestProductSeries(BrowserTestCase):
28
29 layer = DatabaseFunctionalLayer
30@@ -73,6 +74,56 @@
31 privacy_portlet_proprietary))
32
33
34+=======
35+class TestProductSeries(BrowserTestCase):
36+
37+ layer = DatabaseFunctionalLayer
38+
39+ def test_information_type_public(self):
40+ # A ProductSeries view should include its information_type,
41+ # which defaults to Public for new projects.
42+ series = self.factory.makeProductSeries()
43+ view = create_initialized_view(series, '+index')
44+ self.assertEqual('Public', view.information_type)
45+
46+ def test_information_type_proprietary(self):
47+ # A ProductSeries view should get its information_type
48+ # from the related product even if the product is changed to
49+ # PROPRIETARY.
50+ owner = self.factory.makePerson()
51+ information_type = InformationType.PROPRIETARY
52+ product = self.factory.makeProduct(
53+ owner=owner, information_type=information_type)
54+ series = self.factory.makeProductSeries(product=product)
55+ with person_logged_in(owner):
56+ view = create_initialized_view(series, '+index')
57+ self.assertEqual('Proprietary', view.information_type)
58+
59+ def test_privacy_portlet(self):
60+ # A ProductSeries page should include a privacy portlet that
61+ # accurately describes the information_type.
62+ owner = self.factory.makePerson()
63+ information_type = InformationType.PROPRIETARY
64+ product = self.factory.makeProduct(
65+ owner=owner, information_type=information_type)
66+ series = self.factory.makeProductSeries(product=product)
67+ privacy_portlet = soupmatchers.Tag(
68+ 'info-type-portlet', 'span',
69+ attrs={'id': 'information-type-summary'})
70+ privacy_portlet_proprietary = soupmatchers.Tag(
71+ 'info-type-text', 'strong', attrs={'id': 'information-type'},
72+ text='Proprietary')
73+ browser = self.getViewBrowser(series, '+index', user=owner)
74+ # First, assert that the portlet exists.
75+ self.assertThat(
76+ browser.contents, soupmatchers.HTMLContains(privacy_portlet))
77+ # Then, assert that the text displayed matches the information_type.
78+ self.assertThat(
79+ browser.contents, soupmatchers.HTMLContains(
80+ privacy_portlet_proprietary))
81+
82+
83+>>>>>>> MERGE-SOURCE
84 class TestProductSeriesHelp(TestCaseWithFactory):
85 layer = DatabaseFunctionalLayer
86
87
88=== modified file 'lib/lp/registry/browser/tests/test_sourcepackage_views.py'
89--- lib/lp/registry/browser/tests/test_sourcepackage_views.py 2012-10-15 15:35:32 +0000
90+++ lib/lp/registry/browser/tests/test_sourcepackage_views.py 2012-10-19 08:28:25 +0000
91@@ -319,6 +319,7 @@
92 product = self.factory.makeProduct(
93 name=product_name, owner=product_owner)
94 series = self.factory.makeProductSeries(product=product)
95+ series_displayname = series.displayname
96 ubuntu_series = self.factory.makeUbuntuDistroSeries()
97 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
98 browser = self.getViewBrowser(
99@@ -327,7 +328,7 @@
100 browser.getControl('Continue').click()
101 with person_logged_in(product_owner):
102 product.information_type = InformationType.PROPRIETARY
103- browser.getControl(series.displayname).selected = True
104+ browser.getControl(series_displayname).selected = True
105 browser.getControl('Change').click()
106 self.assertIn(
107 'Only Public projects can be packaged, not Proprietary.',
108
109=== modified file 'lib/lp/registry/configure.zcml'
110--- lib/lp/registry/configure.zcml 2012-10-17 20:18:20 +0000
111+++ lib/lp/registry/configure.zcml 2012-10-19 08:28:25 +0000
112@@ -1508,10 +1508,16 @@
113 <allow
114 interface="lp.registry.interfaces.productseries.IProductSeriesPublic"/>
115 <require
116+ permission="launchpad.View"
117+ interface="lp.registry.interfaces.productseries.IProductSeriesView"/>
118+ <require
119 permission="launchpad.Edit"
120 interface="lp.registry.interfaces.productseries.IProductSeriesEditRestricted"/>
121- <allow interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
122- <allow
123+ <require
124+ permission="launchpad.View"
125+ interface="lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
126+ <require
127+ permission="launchpad.View"
128 interface="lp.bugs.interfaces.bugtarget.ISeriesBugTarget"/>
129 <require
130 permission="launchpad.Edit"
131@@ -1522,16 +1528,17 @@
132 translations_branch status releasefileglob
133 translations_autoimport_mode"/>
134 <require
135- permission="launchpad.AnyPerson"
136+ permission="launchpad.AnyAllowedPerson"
137 set_attributes="importstatus rcstype cvsroot cvsmodule
138 cvstarfileurl cvsbranch svnrepository"/>
139
140 <!-- IStructuralSubscriptionTarget -->
141
142- <allow
143+ <require
144+ permission="launchpad.View"
145 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetRead" />
146 <require
147- permission="launchpad.AnyPerson"
148+ permission="launchpad.AnyAllowedPerson"
149 interface="lp.bugs.interfaces.structuralsubscription.IStructuralSubscriptionTargetWrite" />
150
151 </class>
152
153=== modified file 'lib/lp/registry/interfaces/productseries.py'
154--- lib/lp/registry/interfaces/productseries.py 2012-10-06 23:40:20 +0000
155+++ lib/lp/registry/interfaces/productseries.py 2012-10-19 08:28:25 +0000
156@@ -12,6 +12,7 @@
157 'IProductSeriesEditRestricted',
158 'IProductSeriesPublic',
159 'IProductSeriesSet',
160+ 'IProductSeriesView',
161 'NoSuchProductSeries',
162 'ITimelineProductSeries',
163 ]
164@@ -129,13 +130,18 @@
165 """Create a new milestone for this ProjectSeries."""
166
167
168-class IProductSeriesPublic(
169+class IProductSeriesPublic(Interface):
170+ """Public IProductSeries properties."""
171+ id = Int(title=_('ID'))
172+
173+ def userCanView(user):
174+ """True if the given user has access to this product."""
175+
176+
177+class IProductSeriesView(
178 ISeriesMixin, IHasAppointedDriver, IHasOwner, IBugTarget,
179 ISpecificationGoal, IHasMilestones, IHasOfficialBugTags, IHasExpirableBugs,
180 IHasTranslationImports, IHasTranslationTemplates, IServiceUsage):
181- """Public IProductSeries properties."""
182- id = Int(title=_('ID'))
183-
184 product = exported(
185 ReferenceChoice(title=_('Project'), required=True,
186 vocabulary='Product', schema=Interface), # really IProduct
187@@ -330,7 +336,7 @@
188
189
190 class IProductSeries(IProductSeriesEditRestricted, IProductSeriesPublic,
191- IStructuralSubscriptionTarget):
192+ IProductSeriesView, IStructuralSubscriptionTarget):
193 """A series of releases. For example '2.0' or '1.3' or 'dev'."""
194 export_as_webservice_entry('project_series')
195
196
197=== modified file 'lib/lp/registry/model/packaging.py'
198--- lib/lp/registry/model/packaging.py 2012-10-12 19:37:30 +0000
199+++ lib/lp/registry/model/packaging.py 2012-10-19 08:28:25 +0000
200@@ -111,6 +111,10 @@
201 (sourcepackagename.name, distroseries.name))
202 # XXX: AaronBentley: 2012-08-12 bug=1066063 Cannot adapt ProductSeries
203 # to IInformationType.
204+ # The line below causes a failure of
205+ # lp.registry.tests.test_distroseries.TestDistroSeriesPackaging.
206+ # test_getPrioritizedPackagings_bug_tracker because
207+ # productseries.product loses all set permissions.
208 # info_type = IInformationType(productseries).information_type
209 info_type = productseries.product.information_type
210 if info_type != InformationType.PUBLIC:
211
212=== modified file 'lib/lp/registry/model/productseries.py'
213--- lib/lp/registry/model/productseries.py 2012-10-11 15:01:49 +0000
214+++ lib/lp/registry/model/productseries.py 2012-10-19 08:28:25 +0000
215@@ -679,6 +679,11 @@
216 return OrderedBugTask(3, bugtask.id, bugtask)
217 return weight_function
218
219+ def userCanView(self, user):
220+ """See `IproductSeriesPublic`."""
221+ # Deleate the permission check to the parent product.
222+ return self.product.userCanView(user)
223+
224
225 class TimelineProductSeries:
226 """See `ITimelineProductSeries`."""
227
228=== modified file 'lib/lp/registry/tests/test_packaging.py'
229--- lib/lp/registry/tests/test_packaging.py 2012-10-11 16:06:49 +0000
230+++ lib/lp/registry/tests/test_packaging.py 2012-10-19 08:28:25 +0000
231@@ -174,25 +174,33 @@
232
233 def test_createPackaging_refuses_PROPRIETARY(self):
234 """Packaging cannot be created for PROPRIETARY productseries"""
235+ owner = self.factory.makePerson()
236 product = self.factory.makeProduct(
237+ owner=owner,
238 information_type=InformationType.PROPRIETARY)
239 series = self.factory.makeProductSeries(product=product)
240- with ExpectedException(CannotPackageProprietaryProduct,
241- 'Only Public project series can be packaged, not Proprietary.'):
242- self.packaging_util.createPackaging(
243- series, self.sourcepackagename, self.distroseries,
244- PackagingType.PRIME, owner=self.owner)
245+ expected_message = (
246+ 'Only Public project series can be packaged, not Proprietary.')
247+ with person_logged_in(owner):
248+ with ExpectedException(CannotPackageProprietaryProduct,
249+ expected_message):
250+ self.packaging_util.createPackaging(
251+ series, self.sourcepackagename, self.distroseries,
252+ PackagingType.PRIME, owner=self.owner)
253
254 def test_createPackaging_refuses_EMBARGOED(self):
255 """Packaging cannot be created for EMBARGOED productseries"""
256+ owner = self.factory.makePerson()
257 product = self.factory.makeProduct(
258+ owner=owner,
259 information_type=InformationType.EMBARGOED)
260 series = self.factory.makeProductSeries(product=product)
261- with ExpectedException(CannotPackageProprietaryProduct,
262- 'Only Public project series can be packaged, not Embargoed.'):
263- self.packaging_util.createPackaging(
264- series, self.sourcepackagename, self.distroseries,
265- PackagingType.PRIME, owner=self.owner)
266+ with person_logged_in(owner):
267+ with ExpectedException(CannotPackageProprietaryProduct,
268+ 'Only Public project series can be packaged, not Embargoed.'):
269+ self.packaging_util.createPackaging(
270+ series, self.sourcepackagename, self.distroseries,
271+ PackagingType.PRIME, owner=self.owner)
272
273
274 class TestPackagingEntryExists(PackagingUtilMixin, TestCaseWithFactory):
275
276=== modified file 'lib/lp/registry/tests/test_productseries.py'
277--- lib/lp/registry/tests/test_productseries.py 2012-10-16 15:12:09 +0000
278+++ lib/lp/registry/tests/test_productseries.py 2012-10-19 08:28:25 +0000
279@@ -5,13 +5,25 @@
280
281 __metaclass__ = type
282
283+from storm.exceptions import NoneError
284 from testtools.testcase import ExpectedException
285 import transaction
286+from zope.security.checker import (
287+ CheckerPublic,
288+ getChecker,
289+ )
290 from zope.component import getUtility
291+from zope.security.interfaces import Unauthorized
292 from zope.security.proxy import removeSecurityProxy
293
294 from lp.app.enums import InformationType
295-from lp.app.interfaces.informationtype import IInformationType
296+<<<<<<< TREE
297+from lp.app.interfaces.informationtype import IInformationType
298+=======
299+from lp.app.interfaces.informationtype import IInformationType
300+from lp.app.interfaces.services import IService
301+from lp.registry.enums import SharingPermission
302+>>>>>>> MERGE-SOURCE
303 from lp.registry.errors import CannotPackageProprietaryProduct
304 from lp.registry.interfaces.distribution import IDistributionSet
305 from lp.registry.interfaces.distroseries import IDistroSeriesSet
306@@ -22,7 +34,10 @@
307 from lp.registry.interfaces.series import SeriesStatus
308 from lp.services.database.lpstorm import IStore
309 from lp.testing import (
310+ ANONYMOUS,
311+ celebrity_logged_in,
312 login,
313+ person_logged_in,
314 TestCaseWithFactory,
315 WebServiceTestCase,
316 )
317@@ -37,6 +52,7 @@
318 )
319
320
321+<<<<<<< TREE
322
323 class TestProductSeries(TestCaseWithFactory):
324 """Tests for ProductSeries."""
325@@ -54,6 +70,25 @@
326 IInformationType(series).information_type, information_type)
327
328
329+=======
330+class TestProductSeries(TestCaseWithFactory):
331+ """Tests for ProductSeries."""
332+
333+ layer = DatabaseFunctionalLayer
334+
335+ def test_information_type_from_product(self):
336+ # ProductSeries should inherit information_type from its product."""
337+ owner = self.factory.makePerson()
338+ information_type = InformationType.PROPRIETARY
339+ product = self.factory.makeProduct(
340+ owner=owner, information_type=information_type)
341+ series = self.factory.makeProductSeries(product=product)
342+ with person_logged_in(owner):
343+ self.assertEqual(
344+ IInformationType(series).information_type, information_type)
345+
346+
347+>>>>>>> MERGE-SOURCE
348 class ProductSeriesReleasesTestCase(TestCaseWithFactory):
349 """Test for ProductSeries.release property."""
350
351@@ -570,3 +605,293 @@
352 mode = TranslationsBranchImportMode.IMPORT_TRANSLATIONS
353 ws_series.translations_autoimport_mode = mode.title
354 ws_series.lp_save()
355+
356+
357+class ProductSeriesSecurityAdaperTestCase(TestCaseWithFactory):
358+ """Test for permissions of IProductSeries."""
359+
360+ layer = DatabaseFunctionalLayer
361+
362+ def setUp(self):
363+ super(ProductSeriesSecurityAdaperTestCase, self).setUp()
364+ self.public_product = self.factory.makeProduct()
365+ self.public_series = self.factory.makeProductSeries(
366+ product=self.public_product)
367+ self.proprietary_product_owner = self.factory.makePerson()
368+ self.proprietary_product = self.factory.makeProduct(
369+ owner=self.proprietary_product_owner,
370+ information_type=InformationType.PROPRIETARY)
371+ self.proprietary_series = self.factory.makeProductSeries(
372+ product=self.proprietary_product)
373+
374+ expected_get_permissions = {
375+ CheckerPublic: set((
376+ 'id', 'userCanView',
377+ )),
378+ 'launchpad.AnyAllowedPerson': set((
379+ 'addBugSubscription', 'addBugSubscriptionFilter',
380+ 'addSubscription', 'removeBugSubscription',
381+ )),
382+ 'launchpad.Edit': set(('newMilestone', )),
383+ 'launchpad.View': set((
384+ '_all_specifications', '_getOfficialTagClause',
385+ '_valid_specifications', 'active', 'all_milestones',
386+ 'answers_usage', 'blueprints_usage', 'branch',
387+ 'bug_reported_acknowledgement', 'bug_reporting_guidelines',
388+ 'bug_subscriptions', 'bug_supervisor', 'bug_tracking_usage',
389+ 'bugtargetdisplayname', 'bugtarget_parent', 'bugtargetname',
390+ 'codehosting_usage', 'createBug', 'datecreated', 'displayname',
391+ 'driver', 'drivers', 'enable_bugfiling_duplicate_search',
392+ 'getAllowedSpecificationInformationTypes',
393+ 'getBugSummaryContextWhereClause',
394+ 'getBugTaskWeightFunction', 'getCachedReleases',
395+ 'getCurrentTemplatesCollection', 'getCurrentTranslationFiles',
396+ 'getCurrentTranslationTemplates',
397+ 'getDefaultSpecificationInformationType',
398+ 'getFirstEntryToImport', 'getLatestRelease', 'getPOTemplate',
399+ 'getPackage', 'getPackagingInDistribution', 'getRelease',
400+ 'getSharingPartner', 'getSpecification', 'getSubscription',
401+ 'getSubscriptions', 'getTemplatesAndLanguageCounts',
402+ 'getTemplatesCollection', 'getTimeline',
403+ 'getTranslationImportQueueEntries',
404+ 'getTranslationTemplateByName', 'getTranslationTemplateFormats',
405+ 'getTranslationTemplates', 'getUbuntuTranslationFocusPackage',
406+ 'getUsedBugTagsWithOpenCounts',
407+ 'has_current_translation_templates', 'has_milestones',
408+ 'has_obsolete_translation_templates',
409+ 'has_sharing_translation_templates', 'has_translation_files',
410+ 'has_translation_templates', 'is_development_focus', 'milestones',
411+ 'name', 'official_bug_tags', 'owner', 'packagings', 'parent',
412+ 'parent_subscription_target',
413+ 'personHasDriverRights', 'pillar', 'potemplate_count', 'product',
414+ 'productID', 'productserieslanguages', 'release_files',
415+ 'releasefileglob', 'releases', 'releaseverstyle', 'searchTasks',
416+ 'series', 'setPackaging', 'sourcepackages', 'specifications',
417+ 'status', 'summary', 'target_type_display', 'title',
418+ 'translations_autoimport_mode', 'userCanAlterBugSubscription',
419+ 'userCanAlterSubscription', 'userHasBugSubscriptions',
420+ 'translations_branch', 'translations_usage', 'uses_launchpad',
421+ )),
422+ }
423+
424+ def test_get_permissions(self):
425+ checker = getChecker(self.public_series)
426+ self.checkPermissions(
427+ self.expected_get_permissions, checker.get_permissions, 'get')
428+
429+ expected_set_permissions = {
430+ 'launchpad.Edit': set((
431+ 'answers_usage', 'blueprints_usage', 'branch',
432+ 'bug_tracking_usage', 'codehosting_usage', 'driver', 'name',
433+ 'owner', 'product', 'releasefileglob', 'status', 'summary',
434+ 'translations_autoimport_mode', 'translations_branch',
435+ 'translations_usage', 'uses_launchpad',
436+ )),
437+ 'launchpad.AnyAllowedPerson': set((
438+ 'cvsbranch', 'cvsmodule', 'cvsroot', 'cvstarfileurl',
439+ 'importstatus', 'rcstype', 'svnrepository',
440+ )),
441+ }
442+
443+ def test_set_permissions(self):
444+ checker = getChecker(self.public_series)
445+ self.checkPermissions(
446+ self.expected_set_permissions, checker.set_permissions, 'set')
447+
448+ def assertAccessAuthorized(self, attribute_names, obj):
449+ # Try to access the given attributes of obj. No exception
450+ # should be raised.
451+ for name in attribute_names:
452+ getattr(obj, name)
453+
454+ def assertAccessUnauthorized(self, attribute_names, obj):
455+ # Try to access the given attributes of obj. Unauthorized
456+ # should be raised.
457+ for name in attribute_names:
458+ self.assertRaises(Unauthorized, getattr, obj, name)
459+
460+ def assertChangeAuthorized(self, attribute_names, obj):
461+ # Try to changes the given attributes of obj. Unauthorized
462+ # should not be raised.
463+ for name in attribute_names:
464+ # Not all attributes declared in configure.zcml to be
465+ # settable actually exist or are settable. Attempts to set
466+ # them raise an AttributeError. Similary, the naive attempt
467+ # to set an attribute to None may raise a NoneError
468+ #
469+ # Both errors can be ignored here: This method intends only
470+ # to prove that Unauthorized is not raised.
471+ try:
472+ setattr(obj, name, None)
473+ except (AttributeError, NoneError):
474+ pass
475+
476+ def assertChangeUnauthorized(self, attribute_names, obj):
477+ # Try to changes the given attributes of obj. Unauthorized
478+ # should be raised.
479+ for name in attribute_names:
480+ self.assertRaises(Unauthorized, setattr, obj, name, None)
481+
482+ def test_access_for_anonymous(self):
483+ # Anonymous users have access to public attributes of
484+ # a series for a private or public product.
485+ with person_logged_in(ANONYMOUS):
486+ self.assertAccessAuthorized(
487+ self.expected_get_permissions[CheckerPublic],
488+ self.public_series)
489+ self.assertAccessAuthorized(
490+ self.expected_get_permissions[CheckerPublic],
491+ self.proprietary_series)
492+
493+ # They have access to attributes requiring the permission
494+ # launchpad.View of a series for a public product...
495+ self.assertAccessAuthorized(
496+ self.expected_get_permissions['launchpad.View'],
497+ self.public_series)
498+
499+ # ...but not to the same attributes of a series for s private
500+ # product.
501+ self.assertAccessUnauthorized(
502+ self.expected_get_permissions['launchpad.View'],
503+ self.proprietary_series)
504+
505+ # The cannot access other attributes of a series for
506+ # public and private products.
507+ for permission, names in self.expected_get_permissions.items():
508+ if permission in (CheckerPublic, 'launchpad.View'):
509+ continue
510+ self.assertAccessUnauthorized(names, self.public_series)
511+ self.assertAccessUnauthorized(names, self.proprietary_series)
512+
513+ # They cannot change any attributes.
514+ for permission, names in self.expected_set_permissions.items():
515+ self.assertChangeUnauthorized(names, self.public_series)
516+ self.assertChangeUnauthorized(names, self.proprietary_series)
517+
518+ def test_access_for_regular_user(self):
519+ # Regular users have access to public attributes of
520+ # a series for a private or public product.
521+ user = self.factory.makePerson()
522+ with person_logged_in(user):
523+ self.assertAccessAuthorized(
524+ self.expected_get_permissions[CheckerPublic],
525+ self.public_series)
526+ self.assertAccessAuthorized(
527+ self.expected_get_permissions[CheckerPublic],
528+ self.proprietary_series)
529+
530+ # They have access to attributes requiring the permissions
531+ # launchpad.View and launchpadAnyAllowedPerson of a series
532+ # for a public product...
533+ self.assertAccessAuthorized(
534+ self.expected_get_permissions['launchpad.View'],
535+ self.public_series)
536+ self.assertAccessAuthorized(
537+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
538+ self.public_series)
539+
540+ # ...but not to the same attributes of a series for s private
541+ # product.
542+ self.assertAccessUnauthorized(
543+ self.expected_get_permissions['launchpad.View'],
544+ self.proprietary_series)
545+ self.assertAccessUnauthorized(
546+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
547+ self.proprietary_series)
548+
549+ # The cannot access other attributes of a series for
550+ # public and private products.
551+ for permission, names in self.expected_get_permissions.items():
552+ if permission in (CheckerPublic, 'launchpad.View',
553+ 'launchpad.AnyAllowedPerson'):
554+ continue
555+ self.assertAccessUnauthorized(names, self.public_series)
556+ self.assertAccessUnauthorized(names, self.proprietary_series)
557+
558+ # They can change attributes requiring the permission
559+ # launchpad.AnyAllowedPerson of a series for a public project...
560+ self.assertChangeAuthorized(
561+ self.expected_set_permissions['launchpad.AnyAllowedPerson'],
562+ self.public_series)
563+ #... but not for a private project.
564+ self.assertChangeUnauthorized(
565+ self.expected_set_permissions['launchpad.AnyAllowedPerson'],
566+ self.proprietary_series)
567+
568+ # They cannot change any attributes that require another
569+ # permission than launchpad.AnyALlowedPerson.
570+ for permission, names in self.expected_set_permissions.items():
571+ if permission == 'launchpad.AnyAllowedPerson':
572+ continue
573+ self.assertChangeUnauthorized(names, self.public_series)
574+ self.assertChangeUnauthorized(names, self.proprietary_series)
575+
576+ def test_access_for_user_with_policy_grant(self):
577+ # Users with a policy grant for the parent product can access
578+ # properties requring the permission lanchpad.View or
579+ # launchpad.ANyALlowedPerson of a series.
580+ user = self.factory.makePerson()
581+ with person_logged_in(self.proprietary_product_owner):
582+ getUtility(IService, 'sharing').sharePillarInformation(
583+ self.proprietary_product, user, self.proprietary_product_owner,
584+ {InformationType.PROPRIETARY: SharingPermission.ALL})
585+ with person_logged_in(user):
586+ self.assertAccessAuthorized(
587+ self.expected_get_permissions['launchpad.View'],
588+ self.proprietary_series)
589+ self.assertAccessAuthorized(
590+ self.expected_get_permissions['launchpad.AnyAllowedPerson'],
591+ self.proprietary_series)
592+
593+ # The cannot access other attributes of a series for
594+ # private products.
595+ for permission, names in self.expected_get_permissions.items():
596+ if permission in (CheckerPublic, 'launchpad.View',
597+ 'launchpad.AnyAllowedPerson'):
598+ continue
599+ self.assertAccessUnauthorized(names, self.proprietary_series)
600+
601+ # They can change attributes requiring the permission
602+ # launchpad.AnyAllowedPerson of a series for a provate project...
603+ self.assertChangeAuthorized(
604+ self.expected_set_permissions['launchpad.AnyAllowedPerson'],
605+ self.proprietary_series)
606+
607+ # They cannot change any attributes that require another
608+ # permission than launchpad.AnyALlowedPerson.
609+ for permission, names in self.expected_set_permissions.items():
610+ if permission == 'launchpad.AnyAllowedPerson':
611+ continue
612+ self.assertChangeUnauthorized(names, self.proprietary_series)
613+
614+ def test_access_for_product_owner(self):
615+ # The owner of a project has access to all attributes of
616+ # a product series.
617+ with person_logged_in(self.proprietary_product_owner):
618+ for permission, names in self.expected_get_permissions.items():
619+ self.assertAccessAuthorized(names, self.proprietary_series)
620+
621+ # They can change all attributes.
622+ for permission, names in self.expected_set_permissions.items():
623+ self.assertChangeAuthorized(names, self.proprietary_series)
624+
625+ with person_logged_in(self.public_product.owner):
626+ for permission, names in self.expected_get_permissions.items():
627+ self.assertAccessAuthorized(names, self.public_series)
628+
629+ # They can change all attributes.
630+ for permission, names in self.expected_set_permissions.items():
631+ self.assertChangeAuthorized(names, self.public_series)
632+
633+ def test_access_for_lp_admins(self):
634+ # Launchpad admins can access and change any attribute of a series
635+ # of public and private product.
636+ with celebrity_logged_in('admin'):
637+ for permission, names in self.expected_get_permissions.items():
638+ self.assertAccessAuthorized(names, self.public_series)
639+ self.assertAccessAuthorized(names, self.proprietary_series)
640+
641+ # They can change all attributes.
642+ for permission, names in self.expected_set_permissions.items():
643+ self.assertChangeAuthorized(names, self.public_series)
644+ self.assertChangeAuthorized(names, self.proprietary_series)
645
646=== modified file 'lib/lp/registry/tests/test_sourcepackage.py'
647--- lib/lp/registry/tests/test_sourcepackage.py 2012-10-11 18:32:36 +0000
648+++ lib/lp/registry/tests/test_sourcepackage.py 2012-10-19 08:28:25 +0000
649@@ -327,14 +327,19 @@
650
651 def test_refuses_PROPRIETARY(self):
652 """Packaging cannot be created for PROPRIETARY productseries"""
653+ owner = self.factory.makePerson()
654 product = self.factory.makeProduct(
655+ owner=owner,
656 information_type=InformationType.PROPRIETARY)
657 series = self.factory.makeProductSeries(product=product)
658 ubuntu_series = self.factory.makeUbuntuDistroSeries()
659 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
660- with ExpectedException(CannotPackageProprietaryProduct,
661- 'Only Public project series can be packaged, not Proprietary.'):
662- sp.setPackaging(series, series.owner)
663+ with person_logged_in(owner):
664+ with ExpectedException(
665+ CannotPackageProprietaryProduct,
666+ 'Only Public project series can be packaged, not '
667+ 'Proprietary.'):
668+ sp.setPackaging(series, owner)
669
670 def test_setPackagingReturnSharingDetailPermissions__ordinary_user(self):
671 """An ordinary user can create a packaging link but he cannot
672
673=== modified file 'lib/lp/security.py'
674--- lib/lp/security.py 2012-10-18 14:43:24 +0000
675+++ lib/lp/security.py 2012-10-19 08:28:25 +0000
676@@ -154,6 +154,7 @@
677 )
678 from lp.registry.interfaces.productseries import (
679 IProductSeries,
680+ IProductSeriesView,
681 ITimelineProductSeries,
682 )
683 from lp.registry.interfaces.projectgroup import (
684@@ -1285,9 +1286,26 @@
685 or False)
686
687
688-class ViewProductSeries(AnonymousAuthorization):
689-
690- usedfor = IProductSeries
691+class ViewProductSeries(AuthorizationBase):
692+ permission = 'launchpad.View'
693+ usedfor = IProductSeriesView
694+
695+ def checkAuthenticated(self, user):
696+ return self.obj.userCanView(user)
697+
698+ def checkUnauthenticated(self):
699+ return self.obj.userCanView(None)
700+
701+
702+class ChangeProductSeries(ViewProductSeries):
703+ permission = 'launchpad.AnyAllowedPerson'
704+ usedfor = IProductSeriesView
705+
706+ def checkAuthenticated(self, user):
707+ return self.obj.userCanView(user)
708+
709+ def checkUnauthenticated(self):
710+ return False
711
712
713 class EditProductSeries(EditByOwnersOrAdmins):
714
715=== modified file 'lib/lp/services/features/flags.py'
716=== modified file 'lib/lp/translations/stories/webservice/xx-potemplate.txt'
717--- lib/lp/translations/stories/webservice/xx-potemplate.txt 2012-10-09 10:28:02 +0000
718+++ lib/lp/translations/stories/webservice/xx-potemplate.txt 2012-10-19 08:28:25 +0000
719@@ -71,6 +71,7 @@
720
721 >>> login('admin@canonical.com')
722 >>> productseries = factory.makeProductSeries()
723+ >>> productseries_name = productseries.name
724 >>> product_name = productseries.product.name
725 >>> potemplate_1 = factory.makePOTemplate(productseries=productseries)
726 >>> potemplate_2 = factory.makePOTemplate(productseries=productseries)
727@@ -79,7 +80,7 @@
728 >>> all_translation_templates = anon_webservice.named_get(
729 ... '/%s/%s' % (
730 ... product_name,
731- ... productseries.name),
732+ ... productseries_name),
733 ... 'getTranslationTemplates'
734 ... ).jsonBody()
735 >>> api_count = all_translation_templates['total_size']