Merge ~andrey-fedoseev/launchpad:packaging-security into launchpad:master

Proposed by Andrey Fedoseev
Status: Merged
Approved by: Andrey Fedoseev
Approved revision: 39957cba69557efc4a5fc74b07e152d893350dc6
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~andrey-fedoseev/launchpad:packaging-security
Merge into: launchpad:master
Diff against target: 1676 lines (+415/-392)
26 files modified
lib/lp/registry/browser/configure.zcml (+2/-2)
lib/lp/registry/browser/productseries.py (+1/-1)
lib/lp/registry/browser/sourcepackage.py (+23/-15)
lib/lp/registry/browser/tests/packaging-views.rst (+2/-2)
lib/lp/registry/browser/tests/sourcepackage-views.rst (+16/-11)
lib/lp/registry/browser/tests/test_packaging.py (+44/-20)
lib/lp/registry/browser/tests/test_product.py (+3/-1)
lib/lp/registry/browser/tests/test_sourcepackage_views.py (+97/-60)
lib/lp/registry/configure.zcml (+4/-2)
lib/lp/registry/interfaces/packaging.py (+3/-8)
lib/lp/registry/interfaces/productseries.py (+5/-5)
lib/lp/registry/interfaces/sourcepackage.py (+15/-26)
lib/lp/registry/model/packaging.py (+18/-39)
lib/lp/registry/model/productseries.py (+4/-4)
lib/lp/registry/model/sourcepackage.py (+5/-10)
lib/lp/registry/security.py (+7/-1)
lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.rst (+3/-3)
lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.rst (+8/-8)
lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst (+47/-74)
lib/lp/registry/stories/product/xx-product-package-pages.rst (+5/-3)
lib/lp/registry/tests/test_distroseries.py (+1/-1)
lib/lp/registry/tests/test_packaging.py (+62/-23)
lib/lp/registry/tests/test_productseries.py (+1/-2)
lib/lp/registry/tests/test_sourcepackage.py (+34/-69)
lib/lp/soyuz/tests/test_publishing.py (+3/-1)
lib/lp/testing/factory.py (+2/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+429758@code.launchpad.net

Commit message

New security model for `Packaging`

Description of the change

Only the product owners, package maintainers, admins and registry experts are now allowed to create, edit or remove package links

- {IProductSeries,ISourcePackage}.setPackaging is now protected with `launchpad.Edit` permission

- `launchpad.Edit` on `IPackaging`: True if user has `launchpad.Edit` on either corresponding `IProductSeries` or `ISourcePackage`

- /+ubuntupkg on `IProductSeries` is now protected with `launchpad.Edit`

- /+edit-packaging on `ISourcePackage` is now protected with `launchpad.Edit`

- /+remove-packaging on `ISourcePackage` is protected with `launchpad.Edit` applied to `sourcepackage.direct_packaging`; this allows product owners to unlink their products

- all tests are updated accordingly

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Andrey Fedoseev (andrey-fedoseev) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
2index 6f041d4..b90507b 100644
3--- a/lib/lp/registry/browser/configure.zcml
4+++ b/lib/lp/registry/browser/configure.zcml
5@@ -2022,7 +2022,7 @@
6 name="+ubuntupkg"
7 for="lp.registry.interfaces.productseries.IProductSeries"
8 class="lp.registry.browser.productseries.ProductSeriesUbuntuPackagingView"
9- permission="launchpad.AnyPerson"
10+ permission="launchpad.Edit"
11 template="../templates/productseries-ubuntupkg.pt"
12 />
13 <browser:pages
14@@ -2621,7 +2621,7 @@
15 name="+edit-packaging"
16 for="lp.registry.interfaces.sourcepackage.ISourcePackage"
17 class="lp.registry.browser.sourcepackage.SourcePackageChangeUpstreamView"
18- permission="launchpad.AnyPerson"
19+ permission="launchpad.Edit"
20 template="../templates/sourcepackage-edit-packaging.pt"
21 />
22 <browser:page
23diff --git a/lib/lp/registry/browser/productseries.py b/lib/lp/registry/browser/productseries.py
24index 8759e95..9b05317 100644
25--- a/lib/lp/registry/browser/productseries.py
26+++ b/lib/lp/registry/browser/productseries.py
27@@ -278,7 +278,7 @@ class ProductSeriesOverviewMenu(
28 summary = "Change the branch for this series"
29 return Link("+setbranch", text, summary, icon=icon)
30
31- @enabled_with_permission("launchpad.AnyPerson")
32+ @enabled_with_permission("launchpad.Edit")
33 def ubuntupkg(self):
34 """Return a link to link this series to an ubuntu sourcepackage."""
35 text = "Link to Ubuntu package"
36diff --git a/lib/lp/registry/browser/sourcepackage.py b/lib/lp/registry/browser/sourcepackage.py
37index 3230cf4..461a963 100644
38--- a/lib/lp/registry/browser/sourcepackage.py
39+++ b/lib/lp/registry/browser/sourcepackage.py
40@@ -34,6 +34,7 @@ from zope.schema.vocabulary import (
41 SimpleVocabulary,
42 getVocabularyRegistry,
43 )
44+from zope.security.interfaces import Unauthorized
45
46 from lp import _
47 from lp.app.browser.launchpadform import (
48@@ -61,9 +62,11 @@ from lp.services.webapp import (
49 canonical_url,
50 stepto,
51 )
52+from lp.services.webapp.authorization import check_permission
53 from lp.services.webapp.breadcrumb import Breadcrumb
54 from lp.services.webapp.escaping import structured
55 from lp.services.webapp.interfaces import IBreadcrumb
56+from lp.services.webapp.menu import enabled_with_permission
57 from lp.services.webapp.publisher import LaunchpadView
58 from lp.services.worlddata.helpers import browser_languages
59 from lp.services.worlddata.interfaces.country import ICountry
60@@ -214,13 +217,9 @@ class SourcePackageOverviewMenu(ApplicationMenu):
61 def copyright(self):
62 return Link("+copyright", "View copyright", icon="info")
63
64+ @enabled_with_permission("launchpad.Edit")
65 def edit_packaging(self):
66- return Link(
67- "+edit-packaging",
68- "Change upstream link",
69- icon="edit",
70- enabled=self.userCanDeletePackaging(),
71- )
72+ return Link("+edit-packaging", "Change upstream link", icon="edit")
73
74 def remove_packaging(self):
75 return Link(
76@@ -230,13 +229,9 @@ class SourcePackageOverviewMenu(ApplicationMenu):
77 enabled=self.userCanDeletePackaging(),
78 )
79
80+ @enabled_with_permission("launchpad.Edit")
81 def set_upstream(self):
82- return Link(
83- "+edit-packaging",
84- "Set upstream link",
85- icon="add",
86- enabled=self.userCanDeletePackaging(),
87- )
88+ return Link("+edit-packaging", "Set upstream link", icon="add")
89
90 def builds(self):
91 text = "Show builds"
92@@ -245,8 +240,8 @@ class SourcePackageOverviewMenu(ApplicationMenu):
93 def userCanDeletePackaging(self):
94 packaging = self.context.direct_packaging
95 if packaging is None:
96- return True
97- return packaging.userCanDelete()
98+ return False
99+ return check_permission("launchpad.Edit", packaging)
100
101
102 class SourcePackageChangeUpstreamStepOne(ReturnToReferrerMixin, StepView):
103@@ -425,10 +420,23 @@ class SourcePackageRemoveUpstreamView(
104 label = "Unlink an upstream project"
105 page_title = label
106
107+ def initialize(self):
108+ self.packaging = self.context.direct_packaging
109+ if self.packaging is not None:
110+ # We do permission check here rather than protecting the view
111+ # in ZCML because product owners should also be allowed to unlink
112+ # projects, even if they don't have edit permission on the source
113+ # package.
114+ if not check_permission("launchpad.Edit", self.packaging):
115+ raise Unauthorized(
116+ "You are not allowed to unlink an upstream project"
117+ )
118+ super().initialize()
119+
120 @action("Unlink")
121 def unlink(self, action, data):
122 old_series = self.context.productseries
123- if self.context.direct_packaging is not None:
124+ if self.packaging is not None:
125 getUtility(IPackagingUtil).deletePackaging(
126 self.context.productseries,
127 self.context.sourcepackagename,
128diff --git a/lib/lp/registry/browser/tests/packaging-views.rst b/lib/lp/registry/browser/tests/packaging-views.rst
129index 3610369..260ec7f 100644
130--- a/lib/lp/registry/browser/tests/packaging-views.rst
131+++ b/lib/lp/registry/browser/tests/packaging-views.rst
132@@ -30,6 +30,7 @@ The view has a label and requires a distro series and a source package name.
133 The distroseries field's vocabulary is the same as the ubuntu.series
134 attribute.
135
136+ >>> _ = login_person(product.owner)
137 >>> view = create_view(productseries, "+ubuntupkg")
138 >>> print(view.label)
139 Ubuntu source packaging
140@@ -200,8 +201,7 @@ and a new entry can be added to the packaging history.
141 ... )
142 >>> grumpy_series.status = SeriesStatus.FROZEN
143
144- >>> a_user = factory.makePerson(name="hedgehog")
145- >>> ignored = login_person(a_user)
146+ >>> _ = login_person(product.owner)
147 >>> form = {
148 ... "field.sourcepackagename": "hot",
149 ... "field.actions.continue": "Update",
150diff --git a/lib/lp/registry/browser/tests/sourcepackage-views.rst b/lib/lp/registry/browser/tests/sourcepackage-views.rst
151index b759ece..32a971a 100644
152--- a/lib/lp/registry/browser/tests/sourcepackage-views.rst
153+++ b/lib/lp/registry/browser/tests/sourcepackage-views.rst
154@@ -8,8 +8,9 @@ Edit packaging view
155 >>> productseries = factory.makeProductSeries(
156 ... name="crazy", product=product
157 ... )
158+ >>> distro_owner = factory.makePerson()
159 >>> distribution = factory.makeDistribution(
160- ... name="youbuntu", displayname="Youbuntu"
161+ ... name="youbuntu", displayname="Youbuntu", owner=distro_owner
162 ... )
163 >>> distroseries = factory.makeDistroSeries(
164 ... name="busy", distribution=distribution
165@@ -53,7 +54,7 @@ This is a multistep view. In the first step, the product is specified.
166 >>> print(view.view.request.form)
167 {'field.__visited_steps__': 'sourcepackage_change_upstream_step1'}
168
169- >>> ignored = login_person(product.owner)
170+ >>> _ = login_person(distro_owner)
171 >>> form = {
172 ... "field.product": "bonkers",
173 ... "field.actions.continue": "Continue",
174@@ -63,7 +64,7 @@ This is a multistep view. In the first step, the product is specified.
175 ... package,
176 ... name="+edit-packaging",
177 ... form=form,
178- ... principal=product.owner,
179+ ... principal=distro_owner,
180 ... )
181 >>> view.view.errors
182 []
183@@ -88,7 +89,7 @@ product can be chosen from a list of options.
184 ... package,
185 ... name="+edit-packaging",
186 ... form=form,
187- ... principal=product.owner,
188+ ... principal=distro_owner,
189 ... )
190
191 >>> ignored = view.view.render()
192@@ -124,7 +125,7 @@ then the current product series will be the selected option.
193 ... package,
194 ... name="+edit-packaging",
195 ... form=form,
196- ... principal=product.owner,
197+ ... principal=distro_owner,
198 ... )
199 >>> print(view.view.widgets.get("productseries")._getFormValue().name)
200 crazy
201@@ -141,7 +142,7 @@ empty.
202 ... package,
203 ... name="+edit-packaging",
204 ... form=form,
205- ... principal=product.owner,
206+ ... principal=distro_owner,
207 ... )
208 >>> for error in view.view.errors:
209 ... print(pretty(error.args))
210@@ -161,7 +162,7 @@ but there is no notification message that the upstream link was updated.
211 ... package,
212 ... name="+edit-packaging",
213 ... form=form,
214- ... principal=product.owner,
215+ ... principal=distro_owner,
216 ... )
217 >>> print(view.view)
218 <...SourcePackageChangeUpstreamStepTwo object...>
219@@ -382,11 +383,12 @@ to the project series.
220 >>> print(view.cancel_url)
221 http://launchpad.test/youbuntu/wonky/+source/stinkypackage
222
223- >>> user = package.packaging.owner
224- >>> ignored = login_person(user)
225 >>> form = {"field.actions.unlink": "Unlink"}
226 >>> view = create_initialized_view(
227- ... package, name="+remove-packaging", form=form, principal=user
228+ ... package,
229+ ... name="+remove-packaging",
230+ ... form=form,
231+ ... principal=distro_owner,
232 ... )
233 >>> view.errors
234 []
235@@ -401,7 +403,10 @@ they get a message telling them that the link has already been
236 deleted.
237
238 >>> view = create_initialized_view(
239- ... package, name="+remove-packaging", form=form, principal=user
240+ ... package,
241+ ... name="+remove-packaging",
242+ ... form=form,
243+ ... principal=distro_owner,
244 ... )
245 >>> view.errors
246 []
247diff --git a/lib/lp/registry/browser/tests/test_packaging.py b/lib/lp/registry/browser/tests/test_packaging.py
248index 7549a51..d45577a 100644
249--- a/lib/lp/registry/browser/tests/test_packaging.py
250+++ b/lib/lp/registry/browser/tests/test_packaging.py
251@@ -12,7 +12,7 @@ from lp.registry.interfaces.packaging import IPackagingUtil, PackagingType
252 from lp.registry.interfaces.product import IProductSet
253 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
254 from lp.services.features.testing import FeatureFixture
255-from lp.testing import TestCaseWithFactory, login, logout
256+from lp.testing import TestCaseWithFactory, login, logout, person_logged_in
257 from lp.testing.layers import DatabaseFunctionalLayer
258 from lp.testing.pages import setupBrowser
259 from lp.testing.views import create_initialized_view
260@@ -49,7 +49,7 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
261 )
262 self.packaging_util = getUtility(IPackagingUtil)
263
264- def test_no_error_when_trying_to_readd_same_package(self):
265+ def test_no_error_when_trying_to_re_add_same_package(self):
266 # There is no reason to display an error when the user's action
267 # wouldn't cause a state change.
268 self.packaging_util.createPackaging(
269@@ -65,9 +65,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
270 "field.sourcepackagename": self.sourcepackagename.name,
271 "field.actions.continue": "Continue",
272 }
273- view = create_initialized_view(
274- self.productseries, "+ubuntupkg", form=form
275- )
276+ with person_logged_in(self.product.owner):
277+ view = create_initialized_view(
278+ self.productseries,
279+ "+ubuntupkg",
280+ form=form,
281+ principal=self.product.owner,
282+ )
283 self.assertEqual([], view.errors)
284
285 def test_cannot_link_to_linked_package(self):
286@@ -78,9 +82,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
287 "field.sourcepackagename": "hot",
288 "field.actions.continue": "Continue",
289 }
290- view = create_initialized_view(
291- self.productseries, "+ubuntupkg", form=form
292- )
293+ with person_logged_in(self.product.owner):
294+ view = create_initialized_view(
295+ self.productseries,
296+ "+ubuntupkg",
297+ form=form,
298+ principal=self.product.owner,
299+ )
300 self.assertEqual([], view.errors)
301 other_productseries = self.factory.makeProductSeries(
302 product=self.product, name="hottest"
303@@ -90,9 +98,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
304 "field.sourcepackagename": "hot",
305 "field.actions.continue": "Continue",
306 }
307- view = create_initialized_view(
308- other_productseries, "+ubuntupkg", form=form
309- )
310+ with person_logged_in(self.product.owner):
311+ view = create_initialized_view(
312+ other_productseries,
313+ "+ubuntupkg",
314+ form=form,
315+ principal=self.product.owner,
316+ )
317 view_errors = [
318 'The <a href="http://launchpad.test/ubuntu/hoary/+source/hot">'
319 "hot</a> package in Hoary is already linked to another series."
320@@ -106,9 +118,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
321 "field.sourcepackagename": "",
322 "field.actions.continue": "Continue",
323 }
324- view = create_initialized_view(
325- self.productseries, "+ubuntupkg", form=form
326- )
327+ with person_logged_in(self.product.owner):
328+ view = create_initialized_view(
329+ self.productseries,
330+ "+ubuntupkg",
331+ form=form,
332+ principal=self.product.owner,
333+ )
334 self.assertEqual(1, len(view.errors))
335 self.assertEqual("sourcepackagename", view.errors[0].field_name)
336 self.assertEqual("Required input is missing.", view.errors[0].doc())
337@@ -125,9 +141,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
338 "field.sourcepackagename": "vapor",
339 "field.actions.continue": "Continue",
340 }
341- view = create_initialized_view(
342- self.productseries, "+ubuntupkg", form=form
343- )
344+ with person_logged_in(self.product.owner):
345+ view = create_initialized_view(
346+ self.productseries,
347+ "+ubuntupkg",
348+ form=form,
349+ principal=self.product.owner,
350+ )
351 view_errors = ["The source package is not published in Hoary."]
352 self.assertEqual(view_errors, view.errors)
353
354@@ -142,9 +162,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
355 "field.sourcepackagename": "hot",
356 "field.actions.continue": "Continue",
357 }
358- view = create_initialized_view(
359- self.productseries, "+ubuntupkg", form=form
360- )
361+ with person_logged_in(self.product.owner):
362+ view = create_initialized_view(
363+ self.productseries,
364+ "+ubuntupkg",
365+ form=form,
366+ principal=self.product.owner,
367+ )
368 self.assertEqual([], view.errors)
369 has_packaging = self.packaging_util.packagingEntryExists(
370 self.sourcepackagename, warty, self.productseries
371diff --git a/lib/lp/registry/browser/tests/test_product.py b/lib/lp/registry/browser/tests/test_product.py
372index de8f154..61618f0 100644
373--- a/lib/lp/registry/browser/tests/test_product.py
374+++ b/lib/lp/registry/browser/tests/test_product.py
375@@ -862,7 +862,9 @@ class TestProductEditView(BrowserTestCase):
376 # It should be an error to make a Product private if it is packaged.
377 product = self.factory.makeProduct()
378 sourcepackage = self.factory.makeSourcePackage()
379- sourcepackage.setPackaging(product.development_focus, product.owner)
380+ removeSecurityProxy(sourcepackage).setPackaging(
381+ product.development_focus, product.owner
382+ )
383 browser = self.getViewBrowser(product, "+edit", user=product.owner)
384 info_type = browser.getControl(name="field.information_type")
385 info_type.value = ["PROPRIETARY"]
386diff --git a/lib/lp/registry/browser/tests/test_sourcepackage_views.py b/lib/lp/registry/browser/tests/test_sourcepackage_views.py
387index 291c28a..8044c1a 100644
388--- a/lib/lp/registry/browser/tests/test_sourcepackage_views.py
389+++ b/lib/lp/registry/browser/tests/test_sourcepackage_views.py
390@@ -244,7 +244,7 @@ class TestSourcePackageUpstreamConnectionsView(TestCaseWithFactory):
391 distroseries=distroseries,
392 version="1.5-0ubuntu1",
393 )
394- self.source_package.setPackaging(
395+ removeSecurityProxy(self.source_package).setPackaging(
396 productseries, productseries.product.owner
397 )
398
399@@ -301,80 +301,119 @@ class TestSourcePackagePackagingLinks(TestCaseWithFactory):
400
401 layer = DatabaseFunctionalLayer
402
403- def makeSourcePackageOverviewMenu(self, with_packaging, karma=None):
404- sourcepackage = self.factory.makeSourcePackage()
405- registrant = self.factory.makePerson()
406- if with_packaging:
407- self.factory.makePackagingLink(
408- sourcepackagename=sourcepackage.sourcepackagename,
409- distroseries=sourcepackage.distroseries,
410- owner=registrant,
411- )
412- user = self.factory.makePerson(karma=karma)
413+ def setUp(self, *args, **kwargs):
414+ super().setUp(*args, **kwargs)
415+ self.sourcepackage = self.factory.makeSourcePackage()
416+ self.maintainer = self.sourcepackage.distribution.owner
417+ self.product_owner = self.factory.makePerson()
418+ self.product = self.factory.makeProduct(owner=self.product_owner)
419+ self.productseries = self.factory.makeProductSeries(self.product)
420+
421+ def makePackaging(self):
422+ self.factory.makePackagingLink(
423+ sourcepackagename=self.sourcepackage.sourcepackagename,
424+ distroseries=self.sourcepackage.distroseries,
425+ productseries=self.productseries,
426+ )
427+
428+ def makeSourcePackageOverviewMenu(self, user):
429 with person_logged_in(user):
430- menu = SourcePackageOverviewMenu(sourcepackage)
431- return menu, user
432+ menu = SourcePackageOverviewMenu(self.sourcepackage)
433+ return menu
434
435- def test_edit_packaging_link__enabled_without_packaging(self):
436- # If no packging exists, the edit_packaging link is always
437+ def test_edit_packaging_link__enabled_without_packaging_maintainer(self):
438+ # If no packaging exists, the edit_packaging link is always
439 # enabled.
440- menu, user = self.makeSourcePackageOverviewMenu(False, None)
441- with person_logged_in(user):
442+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
443+ with person_logged_in(self.maintainer):
444 self.assertTrue(menu.edit_packaging().enabled)
445
446- def test_set_upstrem_link__enabled_without_packaging(self):
447- # If no packging exists, the set_upstream link is always
448+ def test_set_upstream_link__enabled_without_packaging_maintainer(self):
449+ # If no packaging exists, the set_upstream link is always
450 # enabled.
451- menu, user = self.makeSourcePackageOverviewMenu(False, None)
452- with person_logged_in(user):
453+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
454+ with person_logged_in(self.maintainer):
455 self.assertTrue(menu.set_upstream().enabled)
456
457- def test_remove_packaging_link__enabled_without_packaging(self):
458- # If no packging exists, the remove_packaging link is always
459- # enabled.
460- menu, user = self.makeSourcePackageOverviewMenu(False, None)
461- with person_logged_in(user):
462- self.assertTrue(menu.remove_packaging().enabled)
463+ def test_remove_packaging_link__enabled_without_packaging_maintainer(self):
464+ # If no packaging exists, the remove_packaging link is always
465+ # disabled.
466+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
467+ with person_logged_in(self.maintainer):
468+ self.assertFalse(menu.remove_packaging().enabled)
469
470- def test_edit_packaging_link__enabled_with_packaging_non_probation(self):
471- # If a packging exists, the edit_packaging link is enabled
472- # for the non-probationary users.
473- menu, user = self.makeSourcePackageOverviewMenu(True, 100)
474- with person_logged_in(user):
475+ def test_edit_packaging_link__enabled_with_packaging_maintainer(self):
476+ # If a packaging exists, the edit_packaging link is enabled
477+ # for the package maintainer.
478+ self.makePackaging()
479+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
480+ with person_logged_in(self.maintainer):
481 self.assertTrue(menu.edit_packaging().enabled)
482
483- def test_set_upstrem_link__enabled_with_packaging_non_probation(self):
484- # If a packging exists, the set_upstream link is enabled
485- # for the non-probationary users.
486- menu, user = self.makeSourcePackageOverviewMenu(True, 100)
487- with person_logged_in(user):
488+ def test_set_upstream_link__enabled_with_packaging_maintainer(self):
489+ # If a packaging exists, the set_upstream link is enabled
490+ # for the package maintainer.
491+ self.makePackaging()
492+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
493+ with person_logged_in(self.maintainer):
494 self.assertTrue(menu.set_upstream().enabled)
495
496- def test_remove_packaging_link__enabled_with_packaging_non_probation(self):
497- # If a packging exists, the remove_packaging link is enabled
498- # for the non-probationary users.
499- menu, user = self.makeSourcePackageOverviewMenu(True, 100)
500- with person_logged_in(user):
501+ def test_remove_packaging_link__enabled_with_packaging_maintainer(self):
502+ # If a packaging exists, the remove_packaging link is enabled
503+ # for the package maintainer.
504+ self.makePackaging()
505+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
506+ with person_logged_in(self.maintainer):
507+ self.assertTrue(menu.remove_packaging().enabled)
508+
509+ def test_edit_packaging_link__disabled_for_product_owner(self):
510+ # If a packaging exists, the edit_packaging link is disabled
511+ # for the product owner.
512+ self.makePackaging()
513+ menu = self.makeSourcePackageOverviewMenu(self.product_owner)
514+ with person_logged_in(self.product_owner):
515+ self.assertFalse(menu.edit_packaging().enabled)
516+
517+ def test_set_upstream_link__disabled_for_product_owner(self):
518+ # If a packaging exists, the set_upstream link is disabled
519+ # for the product owner.
520+ self.makePackaging()
521+ menu = self.makeSourcePackageOverviewMenu(self.product_owner)
522+ with person_logged_in(self.product_owner):
523+ self.assertFalse(menu.set_upstream().enabled)
524+
525+ def test_remove_packaging_link__enabled_for_product_owner(self):
526+ # If a packaging exists, the remove_packaging link is enabled
527+ # for product owner.
528+ self.makePackaging()
529+ menu = self.makeSourcePackageOverviewMenu(self.product_owner)
530+ with person_logged_in(self.product_owner):
531 self.assertTrue(menu.remove_packaging().enabled)
532
533- def test_edit_packaging_link__enabled_with_packaging_probation(self):
534- # If a packging exists, the edit_packaging link is not enabled
535- # for probationary users.
536- menu, user = self.makeSourcePackageOverviewMenu(True, None)
537+ def test_edit_packaging_link__enabled_with_packaging_arbitrary(self):
538+ # If a packaging exists, the edit_packaging link is not enabled
539+ # for arbitrary users.
540+ self.makePackaging()
541+ user = self.factory.makePerson()
542+ menu = self.makeSourcePackageOverviewMenu(user)
543 with person_logged_in(user):
544 self.assertFalse(menu.edit_packaging().enabled)
545
546- def test_set_upstrem_link__enabled_with_packaging_probation(self):
547- # If a packging exists, the set_upstream link is not enabled
548- # for probationary users.
549- menu, user = self.makeSourcePackageOverviewMenu(True, None)
550+ def test_set_upstream_link__enabled_with_packaging_arbitrary(self):
551+ # If a packaging exists, the set_upstream link is not enabled
552+ # for arbitrary users.
553+ self.makePackaging()
554+ user = self.factory.makePerson()
555+ menu = self.makeSourcePackageOverviewMenu(user)
556 with person_logged_in(user):
557 self.assertFalse(menu.set_upstream().enabled)
558
559- def test_remove_packaging_link__enabled_with_packaging_probation(self):
560- # If a packging exists, the remove_packaging link is not enabled
561- # for probationary users.
562- menu, user = self.makeSourcePackageOverviewMenu(True, None)
563+ def test_remove_packaging_link__enabled_with_packaging_arbitrary(self):
564+ # If a packaging exists, the remove_packaging link is not enabled
565+ # for arbitrary users.
566+ self.makePackaging()
567+ user = self.factory.makePerson()
568+ menu = self.makeSourcePackageOverviewMenu(user)
569 with person_logged_in(user):
570 self.assertFalse(menu.remove_packaging().enabled)
571
572@@ -392,10 +431,9 @@ class TestSourcePackageChangeUpstreamView(BrowserTestCase):
573 owner=product_owner,
574 information_type=InformationType.PROPRIETARY,
575 )
576- ubuntu_series = self.factory.makeUbuntuDistroSeries()
577- sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
578+ sp = self.factory.makeSourcePackage()
579 browser = self.getViewBrowser(
580- sp, "+edit-packaging", user=product_owner
581+ sp, "+edit-packaging", user=self.factory.makeAdministrator()
582 )
583 browser.getControl("Project").value = product_name
584 browser.getControl("Continue").click()
585@@ -413,10 +451,9 @@ class TestSourcePackageChangeUpstreamView(BrowserTestCase):
586 )
587 series = self.factory.makeProductSeries(product=product)
588 series_displayname = series.displayname
589- ubuntu_series = self.factory.makeUbuntuDistroSeries()
590- sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
591+ sp = self.factory.makeSourcePackage()
592 browser = self.getViewBrowser(
593- sp, "+edit-packaging", user=product_owner
594+ sp, "+edit-packaging", user=self.factory.makeAdministrator()
595 )
596 browser.getControl("Project").value = product_name
597 browser.getControl("Continue").click()
598diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
599index f2d0433..585bbf1 100644
600--- a/lib/lp/registry/configure.zcml
601+++ b/lib/lp/registry/configure.zcml
602@@ -2188,8 +2188,10 @@
603 <allow
604 interface="lp.registry.interfaces.packaging.IPackaging"/>
605 <require
606- permission="zope.Public"
607- set_schema="lp.registry.interfaces.packaging.IPackaging"/>
608+ permission="launchpad.Edit"
609+ set_schema="lp.registry.interfaces.packaging.IPackaging"
610+ attributes="
611+ destroySelf" />
612 </class>
613
614 <!-- PackagingUtil -->
615diff --git a/lib/lp/registry/interfaces/packaging.py b/lib/lp/registry/interfaces/packaging.py
616index 5ebddd5..ae16be5 100644
617--- a/lib/lp/registry/interfaces/packaging.py
618+++ b/lib/lp/registry/interfaces/packaging.py
619@@ -104,14 +104,6 @@ class IPackaging(IHasOwner):
620 )
621 )
622
623- def userCanDelete():
624- """True, if the current user is allowed to delete this packaging,
625- else False.
626-
627- Non-probationary users can delete packaging links that they believe
628- connect Ubuntu to bogus data.
629- """
630-
631
632 class IPackagingUtil(Interface):
633 """Utilities to handle Packaging."""
634@@ -121,6 +113,9 @@ class IPackagingUtil(Interface):
635 ):
636 """Create Packaging entry."""
637
638+ def get(productseries, sourcepackagename, distroseries):
639+ """Get Packaging entry."""
640+
641 def deletePackaging(productseries, sourcepackagename, distroseries):
642 """Delete a packaging entry."""
643
644diff --git a/lib/lp/registry/interfaces/productseries.py b/lib/lp/registry/interfaces/productseries.py
645index 62e1ef5..f985de4 100644
646--- a/lib/lp/registry/interfaces/productseries.py
647+++ b/lib/lp/registry/interfaces/productseries.py
648@@ -102,6 +102,11 @@ class IProductSeriesEditRestricted(Interface):
649 def newMilestone(name, dateexpected=None, summary=None, code_name=None):
650 """Create a new milestone for this ProjectSeries."""
651
652+ def setPackaging(distroseries, sourcepackagename, owner):
653+ """Create or update a Packaging record for this product series,
654+ connecting it to the given distroseries and source package name.
655+ """
656+
657
658 class IProductSeriesPublic(Interface):
659 """Public IProductSeries properties."""
660@@ -343,11 +348,6 @@ class IProductSeriesView(
661 """Return the SourcePackage that packages this project in Ubuntu's
662 translation focus or current series or any series, in that order."""
663
664- def setPackaging(distroseries, sourcepackagename, owner):
665- """Create or update a Packaging record for this product series,
666- connecting it to the given distroseries and source package name.
667- """
668-
669 def getPackagingInDistribution(distribution):
670 """Return all the Packaging entries for this product series for the
671 given distribution. Note that this only returns EXPLICT packaging
672diff --git a/lib/lp/registry/interfaces/sourcepackage.py b/lib/lp/registry/interfaces/sourcepackage.py
673index d2a4ad2..4cc038e 100644
674--- a/lib/lp/registry/interfaces/sourcepackage.py
675+++ b/lib/lp/registry/interfaces/sourcepackage.py
676@@ -224,32 +224,6 @@ class ISourcePackagePublic(
677 sourcepackagename compare not equal.
678 """
679
680- @operation_parameters(productseries=Reference(schema=IProductSeries))
681- @call_with(owner=REQUEST_USER)
682- @export_write_operation()
683- @operation_for_version("devel")
684- def setPackaging(productseries, owner):
685- """Update the existing packaging record, or create a new packaging
686- record, that links the source package to the given productseries,
687- and record that it was done by the owner.
688- """
689-
690- @operation_parameters(productseries=Reference(schema=IProductSeries))
691- @call_with(owner=REQUEST_USER)
692- @export_write_operation()
693- @operation_for_version("devel")
694- def setPackagingReturnSharingDetailPermissions(productseries, owner):
695- """Like setPackaging(), but returns getSharingDetailPermissions().
696-
697- This method is intended for AJAX usage on the +sharing-details
698- page.
699- """
700-
701- @export_write_operation()
702- @operation_for_version("devel")
703- def deletePackaging():
704- """Delete the packaging for this sourcepackage."""
705-
706 def getSharingDetailPermissions():
707 """Return a dictionary of user permissions for +sharing-details page.
708
709@@ -364,6 +338,21 @@ class ISourcePackageEdit(Interface):
710 :return: None
711 """
712
713+ @operation_parameters(productseries=Reference(schema=IProductSeries))
714+ @call_with(owner=REQUEST_USER)
715+ @export_write_operation()
716+ @operation_for_version("devel")
717+ def setPackaging(productseries, owner):
718+ """Update the existing packaging record, or create a new packaging
719+ record, that links the source package to the given productseries,
720+ and record that it was done by the owner.
721+ """
722+
723+ @export_write_operation()
724+ @operation_for_version("devel")
725+ def deletePackaging():
726+ """Delete the packaging for this sourcepackage."""
727+
728
729 @exported_as_webservice_entry(as_of="beta")
730 class ISourcePackage(ISourcePackagePublic, ISourcePackageEdit):
731diff --git a/lib/lp/registry/model/packaging.py b/lib/lp/registry/model/packaging.py
732index 2735a16..047b0e5 100644
733--- a/lib/lp/registry/model/packaging.py
734+++ b/lib/lp/registry/model/packaging.py
735@@ -7,10 +7,8 @@ from lazr.lifecycle.event import ObjectCreatedEvent, ObjectDeletedEvent
736 from zope.component import getUtility
737 from zope.event import notify
738 from zope.interface import implementer
739-from zope.security.interfaces import Unauthorized
740
741 from lp.app.enums import InformationType
742-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
743 from lp.registry.errors import CannotPackageProprietaryProduct
744 from lp.registry.interfaces.packaging import (
745 IPackaging,
746@@ -23,7 +21,6 @@ from lp.services.database.datetimecol import UtcDateTimeCol
747 from lp.services.database.enumcol import DBEnum
748 from lp.services.database.sqlbase import SQLBase
749 from lp.services.database.sqlobject import ForeignKey
750-from lp.services.webapp.interfaces import ILaunchBag
751
752
753 @implementer(IPackaging)
754@@ -66,29 +63,7 @@ class Packaging(SQLBase):
755 super().__init__(**kwargs)
756 notify(ObjectCreatedEvent(self))
757
758- def userCanDelete(self):
759- """See `IPackaging`."""
760- user = getUtility(ILaunchBag).user
761- if user is None:
762- return False
763- admin = getUtility(ILaunchpadCelebrities).admin
764- registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
765- if (
766- not user.is_probationary
767- or user.inTeam(self.productseries.product.owner)
768- or user.canAccess(self.sourcepackage, "setBranch")
769- or user.inTeam(registry_experts)
770- or user.inTeam(admin)
771- ):
772- return True
773- return False
774-
775 def destroySelf(self):
776- if not self.userCanDelete():
777- raise Unauthorized(
778- "Only the person who created the packaging and package "
779- "maintainers can delete it."
780- )
781 notify(ObjectDeletedEvent(self))
782 super().destroySelf()
783
784@@ -97,16 +72,15 @@ class Packaging(SQLBase):
785 class PackagingUtil:
786 """Utilities for Packaging."""
787
788- @classmethod
789 def createPackaging(
790- cls, productseries, sourcepackagename, distroseries, packaging, owner
791+ self, productseries, sourcepackagename, distroseries, packaging, owner
792 ):
793 """See `IPackaging`.
794
795 Raises an assertion error if there is already packaging for
796 the sourcepackagename in the distroseries.
797 """
798- if cls.packagingEntryExists(sourcepackagename, distroseries):
799+ if self.packagingEntryExists(sourcepackagename, distroseries):
800 raise AssertionError(
801 "A packaging entry for %s in %s already exists."
802 % (sourcepackagename.name, distroseries.name)
803@@ -132,9 +106,18 @@ class PackagingUtil:
804 owner=owner,
805 )
806
807+ def get(self, productseries, sourcepackagename, distroseries):
808+ criteria = {
809+ "sourcepackagename": sourcepackagename,
810+ "distroseries": distroseries,
811+ }
812+ if productseries is not None:
813+ criteria["productseries"] = productseries
814+ return Packaging.selectOneBy(**criteria)
815+
816 def deletePackaging(self, productseries, sourcepackagename, distroseries):
817 """See `IPackaging`."""
818- packaging = Packaging.selectOneBy(
819+ packaging = getUtility(IPackagingUtil).get(
820 productseries=productseries,
821 sourcepackagename=sourcepackagename,
822 distroseries=distroseries,
823@@ -152,17 +135,13 @@ class PackagingUtil:
824 )
825 packaging.destroySelf()
826
827- @staticmethod
828 def packagingEntryExists(
829- sourcepackagename, distroseries, productseries=None
830+ self, sourcepackagename, distroseries, productseries=None
831 ):
832 """See `IPackaging`."""
833- criteria = dict(
834- sourcepackagename=sourcepackagename, distroseries=distroseries
835+ packaging = self.get(
836+ productseries=productseries,
837+ sourcepackagename=sourcepackagename,
838+ distroseries=distroseries,
839 )
840- if productseries is not None:
841- criteria["productseries"] = productseries
842- result = Packaging.selectOneBy(**criteria)
843- if result is None:
844- return False
845- return True
846+ return packaging is not None
847diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py
848index 9574999..10d30ea 100644
849--- a/lib/lp/registry/model/productseries.py
850+++ b/lib/lp/registry/model/productseries.py
851@@ -17,6 +17,7 @@ from storm.locals import And, Desc
852 from storm.store import Store
853 from zope.component import getUtility
854 from zope.interface import implementer
855+from zope.security.proxy import removeSecurityProxy
856
857 from lp.app.enums import service_uses_launchpad
858 from lp.app.errors import NotFoundError
859@@ -35,7 +36,7 @@ from lp.bugs.model.structuralsubscription import (
860 StructuralSubscriptionTargetMixin,
861 )
862 from lp.registry.errors import ProprietaryPillar
863-from lp.registry.interfaces.packaging import PackagingType
864+from lp.registry.interfaces.packaging import IPackagingUtil, PackagingType
865 from lp.registry.interfaces.person import validate_person
866 from lp.registry.interfaces.productrelease import IProductReleaseSet
867 from lp.registry.interfaces.productseries import (
868@@ -45,7 +46,6 @@ from lp.registry.interfaces.productseries import (
869 )
870 from lp.registry.interfaces.series import SeriesStatus
871 from lp.registry.model.milestone import HasMilestonesMixin, Milestone
872-from lp.registry.model.packaging import PackagingUtil
873 from lp.registry.model.productrelease import ProductRelease
874 from lp.registry.model.series import SeriesMixin
875 from lp.services.database.constants import UTC_NOW
876@@ -445,14 +445,14 @@ class ProductSeries(
877
878 # ok, we didn't find a packaging record that matches, let's go ahead
879 # and create one
880- pkg = PackagingUtil.createPackaging(
881+ pkg = getUtility(IPackagingUtil).createPackaging(
882 distroseries=distroseries,
883 sourcepackagename=sourcepackagename,
884 productseries=self,
885 packaging=PackagingType.PRIME,
886 owner=owner,
887 )
888- pkg.sync() # convert UTC_NOW to actual datetime
889+ removeSecurityProxy(pkg).sync() # convert UTC_NOW to actual datetime
890 return pkg
891
892 def getPackagingInDistribution(self, distribution):
893diff --git a/lib/lp/registry/model/sourcepackage.py b/lib/lp/registry/model/sourcepackage.py
894index 1bfacba..8a0280b 100644
895--- a/lib/lp/registry/model/sourcepackage.py
896+++ b/lib/lp/registry/model/sourcepackage.py
897@@ -33,14 +33,14 @@ from lp.code.model.seriessourcepackagebranch import (
898 SeriesSourcePackageBranchSet,
899 )
900 from lp.registry.interfaces.distribution import NoPartnerArchive
901-from lp.registry.interfaces.packaging import PackagingType
902+from lp.registry.interfaces.packaging import IPackagingUtil, PackagingType
903 from lp.registry.interfaces.pocket import PackagePublishingPocket
904 from lp.registry.interfaces.sourcepackage import (
905 ISourcePackage,
906 ISourcePackageFactory,
907 )
908 from lp.registry.model.hasdrivers import HasDriversMixin
909-from lp.registry.model.packaging import Packaging, PackagingUtil
910+from lp.registry.model.packaging import Packaging
911 from lp.registry.model.suitesourcepackage import SuiteSourcePackage
912 from lp.services.database.decoratedresultset import DecoratedResultSet
913 from lp.services.database.interfaces import IStore
914@@ -562,7 +562,7 @@ class SourcePackage(
915 # Delete the current packaging and create a new one so
916 # that the translation sharing jobs are started.
917 self.direct_packaging.destroySelf()
918- PackagingUtil.createPackaging(
919+ getUtility(IPackagingUtil).createPackaging(
920 distroseries=self.distroseries,
921 sourcepackagename=self.sourcepackagename,
922 productseries=productseries,
923@@ -572,11 +572,6 @@ class SourcePackage(
924 # and make sure this change is immediately available
925 flush_database_updates()
926
927- def setPackagingReturnSharingDetailPermissions(self, productseries, owner):
928- """See `ISourcePackage`."""
929- self.setPackaging(productseries, owner)
930- return self.getSharingDetailPermissions()
931-
932 def getSharingDetailPermissions(self):
933 user = getUtility(ILaunchBag).user
934 productseries = self.productseries
935@@ -595,8 +590,8 @@ class SourcePackage(
936 else:
937 permissions.update(
938 {
939- "user_can_change_product_series": (
940- self.direct_packaging.userCanDelete()
941+ "user_can_change_product_series": user.canAccess(
942+ self, "setPackaging"
943 ),
944 "user_can_change_branch": user.canWrite(
945 productseries, "branch"
946diff --git a/lib/lp/registry/security.py b/lib/lp/registry/security.py
947index 2e14ae0..ad48865 100644
948--- a/lib/lp/registry/security.py
949+++ b/lib/lp/registry/security.py
950@@ -219,8 +219,14 @@ class EditProduct(EditByOwnersOrAdmins):
951 )
952
953
954-class EditPackaging(EditByOwnersOrAdmins):
955+class EditPackaging(AuthorizationBase):
956 usedfor = IPackaging
957+ permission = "launchpad.Edit"
958+
959+ def checkAuthenticated(self, user):
960+ return self.forwardCheckAuthenticated(
961+ user, self.obj.productseries
962+ ) or self.forwardCheckAuthenticated(user, self.obj.sourcepackage)
963
964
965 class DownloadFullSourcePackageTranslations(OnlyRosettaExpertsAndAdmins):
966diff --git a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.rst b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.rst
967index f87fa18..d53abcf 100644
968--- a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.rst
969+++ b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.rst
970@@ -5,12 +5,12 @@ When two browsers are used to concurrently delete the same packaging
971 association, only one of them can succeed. The other one does not oops
972 and displays a meaningful error message.
973
974-The No Privilege User may open the same page in two browser tabs.
975+The package maintainer may open the same page in two browser tabs.
976
977- >>> first_browser = setupBrowser(auth="Basic test@canonical.com:test")
978+ >>> first_browser = setupBrowser(auth="Basic admin@canonical.com:test")
979 >>> first_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
980
981- >>> second_browser = setupBrowser(auth="Basic test@canonical.com:test")
982+ >>> second_browser = setupBrowser(auth="Basic admin@canonical.com:test")
983 >>> second_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
984
985 Then the user click the "Delete Link" button in the first tab. The
986diff --git a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.rst b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.rst
987index c616717..3f66179 100644
988--- a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.rst
989+++ b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.rst
990@@ -18,7 +18,7 @@ source package in each series of this distribution.
991 >>> anon_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
992 >>> content = anon_browser.contents
993 >>> print(extract_text(find_tag_by_id(content, "packages_list")))
994- The Hoary Hedgehog Release (active development) Set upstream link
995+ The Hoary Hedgehog Release (active development)
996 1.0.9a-4ubuntu1 release (main) 2005-09-15
997 The Warty Warthog Release (current stable release) alsa-utils trunk series
998 1.0.9a-4 release (main) 2005-09-16
999@@ -28,12 +28,12 @@ source package in each series of this distribution.
1000 Delete Link Button
1001 ------------------
1002
1003-A button is displayed to authenticated users to delete existing
1004+A button is displayed to project owners to delete existing
1005 packaging links.
1006
1007- >>> user_browser = setupBrowser(auth="Basic test@canonical.com:test")
1008- >>> user_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
1009- >>> link = user_browser.getLink(
1010+ >>> owner_browser = setupBrowser(auth="Basic mark@example.com:test")
1011+ >>> owner_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
1012+ >>> link = owner_browser.getLink(
1013 ... url="/ubuntu/warty/+source/alsa-utils/+remove-packaging"
1014 ... )
1015 >>> print(link)
1016@@ -49,12 +49,12 @@ This button is not displayed to anonymous users.
1017
1018 Clicking this button deletes the corresponding packaging association.
1019
1020- >>> link = user_browser.getLink(
1021+ >>> link = owner_browser.getLink(
1022 ... url="/ubuntu/warty/+source/alsa-utils/+remove-packaging"
1023 ... )
1024 >>> link.click()
1025- >>> user_browser.getControl("Unlink").click()
1026- >>> content = user_browser.contents
1027+ >>> owner_browser.getControl("Unlink").click()
1028+ >>> content = owner_browser.contents
1029 >>> for tag in find_tags_by_class(content, "error"):
1030 ... print(extract_text(tag))
1031 ...
1032diff --git a/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst b/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst
1033index 9729757..71e645e 100644
1034--- a/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst
1035+++ b/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst
1036@@ -10,25 +10,20 @@ Create test data.
1037 >>> test_publisher.updatePackageCache(test_data["distroseries"])
1038 >>> logout()
1039
1040-No Privileges Person visit the distroseries upstream links page for Hoary
1041-and sees that pmount is not linked.
1042+A person with permissions to edit packaging visits the distroseries upstream
1043+links page for Hoary and sees that pmount is not linked.
1044
1045- >>> user_browser.open(
1046- ... "http://launchpad.test/ubuntu/hoary/+needs-packaging"
1047- ... )
1048- >>> print(extract_text(find_tag_by_id(user_browser.contents, "packages")))
1049+ >>> browser = setupBrowser(auth="Basic limi@plone.org:test")
1050+ >>> browser.open("http://launchpad.test/ubuntu/hoary/+needs-packaging")
1051+ >>> print(extract_text(find_tag_by_id(browser.contents, "packages")))
1052 Source Package Bugs Translations
1053 pmount No bugs 64 strings ...
1054
1055 They look at the pmount source package page in Hoary and read that the
1056 upstream project is not set.
1057
1058- >>> user_browser.getLink("pmount").click()
1059- >>> print(
1060- ... extract_text(
1061- ... find_tag_by_id(user_browser.contents, "no-upstreams")
1062- ... )
1063- ... )
1064+ >>> browser.getLink("pmount").click()
1065+ >>> print(extract_text(find_tag_by_id(browser.contents, "no-upstreams")))
1066 Launchpad...
1067 There are no projects registered in Launchpad that are a potential
1068 match for this source package. Can you help us find one?
1069@@ -36,46 +31,36 @@ upstream project is not set.
1070 Choose another upstream project
1071 Register the upstream project
1072
1073-No Privileges Person knows that the pmount package comes from the thunderbird
1074+The person knows that the pmount package comes from the thunderbird
1075 project. They set the upstream packaging link and see that it is set.
1076
1077- >>> user_browser.getControl(
1078- ... "Choose another upstream project"
1079- ... ).selected = True
1080- >>> user_browser.getControl("Link to Upstream Project").click()
1081- >>> user_browser.getControl(name="field.product").value = "thunderbird"
1082- >>> user_browser.getControl("Continue").click()
1083- >>> user_browser.getControl(name="field.productseries").value = ["trunk"]
1084- >>> user_browser.getControl("Change").click()
1085- >>> print(
1086- ... extract_text(find_tag_by_id(user_browser.contents, "upstreams"))
1087- ... )
1088+ >>> browser.getControl("Choose another upstream project").selected = True
1089+ >>> browser.getControl("Link to Upstream Project").click()
1090+ >>> browser.getControl(name="field.product").value = "thunderbird"
1091+ >>> browser.getControl("Continue").click()
1092+ >>> browser.getControl(name="field.productseries").value = ["trunk"]
1093+ >>> browser.getControl("Change").click()
1094+ >>> print(extract_text(find_tag_by_id(browser.contents, "upstreams")))
1095 The Mozilla Project...Mozilla Thunderbird...trunk...
1096
1097 They see the "Show upstream links" link and take a look at the project's
1098 packaging in distributions.
1099
1100- >>> user_browser.getLink("Show upstream links").click()
1101+ >>> browser.getLink("Show upstream links").click()
1102 >>> print(
1103 ... extract_text(
1104- ... find_tag_by_id(user_browser.contents, "distribution-series")
1105+ ... find_tag_by_id(browser.contents, "distribution-series")
1106 ... )
1107 ... )
1108 Distribution series Source package Version Project series
1109 Hoary (5.04) pmount 0.1-2 Mozilla Thunderbird trunk...
1110
1111-No Privileges Person returns to the pmount source package page, sees the
1112+The person returns to the pmount source package page, sees the
1113 link to all versions and follows it to the distro source package page.
1114
1115- >>> user_browser.getLink("pmount").click()
1116- >>> user_browser.getLink(
1117- ... "All versions of pmount source in Ubuntu"
1118- ... ).click()
1119- >>> print(
1120- ... extract_text(
1121- ... find_tag_by_id(user_browser.contents, "packages_list")
1122- ... )
1123- ... )
1124+ >>> browser.getLink("pmount").click()
1125+ >>> browser.getLink("All versions of pmount source in Ubuntu").click()
1126+ >>> print(extract_text(find_tag_by_id(browser.contents, "packages_list")))
1127 The Hoary Hedgehog Release (active development) ...
1128 0.1-2 release (main) 2005-08-24
1129
1130@@ -83,73 +68,61 @@ link to all versions and follows it to the distro source package page.
1131 Register a project from a source package
1132 ----------------------------------------
1133
1134-No Privileges Person can register a project for a package, and Launchpad
1135+The person can register a project for a package, and Launchpad
1136 will use the data from the source package to prefill the first
1137 step of the multistep form.
1138
1139- >>> user_browser.open(
1140- ... "http://launchpad.test/youbuntu/busy/+source/bonkers"
1141- ... )
1142- >>> user_browser.getControl(
1143- ... "Register the upstream project"
1144- ... ).selected = True
1145- >>> user_browser.getControl("Link to Upstream Project").click()
1146- >>> print(user_browser.getControl(name="field.name").value)
1147+ >>> browser = setupBrowser(auth="Basic owner@youbuntu.com:test")
1148+
1149+ >>> browser.open("http://launchpad.test/youbuntu/busy/+source/bonkers")
1150+ >>> browser.getControl("Register the upstream project").selected = True
1151+ >>> browser.getControl("Link to Upstream Project").click()
1152+ >>> print(browser.getControl(name="field.name").value)
1153 bonkers
1154- >>> print(user_browser.getControl(name="field.display_name").value)
1155+ >>> print(browser.getControl(name="field.display_name").value)
1156 Bonkers
1157- >>> print(user_browser.getControl(name="field.summary").value)
1158+ >>> print(browser.getControl(name="field.summary").value)
1159 summary for flubber-bin
1160 summary for flubber-lib
1161- >>> print(
1162- ... extract_text(find_tag_by_id(user_browser.contents, "step-title"))
1163- ... )
1164+ >>> print(extract_text(find_tag_by_id(browser.contents, "step-title")))
1165 Step 2 (of 2): Check for duplicate projects
1166
1167-When No Privileges Person selects "Choose another upstream project" and
1168+When the person selects "Choose another upstream project" and
1169 then finds out that the project doesn't exist, they use the
1170 "Link to Upstream Project" button to register the project.
1171
1172- >>> user_browser.open(
1173- ... "http://launchpad.test/youbuntu/busy/+source/bonkers/"
1174- ... )
1175- >>> user_browser.getControl(
1176- ... "Choose another upstream project"
1177- ... ).selected = True
1178- >>> user_browser.getControl("Link to Upstream Project").click()
1179- >>> print(user_browser.url)
1180+ >>> browser.open("http://launchpad.test/youbuntu/busy/+source/bonkers/")
1181+ >>> browser.getControl("Choose another upstream project").selected = True
1182+ >>> browser.getControl("Link to Upstream Project").click()
1183+ >>> print(browser.url)
1184 http://launchpad.test/youbuntu/busy/+source/bonkers/+edit-packaging
1185
1186- >>> user_browser.getLink("Register the upstream project").click()
1187- >>> print(user_browser.getControl(name="field.name").value)
1188+ >>> browser.getLink("Register the upstream project").click()
1189+ >>> print(browser.getControl(name="field.name").value)
1190 bonkers
1191- >>> print(user_browser.getControl(name="field.display_name").value)
1192+ >>> print(browser.getControl(name="field.display_name").value)
1193 Bonkers
1194- >>> print(user_browser.getControl(name="field.summary").value)
1195+ >>> print(browser.getControl(name="field.summary").value)
1196 summary for flubber-bin
1197 summary for flubber-lib
1198- >>> print(
1199- ... extract_text(find_tag_by_id(user_browser.contents, "step-title"))
1200- ... )
1201+ >>> print(extract_text(find_tag_by_id(browser.contents, "step-title")))
1202 Step 2 (of 2): Check for duplicate projects
1203
1204-After No Privileges Person selects the licences, the user is redirected back
1205+After the person selects the licences, the user is redirected back
1206 to the source package page and an informational message will be displayed.
1207
1208- >>> user_browser.getControl(name="field.licenses").value = ["BSD"]
1209- >>> user_browser.getControl(
1210+ >>> browser.getControl(name="field.licenses").value = ["BSD"]
1211+ >>> browser.getControl(
1212 ... "Complete registration and link to bonkers package"
1213 ... ).click()
1214- >>> print(user_browser.url)
1215+ >>> print(browser.url)
1216 http://launchpad.test/youbuntu/busy/+source/bonkers
1217 >>> for tag in find_tags_by_class(
1218- ... user_browser.contents, "informational message"
1219+ ... browser.contents, "informational message"
1220 ... ):
1221 ... print(extract_text(tag))
1222 Linked Bonkers project to bonkers source package.
1223- >>> print(
1224- ... extract_text(find_tag_by_id(user_browser.contents, "upstreams"))
1225- ... )
1226+ >>> print(extract_text(find_tag_by_id(browser.contents, "upstreams")))
1227 Bonkers ā‡’ trunk
1228 Change upstream link
1229 Remove upstream link...
1230diff --git a/lib/lp/registry/stories/product/xx-product-package-pages.rst b/lib/lp/registry/stories/product/xx-product-package-pages.rst
1231index 8d6f05a..8c160aa 100644
1232--- a/lib/lp/registry/stories/product/xx-product-package-pages.rst
1233+++ b/lib/lp/registry/stories/product/xx-product-package-pages.rst
1234@@ -42,11 +42,13 @@ Evolution.
1235 >>> evo_owner.getLink(url="/ubuntu/hoary/+source/evolution") is not None
1236 True
1237
1238-Any logged in users can still see the links to create a packaging link.
1239+Arbitrary users can't see the links to create a packaging link.
1240
1241 >>> user_browser.open("http://launchpad.test/evolution/+packages")
1242- >>> print(user_browser.getLink(url="/evolution/trunk/+ubuntupkg").url)
1243- http://launchpad.test/evolution/trunk/+ubuntupkg
1244+ >>> user_browser.getLink(url="/evolution/trunk/+ubuntupkg")
1245+ Traceback (most recent call last):
1246+ ...
1247+ zope.testbrowser.browser.LinkNotFoundError
1248
1249 >>> anon_browser.open("http://launchpad.test/evolution/+packages")
1250 >>> anon_browser.getLink(url="/evolution/trunk/+ubuntupkg")
1251diff --git a/lib/lp/registry/tests/test_distroseries.py b/lib/lp/registry/tests/test_distroseries.py
1252index d3f86df..ab3c6bf 100644
1253--- a/lib/lp/registry/tests/test_distroseries.py
1254+++ b/lib/lp/registry/tests/test_distroseries.py
1255@@ -580,7 +580,7 @@ class TestDistroSeriesPackaging(TestCaseWithFactory):
1256
1257 def linkPackage(self, name):
1258 product_series = self.factory.makeProductSeries()
1259- product_series.setPackaging(
1260+ removeSecurityProxy(product_series).setPackaging(
1261 self.series, self.packages[name].sourcepackagename, self.user
1262 )
1263 return product_series
1264diff --git a/lib/lp/registry/tests/test_packaging.py b/lib/lp/registry/tests/test_packaging.py
1265index 628bfb6..f4b4db6 100644
1266--- a/lib/lp/registry/tests/test_packaging.py
1267+++ b/lib/lp/registry/tests/test_packaging.py
1268@@ -19,6 +19,8 @@ from lp.registry.model.packaging import Packaging
1269 from lp.testing import (
1270 EventRecorder,
1271 TestCaseWithFactory,
1272+ admin_logged_in,
1273+ anonymous_logged_in,
1274 login,
1275 person_logged_in,
1276 )
1277@@ -61,7 +63,7 @@ class TestPackaging(TestCaseWithFactory):
1278 packaging.distroseries,
1279 )
1280
1281- def test_destroySelf__not_allowed_for_probationary_user(self):
1282+ def test_destroySelf__not_allowed_for_random_user(self):
1283 """Arbitrary users cannot delete a packaging."""
1284 packaging = self.factory.makePackagingLink()
1285 packaging_util = getUtility(IPackagingUtil)
1286@@ -74,26 +76,6 @@ class TestPackaging(TestCaseWithFactory):
1287 packaging.distroseries,
1288 )
1289
1290- def test_destroySelf__allowed_for_non_probationary_user(self):
1291- """An experienced user can delete a packaging."""
1292- packaging = self.factory.makePackagingLink()
1293- sourcepackagename = packaging.sourcepackagename
1294- distroseries = packaging.distroseries
1295- productseries = packaging.productseries
1296- packaging_util = getUtility(IPackagingUtil)
1297- user = self.factory.makePerson(karma=200)
1298- with person_logged_in(user):
1299- packaging_util.deletePackaging(
1300- packaging.productseries,
1301- packaging.sourcepackagename,
1302- packaging.distroseries,
1303- )
1304- self.assertFalse(
1305- packaging_util.packagingEntryExists(
1306- sourcepackagename, distroseries, productseries
1307- )
1308- )
1309-
1310 def test_destroySelf__allowed_for_uploader(self):
1311 """A person with upload rights for the sourcepackage can
1312 delete a packaging link.
1313@@ -335,8 +317,7 @@ class TestDeletePackaging(TestCaseWithFactory):
1314 """Deleting a Packaging creates a notification."""
1315 packaging_util = getUtility(IPackagingUtil)
1316 packaging = self.factory.makePackagingLink()
1317- user = self.factory.makePerson(karma=200)
1318- with person_logged_in(user):
1319+ with person_logged_in(packaging.productseries.product.owner):
1320 with EventRecorder() as recorder:
1321 packaging_util.deletePackaging(
1322 packaging.productseries,
1323@@ -346,3 +327,61 @@ class TestDeletePackaging(TestCaseWithFactory):
1324 (event,) = recorder.events
1325 self.assertIsInstance(event, ObjectDeletedEvent)
1326 self.assertIs(removeSecurityProxy(packaging), event.object)
1327+
1328+
1329+class TestPackagingSecurity(TestCaseWithFactory):
1330+
1331+ layer = DatabaseFunctionalLayer
1332+
1333+ def setUp(self, *args, **kwargs):
1334+ super().setUp(*args, **kwargs)
1335+ self.packaging = self.factory.makePackagingLink()
1336+
1337+ def viewPackaging(self):
1338+ _ = self.packaging.packaging
1339+
1340+ def editPackaging(self):
1341+ self.packaging.packaging = PackagingType.PRIME
1342+
1343+ def test_anyone_can_view(self):
1344+ with anonymous_logged_in():
1345+ try:
1346+ self.viewPackaging()
1347+ except Unauthorized:
1348+ self.fail("Unauthorized exception raised")
1349+
1350+ def test_anonymous_person_cannot_edit(self):
1351+ with anonymous_logged_in():
1352+ self.assertRaises(Unauthorized, self.editPackaging)
1353+
1354+ def test_random_person_cannot_edit(self):
1355+ with person_logged_in(self.factory.makePerson()):
1356+ self.assertRaises(Unauthorized, self.editPackaging)
1357+
1358+ def test_admin_can_edit(self):
1359+ with admin_logged_in():
1360+ try:
1361+ self.editPackaging()
1362+ except Unauthorized:
1363+ self.fail("Unauthorized exception raised")
1364+
1365+ def test_product_owner_can_edit(self):
1366+ with person_logged_in(self.packaging.productseries.product.owner):
1367+ try:
1368+ self.editPackaging()
1369+ except Unauthorized:
1370+ self.fail("Unauthorized exception raised")
1371+
1372+ def test_productseries_owner_can_edit(self):
1373+ with person_logged_in(self.packaging.productseries.owner):
1374+ try:
1375+ self.editPackaging()
1376+ except Unauthorized:
1377+ self.fail("Unauthorized exception raised")
1378+
1379+ def test_distribution_owner_can_edit(self):
1380+ with person_logged_in(self.packaging.distroseries.distribution.owner):
1381+ try:
1382+ self.editPackaging()
1383+ except Unauthorized:
1384+ self.fail("Unauthorized exception raised")
1385diff --git a/lib/lp/registry/tests/test_productseries.py b/lib/lp/registry/tests/test_productseries.py
1386index e0ea6a0..8f6b0fb 100644
1387--- a/lib/lp/registry/tests/test_productseries.py
1388+++ b/lib/lp/registry/tests/test_productseries.py
1389@@ -665,7 +665,7 @@ class ProductSeriesSecurityAdaperTestCase(TestCaseWithFactory):
1390 "addSubscription",
1391 "removeBugSubscription",
1392 },
1393- "launchpad.Edit": {"newMilestone"},
1394+ "launchpad.Edit": {"newMilestone", "setPackaging"},
1395 "launchpad.LimitedView": {
1396 "bugtargetdisplayname",
1397 "bugtarget_parent",
1398@@ -746,7 +746,6 @@ class ProductSeriesSecurityAdaperTestCase(TestCaseWithFactory):
1399 "releases",
1400 "releaseverstyle",
1401 "searchTasks",
1402- "setPackaging",
1403 "sourcepackages",
1404 "specifications",
1405 "status",
1406diff --git a/lib/lp/registry/tests/test_sourcepackage.py b/lib/lp/registry/tests/test_sourcepackage.py
1407index 63e976a..b839a18 100644
1408--- a/lib/lp/registry/tests/test_sourcepackage.py
1409+++ b/lib/lp/registry/tests/test_sourcepackage.py
1410@@ -33,6 +33,7 @@ from lp.testing import (
1411 EventRecorder,
1412 TestCaseWithFactory,
1413 WebServiceTestCase,
1414+ admin_logged_in,
1415 person_logged_in,
1416 )
1417 from lp.testing.layers import DatabaseFunctionalLayer
1418@@ -305,11 +306,10 @@ class TestSourcePackage(TestCaseWithFactory):
1419
1420 def test_deletePackaging(self):
1421 """Ensure deletePackaging completely removes packaging."""
1422- user = self.factory.makePerson(karma=200)
1423 packaging = self.factory.makePackagingLink()
1424 packaging_id = packaging.id
1425 store = Store.of(packaging)
1426- with person_logged_in(user):
1427+ with person_logged_in(packaging.sourcepackage.distribution.owner):
1428 packaging.sourcepackage.deletePackaging()
1429 result = store.find(Packaging, Packaging.id == packaging_id)
1430 self.assertIs(None, result.one())
1431@@ -318,7 +318,7 @@ class TestSourcePackage(TestCaseWithFactory):
1432 """setPackaging() creates a Packaging link."""
1433 sourcepackage = self.factory.makeSourcePackage()
1434 productseries = self.factory.makeProductSeries()
1435- sourcepackage.setPackaging(
1436+ removeSecurityProxy(sourcepackage).setPackaging(
1437 productseries, owner=self.factory.makePerson()
1438 )
1439 packaging = sourcepackage.direct_packaging
1440@@ -329,10 +329,9 @@ class TestSourcePackage(TestCaseWithFactory):
1441 sourcepackage = self.factory.makeSourcePackage()
1442 productseries = self.factory.makeProductSeries()
1443 other_series = self.factory.makeProductSeries()
1444- user = self.factory.makePerson(karma=200)
1445 registrant = self.factory.makePerson()
1446 with EventRecorder() as recorder:
1447- with person_logged_in(user):
1448+ with person_logged_in(sourcepackage.distribution.owner):
1449 sourcepackage.setPackaging(productseries, owner=registrant)
1450 sourcepackage.setPackaging(other_series, owner=registrant)
1451 packaging = sourcepackage.direct_packaging
1452@@ -348,61 +347,26 @@ class TestSourcePackage(TestCaseWithFactory):
1453
1454 def test_refuses_PROPRIETARY(self):
1455 """Packaging cannot be created for PROPRIETARY productseries"""
1456- owner = self.factory.makePerson()
1457 product = self.factory.makeProduct(
1458- owner=owner, information_type=InformationType.PROPRIETARY
1459+ information_type=InformationType.PROPRIETARY
1460 )
1461 series = self.factory.makeProductSeries(product=product)
1462 ubuntu_series = self.factory.makeUbuntuDistroSeries()
1463 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
1464- with person_logged_in(owner):
1465+ with admin_logged_in():
1466 with ExpectedException(
1467 CannotPackageProprietaryProduct,
1468 "Only Public project series can be packaged, not "
1469 "Proprietary.",
1470 ):
1471- sp.setPackaging(series, owner)
1472-
1473- def test_setPackagingReturnSharingDetailPermissions__ordinary_user(self):
1474- """An ordinary user can create a packaging link but they cannot
1475- set the series' branch or translation syncronisation settings,
1476- or the translation usage settings of the product.
1477- """
1478- sourcepackage = self.factory.makeSourcePackage()
1479- productseries = self.factory.makeProductSeries()
1480- packaging_owner = self.factory.makePerson(karma=100)
1481- with person_logged_in(packaging_owner):
1482- permissions = (
1483- sourcepackage.setPackagingReturnSharingDetailPermissions(
1484- productseries, packaging_owner
1485- )
1486- )
1487- self.assertEqual(productseries, sourcepackage.productseries)
1488- self.assertFalse(packaging_owner.canWrite(productseries, "branch"))
1489- self.assertFalse(
1490- packaging_owner.canWrite(
1491- productseries, "translations_autoimport_mode"
1492- )
1493- )
1494- self.assertFalse(
1495- packaging_owner.canWrite(
1496- productseries.product, "translations_usage"
1497- )
1498- )
1499- expected = {
1500- "user_can_change_product_series": True,
1501- "user_can_change_branch": False,
1502- "user_can_change_translation_usage": False,
1503- "user_can_change_translations_autoimport_mode": False,
1504- }
1505- self.assertEqual(expected, permissions)
1506+ sp.setPackaging(series, product.owner)
1507
1508 def test_getSharingDetailPermissions__ordinary_user(self):
1509 """An ordinary user cannot set the series' branch or translation
1510 synchronisation settings, or the translation usage settings of the
1511 product.
1512 """
1513- user = self.factory.makePerson(karma=100)
1514+ user = self.factory.makePerson()
1515 packaging = self.factory.makePackagingLink()
1516 sourcepackage = packaging.sourcepackage
1517 productseries = packaging.productseries
1518@@ -417,7 +381,7 @@ class TestSourcePackage(TestCaseWithFactory):
1519 user.canWrite(productseries.product, "translations_usage")
1520 )
1521 expected = {
1522- "user_can_change_product_series": True,
1523+ "user_can_change_product_series": False,
1524 "user_can_change_branch": False,
1525 "user_can_change_translation_usage": False,
1526 "user_can_change_translations_autoimport_mode": False,
1527@@ -429,8 +393,8 @@ class TestSourcePackage(TestCaseWithFactory):
1528 return self.factory.makeProductSeries(owner=self.factory.makePerson())
1529
1530 def test_getSharingDetailPermissions__product_owner(self):
1531- """A product owner can create a packaging link, and they can set the
1532- series' branch and the translation syncronisation settings, and the
1533+ """A product owner can't change a packaging link, and they can set the
1534+ series' branch and the translation synchronisation settings, and the
1535 translation usage settings of the product.
1536 """
1537 productseries = self.makeDistinctOwnerProductSeries()
1538@@ -454,7 +418,7 @@ class TestSourcePackage(TestCaseWithFactory):
1539 )
1540 )
1541 expected = {
1542- "user_can_change_product_series": True,
1543+ "user_can_change_product_series": False,
1544 "user_can_change_branch": True,
1545 "user_can_change_translation_usage": True,
1546 "user_can_change_translations_autoimport_mode": True,
1547@@ -464,34 +428,26 @@ class TestSourcePackage(TestCaseWithFactory):
1548 def test_getSharingDetailPermissions_change_product(self):
1549 """Test user_can_change_product_series.
1550
1551- Until a Packaging is created, anyone can change product series.
1552- Afterward, random people cannot change product series.
1553+ Random people cannot create package link or change product series.
1554 """
1555 sourcepackage = self.factory.makeSourcePackage()
1556- person1 = self.factory.makePerson(karma=100)
1557- person2 = self.factory.makePerson()
1558+ person = self.factory.makePerson()
1559
1560 def can_change_product_series():
1561 return sourcepackage.getSharingDetailPermissions()[
1562 "user_can_change_product_series"
1563 ]
1564
1565- with person_logged_in(person1):
1566- self.assertTrue(can_change_product_series())
1567- with person_logged_in(person2):
1568- self.assertTrue(can_change_product_series())
1569- self.factory.makePackagingLink(
1570- sourcepackage=sourcepackage, owner=person1
1571- )
1572- with person_logged_in(person1):
1573- self.assertTrue(can_change_product_series())
1574- with person_logged_in(person2):
1575+ with person_logged_in(person):
1576+ self.assertFalse(can_change_product_series())
1577+ self.factory.makePackagingLink(sourcepackage=sourcepackage)
1578+ with person_logged_in(person):
1579 self.assertFalse(can_change_product_series())
1580
1581 def test_getSharingDetailPermissions_no_product_series(self):
1582 sourcepackage = self.factory.makeSourcePackage()
1583 expected = {
1584- "user_can_change_product_series": True,
1585+ "user_can_change_product_series": False,
1586 "user_can_change_branch": False,
1587 "user_can_change_translation_usage": False,
1588 "user_can_change_translations_autoimport_mode": False,
1589@@ -558,8 +514,12 @@ class TestSourcePackageWebService(WebServiceTestCase):
1590 self.assertIs(None, sourcepackage.direct_packaging)
1591 productseries = self.factory.makeProductSeries()
1592 transaction.commit()
1593- ws_sourcepackage = self.wsObject(sourcepackage)
1594- ws_productseries = self.wsObject(productseries)
1595+ ws_sourcepackage = self.wsObject(
1596+ sourcepackage, user=sourcepackage.distribution.owner
1597+ )
1598+ ws_productseries = self.wsObject(
1599+ productseries, user=sourcepackage.distribution.owner
1600+ )
1601 ws_sourcepackage.setPackaging(productseries=ws_productseries)
1602 transaction.commit()
1603 self.assertEqual(
1604@@ -568,11 +528,12 @@ class TestSourcePackageWebService(WebServiceTestCase):
1605
1606 def test_deletePackaging(self):
1607 """Deleting a packaging should work."""
1608- user = self.factory.makePerson(karma=200)
1609 packaging = self.factory.makePackagingLink()
1610 sourcepackage = packaging.sourcepackage
1611 transaction.commit()
1612- self.wsObject(sourcepackage, user=user).deletePackaging()
1613+ self.wsObject(
1614+ sourcepackage, user=sourcepackage.distribution.owner
1615+ ).deletePackaging()
1616 transaction.commit()
1617 self.assertIs(None, sourcepackage.direct_packaging)
1618
1619@@ -580,7 +541,9 @@ class TestSourcePackageWebService(WebServiceTestCase):
1620 """Deleting when there's no packaging should be a no-op."""
1621 sourcepackage = self.factory.makeSourcePackage()
1622 transaction.commit()
1623- self.wsObject(sourcepackage).deletePackaging()
1624+ self.wsObject(
1625+ sourcepackage, user=sourcepackage.distribution.owner
1626+ ).deletePackaging()
1627 transaction.commit()
1628 self.assertIs(None, sourcepackage.direct_packaging)
1629
1630@@ -737,7 +700,9 @@ class TestSourcePackageViews(TestCaseWithFactory):
1631 def test_editpackaging_obsolete_series_in_vocabulary(self):
1632 # The sourcepackage's current product series is included in
1633 # the vocabulary even if it is obsolete.
1634- self.package.setPackaging(self.obsolete_productseries, self.owner)
1635+ removeSecurityProxy(self.package).setPackaging(
1636+ self.obsolete_productseries, self.owner
1637+ )
1638 form = {
1639 "field.product": "bonkers",
1640 "field.actions.continue": "Continue",
1641diff --git a/lib/lp/soyuz/tests/test_publishing.py b/lib/lp/soyuz/tests/test_publishing.py
1642index cd0828d..ff6e308 100644
1643--- a/lib/lp/soyuz/tests/test_publishing.py
1644+++ b/lib/lp/soyuz/tests/test_publishing.py
1645@@ -707,7 +707,9 @@ class SoyuzTestPublisher:
1646 """
1647 if source_pub is None:
1648 distribution = self.factory.makeDistribution(
1649- name="youbuntu", displayname="Youbuntu"
1650+ name="youbuntu",
1651+ displayname="Youbuntu",
1652+ owner=self.factory.makePerson(email="owner@youbuntu.com"),
1653 )
1654 distroseries = self.factory.makeDistroSeries(
1655 name="busy", distribution=distribution
1656diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
1657index 3fd560c..9965749 100644
1658--- a/lib/lp/testing/factory.py
1659+++ b/lib/lp/testing/factory.py
1660@@ -208,6 +208,7 @@ from lp.registry.interfaces.ssh import ISSHKeySet
1661 from lp.registry.model.commercialsubscription import CommercialSubscription
1662 from lp.registry.model.karma import KarmaTotalCache
1663 from lp.registry.model.milestone import Milestone
1664+from lp.registry.model.packaging import Packaging
1665 from lp.registry.model.suitesourcepackage import SuiteSourcePackage
1666 from lp.services.auth.interfaces import IAccessTokenSet
1667 from lp.services.auth.utils import create_access_token_secret
1668@@ -1346,7 +1347,7 @@ class LaunchpadObjectFactory(ObjectFactory):
1669 owner=None,
1670 sourcepackage=None,
1671 in_ubuntu=False,
1672- ):
1673+ ) -> Packaging:
1674 assert sourcepackage is None or (
1675 distroseries is None and sourcepackagename is None
1676 ), (

Subscribers

People subscribed via source and target branches

to status/vote changes: