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