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
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index 6f041d4..b90507b 100644
--- a/lib/lp/registry/browser/configure.zcml
+++ b/lib/lp/registry/browser/configure.zcml
@@ -2022,7 +2022,7 @@
2022 name="+ubuntupkg"2022 name="+ubuntupkg"
2023 for="lp.registry.interfaces.productseries.IProductSeries"2023 for="lp.registry.interfaces.productseries.IProductSeries"
2024 class="lp.registry.browser.productseries.ProductSeriesUbuntuPackagingView"2024 class="lp.registry.browser.productseries.ProductSeriesUbuntuPackagingView"
2025 permission="launchpad.AnyPerson"2025 permission="launchpad.Edit"
2026 template="../templates/productseries-ubuntupkg.pt"2026 template="../templates/productseries-ubuntupkg.pt"
2027 />2027 />
2028 <browser:pages2028 <browser:pages
@@ -2621,7 +2621,7 @@
2621 name="+edit-packaging"2621 name="+edit-packaging"
2622 for="lp.registry.interfaces.sourcepackage.ISourcePackage"2622 for="lp.registry.interfaces.sourcepackage.ISourcePackage"
2623 class="lp.registry.browser.sourcepackage.SourcePackageChangeUpstreamView"2623 class="lp.registry.browser.sourcepackage.SourcePackageChangeUpstreamView"
2624 permission="launchpad.AnyPerson"2624 permission="launchpad.Edit"
2625 template="../templates/sourcepackage-edit-packaging.pt"2625 template="../templates/sourcepackage-edit-packaging.pt"
2626 />2626 />
2627 <browser:page2627 <browser:page
diff --git a/lib/lp/registry/browser/productseries.py b/lib/lp/registry/browser/productseries.py
index 8759e95..9b05317 100644
--- a/lib/lp/registry/browser/productseries.py
+++ b/lib/lp/registry/browser/productseries.py
@@ -278,7 +278,7 @@ class ProductSeriesOverviewMenu(
278 summary = "Change the branch for this series"278 summary = "Change the branch for this series"
279 return Link("+setbranch", text, summary, icon=icon)279 return Link("+setbranch", text, summary, icon=icon)
280280
281 @enabled_with_permission("launchpad.AnyPerson")281 @enabled_with_permission("launchpad.Edit")
282 def ubuntupkg(self):282 def ubuntupkg(self):
283 """Return a link to link this series to an ubuntu sourcepackage."""283 """Return a link to link this series to an ubuntu sourcepackage."""
284 text = "Link to Ubuntu package"284 text = "Link to Ubuntu package"
diff --git a/lib/lp/registry/browser/sourcepackage.py b/lib/lp/registry/browser/sourcepackage.py
index 3230cf4..461a963 100644
--- a/lib/lp/registry/browser/sourcepackage.py
+++ b/lib/lp/registry/browser/sourcepackage.py
@@ -34,6 +34,7 @@ from zope.schema.vocabulary import (
34 SimpleVocabulary,34 SimpleVocabulary,
35 getVocabularyRegistry,35 getVocabularyRegistry,
36)36)
37from zope.security.interfaces import Unauthorized
3738
38from lp import _39from lp import _
39from lp.app.browser.launchpadform import (40from lp.app.browser.launchpadform import (
@@ -61,9 +62,11 @@ from lp.services.webapp import (
61 canonical_url,62 canonical_url,
62 stepto,63 stepto,
63)64)
65from lp.services.webapp.authorization import check_permission
64from lp.services.webapp.breadcrumb import Breadcrumb66from lp.services.webapp.breadcrumb import Breadcrumb
65from lp.services.webapp.escaping import structured67from lp.services.webapp.escaping import structured
66from lp.services.webapp.interfaces import IBreadcrumb68from lp.services.webapp.interfaces import IBreadcrumb
69from lp.services.webapp.menu import enabled_with_permission
67from lp.services.webapp.publisher import LaunchpadView70from lp.services.webapp.publisher import LaunchpadView
68from lp.services.worlddata.helpers import browser_languages71from lp.services.worlddata.helpers import browser_languages
69from lp.services.worlddata.interfaces.country import ICountry72from lp.services.worlddata.interfaces.country import ICountry
@@ -214,13 +217,9 @@ class SourcePackageOverviewMenu(ApplicationMenu):
214 def copyright(self):217 def copyright(self):
215 return Link("+copyright", "View copyright", icon="info")218 return Link("+copyright", "View copyright", icon="info")
216219
220 @enabled_with_permission("launchpad.Edit")
217 def edit_packaging(self):221 def edit_packaging(self):
218 return Link(222 return Link("+edit-packaging", "Change upstream link", icon="edit")
219 "+edit-packaging",
220 "Change upstream link",
221 icon="edit",
222 enabled=self.userCanDeletePackaging(),
223 )
224223
225 def remove_packaging(self):224 def remove_packaging(self):
226 return Link(225 return Link(
@@ -230,13 +229,9 @@ class SourcePackageOverviewMenu(ApplicationMenu):
230 enabled=self.userCanDeletePackaging(),229 enabled=self.userCanDeletePackaging(),
231 )230 )
232231
232 @enabled_with_permission("launchpad.Edit")
233 def set_upstream(self):233 def set_upstream(self):
234 return Link(234 return Link("+edit-packaging", "Set upstream link", icon="add")
235 "+edit-packaging",
236 "Set upstream link",
237 icon="add",
238 enabled=self.userCanDeletePackaging(),
239 )
240235
241 def builds(self):236 def builds(self):
242 text = "Show builds"237 text = "Show builds"
@@ -245,8 +240,8 @@ class SourcePackageOverviewMenu(ApplicationMenu):
245 def userCanDeletePackaging(self):240 def userCanDeletePackaging(self):
246 packaging = self.context.direct_packaging241 packaging = self.context.direct_packaging
247 if packaging is None:242 if packaging is None:
248 return True243 return False
249 return packaging.userCanDelete()244 return check_permission("launchpad.Edit", packaging)
250245
251246
252class SourcePackageChangeUpstreamStepOne(ReturnToReferrerMixin, StepView):247class SourcePackageChangeUpstreamStepOne(ReturnToReferrerMixin, StepView):
@@ -425,10 +420,23 @@ class SourcePackageRemoveUpstreamView(
425 label = "Unlink an upstream project"420 label = "Unlink an upstream project"
426 page_title = label421 page_title = label
427422
423 def initialize(self):
424 self.packaging = self.context.direct_packaging
425 if self.packaging is not None:
426 # We do permission check here rather than protecting the view
427 # in ZCML because product owners should also be allowed to unlink
428 # projects, even if they don't have edit permission on the source
429 # package.
430 if not check_permission("launchpad.Edit", self.packaging):
431 raise Unauthorized(
432 "You are not allowed to unlink an upstream project"
433 )
434 super().initialize()
435
428 @action("Unlink")436 @action("Unlink")
429 def unlink(self, action, data):437 def unlink(self, action, data):
430 old_series = self.context.productseries438 old_series = self.context.productseries
431 if self.context.direct_packaging is not None:439 if self.packaging is not None:
432 getUtility(IPackagingUtil).deletePackaging(440 getUtility(IPackagingUtil).deletePackaging(
433 self.context.productseries,441 self.context.productseries,
434 self.context.sourcepackagename,442 self.context.sourcepackagename,
diff --git a/lib/lp/registry/browser/tests/packaging-views.rst b/lib/lp/registry/browser/tests/packaging-views.rst
index 3610369..260ec7f 100644
--- a/lib/lp/registry/browser/tests/packaging-views.rst
+++ b/lib/lp/registry/browser/tests/packaging-views.rst
@@ -30,6 +30,7 @@ The view has a label and requires a distro series and a source package name.
30The distroseries field's vocabulary is the same as the ubuntu.series30The distroseries field's vocabulary is the same as the ubuntu.series
31attribute.31attribute.
3232
33 >>> _ = login_person(product.owner)
33 >>> view = create_view(productseries, "+ubuntupkg")34 >>> view = create_view(productseries, "+ubuntupkg")
34 >>> print(view.label)35 >>> print(view.label)
35 Ubuntu source packaging36 Ubuntu source packaging
@@ -200,8 +201,7 @@ and a new entry can be added to the packaging history.
200 ... )201 ... )
201 >>> grumpy_series.status = SeriesStatus.FROZEN202 >>> grumpy_series.status = SeriesStatus.FROZEN
202203
203 >>> a_user = factory.makePerson(name="hedgehog")204 >>> _ = login_person(product.owner)
204 >>> ignored = login_person(a_user)
205 >>> form = {205 >>> form = {
206 ... "field.sourcepackagename": "hot",206 ... "field.sourcepackagename": "hot",
207 ... "field.actions.continue": "Update",207 ... "field.actions.continue": "Update",
diff --git a/lib/lp/registry/browser/tests/sourcepackage-views.rst b/lib/lp/registry/browser/tests/sourcepackage-views.rst
index b759ece..32a971a 100644
--- a/lib/lp/registry/browser/tests/sourcepackage-views.rst
+++ b/lib/lp/registry/browser/tests/sourcepackage-views.rst
@@ -8,8 +8,9 @@ Edit packaging view
8 >>> productseries = factory.makeProductSeries(8 >>> productseries = factory.makeProductSeries(
9 ... name="crazy", product=product9 ... name="crazy", product=product
10 ... )10 ... )
11 >>> distro_owner = factory.makePerson()
11 >>> distribution = factory.makeDistribution(12 >>> distribution = factory.makeDistribution(
12 ... name="youbuntu", displayname="Youbuntu"13 ... name="youbuntu", displayname="Youbuntu", owner=distro_owner
13 ... )14 ... )
14 >>> distroseries = factory.makeDistroSeries(15 >>> distroseries = factory.makeDistroSeries(
15 ... name="busy", distribution=distribution16 ... name="busy", distribution=distribution
@@ -53,7 +54,7 @@ This is a multistep view. In the first step, the product is specified.
53 >>> print(view.view.request.form)54 >>> print(view.view.request.form)
54 {'field.__visited_steps__': 'sourcepackage_change_upstream_step1'}55 {'field.__visited_steps__': 'sourcepackage_change_upstream_step1'}
5556
56 >>> ignored = login_person(product.owner)57 >>> _ = login_person(distro_owner)
57 >>> form = {58 >>> form = {
58 ... "field.product": "bonkers",59 ... "field.product": "bonkers",
59 ... "field.actions.continue": "Continue",60 ... "field.actions.continue": "Continue",
@@ -63,7 +64,7 @@ This is a multistep view. In the first step, the product is specified.
63 ... package,64 ... package,
64 ... name="+edit-packaging",65 ... name="+edit-packaging",
65 ... form=form,66 ... form=form,
66 ... principal=product.owner,67 ... principal=distro_owner,
67 ... )68 ... )
68 >>> view.view.errors69 >>> view.view.errors
69 []70 []
@@ -88,7 +89,7 @@ product can be chosen from a list of options.
88 ... package,89 ... package,
89 ... name="+edit-packaging",90 ... name="+edit-packaging",
90 ... form=form,91 ... form=form,
91 ... principal=product.owner,92 ... principal=distro_owner,
92 ... )93 ... )
9394
94 >>> ignored = view.view.render()95 >>> ignored = view.view.render()
@@ -124,7 +125,7 @@ then the current product series will be the selected option.
124 ... package,125 ... package,
125 ... name="+edit-packaging",126 ... name="+edit-packaging",
126 ... form=form,127 ... form=form,
127 ... principal=product.owner,128 ... principal=distro_owner,
128 ... )129 ... )
129 >>> print(view.view.widgets.get("productseries")._getFormValue().name)130 >>> print(view.view.widgets.get("productseries")._getFormValue().name)
130 crazy131 crazy
@@ -141,7 +142,7 @@ empty.
141 ... package,142 ... package,
142 ... name="+edit-packaging",143 ... name="+edit-packaging",
143 ... form=form,144 ... form=form,
144 ... principal=product.owner,145 ... principal=distro_owner,
145 ... )146 ... )
146 >>> for error in view.view.errors:147 >>> for error in view.view.errors:
147 ... print(pretty(error.args))148 ... print(pretty(error.args))
@@ -161,7 +162,7 @@ but there is no notification message that the upstream link was updated.
161 ... package,162 ... package,
162 ... name="+edit-packaging",163 ... name="+edit-packaging",
163 ... form=form,164 ... form=form,
164 ... principal=product.owner,165 ... principal=distro_owner,
165 ... )166 ... )
166 >>> print(view.view)167 >>> print(view.view)
167 <...SourcePackageChangeUpstreamStepTwo object...>168 <...SourcePackageChangeUpstreamStepTwo object...>
@@ -382,11 +383,12 @@ to the project series.
382 >>> print(view.cancel_url)383 >>> print(view.cancel_url)
383 http://launchpad.test/youbuntu/wonky/+source/stinkypackage384 http://launchpad.test/youbuntu/wonky/+source/stinkypackage
384385
385 >>> user = package.packaging.owner
386 >>> ignored = login_person(user)
387 >>> form = {"field.actions.unlink": "Unlink"}386 >>> form = {"field.actions.unlink": "Unlink"}
388 >>> view = create_initialized_view(387 >>> view = create_initialized_view(
389 ... package, name="+remove-packaging", form=form, principal=user388 ... package,
389 ... name="+remove-packaging",
390 ... form=form,
391 ... principal=distro_owner,
390 ... )392 ... )
391 >>> view.errors393 >>> view.errors
392 []394 []
@@ -401,7 +403,10 @@ they get a message telling them that the link has already been
401deleted.403deleted.
402404
403 >>> view = create_initialized_view(405 >>> view = create_initialized_view(
404 ... package, name="+remove-packaging", form=form, principal=user406 ... package,
407 ... name="+remove-packaging",
408 ... form=form,
409 ... principal=distro_owner,
405 ... )410 ... )
406 >>> view.errors411 >>> view.errors
407 []412 []
diff --git a/lib/lp/registry/browser/tests/test_packaging.py b/lib/lp/registry/browser/tests/test_packaging.py
index 7549a51..d45577a 100644
--- a/lib/lp/registry/browser/tests/test_packaging.py
+++ b/lib/lp/registry/browser/tests/test_packaging.py
@@ -12,7 +12,7 @@ from lp.registry.interfaces.packaging import IPackagingUtil, PackagingType
12from lp.registry.interfaces.product import IProductSet12from lp.registry.interfaces.product import IProductSet
13from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet13from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
14from lp.services.features.testing import FeatureFixture14from lp.services.features.testing import FeatureFixture
15from lp.testing import TestCaseWithFactory, login, logout15from lp.testing import TestCaseWithFactory, login, logout, person_logged_in
16from lp.testing.layers import DatabaseFunctionalLayer16from lp.testing.layers import DatabaseFunctionalLayer
17from lp.testing.pages import setupBrowser17from lp.testing.pages import setupBrowser
18from lp.testing.views import create_initialized_view18from lp.testing.views import create_initialized_view
@@ -49,7 +49,7 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
49 )49 )
50 self.packaging_util = getUtility(IPackagingUtil)50 self.packaging_util = getUtility(IPackagingUtil)
5151
52 def test_no_error_when_trying_to_readd_same_package(self):52 def test_no_error_when_trying_to_re_add_same_package(self):
53 # There is no reason to display an error when the user's action53 # There is no reason to display an error when the user's action
54 # wouldn't cause a state change.54 # wouldn't cause a state change.
55 self.packaging_util.createPackaging(55 self.packaging_util.createPackaging(
@@ -65,9 +65,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
65 "field.sourcepackagename": self.sourcepackagename.name,65 "field.sourcepackagename": self.sourcepackagename.name,
66 "field.actions.continue": "Continue",66 "field.actions.continue": "Continue",
67 }67 }
68 view = create_initialized_view(68 with person_logged_in(self.product.owner):
69 self.productseries, "+ubuntupkg", form=form69 view = create_initialized_view(
70 )70 self.productseries,
71 "+ubuntupkg",
72 form=form,
73 principal=self.product.owner,
74 )
71 self.assertEqual([], view.errors)75 self.assertEqual([], view.errors)
7276
73 def test_cannot_link_to_linked_package(self):77 def test_cannot_link_to_linked_package(self):
@@ -78,9 +82,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
78 "field.sourcepackagename": "hot",82 "field.sourcepackagename": "hot",
79 "field.actions.continue": "Continue",83 "field.actions.continue": "Continue",
80 }84 }
81 view = create_initialized_view(85 with person_logged_in(self.product.owner):
82 self.productseries, "+ubuntupkg", form=form86 view = create_initialized_view(
83 )87 self.productseries,
88 "+ubuntupkg",
89 form=form,
90 principal=self.product.owner,
91 )
84 self.assertEqual([], view.errors)92 self.assertEqual([], view.errors)
85 other_productseries = self.factory.makeProductSeries(93 other_productseries = self.factory.makeProductSeries(
86 product=self.product, name="hottest"94 product=self.product, name="hottest"
@@ -90,9 +98,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
90 "field.sourcepackagename": "hot",98 "field.sourcepackagename": "hot",
91 "field.actions.continue": "Continue",99 "field.actions.continue": "Continue",
92 }100 }
93 view = create_initialized_view(101 with person_logged_in(self.product.owner):
94 other_productseries, "+ubuntupkg", form=form102 view = create_initialized_view(
95 )103 other_productseries,
104 "+ubuntupkg",
105 form=form,
106 principal=self.product.owner,
107 )
96 view_errors = [108 view_errors = [
97 'The <a href="http://launchpad.test/ubuntu/hoary/+source/hot">'109 'The <a href="http://launchpad.test/ubuntu/hoary/+source/hot">'
98 "hot</a> package in Hoary is already linked to another series."110 "hot</a> package in Hoary is already linked to another series."
@@ -106,9 +118,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
106 "field.sourcepackagename": "",118 "field.sourcepackagename": "",
107 "field.actions.continue": "Continue",119 "field.actions.continue": "Continue",
108 }120 }
109 view = create_initialized_view(121 with person_logged_in(self.product.owner):
110 self.productseries, "+ubuntupkg", form=form122 view = create_initialized_view(
111 )123 self.productseries,
124 "+ubuntupkg",
125 form=form,
126 principal=self.product.owner,
127 )
112 self.assertEqual(1, len(view.errors))128 self.assertEqual(1, len(view.errors))
113 self.assertEqual("sourcepackagename", view.errors[0].field_name)129 self.assertEqual("sourcepackagename", view.errors[0].field_name)
114 self.assertEqual("Required input is missing.", view.errors[0].doc())130 self.assertEqual("Required input is missing.", view.errors[0].doc())
@@ -125,9 +141,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
125 "field.sourcepackagename": "vapor",141 "field.sourcepackagename": "vapor",
126 "field.actions.continue": "Continue",142 "field.actions.continue": "Continue",
127 }143 }
128 view = create_initialized_view(144 with person_logged_in(self.product.owner):
129 self.productseries, "+ubuntupkg", form=form145 view = create_initialized_view(
130 )146 self.productseries,
147 "+ubuntupkg",
148 form=form,
149 principal=self.product.owner,
150 )
131 view_errors = ["The source package is not published in Hoary."]151 view_errors = ["The source package is not published in Hoary."]
132 self.assertEqual(view_errors, view.errors)152 self.assertEqual(view_errors, view.errors)
133153
@@ -142,9 +162,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
142 "field.sourcepackagename": "hot",162 "field.sourcepackagename": "hot",
143 "field.actions.continue": "Continue",163 "field.actions.continue": "Continue",
144 }164 }
145 view = create_initialized_view(165 with person_logged_in(self.product.owner):
146 self.productseries, "+ubuntupkg", form=form166 view = create_initialized_view(
147 )167 self.productseries,
168 "+ubuntupkg",
169 form=form,
170 principal=self.product.owner,
171 )
148 self.assertEqual([], view.errors)172 self.assertEqual([], view.errors)
149 has_packaging = self.packaging_util.packagingEntryExists(173 has_packaging = self.packaging_util.packagingEntryExists(
150 self.sourcepackagename, warty, self.productseries174 self.sourcepackagename, warty, self.productseries
diff --git a/lib/lp/registry/browser/tests/test_product.py b/lib/lp/registry/browser/tests/test_product.py
index de8f154..61618f0 100644
--- a/lib/lp/registry/browser/tests/test_product.py
+++ b/lib/lp/registry/browser/tests/test_product.py
@@ -862,7 +862,9 @@ class TestProductEditView(BrowserTestCase):
862 # It should be an error to make a Product private if it is packaged.862 # It should be an error to make a Product private if it is packaged.
863 product = self.factory.makeProduct()863 product = self.factory.makeProduct()
864 sourcepackage = self.factory.makeSourcePackage()864 sourcepackage = self.factory.makeSourcePackage()
865 sourcepackage.setPackaging(product.development_focus, product.owner)865 removeSecurityProxy(sourcepackage).setPackaging(
866 product.development_focus, product.owner
867 )
866 browser = self.getViewBrowser(product, "+edit", user=product.owner)868 browser = self.getViewBrowser(product, "+edit", user=product.owner)
867 info_type = browser.getControl(name="field.information_type")869 info_type = browser.getControl(name="field.information_type")
868 info_type.value = ["PROPRIETARY"]870 info_type.value = ["PROPRIETARY"]
diff --git a/lib/lp/registry/browser/tests/test_sourcepackage_views.py b/lib/lp/registry/browser/tests/test_sourcepackage_views.py
index 291c28a..8044c1a 100644
--- a/lib/lp/registry/browser/tests/test_sourcepackage_views.py
+++ b/lib/lp/registry/browser/tests/test_sourcepackage_views.py
@@ -244,7 +244,7 @@ class TestSourcePackageUpstreamConnectionsView(TestCaseWithFactory):
244 distroseries=distroseries,244 distroseries=distroseries,
245 version="1.5-0ubuntu1",245 version="1.5-0ubuntu1",
246 )246 )
247 self.source_package.setPackaging(247 removeSecurityProxy(self.source_package).setPackaging(
248 productseries, productseries.product.owner248 productseries, productseries.product.owner
249 )249 )
250250
@@ -301,80 +301,119 @@ class TestSourcePackagePackagingLinks(TestCaseWithFactory):
301301
302 layer = DatabaseFunctionalLayer302 layer = DatabaseFunctionalLayer
303303
304 def makeSourcePackageOverviewMenu(self, with_packaging, karma=None):304 def setUp(self, *args, **kwargs):
305 sourcepackage = self.factory.makeSourcePackage()305 super().setUp(*args, **kwargs)
306 registrant = self.factory.makePerson()306 self.sourcepackage = self.factory.makeSourcePackage()
307 if with_packaging:307 self.maintainer = self.sourcepackage.distribution.owner
308 self.factory.makePackagingLink(308 self.product_owner = self.factory.makePerson()
309 sourcepackagename=sourcepackage.sourcepackagename,309 self.product = self.factory.makeProduct(owner=self.product_owner)
310 distroseries=sourcepackage.distroseries,310 self.productseries = self.factory.makeProductSeries(self.product)
311 owner=registrant,311
312 )312 def makePackaging(self):
313 user = self.factory.makePerson(karma=karma)313 self.factory.makePackagingLink(
314 sourcepackagename=self.sourcepackage.sourcepackagename,
315 distroseries=self.sourcepackage.distroseries,
316 productseries=self.productseries,
317 )
318
319 def makeSourcePackageOverviewMenu(self, user):
314 with person_logged_in(user):320 with person_logged_in(user):
315 menu = SourcePackageOverviewMenu(sourcepackage)321 menu = SourcePackageOverviewMenu(self.sourcepackage)
316 return menu, user322 return menu
317323
318 def test_edit_packaging_link__enabled_without_packaging(self):324 def test_edit_packaging_link__enabled_without_packaging_maintainer(self):
319 # If no packging exists, the edit_packaging link is always325 # If no packaging exists, the edit_packaging link is always
320 # enabled.326 # enabled.
321 menu, user = self.makeSourcePackageOverviewMenu(False, None)327 menu = self.makeSourcePackageOverviewMenu(self.maintainer)
322 with person_logged_in(user):328 with person_logged_in(self.maintainer):
323 self.assertTrue(menu.edit_packaging().enabled)329 self.assertTrue(menu.edit_packaging().enabled)
324330
325 def test_set_upstrem_link__enabled_without_packaging(self):331 def test_set_upstream_link__enabled_without_packaging_maintainer(self):
326 # If no packging exists, the set_upstream link is always332 # If no packaging exists, the set_upstream link is always
327 # enabled.333 # enabled.
328 menu, user = self.makeSourcePackageOverviewMenu(False, None)334 menu = self.makeSourcePackageOverviewMenu(self.maintainer)
329 with person_logged_in(user):335 with person_logged_in(self.maintainer):
330 self.assertTrue(menu.set_upstream().enabled)336 self.assertTrue(menu.set_upstream().enabled)
331337
332 def test_remove_packaging_link__enabled_without_packaging(self):338 def test_remove_packaging_link__enabled_without_packaging_maintainer(self):
333 # If no packging exists, the remove_packaging link is always339 # If no packaging exists, the remove_packaging link is always
334 # enabled.340 # disabled.
335 menu, user = self.makeSourcePackageOverviewMenu(False, None)341 menu = self.makeSourcePackageOverviewMenu(self.maintainer)
336 with person_logged_in(user):342 with person_logged_in(self.maintainer):
337 self.assertTrue(menu.remove_packaging().enabled)343 self.assertFalse(menu.remove_packaging().enabled)
338344
339 def test_edit_packaging_link__enabled_with_packaging_non_probation(self):345 def test_edit_packaging_link__enabled_with_packaging_maintainer(self):
340 # If a packging exists, the edit_packaging link is enabled346 # If a packaging exists, the edit_packaging link is enabled
341 # for the non-probationary users.347 # for the package maintainer.
342 menu, user = self.makeSourcePackageOverviewMenu(True, 100)348 self.makePackaging()
343 with person_logged_in(user):349 menu = self.makeSourcePackageOverviewMenu(self.maintainer)
350 with person_logged_in(self.maintainer):
344 self.assertTrue(menu.edit_packaging().enabled)351 self.assertTrue(menu.edit_packaging().enabled)
345352
346 def test_set_upstrem_link__enabled_with_packaging_non_probation(self):353 def test_set_upstream_link__enabled_with_packaging_maintainer(self):
347 # If a packging exists, the set_upstream link is enabled354 # If a packaging exists, the set_upstream link is enabled
348 # for the non-probationary users.355 # for the package maintainer.
349 menu, user = self.makeSourcePackageOverviewMenu(True, 100)356 self.makePackaging()
350 with person_logged_in(user):357 menu = self.makeSourcePackageOverviewMenu(self.maintainer)
358 with person_logged_in(self.maintainer):
351 self.assertTrue(menu.set_upstream().enabled)359 self.assertTrue(menu.set_upstream().enabled)
352360
353 def test_remove_packaging_link__enabled_with_packaging_non_probation(self):361 def test_remove_packaging_link__enabled_with_packaging_maintainer(self):
354 # If a packging exists, the remove_packaging link is enabled362 # If a packaging exists, the remove_packaging link is enabled
355 # for the non-probationary users.363 # for the package maintainer.
356 menu, user = self.makeSourcePackageOverviewMenu(True, 100)364 self.makePackaging()
357 with person_logged_in(user):365 menu = self.makeSourcePackageOverviewMenu(self.maintainer)
366 with person_logged_in(self.maintainer):
367 self.assertTrue(menu.remove_packaging().enabled)
368
369 def test_edit_packaging_link__disabled_for_product_owner(self):
370 # If a packaging exists, the edit_packaging link is disabled
371 # for the product owner.
372 self.makePackaging()
373 menu = self.makeSourcePackageOverviewMenu(self.product_owner)
374 with person_logged_in(self.product_owner):
375 self.assertFalse(menu.edit_packaging().enabled)
376
377 def test_set_upstream_link__disabled_for_product_owner(self):
378 # If a packaging exists, the set_upstream link is disabled
379 # for the product owner.
380 self.makePackaging()
381 menu = self.makeSourcePackageOverviewMenu(self.product_owner)
382 with person_logged_in(self.product_owner):
383 self.assertFalse(menu.set_upstream().enabled)
384
385 def test_remove_packaging_link__enabled_for_product_owner(self):
386 # If a packaging exists, the remove_packaging link is enabled
387 # for product owner.
388 self.makePackaging()
389 menu = self.makeSourcePackageOverviewMenu(self.product_owner)
390 with person_logged_in(self.product_owner):
358 self.assertTrue(menu.remove_packaging().enabled)391 self.assertTrue(menu.remove_packaging().enabled)
359392
360 def test_edit_packaging_link__enabled_with_packaging_probation(self):393 def test_edit_packaging_link__enabled_with_packaging_arbitrary(self):
361 # If a packging exists, the edit_packaging link is not enabled394 # If a packaging exists, the edit_packaging link is not enabled
362 # for probationary users.395 # for arbitrary users.
363 menu, user = self.makeSourcePackageOverviewMenu(True, None)396 self.makePackaging()
397 user = self.factory.makePerson()
398 menu = self.makeSourcePackageOverviewMenu(user)
364 with person_logged_in(user):399 with person_logged_in(user):
365 self.assertFalse(menu.edit_packaging().enabled)400 self.assertFalse(menu.edit_packaging().enabled)
366401
367 def test_set_upstrem_link__enabled_with_packaging_probation(self):402 def test_set_upstream_link__enabled_with_packaging_arbitrary(self):
368 # If a packging exists, the set_upstream link is not enabled403 # If a packaging exists, the set_upstream link is not enabled
369 # for probationary users.404 # for arbitrary users.
370 menu, user = self.makeSourcePackageOverviewMenu(True, None)405 self.makePackaging()
406 user = self.factory.makePerson()
407 menu = self.makeSourcePackageOverviewMenu(user)
371 with person_logged_in(user):408 with person_logged_in(user):
372 self.assertFalse(menu.set_upstream().enabled)409 self.assertFalse(menu.set_upstream().enabled)
373410
374 def test_remove_packaging_link__enabled_with_packaging_probation(self):411 def test_remove_packaging_link__enabled_with_packaging_arbitrary(self):
375 # If a packging exists, the remove_packaging link is not enabled412 # If a packaging exists, the remove_packaging link is not enabled
376 # for probationary users.413 # for arbitrary users.
377 menu, user = self.makeSourcePackageOverviewMenu(True, None)414 self.makePackaging()
415 user = self.factory.makePerson()
416 menu = self.makeSourcePackageOverviewMenu(user)
378 with person_logged_in(user):417 with person_logged_in(user):
379 self.assertFalse(menu.remove_packaging().enabled)418 self.assertFalse(menu.remove_packaging().enabled)
380419
@@ -392,10 +431,9 @@ class TestSourcePackageChangeUpstreamView(BrowserTestCase):
392 owner=product_owner,431 owner=product_owner,
393 information_type=InformationType.PROPRIETARY,432 information_type=InformationType.PROPRIETARY,
394 )433 )
395 ubuntu_series = self.factory.makeUbuntuDistroSeries()434 sp = self.factory.makeSourcePackage()
396 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
397 browser = self.getViewBrowser(435 browser = self.getViewBrowser(
398 sp, "+edit-packaging", user=product_owner436 sp, "+edit-packaging", user=self.factory.makeAdministrator()
399 )437 )
400 browser.getControl("Project").value = product_name438 browser.getControl("Project").value = product_name
401 browser.getControl("Continue").click()439 browser.getControl("Continue").click()
@@ -413,10 +451,9 @@ class TestSourcePackageChangeUpstreamView(BrowserTestCase):
413 )451 )
414 series = self.factory.makeProductSeries(product=product)452 series = self.factory.makeProductSeries(product=product)
415 series_displayname = series.displayname453 series_displayname = series.displayname
416 ubuntu_series = self.factory.makeUbuntuDistroSeries()454 sp = self.factory.makeSourcePackage()
417 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
418 browser = self.getViewBrowser(455 browser = self.getViewBrowser(
419 sp, "+edit-packaging", user=product_owner456 sp, "+edit-packaging", user=self.factory.makeAdministrator()
420 )457 )
421 browser.getControl("Project").value = product_name458 browser.getControl("Project").value = product_name
422 browser.getControl("Continue").click()459 browser.getControl("Continue").click()
diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
index f2d0433..585bbf1 100644
--- a/lib/lp/registry/configure.zcml
+++ b/lib/lp/registry/configure.zcml
@@ -2188,8 +2188,10 @@
2188 <allow2188 <allow
2189 interface="lp.registry.interfaces.packaging.IPackaging"/>2189 interface="lp.registry.interfaces.packaging.IPackaging"/>
2190 <require2190 <require
2191 permission="zope.Public"2191 permission="launchpad.Edit"
2192 set_schema="lp.registry.interfaces.packaging.IPackaging"/>2192 set_schema="lp.registry.interfaces.packaging.IPackaging"
2193 attributes="
2194 destroySelf" />
2193 </class>2195 </class>
21942196
2195 <!-- PackagingUtil -->2197 <!-- PackagingUtil -->
diff --git a/lib/lp/registry/interfaces/packaging.py b/lib/lp/registry/interfaces/packaging.py
index 5ebddd5..ae16be5 100644
--- a/lib/lp/registry/interfaces/packaging.py
+++ b/lib/lp/registry/interfaces/packaging.py
@@ -104,14 +104,6 @@ class IPackaging(IHasOwner):
104 )104 )
105 )105 )
106106
107 def userCanDelete():
108 """True, if the current user is allowed to delete this packaging,
109 else False.
110
111 Non-probationary users can delete packaging links that they believe
112 connect Ubuntu to bogus data.
113 """
114
115107
116class IPackagingUtil(Interface):108class IPackagingUtil(Interface):
117 """Utilities to handle Packaging."""109 """Utilities to handle Packaging."""
@@ -121,6 +113,9 @@ class IPackagingUtil(Interface):
121 ):113 ):
122 """Create Packaging entry."""114 """Create Packaging entry."""
123115
116 def get(productseries, sourcepackagename, distroseries):
117 """Get Packaging entry."""
118
124 def deletePackaging(productseries, sourcepackagename, distroseries):119 def deletePackaging(productseries, sourcepackagename, distroseries):
125 """Delete a packaging entry."""120 """Delete a packaging entry."""
126121
diff --git a/lib/lp/registry/interfaces/productseries.py b/lib/lp/registry/interfaces/productseries.py
index 62e1ef5..f985de4 100644
--- a/lib/lp/registry/interfaces/productseries.py
+++ b/lib/lp/registry/interfaces/productseries.py
@@ -102,6 +102,11 @@ class IProductSeriesEditRestricted(Interface):
102 def newMilestone(name, dateexpected=None, summary=None, code_name=None):102 def newMilestone(name, dateexpected=None, summary=None, code_name=None):
103 """Create a new milestone for this ProjectSeries."""103 """Create a new milestone for this ProjectSeries."""
104104
105 def setPackaging(distroseries, sourcepackagename, owner):
106 """Create or update a Packaging record for this product series,
107 connecting it to the given distroseries and source package name.
108 """
109
105110
106class IProductSeriesPublic(Interface):111class IProductSeriesPublic(Interface):
107 """Public IProductSeries properties."""112 """Public IProductSeries properties."""
@@ -343,11 +348,6 @@ class IProductSeriesView(
343 """Return the SourcePackage that packages this project in Ubuntu's348 """Return the SourcePackage that packages this project in Ubuntu's
344 translation focus or current series or any series, in that order."""349 translation focus or current series or any series, in that order."""
345350
346 def setPackaging(distroseries, sourcepackagename, owner):
347 """Create or update a Packaging record for this product series,
348 connecting it to the given distroseries and source package name.
349 """
350
351 def getPackagingInDistribution(distribution):351 def getPackagingInDistribution(distribution):
352 """Return all the Packaging entries for this product series for the352 """Return all the Packaging entries for this product series for the
353 given distribution. Note that this only returns EXPLICT packaging353 given distribution. Note that this only returns EXPLICT packaging
diff --git a/lib/lp/registry/interfaces/sourcepackage.py b/lib/lp/registry/interfaces/sourcepackage.py
index d2a4ad2..4cc038e 100644
--- a/lib/lp/registry/interfaces/sourcepackage.py
+++ b/lib/lp/registry/interfaces/sourcepackage.py
@@ -224,32 +224,6 @@ class ISourcePackagePublic(
224 sourcepackagename compare not equal.224 sourcepackagename compare not equal.
225 """225 """
226226
227 @operation_parameters(productseries=Reference(schema=IProductSeries))
228 @call_with(owner=REQUEST_USER)
229 @export_write_operation()
230 @operation_for_version("devel")
231 def setPackaging(productseries, owner):
232 """Update the existing packaging record, or create a new packaging
233 record, that links the source package to the given productseries,
234 and record that it was done by the owner.
235 """
236
237 @operation_parameters(productseries=Reference(schema=IProductSeries))
238 @call_with(owner=REQUEST_USER)
239 @export_write_operation()
240 @operation_for_version("devel")
241 def setPackagingReturnSharingDetailPermissions(productseries, owner):
242 """Like setPackaging(), but returns getSharingDetailPermissions().
243
244 This method is intended for AJAX usage on the +sharing-details
245 page.
246 """
247
248 @export_write_operation()
249 @operation_for_version("devel")
250 def deletePackaging():
251 """Delete the packaging for this sourcepackage."""
252
253 def getSharingDetailPermissions():227 def getSharingDetailPermissions():
254 """Return a dictionary of user permissions for +sharing-details page.228 """Return a dictionary of user permissions for +sharing-details page.
255229
@@ -364,6 +338,21 @@ class ISourcePackageEdit(Interface):
364 :return: None338 :return: None
365 """339 """
366340
341 @operation_parameters(productseries=Reference(schema=IProductSeries))
342 @call_with(owner=REQUEST_USER)
343 @export_write_operation()
344 @operation_for_version("devel")
345 def setPackaging(productseries, owner):
346 """Update the existing packaging record, or create a new packaging
347 record, that links the source package to the given productseries,
348 and record that it was done by the owner.
349 """
350
351 @export_write_operation()
352 @operation_for_version("devel")
353 def deletePackaging():
354 """Delete the packaging for this sourcepackage."""
355
367356
368@exported_as_webservice_entry(as_of="beta")357@exported_as_webservice_entry(as_of="beta")
369class ISourcePackage(ISourcePackagePublic, ISourcePackageEdit):358class ISourcePackage(ISourcePackagePublic, ISourcePackageEdit):
diff --git a/lib/lp/registry/model/packaging.py b/lib/lp/registry/model/packaging.py
index 2735a16..047b0e5 100644
--- a/lib/lp/registry/model/packaging.py
+++ b/lib/lp/registry/model/packaging.py
@@ -7,10 +7,8 @@ from lazr.lifecycle.event import ObjectCreatedEvent, ObjectDeletedEvent
7from zope.component import getUtility7from zope.component import getUtility
8from zope.event import notify8from zope.event import notify
9from zope.interface import implementer9from zope.interface import implementer
10from zope.security.interfaces import Unauthorized
1110
12from lp.app.enums import InformationType11from lp.app.enums import InformationType
13from lp.app.interfaces.launchpad import ILaunchpadCelebrities
14from lp.registry.errors import CannotPackageProprietaryProduct12from lp.registry.errors import CannotPackageProprietaryProduct
15from lp.registry.interfaces.packaging import (13from lp.registry.interfaces.packaging import (
16 IPackaging,14 IPackaging,
@@ -23,7 +21,6 @@ from lp.services.database.datetimecol import UtcDateTimeCol
23from lp.services.database.enumcol import DBEnum21from lp.services.database.enumcol import DBEnum
24from lp.services.database.sqlbase import SQLBase22from lp.services.database.sqlbase import SQLBase
25from lp.services.database.sqlobject import ForeignKey23from lp.services.database.sqlobject import ForeignKey
26from lp.services.webapp.interfaces import ILaunchBag
2724
2825
29@implementer(IPackaging)26@implementer(IPackaging)
@@ -66,29 +63,7 @@ class Packaging(SQLBase):
66 super().__init__(**kwargs)63 super().__init__(**kwargs)
67 notify(ObjectCreatedEvent(self))64 notify(ObjectCreatedEvent(self))
6865
69 def userCanDelete(self):
70 """See `IPackaging`."""
71 user = getUtility(ILaunchBag).user
72 if user is None:
73 return False
74 admin = getUtility(ILaunchpadCelebrities).admin
75 registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
76 if (
77 not user.is_probationary
78 or user.inTeam(self.productseries.product.owner)
79 or user.canAccess(self.sourcepackage, "setBranch")
80 or user.inTeam(registry_experts)
81 or user.inTeam(admin)
82 ):
83 return True
84 return False
85
86 def destroySelf(self):66 def destroySelf(self):
87 if not self.userCanDelete():
88 raise Unauthorized(
89 "Only the person who created the packaging and package "
90 "maintainers can delete it."
91 )
92 notify(ObjectDeletedEvent(self))67 notify(ObjectDeletedEvent(self))
93 super().destroySelf()68 super().destroySelf()
9469
@@ -97,16 +72,15 @@ class Packaging(SQLBase):
97class PackagingUtil:72class PackagingUtil:
98 """Utilities for Packaging."""73 """Utilities for Packaging."""
9974
100 @classmethod
101 def createPackaging(75 def createPackaging(
102 cls, productseries, sourcepackagename, distroseries, packaging, owner76 self, productseries, sourcepackagename, distroseries, packaging, owner
103 ):77 ):
104 """See `IPackaging`.78 """See `IPackaging`.
10579
106 Raises an assertion error if there is already packaging for80 Raises an assertion error if there is already packaging for
107 the sourcepackagename in the distroseries.81 the sourcepackagename in the distroseries.
108 """82 """
109 if cls.packagingEntryExists(sourcepackagename, distroseries):83 if self.packagingEntryExists(sourcepackagename, distroseries):
110 raise AssertionError(84 raise AssertionError(
111 "A packaging entry for %s in %s already exists."85 "A packaging entry for %s in %s already exists."
112 % (sourcepackagename.name, distroseries.name)86 % (sourcepackagename.name, distroseries.name)
@@ -132,9 +106,18 @@ class PackagingUtil:
132 owner=owner,106 owner=owner,
133 )107 )
134108
109 def get(self, productseries, sourcepackagename, distroseries):
110 criteria = {
111 "sourcepackagename": sourcepackagename,
112 "distroseries": distroseries,
113 }
114 if productseries is not None:
115 criteria["productseries"] = productseries
116 return Packaging.selectOneBy(**criteria)
117
135 def deletePackaging(self, productseries, sourcepackagename, distroseries):118 def deletePackaging(self, productseries, sourcepackagename, distroseries):
136 """See `IPackaging`."""119 """See `IPackaging`."""
137 packaging = Packaging.selectOneBy(120 packaging = getUtility(IPackagingUtil).get(
138 productseries=productseries,121 productseries=productseries,
139 sourcepackagename=sourcepackagename,122 sourcepackagename=sourcepackagename,
140 distroseries=distroseries,123 distroseries=distroseries,
@@ -152,17 +135,13 @@ class PackagingUtil:
152 )135 )
153 packaging.destroySelf()136 packaging.destroySelf()
154137
155 @staticmethod
156 def packagingEntryExists(138 def packagingEntryExists(
157 sourcepackagename, distroseries, productseries=None139 self, sourcepackagename, distroseries, productseries=None
158 ):140 ):
159 """See `IPackaging`."""141 """See `IPackaging`."""
160 criteria = dict(142 packaging = self.get(
161 sourcepackagename=sourcepackagename, distroseries=distroseries143 productseries=productseries,
144 sourcepackagename=sourcepackagename,
145 distroseries=distroseries,
162 )146 )
163 if productseries is not None:147 return packaging is not None
164 criteria["productseries"] = productseries
165 result = Packaging.selectOneBy(**criteria)
166 if result is None:
167 return False
168 return True
diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py
index 9574999..10d30ea 100644
--- a/lib/lp/registry/model/productseries.py
+++ b/lib/lp/registry/model/productseries.py
@@ -17,6 +17,7 @@ from storm.locals import And, Desc
17from storm.store import Store17from storm.store import Store
18from zope.component import getUtility18from zope.component import getUtility
19from zope.interface import implementer19from zope.interface import implementer
20from zope.security.proxy import removeSecurityProxy
2021
21from lp.app.enums import service_uses_launchpad22from lp.app.enums import service_uses_launchpad
22from lp.app.errors import NotFoundError23from lp.app.errors import NotFoundError
@@ -35,7 +36,7 @@ from lp.bugs.model.structuralsubscription import (
35 StructuralSubscriptionTargetMixin,36 StructuralSubscriptionTargetMixin,
36)37)
37from lp.registry.errors import ProprietaryPillar38from lp.registry.errors import ProprietaryPillar
38from lp.registry.interfaces.packaging import PackagingType39from lp.registry.interfaces.packaging import IPackagingUtil, PackagingType
39from lp.registry.interfaces.person import validate_person40from lp.registry.interfaces.person import validate_person
40from lp.registry.interfaces.productrelease import IProductReleaseSet41from lp.registry.interfaces.productrelease import IProductReleaseSet
41from lp.registry.interfaces.productseries import (42from lp.registry.interfaces.productseries import (
@@ -45,7 +46,6 @@ from lp.registry.interfaces.productseries import (
45)46)
46from lp.registry.interfaces.series import SeriesStatus47from lp.registry.interfaces.series import SeriesStatus
47from lp.registry.model.milestone import HasMilestonesMixin, Milestone48from lp.registry.model.milestone import HasMilestonesMixin, Milestone
48from lp.registry.model.packaging import PackagingUtil
49from lp.registry.model.productrelease import ProductRelease49from lp.registry.model.productrelease import ProductRelease
50from lp.registry.model.series import SeriesMixin50from lp.registry.model.series import SeriesMixin
51from lp.services.database.constants import UTC_NOW51from lp.services.database.constants import UTC_NOW
@@ -445,14 +445,14 @@ class ProductSeries(
445445
446 # ok, we didn't find a packaging record that matches, let's go ahead446 # ok, we didn't find a packaging record that matches, let's go ahead
447 # and create one447 # and create one
448 pkg = PackagingUtil.createPackaging(448 pkg = getUtility(IPackagingUtil).createPackaging(
449 distroseries=distroseries,449 distroseries=distroseries,
450 sourcepackagename=sourcepackagename,450 sourcepackagename=sourcepackagename,
451 productseries=self,451 productseries=self,
452 packaging=PackagingType.PRIME,452 packaging=PackagingType.PRIME,
453 owner=owner,453 owner=owner,
454 )454 )
455 pkg.sync() # convert UTC_NOW to actual datetime455 removeSecurityProxy(pkg).sync() # convert UTC_NOW to actual datetime
456 return pkg456 return pkg
457457
458 def getPackagingInDistribution(self, distribution):458 def getPackagingInDistribution(self, distribution):
diff --git a/lib/lp/registry/model/sourcepackage.py b/lib/lp/registry/model/sourcepackage.py
index 1bfacba..8a0280b 100644
--- a/lib/lp/registry/model/sourcepackage.py
+++ b/lib/lp/registry/model/sourcepackage.py
@@ -33,14 +33,14 @@ from lp.code.model.seriessourcepackagebranch import (
33 SeriesSourcePackageBranchSet,33 SeriesSourcePackageBranchSet,
34)34)
35from lp.registry.interfaces.distribution import NoPartnerArchive35from lp.registry.interfaces.distribution import NoPartnerArchive
36from lp.registry.interfaces.packaging import PackagingType36from lp.registry.interfaces.packaging import IPackagingUtil, PackagingType
37from lp.registry.interfaces.pocket import PackagePublishingPocket37from lp.registry.interfaces.pocket import PackagePublishingPocket
38from lp.registry.interfaces.sourcepackage import (38from lp.registry.interfaces.sourcepackage import (
39 ISourcePackage,39 ISourcePackage,
40 ISourcePackageFactory,40 ISourcePackageFactory,
41)41)
42from lp.registry.model.hasdrivers import HasDriversMixin42from lp.registry.model.hasdrivers import HasDriversMixin
43from lp.registry.model.packaging import Packaging, PackagingUtil43from lp.registry.model.packaging import Packaging
44from lp.registry.model.suitesourcepackage import SuiteSourcePackage44from lp.registry.model.suitesourcepackage import SuiteSourcePackage
45from lp.services.database.decoratedresultset import DecoratedResultSet45from lp.services.database.decoratedresultset import DecoratedResultSet
46from lp.services.database.interfaces import IStore46from lp.services.database.interfaces import IStore
@@ -562,7 +562,7 @@ class SourcePackage(
562 # Delete the current packaging and create a new one so562 # Delete the current packaging and create a new one so
563 # that the translation sharing jobs are started.563 # that the translation sharing jobs are started.
564 self.direct_packaging.destroySelf()564 self.direct_packaging.destroySelf()
565 PackagingUtil.createPackaging(565 getUtility(IPackagingUtil).createPackaging(
566 distroseries=self.distroseries,566 distroseries=self.distroseries,
567 sourcepackagename=self.sourcepackagename,567 sourcepackagename=self.sourcepackagename,
568 productseries=productseries,568 productseries=productseries,
@@ -572,11 +572,6 @@ class SourcePackage(
572 # and make sure this change is immediately available572 # and make sure this change is immediately available
573 flush_database_updates()573 flush_database_updates()
574574
575 def setPackagingReturnSharingDetailPermissions(self, productseries, owner):
576 """See `ISourcePackage`."""
577 self.setPackaging(productseries, owner)
578 return self.getSharingDetailPermissions()
579
580 def getSharingDetailPermissions(self):575 def getSharingDetailPermissions(self):
581 user = getUtility(ILaunchBag).user576 user = getUtility(ILaunchBag).user
582 productseries = self.productseries577 productseries = self.productseries
@@ -595,8 +590,8 @@ class SourcePackage(
595 else:590 else:
596 permissions.update(591 permissions.update(
597 {592 {
598 "user_can_change_product_series": (593 "user_can_change_product_series": user.canAccess(
599 self.direct_packaging.userCanDelete()594 self, "setPackaging"
600 ),595 ),
601 "user_can_change_branch": user.canWrite(596 "user_can_change_branch": user.canWrite(
602 productseries, "branch"597 productseries, "branch"
diff --git a/lib/lp/registry/security.py b/lib/lp/registry/security.py
index 2e14ae0..ad48865 100644
--- a/lib/lp/registry/security.py
+++ b/lib/lp/registry/security.py
@@ -219,8 +219,14 @@ class EditProduct(EditByOwnersOrAdmins):
219 )219 )
220220
221221
222class EditPackaging(EditByOwnersOrAdmins):222class EditPackaging(AuthorizationBase):
223 usedfor = IPackaging223 usedfor = IPackaging
224 permission = "launchpad.Edit"
225
226 def checkAuthenticated(self, user):
227 return self.forwardCheckAuthenticated(
228 user, self.obj.productseries
229 ) or self.forwardCheckAuthenticated(user, self.obj.sourcepackage)
224230
225231
226class DownloadFullSourcePackageTranslations(OnlyRosettaExpertsAndAdmins):232class DownloadFullSourcePackageTranslations(OnlyRosettaExpertsAndAdmins):
diff --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
index f87fa18..d53abcf 100644
--- a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.rst
+++ b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.rst
@@ -5,12 +5,12 @@ When two browsers are used to concurrently delete the same packaging
5association, only one of them can succeed. The other one does not oops5association, only one of them can succeed. The other one does not oops
6and displays a meaningful error message.6and displays a meaningful error message.
77
8The No Privilege User may open the same page in two browser tabs.8The package maintainer may open the same page in two browser tabs.
99
10 >>> first_browser = setupBrowser(auth="Basic test@canonical.com:test")10 >>> first_browser = setupBrowser(auth="Basic admin@canonical.com:test")
11 >>> first_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")11 >>> first_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
1212
13 >>> second_browser = setupBrowser(auth="Basic test@canonical.com:test")13 >>> second_browser = setupBrowser(auth="Basic admin@canonical.com:test")
14 >>> second_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")14 >>> second_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
1515
16Then the user click the "Delete Link" button in the first tab. The16Then the user click the "Delete Link" button in the first tab. The
diff --git a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.rst b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.rst
index c616717..3f66179 100644
--- a/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.rst
+++ b/lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.rst
@@ -18,7 +18,7 @@ source package in each series of this distribution.
18 >>> anon_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")18 >>> anon_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
19 >>> content = anon_browser.contents19 >>> content = anon_browser.contents
20 >>> print(extract_text(find_tag_by_id(content, "packages_list")))20 >>> print(extract_text(find_tag_by_id(content, "packages_list")))
21 The Hoary Hedgehog Release (active development) Set upstream link21 The Hoary Hedgehog Release (active development)
22 1.0.9a-4ubuntu1 release (main) 2005-09-1522 1.0.9a-4ubuntu1 release (main) 2005-09-15
23 The Warty Warthog Release (current stable release) alsa-utils trunk series23 The Warty Warthog Release (current stable release) alsa-utils trunk series
24 1.0.9a-4 release (main) 2005-09-1624 1.0.9a-4 release (main) 2005-09-16
@@ -28,12 +28,12 @@ source package in each series of this distribution.
28Delete Link Button28Delete Link Button
29------------------29------------------
3030
31A button is displayed to authenticated users to delete existing31A button is displayed to project owners to delete existing
32packaging links.32packaging links.
3333
34 >>> user_browser = setupBrowser(auth="Basic test@canonical.com:test")34 >>> owner_browser = setupBrowser(auth="Basic mark@example.com:test")
35 >>> user_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")35 >>> owner_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
36 >>> link = user_browser.getLink(36 >>> link = owner_browser.getLink(
37 ... url="/ubuntu/warty/+source/alsa-utils/+remove-packaging"37 ... url="/ubuntu/warty/+source/alsa-utils/+remove-packaging"
38 ... )38 ... )
39 >>> print(link)39 >>> print(link)
@@ -49,12 +49,12 @@ This button is not displayed to anonymous users.
4949
50Clicking this button deletes the corresponding packaging association.50Clicking this button deletes the corresponding packaging association.
5151
52 >>> link = user_browser.getLink(52 >>> link = owner_browser.getLink(
53 ... url="/ubuntu/warty/+source/alsa-utils/+remove-packaging"53 ... url="/ubuntu/warty/+source/alsa-utils/+remove-packaging"
54 ... )54 ... )
55 >>> link.click()55 >>> link.click()
56 >>> user_browser.getControl("Unlink").click()56 >>> owner_browser.getControl("Unlink").click()
57 >>> content = user_browser.contents57 >>> content = owner_browser.contents
58 >>> for tag in find_tags_by_class(content, "error"):58 >>> for tag in find_tags_by_class(content, "error"):
59 ... print(extract_text(tag))59 ... print(extract_text(tag))
60 ...60 ...
diff --git a/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst b/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst
index 9729757..71e645e 100644
--- a/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst
+++ b/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst
@@ -10,25 +10,20 @@ Create test data.
10 >>> test_publisher.updatePackageCache(test_data["distroseries"])10 >>> test_publisher.updatePackageCache(test_data["distroseries"])
11 >>> logout()11 >>> logout()
1212
13No Privileges Person visit the distroseries upstream links page for Hoary13A person with permissions to edit packaging visits the distroseries upstream
14and sees that pmount is not linked.14links page for Hoary and sees that pmount is not linked.
1515
16 >>> user_browser.open(16 >>> browser = setupBrowser(auth="Basic limi@plone.org:test")
17 ... "http://launchpad.test/ubuntu/hoary/+needs-packaging"17 >>> browser.open("http://launchpad.test/ubuntu/hoary/+needs-packaging")
18 ... )18 >>> print(extract_text(find_tag_by_id(browser.contents, "packages")))
19 >>> print(extract_text(find_tag_by_id(user_browser.contents, "packages")))
20 Source Package Bugs Translations19 Source Package Bugs Translations
21 pmount No bugs 64 strings ...20 pmount No bugs 64 strings ...
2221
23They look at the pmount source package page in Hoary and read that the22They look at the pmount source package page in Hoary and read that the
24upstream project is not set.23upstream project is not set.
2524
26 >>> user_browser.getLink("pmount").click()25 >>> browser.getLink("pmount").click()
27 >>> print(26 >>> print(extract_text(find_tag_by_id(browser.contents, "no-upstreams")))
28 ... extract_text(
29 ... find_tag_by_id(user_browser.contents, "no-upstreams")
30 ... )
31 ... )
32 Launchpad...27 Launchpad...
33 There are no projects registered in Launchpad that are a potential28 There are no projects registered in Launchpad that are a potential
34 match for this source package. Can you help us find one?29 match for this source package. Can you help us find one?
@@ -36,46 +31,36 @@ upstream project is not set.
36 Choose another upstream project31 Choose another upstream project
37 Register the upstream project32 Register the upstream project
3833
39No Privileges Person knows that the pmount package comes from the thunderbird34The person knows that the pmount package comes from the thunderbird
40project. They set the upstream packaging link and see that it is set.35project. They set the upstream packaging link and see that it is set.
4136
42 >>> user_browser.getControl(37 >>> browser.getControl("Choose another upstream project").selected = True
43 ... "Choose another upstream project"38 >>> browser.getControl("Link to Upstream Project").click()
44 ... ).selected = True39 >>> browser.getControl(name="field.product").value = "thunderbird"
45 >>> user_browser.getControl("Link to Upstream Project").click()40 >>> browser.getControl("Continue").click()
46 >>> user_browser.getControl(name="field.product").value = "thunderbird"41 >>> browser.getControl(name="field.productseries").value = ["trunk"]
47 >>> user_browser.getControl("Continue").click()42 >>> browser.getControl("Change").click()
48 >>> user_browser.getControl(name="field.productseries").value = ["trunk"]43 >>> print(extract_text(find_tag_by_id(browser.contents, "upstreams")))
49 >>> user_browser.getControl("Change").click()
50 >>> print(
51 ... extract_text(find_tag_by_id(user_browser.contents, "upstreams"))
52 ... )
53 The Mozilla Project...Mozilla Thunderbird...trunk...44 The Mozilla Project...Mozilla Thunderbird...trunk...
5445
55They see the "Show upstream links" link and take a look at the project's46They see the "Show upstream links" link and take a look at the project's
56packaging in distributions.47packaging in distributions.
5748
58 >>> user_browser.getLink("Show upstream links").click()49 >>> browser.getLink("Show upstream links").click()
59 >>> print(50 >>> print(
60 ... extract_text(51 ... extract_text(
61 ... find_tag_by_id(user_browser.contents, "distribution-series")52 ... find_tag_by_id(browser.contents, "distribution-series")
62 ... )53 ... )
63 ... )54 ... )
64 Distribution series Source package Version Project series55 Distribution series Source package Version Project series
65 Hoary (5.04) pmount 0.1-2 Mozilla Thunderbird trunk...56 Hoary (5.04) pmount 0.1-2 Mozilla Thunderbird trunk...
6657
67No Privileges Person returns to the pmount source package page, sees the58The person returns to the pmount source package page, sees the
68link to all versions and follows it to the distro source package page.59link to all versions and follows it to the distro source package page.
6960
70 >>> user_browser.getLink("pmount").click()61 >>> browser.getLink("pmount").click()
71 >>> user_browser.getLink(62 >>> browser.getLink("All versions of pmount source in Ubuntu").click()
72 ... "All versions of pmount source in Ubuntu"63 >>> print(extract_text(find_tag_by_id(browser.contents, "packages_list")))
73 ... ).click()
74 >>> print(
75 ... extract_text(
76 ... find_tag_by_id(user_browser.contents, "packages_list")
77 ... )
78 ... )
79 The Hoary Hedgehog Release (active development) ...64 The Hoary Hedgehog Release (active development) ...
80 0.1-2 release (main) 2005-08-2465 0.1-2 release (main) 2005-08-24
8166
@@ -83,73 +68,61 @@ link to all versions and follows it to the distro source package page.
83Register a project from a source package68Register a project from a source package
84----------------------------------------69----------------------------------------
8570
86No Privileges Person can register a project for a package, and Launchpad71The person can register a project for a package, and Launchpad
87will use the data from the source package to prefill the first72will use the data from the source package to prefill the first
88step of the multistep form.73step of the multistep form.
8974
90 >>> user_browser.open(75 >>> browser = setupBrowser(auth="Basic owner@youbuntu.com:test")
91 ... "http://launchpad.test/youbuntu/busy/+source/bonkers"76
92 ... )77 >>> browser.open("http://launchpad.test/youbuntu/busy/+source/bonkers")
93 >>> user_browser.getControl(78 >>> browser.getControl("Register the upstream project").selected = True
94 ... "Register the upstream project"79 >>> browser.getControl("Link to Upstream Project").click()
95 ... ).selected = True80 >>> print(browser.getControl(name="field.name").value)
96 >>> user_browser.getControl("Link to Upstream Project").click()
97 >>> print(user_browser.getControl(name="field.name").value)
98 bonkers81 bonkers
99 >>> print(user_browser.getControl(name="field.display_name").value)82 >>> print(browser.getControl(name="field.display_name").value)
100 Bonkers83 Bonkers
101 >>> print(user_browser.getControl(name="field.summary").value)84 >>> print(browser.getControl(name="field.summary").value)
102 summary for flubber-bin85 summary for flubber-bin
103 summary for flubber-lib86 summary for flubber-lib
104 >>> print(87 >>> print(extract_text(find_tag_by_id(browser.contents, "step-title")))
105 ... extract_text(find_tag_by_id(user_browser.contents, "step-title"))
106 ... )
107 Step 2 (of 2): Check for duplicate projects88 Step 2 (of 2): Check for duplicate projects
10889
109When No Privileges Person selects "Choose another upstream project" and90When the person selects "Choose another upstream project" and
110then finds out that the project doesn't exist, they use the91then finds out that the project doesn't exist, they use the
111"Link to Upstream Project" button to register the project.92"Link to Upstream Project" button to register the project.
11293
113 >>> user_browser.open(94 >>> browser.open("http://launchpad.test/youbuntu/busy/+source/bonkers/")
114 ... "http://launchpad.test/youbuntu/busy/+source/bonkers/"95 >>> browser.getControl("Choose another upstream project").selected = True
115 ... )96 >>> browser.getControl("Link to Upstream Project").click()
116 >>> user_browser.getControl(97 >>> print(browser.url)
117 ... "Choose another upstream project"
118 ... ).selected = True
119 >>> user_browser.getControl("Link to Upstream Project").click()
120 >>> print(user_browser.url)
121 http://launchpad.test/youbuntu/busy/+source/bonkers/+edit-packaging98 http://launchpad.test/youbuntu/busy/+source/bonkers/+edit-packaging
12299
123 >>> user_browser.getLink("Register the upstream project").click()100 >>> browser.getLink("Register the upstream project").click()
124 >>> print(user_browser.getControl(name="field.name").value)101 >>> print(browser.getControl(name="field.name").value)
125 bonkers102 bonkers
126 >>> print(user_browser.getControl(name="field.display_name").value)103 >>> print(browser.getControl(name="field.display_name").value)
127 Bonkers104 Bonkers
128 >>> print(user_browser.getControl(name="field.summary").value)105 >>> print(browser.getControl(name="field.summary").value)
129 summary for flubber-bin106 summary for flubber-bin
130 summary for flubber-lib107 summary for flubber-lib
131 >>> print(108 >>> print(extract_text(find_tag_by_id(browser.contents, "step-title")))
132 ... extract_text(find_tag_by_id(user_browser.contents, "step-title"))
133 ... )
134 Step 2 (of 2): Check for duplicate projects109 Step 2 (of 2): Check for duplicate projects
135110
136After No Privileges Person selects the licences, the user is redirected back111After the person selects the licences, the user is redirected back
137to the source package page and an informational message will be displayed.112to the source package page and an informational message will be displayed.
138113
139 >>> user_browser.getControl(name="field.licenses").value = ["BSD"]114 >>> browser.getControl(name="field.licenses").value = ["BSD"]
140 >>> user_browser.getControl(115 >>> browser.getControl(
141 ... "Complete registration and link to bonkers package"116 ... "Complete registration and link to bonkers package"
142 ... ).click()117 ... ).click()
143 >>> print(user_browser.url)118 >>> print(browser.url)
144 http://launchpad.test/youbuntu/busy/+source/bonkers119 http://launchpad.test/youbuntu/busy/+source/bonkers
145 >>> for tag in find_tags_by_class(120 >>> for tag in find_tags_by_class(
146 ... user_browser.contents, "informational message"121 ... browser.contents, "informational message"
147 ... ):122 ... ):
148 ... print(extract_text(tag))123 ... print(extract_text(tag))
149 Linked Bonkers project to bonkers source package.124 Linked Bonkers project to bonkers source package.
150 >>> print(125 >>> print(extract_text(find_tag_by_id(browser.contents, "upstreams")))
151 ... extract_text(find_tag_by_id(user_browser.contents, "upstreams"))
152 ... )
153 Bonkers ā‡’ trunk126 Bonkers ā‡’ trunk
154 Change upstream link127 Change upstream link
155 Remove upstream link...128 Remove upstream link...
diff --git a/lib/lp/registry/stories/product/xx-product-package-pages.rst b/lib/lp/registry/stories/product/xx-product-package-pages.rst
index 8d6f05a..8c160aa 100644
--- a/lib/lp/registry/stories/product/xx-product-package-pages.rst
+++ b/lib/lp/registry/stories/product/xx-product-package-pages.rst
@@ -42,11 +42,13 @@ Evolution.
42 >>> evo_owner.getLink(url="/ubuntu/hoary/+source/evolution") is not None42 >>> evo_owner.getLink(url="/ubuntu/hoary/+source/evolution") is not None
43 True43 True
4444
45Any logged in users can still see the links to create a packaging link.45Arbitrary users can't see the links to create a packaging link.
4646
47 >>> user_browser.open("http://launchpad.test/evolution/+packages")47 >>> user_browser.open("http://launchpad.test/evolution/+packages")
48 >>> print(user_browser.getLink(url="/evolution/trunk/+ubuntupkg").url)48 >>> user_browser.getLink(url="/evolution/trunk/+ubuntupkg")
49 http://launchpad.test/evolution/trunk/+ubuntupkg49 Traceback (most recent call last):
50 ...
51 zope.testbrowser.browser.LinkNotFoundError
5052
51 >>> anon_browser.open("http://launchpad.test/evolution/+packages")53 >>> anon_browser.open("http://launchpad.test/evolution/+packages")
52 >>> anon_browser.getLink(url="/evolution/trunk/+ubuntupkg")54 >>> anon_browser.getLink(url="/evolution/trunk/+ubuntupkg")
diff --git a/lib/lp/registry/tests/test_distroseries.py b/lib/lp/registry/tests/test_distroseries.py
index d3f86df..ab3c6bf 100644
--- a/lib/lp/registry/tests/test_distroseries.py
+++ b/lib/lp/registry/tests/test_distroseries.py
@@ -580,7 +580,7 @@ class TestDistroSeriesPackaging(TestCaseWithFactory):
580580
581 def linkPackage(self, name):581 def linkPackage(self, name):
582 product_series = self.factory.makeProductSeries()582 product_series = self.factory.makeProductSeries()
583 product_series.setPackaging(583 removeSecurityProxy(product_series).setPackaging(
584 self.series, self.packages[name].sourcepackagename, self.user584 self.series, self.packages[name].sourcepackagename, self.user
585 )585 )
586 return product_series586 return product_series
diff --git a/lib/lp/registry/tests/test_packaging.py b/lib/lp/registry/tests/test_packaging.py
index 628bfb6..f4b4db6 100644
--- a/lib/lp/registry/tests/test_packaging.py
+++ b/lib/lp/registry/tests/test_packaging.py
@@ -19,6 +19,8 @@ from lp.registry.model.packaging import Packaging
19from lp.testing import (19from lp.testing import (
20 EventRecorder,20 EventRecorder,
21 TestCaseWithFactory,21 TestCaseWithFactory,
22 admin_logged_in,
23 anonymous_logged_in,
22 login,24 login,
23 person_logged_in,25 person_logged_in,
24)26)
@@ -61,7 +63,7 @@ class TestPackaging(TestCaseWithFactory):
61 packaging.distroseries,63 packaging.distroseries,
62 )64 )
6365
64 def test_destroySelf__not_allowed_for_probationary_user(self):66 def test_destroySelf__not_allowed_for_random_user(self):
65 """Arbitrary users cannot delete a packaging."""67 """Arbitrary users cannot delete a packaging."""
66 packaging = self.factory.makePackagingLink()68 packaging = self.factory.makePackagingLink()
67 packaging_util = getUtility(IPackagingUtil)69 packaging_util = getUtility(IPackagingUtil)
@@ -74,26 +76,6 @@ class TestPackaging(TestCaseWithFactory):
74 packaging.distroseries,76 packaging.distroseries,
75 )77 )
7678
77 def test_destroySelf__allowed_for_non_probationary_user(self):
78 """An experienced user can delete a packaging."""
79 packaging = self.factory.makePackagingLink()
80 sourcepackagename = packaging.sourcepackagename
81 distroseries = packaging.distroseries
82 productseries = packaging.productseries
83 packaging_util = getUtility(IPackagingUtil)
84 user = self.factory.makePerson(karma=200)
85 with person_logged_in(user):
86 packaging_util.deletePackaging(
87 packaging.productseries,
88 packaging.sourcepackagename,
89 packaging.distroseries,
90 )
91 self.assertFalse(
92 packaging_util.packagingEntryExists(
93 sourcepackagename, distroseries, productseries
94 )
95 )
96
97 def test_destroySelf__allowed_for_uploader(self):79 def test_destroySelf__allowed_for_uploader(self):
98 """A person with upload rights for the sourcepackage can80 """A person with upload rights for the sourcepackage can
99 delete a packaging link.81 delete a packaging link.
@@ -335,8 +317,7 @@ class TestDeletePackaging(TestCaseWithFactory):
335 """Deleting a Packaging creates a notification."""317 """Deleting a Packaging creates a notification."""
336 packaging_util = getUtility(IPackagingUtil)318 packaging_util = getUtility(IPackagingUtil)
337 packaging = self.factory.makePackagingLink()319 packaging = self.factory.makePackagingLink()
338 user = self.factory.makePerson(karma=200)320 with person_logged_in(packaging.productseries.product.owner):
339 with person_logged_in(user):
340 with EventRecorder() as recorder:321 with EventRecorder() as recorder:
341 packaging_util.deletePackaging(322 packaging_util.deletePackaging(
342 packaging.productseries,323 packaging.productseries,
@@ -346,3 +327,61 @@ class TestDeletePackaging(TestCaseWithFactory):
346 (event,) = recorder.events327 (event,) = recorder.events
347 self.assertIsInstance(event, ObjectDeletedEvent)328 self.assertIsInstance(event, ObjectDeletedEvent)
348 self.assertIs(removeSecurityProxy(packaging), event.object)329 self.assertIs(removeSecurityProxy(packaging), event.object)
330
331
332class TestPackagingSecurity(TestCaseWithFactory):
333
334 layer = DatabaseFunctionalLayer
335
336 def setUp(self, *args, **kwargs):
337 super().setUp(*args, **kwargs)
338 self.packaging = self.factory.makePackagingLink()
339
340 def viewPackaging(self):
341 _ = self.packaging.packaging
342
343 def editPackaging(self):
344 self.packaging.packaging = PackagingType.PRIME
345
346 def test_anyone_can_view(self):
347 with anonymous_logged_in():
348 try:
349 self.viewPackaging()
350 except Unauthorized:
351 self.fail("Unauthorized exception raised")
352
353 def test_anonymous_person_cannot_edit(self):
354 with anonymous_logged_in():
355 self.assertRaises(Unauthorized, self.editPackaging)
356
357 def test_random_person_cannot_edit(self):
358 with person_logged_in(self.factory.makePerson()):
359 self.assertRaises(Unauthorized, self.editPackaging)
360
361 def test_admin_can_edit(self):
362 with admin_logged_in():
363 try:
364 self.editPackaging()
365 except Unauthorized:
366 self.fail("Unauthorized exception raised")
367
368 def test_product_owner_can_edit(self):
369 with person_logged_in(self.packaging.productseries.product.owner):
370 try:
371 self.editPackaging()
372 except Unauthorized:
373 self.fail("Unauthorized exception raised")
374
375 def test_productseries_owner_can_edit(self):
376 with person_logged_in(self.packaging.productseries.owner):
377 try:
378 self.editPackaging()
379 except Unauthorized:
380 self.fail("Unauthorized exception raised")
381
382 def test_distribution_owner_can_edit(self):
383 with person_logged_in(self.packaging.distroseries.distribution.owner):
384 try:
385 self.editPackaging()
386 except Unauthorized:
387 self.fail("Unauthorized exception raised")
diff --git a/lib/lp/registry/tests/test_productseries.py b/lib/lp/registry/tests/test_productseries.py
index e0ea6a0..8f6b0fb 100644
--- a/lib/lp/registry/tests/test_productseries.py
+++ b/lib/lp/registry/tests/test_productseries.py
@@ -665,7 +665,7 @@ class ProductSeriesSecurityAdaperTestCase(TestCaseWithFactory):
665 "addSubscription",665 "addSubscription",
666 "removeBugSubscription",666 "removeBugSubscription",
667 },667 },
668 "launchpad.Edit": {"newMilestone"},668 "launchpad.Edit": {"newMilestone", "setPackaging"},
669 "launchpad.LimitedView": {669 "launchpad.LimitedView": {
670 "bugtargetdisplayname",670 "bugtargetdisplayname",
671 "bugtarget_parent",671 "bugtarget_parent",
@@ -746,7 +746,6 @@ class ProductSeriesSecurityAdaperTestCase(TestCaseWithFactory):
746 "releases",746 "releases",
747 "releaseverstyle",747 "releaseverstyle",
748 "searchTasks",748 "searchTasks",
749 "setPackaging",
750 "sourcepackages",749 "sourcepackages",
751 "specifications",750 "specifications",
752 "status",751 "status",
diff --git a/lib/lp/registry/tests/test_sourcepackage.py b/lib/lp/registry/tests/test_sourcepackage.py
index 63e976a..b839a18 100644
--- a/lib/lp/registry/tests/test_sourcepackage.py
+++ b/lib/lp/registry/tests/test_sourcepackage.py
@@ -33,6 +33,7 @@ from lp.testing import (
33 EventRecorder,33 EventRecorder,
34 TestCaseWithFactory,34 TestCaseWithFactory,
35 WebServiceTestCase,35 WebServiceTestCase,
36 admin_logged_in,
36 person_logged_in,37 person_logged_in,
37)38)
38from lp.testing.layers import DatabaseFunctionalLayer39from lp.testing.layers import DatabaseFunctionalLayer
@@ -305,11 +306,10 @@ class TestSourcePackage(TestCaseWithFactory):
305306
306 def test_deletePackaging(self):307 def test_deletePackaging(self):
307 """Ensure deletePackaging completely removes packaging."""308 """Ensure deletePackaging completely removes packaging."""
308 user = self.factory.makePerson(karma=200)
309 packaging = self.factory.makePackagingLink()309 packaging = self.factory.makePackagingLink()
310 packaging_id = packaging.id310 packaging_id = packaging.id
311 store = Store.of(packaging)311 store = Store.of(packaging)
312 with person_logged_in(user):312 with person_logged_in(packaging.sourcepackage.distribution.owner):
313 packaging.sourcepackage.deletePackaging()313 packaging.sourcepackage.deletePackaging()
314 result = store.find(Packaging, Packaging.id == packaging_id)314 result = store.find(Packaging, Packaging.id == packaging_id)
315 self.assertIs(None, result.one())315 self.assertIs(None, result.one())
@@ -318,7 +318,7 @@ class TestSourcePackage(TestCaseWithFactory):
318 """setPackaging() creates a Packaging link."""318 """setPackaging() creates a Packaging link."""
319 sourcepackage = self.factory.makeSourcePackage()319 sourcepackage = self.factory.makeSourcePackage()
320 productseries = self.factory.makeProductSeries()320 productseries = self.factory.makeProductSeries()
321 sourcepackage.setPackaging(321 removeSecurityProxy(sourcepackage).setPackaging(
322 productseries, owner=self.factory.makePerson()322 productseries, owner=self.factory.makePerson()
323 )323 )
324 packaging = sourcepackage.direct_packaging324 packaging = sourcepackage.direct_packaging
@@ -329,10 +329,9 @@ class TestSourcePackage(TestCaseWithFactory):
329 sourcepackage = self.factory.makeSourcePackage()329 sourcepackage = self.factory.makeSourcePackage()
330 productseries = self.factory.makeProductSeries()330 productseries = self.factory.makeProductSeries()
331 other_series = self.factory.makeProductSeries()331 other_series = self.factory.makeProductSeries()
332 user = self.factory.makePerson(karma=200)
333 registrant = self.factory.makePerson()332 registrant = self.factory.makePerson()
334 with EventRecorder() as recorder:333 with EventRecorder() as recorder:
335 with person_logged_in(user):334 with person_logged_in(sourcepackage.distribution.owner):
336 sourcepackage.setPackaging(productseries, owner=registrant)335 sourcepackage.setPackaging(productseries, owner=registrant)
337 sourcepackage.setPackaging(other_series, owner=registrant)336 sourcepackage.setPackaging(other_series, owner=registrant)
338 packaging = sourcepackage.direct_packaging337 packaging = sourcepackage.direct_packaging
@@ -348,61 +347,26 @@ class TestSourcePackage(TestCaseWithFactory):
348347
349 def test_refuses_PROPRIETARY(self):348 def test_refuses_PROPRIETARY(self):
350 """Packaging cannot be created for PROPRIETARY productseries"""349 """Packaging cannot be created for PROPRIETARY productseries"""
351 owner = self.factory.makePerson()
352 product = self.factory.makeProduct(350 product = self.factory.makeProduct(
353 owner=owner, information_type=InformationType.PROPRIETARY351 information_type=InformationType.PROPRIETARY
354 )352 )
355 series = self.factory.makeProductSeries(product=product)353 series = self.factory.makeProductSeries(product=product)
356 ubuntu_series = self.factory.makeUbuntuDistroSeries()354 ubuntu_series = self.factory.makeUbuntuDistroSeries()
357 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)355 sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
358 with person_logged_in(owner):356 with admin_logged_in():
359 with ExpectedException(357 with ExpectedException(
360 CannotPackageProprietaryProduct,358 CannotPackageProprietaryProduct,
361 "Only Public project series can be packaged, not "359 "Only Public project series can be packaged, not "
362 "Proprietary.",360 "Proprietary.",
363 ):361 ):
364 sp.setPackaging(series, owner)362 sp.setPackaging(series, product.owner)
365
366 def test_setPackagingReturnSharingDetailPermissions__ordinary_user(self):
367 """An ordinary user can create a packaging link but they cannot
368 set the series' branch or translation syncronisation settings,
369 or the translation usage settings of the product.
370 """
371 sourcepackage = self.factory.makeSourcePackage()
372 productseries = self.factory.makeProductSeries()
373 packaging_owner = self.factory.makePerson(karma=100)
374 with person_logged_in(packaging_owner):
375 permissions = (
376 sourcepackage.setPackagingReturnSharingDetailPermissions(
377 productseries, packaging_owner
378 )
379 )
380 self.assertEqual(productseries, sourcepackage.productseries)
381 self.assertFalse(packaging_owner.canWrite(productseries, "branch"))
382 self.assertFalse(
383 packaging_owner.canWrite(
384 productseries, "translations_autoimport_mode"
385 )
386 )
387 self.assertFalse(
388 packaging_owner.canWrite(
389 productseries.product, "translations_usage"
390 )
391 )
392 expected = {
393 "user_can_change_product_series": True,
394 "user_can_change_branch": False,
395 "user_can_change_translation_usage": False,
396 "user_can_change_translations_autoimport_mode": False,
397 }
398 self.assertEqual(expected, permissions)
399363
400 def test_getSharingDetailPermissions__ordinary_user(self):364 def test_getSharingDetailPermissions__ordinary_user(self):
401 """An ordinary user cannot set the series' branch or translation365 """An ordinary user cannot set the series' branch or translation
402 synchronisation settings, or the translation usage settings of the366 synchronisation settings, or the translation usage settings of the
403 product.367 product.
404 """368 """
405 user = self.factory.makePerson(karma=100)369 user = self.factory.makePerson()
406 packaging = self.factory.makePackagingLink()370 packaging = self.factory.makePackagingLink()
407 sourcepackage = packaging.sourcepackage371 sourcepackage = packaging.sourcepackage
408 productseries = packaging.productseries372 productseries = packaging.productseries
@@ -417,7 +381,7 @@ class TestSourcePackage(TestCaseWithFactory):
417 user.canWrite(productseries.product, "translations_usage")381 user.canWrite(productseries.product, "translations_usage")
418 )382 )
419 expected = {383 expected = {
420 "user_can_change_product_series": True,384 "user_can_change_product_series": False,
421 "user_can_change_branch": False,385 "user_can_change_branch": False,
422 "user_can_change_translation_usage": False,386 "user_can_change_translation_usage": False,
423 "user_can_change_translations_autoimport_mode": False,387 "user_can_change_translations_autoimport_mode": False,
@@ -429,8 +393,8 @@ class TestSourcePackage(TestCaseWithFactory):
429 return self.factory.makeProductSeries(owner=self.factory.makePerson())393 return self.factory.makeProductSeries(owner=self.factory.makePerson())
430394
431 def test_getSharingDetailPermissions__product_owner(self):395 def test_getSharingDetailPermissions__product_owner(self):
432 """A product owner can create a packaging link, and they can set the396 """A product owner can't change a packaging link, and they can set the
433 series' branch and the translation syncronisation settings, and the397 series' branch and the translation synchronisation settings, and the
434 translation usage settings of the product.398 translation usage settings of the product.
435 """399 """
436 productseries = self.makeDistinctOwnerProductSeries()400 productseries = self.makeDistinctOwnerProductSeries()
@@ -454,7 +418,7 @@ class TestSourcePackage(TestCaseWithFactory):
454 )418 )
455 )419 )
456 expected = {420 expected = {
457 "user_can_change_product_series": True,421 "user_can_change_product_series": False,
458 "user_can_change_branch": True,422 "user_can_change_branch": True,
459 "user_can_change_translation_usage": True,423 "user_can_change_translation_usage": True,
460 "user_can_change_translations_autoimport_mode": True,424 "user_can_change_translations_autoimport_mode": True,
@@ -464,34 +428,26 @@ class TestSourcePackage(TestCaseWithFactory):
464 def test_getSharingDetailPermissions_change_product(self):428 def test_getSharingDetailPermissions_change_product(self):
465 """Test user_can_change_product_series.429 """Test user_can_change_product_series.
466430
467 Until a Packaging is created, anyone can change product series.431 Random people cannot create package link or change product series.
468 Afterward, random people cannot change product series.
469 """432 """
470 sourcepackage = self.factory.makeSourcePackage()433 sourcepackage = self.factory.makeSourcePackage()
471 person1 = self.factory.makePerson(karma=100)434 person = self.factory.makePerson()
472 person2 = self.factory.makePerson()
473435
474 def can_change_product_series():436 def can_change_product_series():
475 return sourcepackage.getSharingDetailPermissions()[437 return sourcepackage.getSharingDetailPermissions()[
476 "user_can_change_product_series"438 "user_can_change_product_series"
477 ]439 ]
478440
479 with person_logged_in(person1):441 with person_logged_in(person):
480 self.assertTrue(can_change_product_series())442 self.assertFalse(can_change_product_series())
481 with person_logged_in(person2):443 self.factory.makePackagingLink(sourcepackage=sourcepackage)
482 self.assertTrue(can_change_product_series())444 with person_logged_in(person):
483 self.factory.makePackagingLink(
484 sourcepackage=sourcepackage, owner=person1
485 )
486 with person_logged_in(person1):
487 self.assertTrue(can_change_product_series())
488 with person_logged_in(person2):
489 self.assertFalse(can_change_product_series())445 self.assertFalse(can_change_product_series())
490446
491 def test_getSharingDetailPermissions_no_product_series(self):447 def test_getSharingDetailPermissions_no_product_series(self):
492 sourcepackage = self.factory.makeSourcePackage()448 sourcepackage = self.factory.makeSourcePackage()
493 expected = {449 expected = {
494 "user_can_change_product_series": True,450 "user_can_change_product_series": False,
495 "user_can_change_branch": False,451 "user_can_change_branch": False,
496 "user_can_change_translation_usage": False,452 "user_can_change_translation_usage": False,
497 "user_can_change_translations_autoimport_mode": False,453 "user_can_change_translations_autoimport_mode": False,
@@ -558,8 +514,12 @@ class TestSourcePackageWebService(WebServiceTestCase):
558 self.assertIs(None, sourcepackage.direct_packaging)514 self.assertIs(None, sourcepackage.direct_packaging)
559 productseries = self.factory.makeProductSeries()515 productseries = self.factory.makeProductSeries()
560 transaction.commit()516 transaction.commit()
561 ws_sourcepackage = self.wsObject(sourcepackage)517 ws_sourcepackage = self.wsObject(
562 ws_productseries = self.wsObject(productseries)518 sourcepackage, user=sourcepackage.distribution.owner
519 )
520 ws_productseries = self.wsObject(
521 productseries, user=sourcepackage.distribution.owner
522 )
563 ws_sourcepackage.setPackaging(productseries=ws_productseries)523 ws_sourcepackage.setPackaging(productseries=ws_productseries)
564 transaction.commit()524 transaction.commit()
565 self.assertEqual(525 self.assertEqual(
@@ -568,11 +528,12 @@ class TestSourcePackageWebService(WebServiceTestCase):
568528
569 def test_deletePackaging(self):529 def test_deletePackaging(self):
570 """Deleting a packaging should work."""530 """Deleting a packaging should work."""
571 user = self.factory.makePerson(karma=200)
572 packaging = self.factory.makePackagingLink()531 packaging = self.factory.makePackagingLink()
573 sourcepackage = packaging.sourcepackage532 sourcepackage = packaging.sourcepackage
574 transaction.commit()533 transaction.commit()
575 self.wsObject(sourcepackage, user=user).deletePackaging()534 self.wsObject(
535 sourcepackage, user=sourcepackage.distribution.owner
536 ).deletePackaging()
576 transaction.commit()537 transaction.commit()
577 self.assertIs(None, sourcepackage.direct_packaging)538 self.assertIs(None, sourcepackage.direct_packaging)
578539
@@ -580,7 +541,9 @@ class TestSourcePackageWebService(WebServiceTestCase):
580 """Deleting when there's no packaging should be a no-op."""541 """Deleting when there's no packaging should be a no-op."""
581 sourcepackage = self.factory.makeSourcePackage()542 sourcepackage = self.factory.makeSourcePackage()
582 transaction.commit()543 transaction.commit()
583 self.wsObject(sourcepackage).deletePackaging()544 self.wsObject(
545 sourcepackage, user=sourcepackage.distribution.owner
546 ).deletePackaging()
584 transaction.commit()547 transaction.commit()
585 self.assertIs(None, sourcepackage.direct_packaging)548 self.assertIs(None, sourcepackage.direct_packaging)
586549
@@ -737,7 +700,9 @@ class TestSourcePackageViews(TestCaseWithFactory):
737 def test_editpackaging_obsolete_series_in_vocabulary(self):700 def test_editpackaging_obsolete_series_in_vocabulary(self):
738 # The sourcepackage's current product series is included in701 # The sourcepackage's current product series is included in
739 # the vocabulary even if it is obsolete.702 # the vocabulary even if it is obsolete.
740 self.package.setPackaging(self.obsolete_productseries, self.owner)703 removeSecurityProxy(self.package).setPackaging(
704 self.obsolete_productseries, self.owner
705 )
741 form = {706 form = {
742 "field.product": "bonkers",707 "field.product": "bonkers",
743 "field.actions.continue": "Continue",708 "field.actions.continue": "Continue",
diff --git a/lib/lp/soyuz/tests/test_publishing.py b/lib/lp/soyuz/tests/test_publishing.py
index cd0828d..ff6e308 100644
--- a/lib/lp/soyuz/tests/test_publishing.py
+++ b/lib/lp/soyuz/tests/test_publishing.py
@@ -707,7 +707,9 @@ class SoyuzTestPublisher:
707 """707 """
708 if source_pub is None:708 if source_pub is None:
709 distribution = self.factory.makeDistribution(709 distribution = self.factory.makeDistribution(
710 name="youbuntu", displayname="Youbuntu"710 name="youbuntu",
711 displayname="Youbuntu",
712 owner=self.factory.makePerson(email="owner@youbuntu.com"),
711 )713 )
712 distroseries = self.factory.makeDistroSeries(714 distroseries = self.factory.makeDistroSeries(
713 name="busy", distribution=distribution715 name="busy", distribution=distribution
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 3fd560c..9965749 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -208,6 +208,7 @@ from lp.registry.interfaces.ssh import ISSHKeySet
208from lp.registry.model.commercialsubscription import CommercialSubscription208from lp.registry.model.commercialsubscription import CommercialSubscription
209from lp.registry.model.karma import KarmaTotalCache209from lp.registry.model.karma import KarmaTotalCache
210from lp.registry.model.milestone import Milestone210from lp.registry.model.milestone import Milestone
211from lp.registry.model.packaging import Packaging
211from lp.registry.model.suitesourcepackage import SuiteSourcePackage212from lp.registry.model.suitesourcepackage import SuiteSourcePackage
212from lp.services.auth.interfaces import IAccessTokenSet213from lp.services.auth.interfaces import IAccessTokenSet
213from lp.services.auth.utils import create_access_token_secret214from lp.services.auth.utils import create_access_token_secret
@@ -1346,7 +1347,7 @@ class LaunchpadObjectFactory(ObjectFactory):
1346 owner=None,1347 owner=None,
1347 sourcepackage=None,1348 sourcepackage=None,
1348 in_ubuntu=False,1349 in_ubuntu=False,
1349 ):1350 ) -> Packaging:
1350 assert sourcepackage is None or (1351 assert sourcepackage is None or (
1351 distroseries is None and sourcepackagename is None1352 distroseries is None and sourcepackagename is None
1352 ), (1353 ), (

Subscribers

People subscribed via source and target branches

to status/vote changes: