launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #29174
[Merge] ~andrey-fedoseev/launchpad:packaging-security into launchpad:master
Andrey Fedoseev has proposed merging ~andrey-fedoseev/launchpad:packaging-security into launchpad:master.
Commit message:
New security model for `Packaging`
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~andrey-fedoseev/launchpad/+git/launchpad/+merge/429758
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
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~andrey-fedoseev/launchpad:packaging-security into launchpad:master.
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 @@
name="+ubuntupkg"
for="lp.registry.interfaces.productseries.IProductSeries"
class="lp.registry.browser.productseries.ProductSeriesUbuntuPackagingView"
- permission="launchpad.AnyPerson"
+ permission="launchpad.Edit"
template="../templates/productseries-ubuntupkg.pt"
/>
<browser:pages
@@ -2621,7 +2621,7 @@
name="+edit-packaging"
for="lp.registry.interfaces.sourcepackage.ISourcePackage"
class="lp.registry.browser.sourcepackage.SourcePackageChangeUpstreamView"
- permission="launchpad.AnyPerson"
+ permission="launchpad.Edit"
template="../templates/sourcepackage-edit-packaging.pt"
/>
<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(
summary = "Change the branch for this series"
return Link("+setbranch", text, summary, icon=icon)
- @enabled_with_permission("launchpad.AnyPerson")
+ @enabled_with_permission("launchpad.Edit")
def ubuntupkg(self):
"""Return a link to link this series to an ubuntu sourcepackage."""
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 (
SimpleVocabulary,
getVocabularyRegistry,
)
+from zope.security.interfaces import Unauthorized
from lp import _
from lp.app.browser.launchpadform import (
@@ -61,9 +62,11 @@ from lp.services.webapp import (
canonical_url,
stepto,
)
+from lp.services.webapp.authorization import check_permission
from lp.services.webapp.breadcrumb import Breadcrumb
from lp.services.webapp.escaping import structured
from lp.services.webapp.interfaces import IBreadcrumb
+from lp.services.webapp.menu import enabled_with_permission
from lp.services.webapp.publisher import LaunchpadView
from lp.services.worlddata.helpers import browser_languages
from lp.services.worlddata.interfaces.country import ICountry
@@ -214,13 +217,9 @@ class SourcePackageOverviewMenu(ApplicationMenu):
def copyright(self):
return Link("+copyright", "View copyright", icon="info")
+ @enabled_with_permission("launchpad.Edit")
def edit_packaging(self):
- return Link(
- "+edit-packaging",
- "Change upstream link",
- icon="edit",
- enabled=self.userCanDeletePackaging(),
- )
+ return Link("+edit-packaging", "Change upstream link", icon="edit")
def remove_packaging(self):
return Link(
@@ -230,13 +229,9 @@ class SourcePackageOverviewMenu(ApplicationMenu):
enabled=self.userCanDeletePackaging(),
)
+ @enabled_with_permission("launchpad.Edit")
def set_upstream(self):
- return Link(
- "+edit-packaging",
- "Set upstream link",
- icon="add",
- enabled=self.userCanDeletePackaging(),
- )
+ return Link("+edit-packaging", "Set upstream link", icon="add")
def builds(self):
text = "Show builds"
@@ -245,8 +240,8 @@ class SourcePackageOverviewMenu(ApplicationMenu):
def userCanDeletePackaging(self):
packaging = self.context.direct_packaging
if packaging is None:
- return True
- return packaging.userCanDelete()
+ return False
+ return check_permission("launchpad.Edit", packaging)
class SourcePackageChangeUpstreamStepOne(ReturnToReferrerMixin, StepView):
@@ -425,10 +420,23 @@ class SourcePackageRemoveUpstreamView(
label = "Unlink an upstream project"
page_title = label
+ def initialize(self):
+ self.packaging = self.context.direct_packaging
+ if self.packaging is not None:
+ # We do permission check here rather than protecting the view
+ # in ZCML because product owners should also be allowed to unlink
+ # projects, even if they don't have edit permission on the source
+ # package.
+ if not check_permission("launchpad.Edit", self.packaging):
+ raise Unauthorized(
+ "You are not allowed to unlink an upstream project"
+ )
+ super().initialize()
+
@action("Unlink")
def unlink(self, action, data):
old_series = self.context.productseries
- if self.context.direct_packaging is not None:
+ if self.packaging is not None:
getUtility(IPackagingUtil).deletePackaging(
self.context.productseries,
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.
The distroseries field's vocabulary is the same as the ubuntu.series
attribute.
+ >>> _ = login_person(product.owner)
>>> view = create_view(productseries, "+ubuntupkg")
>>> print(view.label)
Ubuntu source packaging
@@ -200,8 +201,7 @@ and a new entry can be added to the packaging history.
... )
>>> grumpy_series.status = SeriesStatus.FROZEN
- >>> a_user = factory.makePerson(name="hedgehog")
- >>> ignored = login_person(a_user)
+ >>> _ = login_person(product.owner)
>>> form = {
... "field.sourcepackagename": "hot",
... "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..b3d2265 100644
--- a/lib/lp/registry/browser/tests/sourcepackage-views.rst
+++ b/lib/lp/registry/browser/tests/sourcepackage-views.rst
@@ -53,7 +53,9 @@ This is a multistep view. In the first step, the product is specified.
>>> print(view.view.request.form)
{'field.__visited_steps__': 'sourcepackage_change_upstream_step1'}
- >>> ignored = login_person(product.owner)
+ >>> from lp.registry.interfaces.person import IPersonSet
+ >>> admin = getUtility(IPersonSet).getByEmail("admin@xxxxxxxxxxxxx")
+ >>> _ = login_person(admin)
>>> form = {
... "field.product": "bonkers",
... "field.actions.continue": "Continue",
@@ -63,7 +65,7 @@ This is a multistep view. In the first step, the product is specified.
... package,
... name="+edit-packaging",
... form=form,
- ... principal=product.owner,
+ ... principal=admin,
... )
>>> view.view.errors
[]
@@ -88,7 +90,7 @@ product can be chosen from a list of options.
... package,
... name="+edit-packaging",
... form=form,
- ... principal=product.owner,
+ ... principal=admin,
... )
>>> ignored = view.view.render()
@@ -124,7 +126,7 @@ then the current product series will be the selected option.
... package,
... name="+edit-packaging",
... form=form,
- ... principal=product.owner,
+ ... principal=admin,
... )
>>> print(view.view.widgets.get("productseries")._getFormValue().name)
crazy
@@ -141,7 +143,7 @@ empty.
... package,
... name="+edit-packaging",
... form=form,
- ... principal=product.owner,
+ ... principal=admin,
... )
>>> for error in view.view.errors:
... print(pretty(error.args))
@@ -161,7 +163,7 @@ but there is no notification message that the upstream link was updated.
... package,
... name="+edit-packaging",
... form=form,
- ... principal=product.owner,
+ ... principal=admin,
... )
>>> print(view.view)
<...SourcePackageChangeUpstreamStepTwo object...>
@@ -386,7 +388,7 @@ to the project series.
>>> ignored = login_person(user)
>>> form = {"field.actions.unlink": "Unlink"}
>>> view = create_initialized_view(
- ... package, name="+remove-packaging", form=form, principal=user
+ ... package, name="+remove-packaging", form=form, principal=admin
... )
>>> view.errors
[]
@@ -401,7 +403,7 @@ they get a message telling them that the link has already been
deleted.
>>> view = create_initialized_view(
- ... package, name="+remove-packaging", form=form, principal=user
+ ... package, name="+remove-packaging", form=form, principal=admin
... )
>>> view.errors
[]
diff --git a/lib/lp/registry/browser/tests/test_packaging.py b/lib/lp/registry/browser/tests/test_packaging.py
index 7549a51..86bac22 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
from lp.registry.interfaces.product import IProductSet
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
from lp.services.features.testing import FeatureFixture
-from lp.testing import TestCaseWithFactory, login, logout
+from lp.testing import TestCaseWithFactory, login, logout, person_logged_in
from lp.testing.layers import DatabaseFunctionalLayer
from lp.testing.pages import setupBrowser
from lp.testing.views import create_initialized_view
@@ -49,7 +49,7 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
)
self.packaging_util = getUtility(IPackagingUtil)
- def test_no_error_when_trying_to_readd_same_package(self):
+ def test_no_error_when_trying_to_read_same_package(self):
# There is no reason to display an error when the user's action
# wouldn't cause a state change.
self.packaging_util.createPackaging(
@@ -65,9 +65,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
"field.sourcepackagename": self.sourcepackagename.name,
"field.actions.continue": "Continue",
}
- view = create_initialized_view(
- self.productseries, "+ubuntupkg", form=form
- )
+ with person_logged_in(self.product.owner):
+ view = create_initialized_view(
+ self.productseries,
+ "+ubuntupkg",
+ form=form,
+ principal=self.product.owner,
+ )
self.assertEqual([], view.errors)
def test_cannot_link_to_linked_package(self):
@@ -78,9 +82,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
"field.sourcepackagename": "hot",
"field.actions.continue": "Continue",
}
- view = create_initialized_view(
- self.productseries, "+ubuntupkg", form=form
- )
+ with person_logged_in(self.product.owner):
+ view = create_initialized_view(
+ self.productseries,
+ "+ubuntupkg",
+ form=form,
+ principal=self.product.owner,
+ )
self.assertEqual([], view.errors)
other_productseries = self.factory.makeProductSeries(
product=self.product, name="hottest"
@@ -90,9 +98,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
"field.sourcepackagename": "hot",
"field.actions.continue": "Continue",
}
- view = create_initialized_view(
- other_productseries, "+ubuntupkg", form=form
- )
+ with person_logged_in(self.product.owner):
+ view = create_initialized_view(
+ other_productseries,
+ "+ubuntupkg",
+ form=form,
+ principal=self.product.owner,
+ )
view_errors = [
'The <a href="http://launchpad.test/ubuntu/hoary/+source/hot">'
"hot</a> package in Hoary is already linked to another series."
@@ -106,9 +118,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
"field.sourcepackagename": "",
"field.actions.continue": "Continue",
}
- view = create_initialized_view(
- self.productseries, "+ubuntupkg", form=form
- )
+ with person_logged_in(self.product.owner):
+ view = create_initialized_view(
+ self.productseries,
+ "+ubuntupkg",
+ form=form,
+ principal=self.product.owner,
+ )
self.assertEqual(1, len(view.errors))
self.assertEqual("sourcepackagename", view.errors[0].field_name)
self.assertEqual("Required input is missing.", view.errors[0].doc())
@@ -125,9 +141,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
"field.sourcepackagename": "vapor",
"field.actions.continue": "Continue",
}
- view = create_initialized_view(
- self.productseries, "+ubuntupkg", form=form
- )
+ with person_logged_in(self.product.owner):
+ view = create_initialized_view(
+ self.productseries,
+ "+ubuntupkg",
+ form=form,
+ principal=self.product.owner,
+ )
view_errors = ["The source package is not published in Hoary."]
self.assertEqual(view_errors, view.errors)
@@ -142,9 +162,13 @@ class TestProductSeriesUbuntuPackagingView(WithScenarios, TestCaseWithFactory):
"field.sourcepackagename": "hot",
"field.actions.continue": "Continue",
}
- view = create_initialized_view(
- self.productseries, "+ubuntupkg", form=form
- )
+ with person_logged_in(self.product.owner):
+ view = create_initialized_view(
+ self.productseries,
+ "+ubuntupkg",
+ form=form,
+ principal=self.product.owner,
+ )
self.assertEqual([], view.errors)
has_packaging = self.packaging_util.packagingEntryExists(
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):
# It should be an error to make a Product private if it is packaged.
product = self.factory.makeProduct()
sourcepackage = self.factory.makeSourcePackage()
- sourcepackage.setPackaging(product.development_focus, product.owner)
+ removeSecurityProxy(sourcepackage).setPackaging(
+ product.development_focus, product.owner
+ )
browser = self.getViewBrowser(product, "+edit", user=product.owner)
info_type = browser.getControl(name="field.information_type")
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):
distroseries=distroseries,
version="1.5-0ubuntu1",
)
- self.source_package.setPackaging(
+ removeSecurityProxy(self.source_package).setPackaging(
productseries, productseries.product.owner
)
@@ -301,80 +301,119 @@ class TestSourcePackagePackagingLinks(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
- def makeSourcePackageOverviewMenu(self, with_packaging, karma=None):
- sourcepackage = self.factory.makeSourcePackage()
- registrant = self.factory.makePerson()
- if with_packaging:
- self.factory.makePackagingLink(
- sourcepackagename=sourcepackage.sourcepackagename,
- distroseries=sourcepackage.distroseries,
- owner=registrant,
- )
- user = self.factory.makePerson(karma=karma)
+ def setUp(self, *args, **kwargs):
+ super().setUp(*args, **kwargs)
+ self.sourcepackage = self.factory.makeSourcePackage()
+ self.maintainer = self.sourcepackage.distribution.owner
+ self.product_owner = self.factory.makePerson()
+ self.product = self.factory.makeProduct(owner=self.product_owner)
+ self.productseries = self.factory.makeProductSeries(self.product)
+
+ def makePackaging(self):
+ self.factory.makePackagingLink(
+ sourcepackagename=self.sourcepackage.sourcepackagename,
+ distroseries=self.sourcepackage.distroseries,
+ productseries=self.productseries,
+ )
+
+ def makeSourcePackageOverviewMenu(self, user):
with person_logged_in(user):
- menu = SourcePackageOverviewMenu(sourcepackage)
- return menu, user
+ menu = SourcePackageOverviewMenu(self.sourcepackage)
+ return menu
- def test_edit_packaging_link__enabled_without_packaging(self):
- # If no packging exists, the edit_packaging link is always
+ def test_edit_packaging_link__enabled_without_packaging_maintainer(self):
+ # If no packaging exists, the edit_packaging link is always
# enabled.
- menu, user = self.makeSourcePackageOverviewMenu(False, None)
- with person_logged_in(user):
+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
+ with person_logged_in(self.maintainer):
self.assertTrue(menu.edit_packaging().enabled)
- def test_set_upstrem_link__enabled_without_packaging(self):
- # If no packging exists, the set_upstream link is always
+ def test_set_upstream_link__enabled_without_packaging_maintainer(self):
+ # If no packaging exists, the set_upstream link is always
# enabled.
- menu, user = self.makeSourcePackageOverviewMenu(False, None)
- with person_logged_in(user):
+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
+ with person_logged_in(self.maintainer):
self.assertTrue(menu.set_upstream().enabled)
- def test_remove_packaging_link__enabled_without_packaging(self):
- # If no packging exists, the remove_packaging link is always
- # enabled.
- menu, user = self.makeSourcePackageOverviewMenu(False, None)
- with person_logged_in(user):
- self.assertTrue(menu.remove_packaging().enabled)
+ def test_remove_packaging_link__enabled_without_packaging_maintainer(self):
+ # If no packaging exists, the remove_packaging link is always
+ # disabled.
+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
+ with person_logged_in(self.maintainer):
+ self.assertFalse(menu.remove_packaging().enabled)
- def test_edit_packaging_link__enabled_with_packaging_non_probation(self):
- # If a packging exists, the edit_packaging link is enabled
- # for the non-probationary users.
- menu, user = self.makeSourcePackageOverviewMenu(True, 100)
- with person_logged_in(user):
+ def test_edit_packaging_link__enabled_with_packaging_maintainer(self):
+ # If a packaging exists, the edit_packaging link is enabled
+ # for the package maintainer.
+ self.makePackaging()
+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
+ with person_logged_in(self.maintainer):
self.assertTrue(menu.edit_packaging().enabled)
- def test_set_upstrem_link__enabled_with_packaging_non_probation(self):
- # If a packging exists, the set_upstream link is enabled
- # for the non-probationary users.
- menu, user = self.makeSourcePackageOverviewMenu(True, 100)
- with person_logged_in(user):
+ def test_set_upstream_link__enabled_with_packaging_maintainer(self):
+ # If a packaging exists, the set_upstream link is enabled
+ # for the package maintainer.
+ self.makePackaging()
+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
+ with person_logged_in(self.maintainer):
self.assertTrue(menu.set_upstream().enabled)
- def test_remove_packaging_link__enabled_with_packaging_non_probation(self):
- # If a packging exists, the remove_packaging link is enabled
- # for the non-probationary users.
- menu, user = self.makeSourcePackageOverviewMenu(True, 100)
- with person_logged_in(user):
+ def test_remove_packaging_link__enabled_with_packaging_maintainer(self):
+ # If a packaging exists, the remove_packaging link is enabled
+ # for the package maintainer.
+ self.makePackaging()
+ menu = self.makeSourcePackageOverviewMenu(self.maintainer)
+ with person_logged_in(self.maintainer):
+ self.assertTrue(menu.remove_packaging().enabled)
+
+ def test_edit_packaging_link__disabled_for_product_owner(self):
+ # If a packaging exists, the edit_packaging link is disabled
+ # for the product owner.
+ self.makePackaging()
+ menu = self.makeSourcePackageOverviewMenu(self.product_owner)
+ with person_logged_in(self.product_owner):
+ self.assertFalse(menu.edit_packaging().enabled)
+
+ def test_set_upstream_link__disabled_for_product_owner(self):
+ # If a packaging exists, the set_upstream link is disabled
+ # for the product owner.
+ self.makePackaging()
+ menu = self.makeSourcePackageOverviewMenu(self.product_owner)
+ with person_logged_in(self.product_owner):
+ self.assertFalse(menu.set_upstream().enabled)
+
+ def test_remove_packaging_link__enabled_for_product_owner(self):
+ # If a packaging exists, the remove_packaging link is enabled
+ # for product owner.
+ self.makePackaging()
+ menu = self.makeSourcePackageOverviewMenu(self.product_owner)
+ with person_logged_in(self.product_owner):
self.assertTrue(menu.remove_packaging().enabled)
- def test_edit_packaging_link__enabled_with_packaging_probation(self):
- # If a packging exists, the edit_packaging link is not enabled
- # for probationary users.
- menu, user = self.makeSourcePackageOverviewMenu(True, None)
+ def test_edit_packaging_link__enabled_with_packaging_arbitrary(self):
+ # If a packaging exists, the edit_packaging link is not enabled
+ # for arbitrary users.
+ self.makePackaging()
+ user = self.factory.makePerson()
+ menu = self.makeSourcePackageOverviewMenu(user)
with person_logged_in(user):
self.assertFalse(menu.edit_packaging().enabled)
- def test_set_upstrem_link__enabled_with_packaging_probation(self):
- # If a packging exists, the set_upstream link is not enabled
- # for probationary users.
- menu, user = self.makeSourcePackageOverviewMenu(True, None)
+ def test_set_upstream_link__enabled_with_packaging_arbitrary(self):
+ # If a packaging exists, the set_upstream link is not enabled
+ # for arbitrary users.
+ self.makePackaging()
+ user = self.factory.makePerson()
+ menu = self.makeSourcePackageOverviewMenu(user)
with person_logged_in(user):
self.assertFalse(menu.set_upstream().enabled)
- def test_remove_packaging_link__enabled_with_packaging_probation(self):
- # If a packging exists, the remove_packaging link is not enabled
- # for probationary users.
- menu, user = self.makeSourcePackageOverviewMenu(True, None)
+ def test_remove_packaging_link__enabled_with_packaging_arbitrary(self):
+ # If a packaging exists, the remove_packaging link is not enabled
+ # for arbitrary users.
+ self.makePackaging()
+ user = self.factory.makePerson()
+ menu = self.makeSourcePackageOverviewMenu(user)
with person_logged_in(user):
self.assertFalse(menu.remove_packaging().enabled)
@@ -392,10 +431,9 @@ class TestSourcePackageChangeUpstreamView(BrowserTestCase):
owner=product_owner,
information_type=InformationType.PROPRIETARY,
)
- ubuntu_series = self.factory.makeUbuntuDistroSeries()
- sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
+ sp = self.factory.makeSourcePackage()
browser = self.getViewBrowser(
- sp, "+edit-packaging", user=product_owner
+ sp, "+edit-packaging", user=self.factory.makeAdministrator()
)
browser.getControl("Project").value = product_name
browser.getControl("Continue").click()
@@ -413,10 +451,9 @@ class TestSourcePackageChangeUpstreamView(BrowserTestCase):
)
series = self.factory.makeProductSeries(product=product)
series_displayname = series.displayname
- ubuntu_series = self.factory.makeUbuntuDistroSeries()
- sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
+ sp = self.factory.makeSourcePackage()
browser = self.getViewBrowser(
- sp, "+edit-packaging", user=product_owner
+ sp, "+edit-packaging", user=self.factory.makeAdministrator()
)
browser.getControl("Project").value = product_name
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 @@
<allow
interface="lp.registry.interfaces.packaging.IPackaging"/>
<require
- permission="zope.Public"
- set_schema="lp.registry.interfaces.packaging.IPackaging"/>
+ permission="launchpad.Edit"
+ set_schema="lp.registry.interfaces.packaging.IPackaging"
+ attributes="
+ destroySelf" />
</class>
<!-- 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):
)
)
- def userCanDelete():
- """True, if the current user is allowed to delete this packaging,
- else False.
-
- Non-probationary users can delete packaging links that they believe
- connect Ubuntu to bogus data.
- """
-
class IPackagingUtil(Interface):
"""Utilities to handle Packaging."""
@@ -121,6 +113,9 @@ class IPackagingUtil(Interface):
):
"""Create Packaging entry."""
+ def get(productseries, sourcepackagename, distroseries):
+ """Get Packaging entry."""
+
def deletePackaging(productseries, sourcepackagename, distroseries):
"""Delete a packaging entry."""
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):
def newMilestone(name, dateexpected=None, summary=None, code_name=None):
"""Create a new milestone for this ProjectSeries."""
+ def setPackaging(distroseries, sourcepackagename, owner):
+ """Create or update a Packaging record for this product series,
+ connecting it to the given distroseries and source package name.
+ """
+
class IProductSeriesPublic(Interface):
"""Public IProductSeries properties."""
@@ -343,11 +348,6 @@ class IProductSeriesView(
"""Return the SourcePackage that packages this project in Ubuntu's
translation focus or current series or any series, in that order."""
- def setPackaging(distroseries, sourcepackagename, owner):
- """Create or update a Packaging record for this product series,
- connecting it to the given distroseries and source package name.
- """
-
def getPackagingInDistribution(distribution):
"""Return all the Packaging entries for this product series for the
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..bd02454 100644
--- a/lib/lp/registry/interfaces/sourcepackage.py
+++ b/lib/lp/registry/interfaces/sourcepackage.py
@@ -224,32 +224,6 @@ class ISourcePackagePublic(
sourcepackagename compare not equal.
"""
- @operation_parameters(productseries=Reference(schema=IProductSeries))
- @call_with(owner=REQUEST_USER)
- @export_write_operation()
- @operation_for_version("devel")
- def setPackaging(productseries, owner):
- """Update the existing packaging record, or create a new packaging
- record, that links the source package to the given productseries,
- and record that it was done by the owner.
- """
-
- @operation_parameters(productseries=Reference(schema=IProductSeries))
- @call_with(owner=REQUEST_USER)
- @export_write_operation()
- @operation_for_version("devel")
- def setPackagingReturnSharingDetailPermissions(productseries, owner):
- """Like setPackaging(), but returns getSharingDetailPermissions().
-
- This method is intended for AJAX usage on the +sharing-details
- page.
- """
-
- @export_write_operation()
- @operation_for_version("devel")
- def deletePackaging():
- """Delete the packaging for this sourcepackage."""
-
def getSharingDetailPermissions():
"""Return a dictionary of user permissions for +sharing-details page.
@@ -364,6 +338,32 @@ class ISourcePackageEdit(Interface):
:return: None
"""
+ @operation_parameters(productseries=Reference(schema=IProductSeries))
+ @call_with(owner=REQUEST_USER)
+ @export_write_operation()
+ @operation_for_version("devel")
+ def setPackaging(productseries, owner):
+ """Update the existing packaging record, or create a new packaging
+ record, that links the source package to the given productseries,
+ and record that it was done by the owner.
+ """
+
+ @operation_parameters(productseries=Reference(schema=IProductSeries))
+ @call_with(owner=REQUEST_USER)
+ @export_write_operation()
+ @operation_for_version("devel")
+ def setPackagingReturnSharingDetailPermissions(productseries, owner):
+ """Like setPackaging(), but returns getSharingDetailPermissions().
+
+ This method is intended for AJAX usage on the +sharing-details
+ page.
+ """
+
+ @export_write_operation()
+ @operation_for_version("devel")
+ def deletePackaging():
+ """Delete the packaging for this sourcepackage."""
+
@exported_as_webservice_entry(as_of="beta")
class 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
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
-from zope.security.interfaces import Unauthorized
from lp.app.enums import InformationType
-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.registry.errors import CannotPackageProprietaryProduct
from lp.registry.interfaces.packaging import (
IPackaging,
@@ -23,7 +21,6 @@ from lp.services.database.datetimecol import UtcDateTimeCol
from lp.services.database.enumcol import DBEnum
from lp.services.database.sqlbase import SQLBase
from lp.services.database.sqlobject import ForeignKey
-from lp.services.webapp.interfaces import ILaunchBag
@implementer(IPackaging)
@@ -66,29 +63,7 @@ class Packaging(SQLBase):
super().__init__(**kwargs)
notify(ObjectCreatedEvent(self))
- def userCanDelete(self):
- """See `IPackaging`."""
- user = getUtility(ILaunchBag).user
- if user is None:
- return False
- admin = getUtility(ILaunchpadCelebrities).admin
- registry_experts = getUtility(ILaunchpadCelebrities).registry_experts
- if (
- not user.is_probationary
- or user.inTeam(self.productseries.product.owner)
- or user.canAccess(self.sourcepackage, "setBranch")
- or user.inTeam(registry_experts)
- or user.inTeam(admin)
- ):
- return True
- return False
-
def destroySelf(self):
- if not self.userCanDelete():
- raise Unauthorized(
- "Only the person who created the packaging and package "
- "maintainers can delete it."
- )
notify(ObjectDeletedEvent(self))
super().destroySelf()
@@ -97,16 +72,15 @@ class Packaging(SQLBase):
class PackagingUtil:
"""Utilities for Packaging."""
- @classmethod
def createPackaging(
- cls, productseries, sourcepackagename, distroseries, packaging, owner
+ self, productseries, sourcepackagename, distroseries, packaging, owner
):
"""See `IPackaging`.
Raises an assertion error if there is already packaging for
the sourcepackagename in the distroseries.
"""
- if cls.packagingEntryExists(sourcepackagename, distroseries):
+ if self.packagingEntryExists(sourcepackagename, distroseries):
raise AssertionError(
"A packaging entry for %s in %s already exists."
% (sourcepackagename.name, distroseries.name)
@@ -132,9 +106,18 @@ class PackagingUtil:
owner=owner,
)
+ def get(self, productseries, sourcepackagename, distroseries):
+ criteria = {
+ "sourcepackagename": sourcepackagename,
+ "distroseries": distroseries,
+ }
+ if productseries is not None:
+ criteria["productseries"] = productseries
+ return Packaging.selectOneBy(**criteria)
+
def deletePackaging(self, productseries, sourcepackagename, distroseries):
"""See `IPackaging`."""
- packaging = Packaging.selectOneBy(
+ packaging = getUtility(IPackagingUtil).get(
productseries=productseries,
sourcepackagename=sourcepackagename,
distroseries=distroseries,
@@ -152,17 +135,13 @@ class PackagingUtil:
)
packaging.destroySelf()
- @staticmethod
def packagingEntryExists(
- sourcepackagename, distroseries, productseries=None
+ self, sourcepackagename, distroseries, productseries=None
):
"""See `IPackaging`."""
- criteria = dict(
- sourcepackagename=sourcepackagename, distroseries=distroseries
+ packaging = self.get(
+ productseries=productseries,
+ sourcepackagename=sourcepackagename,
+ distroseries=distroseries,
)
- if productseries is not None:
- criteria["productseries"] = productseries
- result = Packaging.selectOneBy(**criteria)
- if result is None:
- return False
- return True
+ return packaging is not None
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
from storm.store import Store
from zope.component import getUtility
from zope.interface import implementer
+from zope.security.proxy import removeSecurityProxy
from lp.app.enums import service_uses_launchpad
from lp.app.errors import NotFoundError
@@ -35,7 +36,7 @@ from lp.bugs.model.structuralsubscription import (
StructuralSubscriptionTargetMixin,
)
from lp.registry.errors import ProprietaryPillar
-from lp.registry.interfaces.packaging import PackagingType
+from lp.registry.interfaces.packaging import IPackagingUtil, PackagingType
from lp.registry.interfaces.person import validate_person
from lp.registry.interfaces.productrelease import IProductReleaseSet
from lp.registry.interfaces.productseries import (
@@ -45,7 +46,6 @@ from lp.registry.interfaces.productseries import (
)
from lp.registry.interfaces.series import SeriesStatus
from lp.registry.model.milestone import HasMilestonesMixin, Milestone
-from lp.registry.model.packaging import PackagingUtil
from lp.registry.model.productrelease import ProductRelease
from lp.registry.model.series import SeriesMixin
from lp.services.database.constants import UTC_NOW
@@ -445,14 +445,14 @@ class ProductSeries(
# ok, we didn't find a packaging record that matches, let's go ahead
# and create one
- pkg = PackagingUtil.createPackaging(
+ pkg = getUtility(IPackagingUtil).createPackaging(
distroseries=distroseries,
sourcepackagename=sourcepackagename,
productseries=self,
packaging=PackagingType.PRIME,
owner=owner,
)
- pkg.sync() # convert UTC_NOW to actual datetime
+ removeSecurityProxy(pkg).sync() # convert UTC_NOW to actual datetime
return pkg
def getPackagingInDistribution(self, distribution):
diff --git a/lib/lp/registry/model/sourcepackage.py b/lib/lp/registry/model/sourcepackage.py
index 1bfacba..0e55f83 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 (
SeriesSourcePackageBranchSet,
)
from lp.registry.interfaces.distribution import NoPartnerArchive
-from lp.registry.interfaces.packaging import PackagingType
+from lp.registry.interfaces.packaging import IPackagingUtil, PackagingType
from lp.registry.interfaces.pocket import PackagePublishingPocket
from lp.registry.interfaces.sourcepackage import (
ISourcePackage,
ISourcePackageFactory,
)
from lp.registry.model.hasdrivers import HasDriversMixin
-from lp.registry.model.packaging import Packaging, PackagingUtil
+from lp.registry.model.packaging import Packaging
from lp.registry.model.suitesourcepackage import SuiteSourcePackage
from lp.services.database.decoratedresultset import DecoratedResultSet
from lp.services.database.interfaces import IStore
@@ -562,7 +562,7 @@ class SourcePackage(
# Delete the current packaging and create a new one so
# that the translation sharing jobs are started.
self.direct_packaging.destroySelf()
- PackagingUtil.createPackaging(
+ getUtility(IPackagingUtil).createPackaging(
distroseries=self.distroseries,
sourcepackagename=self.sourcepackagename,
productseries=productseries,
@@ -595,8 +595,8 @@ class SourcePackage(
else:
permissions.update(
{
- "user_can_change_product_series": (
- self.direct_packaging.userCanDelete()
+ "user_can_change_product_series": user.canAccess(
+ self, "setPackaging"
),
"user_can_change_branch": user.canWrite(
productseries, "branch"
diff --git a/lib/lp/registry/security.py b/lib/lp/registry/security.py
index 2e14ae0..a5b2c3a 100644
--- a/lib/lp/registry/security.py
+++ b/lib/lp/registry/security.py
@@ -219,8 +219,16 @@ class EditProduct(EditByOwnersOrAdmins):
)
-class EditPackaging(EditByOwnersOrAdmins):
+class EditPackaging(AuthorizationBase):
usedfor = IPackaging
+ permission = "launchpad.Edit"
+
+ def checkAuthenticated(self, user):
+ edit_product_series = EditProductSeries(self.obj.productseries)
+ edit_source_package = EditSourcePackage(self.obj.sourcepackage)
+ return edit_product_series.checkAuthenticated(
+ user
+ ) or edit_source_package.checkAuthenticated(user)
class 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
association, only one of them can succeed. The other one does not oops
and displays a meaningful error message.
-The No Privilege User may open the same page in two browser tabs.
+The package maintainer may open the same page in two browser tabs.
- >>> first_browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
+ >>> first_browser = setupBrowser(auth="Basic admin@xxxxxxxxxxxxx:test")
>>> first_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
- >>> second_browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
+ >>> second_browser = setupBrowser(auth="Basic admin@xxxxxxxxxxxxx:test")
>>> second_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
Then 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.
>>> anon_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
>>> content = anon_browser.contents
>>> print(extract_text(find_tag_by_id(content, "packages_list")))
- The Hoary Hedgehog Release (active development) Set upstream link
+ The Hoary Hedgehog Release (active development)
1.0.9a-4ubuntu1 release (main) 2005-09-15
The Warty Warthog Release (current stable release) alsa-utils trunk series
1.0.9a-4 release (main) 2005-09-16
@@ -28,12 +28,12 @@ source package in each series of this distribution.
Delete Link Button
------------------
-A button is displayed to authenticated users to delete existing
+A button is displayed to project owners to delete existing
packaging links.
- >>> user_browser = setupBrowser(auth="Basic test@xxxxxxxxxxxxx:test")
- >>> user_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
- >>> link = user_browser.getLink(
+ >>> owner_browser = setupBrowser(auth="Basic mark@xxxxxxxxxxx:test")
+ >>> owner_browser.open("http://launchpad.test/ubuntu/+source/alsa-utils")
+ >>> link = owner_browser.getLink(
... url="/ubuntu/warty/+source/alsa-utils/+remove-packaging"
... )
>>> print(link)
@@ -49,12 +49,12 @@ This button is not displayed to anonymous users.
Clicking this button deletes the corresponding packaging association.
- >>> link = user_browser.getLink(
+ >>> link = owner_browser.getLink(
... url="/ubuntu/warty/+source/alsa-utils/+remove-packaging"
... )
>>> link.click()
- >>> user_browser.getControl("Unlink").click()
- >>> content = user_browser.contents
+ >>> owner_browser.getControl("Unlink").click()
+ >>> content = owner_browser.contents
>>> for tag in find_tags_by_class(content, "error"):
... print(extract_text(tag))
...
diff --git a/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst b/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst
index 9729757..e32b56a 100644
--- a/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst
+++ b/lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.rst
@@ -10,23 +10,25 @@ Create test data.
>>> test_publisher.updatePackageCache(test_data["distroseries"])
>>> logout()
-No Privileges Person visit the distroseries upstream links page for Hoary
-and sees that pmount is not linked.
+A person with permissions to edit packaging visits the distroseries upstream
+links page for Hoary and sees that pmount is not linked.
- >>> user_browser.open(
+ >>> admin_browser = setupBrowser(auth='Basic admin@xxxxxxxxxxxxx:test')
+ >>> admin_browser.open(
... "http://launchpad.test/ubuntu/hoary/+needs-packaging"
... )
- >>> print(extract_text(find_tag_by_id(user_browser.contents, "packages")))
+ >>> print(extract_text(
+ ... find_tag_by_id(admin_browser.contents, "packages")))
Source Package Bugs Translations
pmount No bugs 64 strings ...
They look at the pmount source package page in Hoary and read that the
upstream project is not set.
- >>> user_browser.getLink("pmount").click()
+ >>> admin_browser.getLink("pmount").click()
>>> print(
... extract_text(
- ... find_tag_by_id(user_browser.contents, "no-upstreams")
+ ... find_tag_by_id(admin_browser.contents, "no-upstreams")
... )
... )
Launchpad...
@@ -36,44 +38,44 @@ upstream project is not set.
Choose another upstream project
Register the upstream project
-No Privileges Person knows that the pmount package comes from the thunderbird
+The person knows that the pmount package comes from the thunderbird
project. They set the upstream packaging link and see that it is set.
- >>> user_browser.getControl(
+ >>> admin_browser.getControl(
... "Choose another upstream project"
... ).selected = True
- >>> user_browser.getControl("Link to Upstream Project").click()
- >>> user_browser.getControl(name="field.product").value = "thunderbird"
- >>> user_browser.getControl("Continue").click()
- >>> user_browser.getControl(name="field.productseries").value = ["trunk"]
- >>> user_browser.getControl("Change").click()
+ >>> admin_browser.getControl("Link to Upstream Project").click()
+ >>> admin_browser.getControl(name="field.product").value = "thunderbird"
+ >>> admin_browser.getControl("Continue").click()
+ >>> admin_browser.getControl(name="field.productseries").value = ["trunk"]
+ >>> admin_browser.getControl("Change").click()
>>> print(
- ... extract_text(find_tag_by_id(user_browser.contents, "upstreams"))
+ ... extract_text(find_tag_by_id(admin_browser.contents, "upstreams"))
... )
The Mozilla Project...Mozilla Thunderbird...trunk...
They see the "Show upstream links" link and take a look at the project's
packaging in distributions.
- >>> user_browser.getLink("Show upstream links").click()
+ >>> admin_browser.getLink("Show upstream links").click()
>>> print(
... extract_text(
- ... find_tag_by_id(user_browser.contents, "distribution-series")
+ ... find_tag_by_id(admin_browser.contents, "distribution-series")
... )
... )
Distribution series Source package Version Project series
Hoary (5.04) pmount 0.1-2 Mozilla Thunderbird trunk...
-No Privileges Person returns to the pmount source package page, sees the
+The person returns to the pmount source package page, sees the
link to all versions and follows it to the distro source package page.
- >>> user_browser.getLink("pmount").click()
- >>> user_browser.getLink(
+ >>> admin_browser.getLink("pmount").click()
+ >>> admin_browser.getLink(
... "All versions of pmount source in Ubuntu"
... ).click()
>>> print(
... extract_text(
- ... find_tag_by_id(user_browser.contents, "packages_list")
+ ... find_tag_by_id(admin_browser.contents, "packages_list")
... )
... )
The Hoary Hedgehog Release (active development) ...
@@ -83,72 +85,72 @@ link to all versions and follows it to the distro source package page.
Register a project from a source package
----------------------------------------
-No Privileges Person can register a project for a package, and Launchpad
+The person can register a project for a package, and Launchpad
will use the data from the source package to prefill the first
step of the multistep form.
- >>> user_browser.open(
+ >>> admin_browser.open(
... "http://launchpad.test/youbuntu/busy/+source/bonkers"
... )
- >>> user_browser.getControl(
+ >>> admin_browser.getControl(
... "Register the upstream project"
... ).selected = True
- >>> user_browser.getControl("Link to Upstream Project").click()
- >>> print(user_browser.getControl(name="field.name").value)
+ >>> admin_browser.getControl("Link to Upstream Project").click()
+ >>> print(admin_browser.getControl(name="field.name").value)
bonkers
- >>> print(user_browser.getControl(name="field.display_name").value)
+ >>> print(admin_browser.getControl(name="field.display_name").value)
Bonkers
- >>> print(user_browser.getControl(name="field.summary").value)
+ >>> print(admin_browser.getControl(name="field.summary").value)
summary for flubber-bin
summary for flubber-lib
>>> print(
- ... extract_text(find_tag_by_id(user_browser.contents, "step-title"))
+ ... extract_text(find_tag_by_id(admin_browser.contents, "step-title"))
... )
Step 2 (of 2): Check for duplicate projects
-When No Privileges Person selects "Choose another upstream project" and
+When the person selects "Choose another upstream project" and
then finds out that the project doesn't exist, they use the
"Link to Upstream Project" button to register the project.
- >>> user_browser.open(
+ >>> admin_browser.open(
... "http://launchpad.test/youbuntu/busy/+source/bonkers/"
... )
- >>> user_browser.getControl(
+ >>> admin_browser.getControl(
... "Choose another upstream project"
... ).selected = True
- >>> user_browser.getControl("Link to Upstream Project").click()
- >>> print(user_browser.url)
+ >>> admin_browser.getControl("Link to Upstream Project").click()
+ >>> print(admin_browser.url)
http://launchpad.test/youbuntu/busy/+source/bonkers/+edit-packaging
- >>> user_browser.getLink("Register the upstream project").click()
- >>> print(user_browser.getControl(name="field.name").value)
+ >>> admin_browser.getLink("Register the upstream project").click()
+ >>> print(admin_browser.getControl(name="field.name").value)
bonkers
- >>> print(user_browser.getControl(name="field.display_name").value)
+ >>> print(admin_browser.getControl(name="field.display_name").value)
Bonkers
- >>> print(user_browser.getControl(name="field.summary").value)
+ >>> print(admin_browser.getControl(name="field.summary").value)
summary for flubber-bin
summary for flubber-lib
>>> print(
- ... extract_text(find_tag_by_id(user_browser.contents, "step-title"))
+ ... extract_text(find_tag_by_id(admin_browser.contents, "step-title"))
... )
Step 2 (of 2): Check for duplicate projects
-After No Privileges Person selects the licences, the user is redirected back
+After the person selects the licences, the user is redirected back
to the source package page and an informational message will be displayed.
- >>> user_browser.getControl(name="field.licenses").value = ["BSD"]
- >>> user_browser.getControl(
+ >>> admin_browser.getControl(name="field.licenses").value = ["BSD"]
+ >>> admin_browser.getControl(
... "Complete registration and link to bonkers package"
... ).click()
- >>> print(user_browser.url)
+ >>> print(admin_browser.url)
http://launchpad.test/youbuntu/busy/+source/bonkers
>>> for tag in find_tags_by_class(
- ... user_browser.contents, "informational message"
+ ... admin_browser.contents, "informational message"
... ):
... print(extract_text(tag))
Linked Bonkers project to bonkers source package.
>>> print(
- ... extract_text(find_tag_by_id(user_browser.contents, "upstreams"))
+ ... extract_text(find_tag_by_id(admin_browser.contents, "upstreams"))
... )
Bonkers ⇒ trunk
Change 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.
>>> evo_owner.getLink(url="/ubuntu/hoary/+source/evolution") is not None
True
-Any logged in users can still see the links to create a packaging link.
+Arbitrary users can't see the links to create a packaging link.
>>> user_browser.open("http://launchpad.test/evolution/+packages")
- >>> print(user_browser.getLink(url="/evolution/trunk/+ubuntupkg").url)
- http://launchpad.test/evolution/trunk/+ubuntupkg
+ >>> user_browser.getLink(url="/evolution/trunk/+ubuntupkg")
+ Traceback (most recent call last):
+ ...
+ zope.testbrowser.browser.LinkNotFoundError
>>> anon_browser.open("http://launchpad.test/evolution/+packages")
>>> 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):
def linkPackage(self, name):
product_series = self.factory.makeProductSeries()
- product_series.setPackaging(
+ removeSecurityProxy(product_series).setPackaging(
self.series, self.packages[name].sourcepackagename, self.user
)
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
from lp.testing import (
EventRecorder,
TestCaseWithFactory,
+ admin_logged_in,
+ anonymous_logged_in,
login,
person_logged_in,
)
@@ -61,7 +63,7 @@ class TestPackaging(TestCaseWithFactory):
packaging.distroseries,
)
- def test_destroySelf__not_allowed_for_probationary_user(self):
+ def test_destroySelf__not_allowed_for_random_user(self):
"""Arbitrary users cannot delete a packaging."""
packaging = self.factory.makePackagingLink()
packaging_util = getUtility(IPackagingUtil)
@@ -74,26 +76,6 @@ class TestPackaging(TestCaseWithFactory):
packaging.distroseries,
)
- def test_destroySelf__allowed_for_non_probationary_user(self):
- """An experienced user can delete a packaging."""
- packaging = self.factory.makePackagingLink()
- sourcepackagename = packaging.sourcepackagename
- distroseries = packaging.distroseries
- productseries = packaging.productseries
- packaging_util = getUtility(IPackagingUtil)
- user = self.factory.makePerson(karma=200)
- with person_logged_in(user):
- packaging_util.deletePackaging(
- packaging.productseries,
- packaging.sourcepackagename,
- packaging.distroseries,
- )
- self.assertFalse(
- packaging_util.packagingEntryExists(
- sourcepackagename, distroseries, productseries
- )
- )
-
def test_destroySelf__allowed_for_uploader(self):
"""A person with upload rights for the sourcepackage can
delete a packaging link.
@@ -335,8 +317,7 @@ class TestDeletePackaging(TestCaseWithFactory):
"""Deleting a Packaging creates a notification."""
packaging_util = getUtility(IPackagingUtil)
packaging = self.factory.makePackagingLink()
- user = self.factory.makePerson(karma=200)
- with person_logged_in(user):
+ with person_logged_in(packaging.productseries.product.owner):
with EventRecorder() as recorder:
packaging_util.deletePackaging(
packaging.productseries,
@@ -346,3 +327,61 @@ class TestDeletePackaging(TestCaseWithFactory):
(event,) = recorder.events
self.assertIsInstance(event, ObjectDeletedEvent)
self.assertIs(removeSecurityProxy(packaging), event.object)
+
+
+class TestPackagingSecurity(TestCaseWithFactory):
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self, *args, **kwargs):
+ super().setUp(*args, **kwargs)
+ self.packaging = self.factory.makePackagingLink()
+
+ def viewPackaging(self):
+ _ = self.packaging.packaging
+
+ def editPackaging(self):
+ self.packaging.packaging = PackagingType.PRIME
+
+ def test_anyone_can_view(self):
+ with anonymous_logged_in():
+ try:
+ self.viewPackaging()
+ except Unauthorized:
+ self.fail("Unauthorized exception raised")
+
+ def test_anonymous_person_cannot_edit(self):
+ with anonymous_logged_in():
+ self.assertRaises(Unauthorized, self.editPackaging)
+
+ def test_random_person_cannot_edit(self):
+ with person_logged_in(self.factory.makePerson()):
+ self.assertRaises(Unauthorized, self.editPackaging)
+
+ def test_admin_can_edit(self):
+ with admin_logged_in():
+ try:
+ self.editPackaging()
+ except Unauthorized:
+ self.fail("Unauthorized exception raised")
+
+ def test_product_owner_can_edit(self):
+ with person_logged_in(self.packaging.productseries.product.owner):
+ try:
+ self.editPackaging()
+ except Unauthorized:
+ self.fail("Unauthorized exception raised")
+
+ def test_productseries_owner_can_edit(self):
+ with person_logged_in(self.packaging.productseries.owner):
+ try:
+ self.editPackaging()
+ except Unauthorized:
+ self.fail("Unauthorized exception raised")
+
+ def test_distribution_owner_can_edit(self):
+ with person_logged_in(self.packaging.distroseries.distribution.owner):
+ try:
+ self.editPackaging()
+ except Unauthorized:
+ 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):
"addSubscription",
"removeBugSubscription",
},
- "launchpad.Edit": {"newMilestone"},
+ "launchpad.Edit": {"newMilestone", "setPackaging"},
"launchpad.LimitedView": {
"bugtargetdisplayname",
"bugtarget_parent",
@@ -746,7 +746,6 @@ class ProductSeriesSecurityAdaperTestCase(TestCaseWithFactory):
"releases",
"releaseverstyle",
"searchTasks",
- "setPackaging",
"sourcepackages",
"specifications",
"status",
diff --git a/lib/lp/registry/tests/test_sourcepackage.py b/lib/lp/registry/tests/test_sourcepackage.py
index 63e976a..3d101d7 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 (
EventRecorder,
TestCaseWithFactory,
WebServiceTestCase,
+ admin_logged_in,
person_logged_in,
)
from lp.testing.layers import DatabaseFunctionalLayer
@@ -305,11 +306,10 @@ class TestSourcePackage(TestCaseWithFactory):
def test_deletePackaging(self):
"""Ensure deletePackaging completely removes packaging."""
- user = self.factory.makePerson(karma=200)
packaging = self.factory.makePackagingLink()
packaging_id = packaging.id
store = Store.of(packaging)
- with person_logged_in(user):
+ with person_logged_in(packaging.sourcepackage.distribution.owner):
packaging.sourcepackage.deletePackaging()
result = store.find(Packaging, Packaging.id == packaging_id)
self.assertIs(None, result.one())
@@ -318,7 +318,7 @@ class TestSourcePackage(TestCaseWithFactory):
"""setPackaging() creates a Packaging link."""
sourcepackage = self.factory.makeSourcePackage()
productseries = self.factory.makeProductSeries()
- sourcepackage.setPackaging(
+ removeSecurityProxy(sourcepackage).setPackaging(
productseries, owner=self.factory.makePerson()
)
packaging = sourcepackage.direct_packaging
@@ -329,10 +329,9 @@ class TestSourcePackage(TestCaseWithFactory):
sourcepackage = self.factory.makeSourcePackage()
productseries = self.factory.makeProductSeries()
other_series = self.factory.makeProductSeries()
- user = self.factory.makePerson(karma=200)
registrant = self.factory.makePerson()
with EventRecorder() as recorder:
- with person_logged_in(user):
+ with person_logged_in(sourcepackage.distribution.owner):
sourcepackage.setPackaging(productseries, owner=registrant)
sourcepackage.setPackaging(other_series, owner=registrant)
packaging = sourcepackage.direct_packaging
@@ -348,36 +347,43 @@ class TestSourcePackage(TestCaseWithFactory):
def test_refuses_PROPRIETARY(self):
"""Packaging cannot be created for PROPRIETARY productseries"""
- owner = self.factory.makePerson()
product = self.factory.makeProduct(
- owner=owner, information_type=InformationType.PROPRIETARY
+ information_type=InformationType.PROPRIETARY
)
series = self.factory.makeProductSeries(product=product)
ubuntu_series = self.factory.makeUbuntuDistroSeries()
sp = self.factory.makeSourcePackage(distroseries=ubuntu_series)
- with person_logged_in(owner):
+ with admin_logged_in():
with ExpectedException(
CannotPackageProprietaryProduct,
"Only Public project series can be packaged, not "
"Proprietary.",
):
- sp.setPackaging(series, owner)
+ sp.setPackaging(series, product.owner)
def test_setPackagingReturnSharingDetailPermissions__ordinary_user(self):
- """An ordinary user can create a packaging link but they cannot
- set the series' branch or translation syncronisation settings,
+ """An ordinary user can own a packaging link, but they cannot
+ set the series' branch or translation synchronization settings,
or the translation usage settings of the product.
"""
sourcepackage = self.factory.makeSourcePackage()
productseries = self.factory.makeProductSeries()
- packaging_owner = self.factory.makePerson(karma=100)
- with person_logged_in(packaging_owner):
+ packaging_owner = self.factory.makePerson()
+ with person_logged_in(sourcepackage.distribution.owner):
permissions = (
sourcepackage.setPackagingReturnSharingDetailPermissions(
productseries, packaging_owner
)
)
- self.assertEqual(productseries, sourcepackage.productseries)
+ expected = {
+ "user_can_change_product_series": True,
+ "user_can_change_branch": False,
+ "user_can_change_translation_usage": False,
+ "user_can_change_translations_autoimport_mode": False,
+ }
+ self.assertEqual(expected, permissions)
+ self.assertEqual(productseries, sourcepackage.productseries)
+ with person_logged_in(packaging_owner):
self.assertFalse(packaging_owner.canWrite(productseries, "branch"))
self.assertFalse(
packaging_owner.canWrite(
@@ -389,20 +395,13 @@ class TestSourcePackage(TestCaseWithFactory):
productseries.product, "translations_usage"
)
)
- expected = {
- "user_can_change_product_series": True,
- "user_can_change_branch": False,
- "user_can_change_translation_usage": False,
- "user_can_change_translations_autoimport_mode": False,
- }
- self.assertEqual(expected, permissions)
def test_getSharingDetailPermissions__ordinary_user(self):
"""An ordinary user cannot set the series' branch or translation
synchronisation settings, or the translation usage settings of the
product.
"""
- user = self.factory.makePerson(karma=100)
+ user = self.factory.makePerson()
packaging = self.factory.makePackagingLink()
sourcepackage = packaging.sourcepackage
productseries = packaging.productseries
@@ -417,7 +416,7 @@ class TestSourcePackage(TestCaseWithFactory):
user.canWrite(productseries.product, "translations_usage")
)
expected = {
- "user_can_change_product_series": True,
+ "user_can_change_product_series": False,
"user_can_change_branch": False,
"user_can_change_translation_usage": False,
"user_can_change_translations_autoimport_mode": False,
@@ -429,8 +428,8 @@ class TestSourcePackage(TestCaseWithFactory):
return self.factory.makeProductSeries(owner=self.factory.makePerson())
def test_getSharingDetailPermissions__product_owner(self):
- """A product owner can create a packaging link, and they can set the
- series' branch and the translation syncronisation settings, and the
+ """A product owner can't change a packaging link, and they can set the
+ series' branch and the translation synchronisation settings, and the
translation usage settings of the product.
"""
productseries = self.makeDistinctOwnerProductSeries()
@@ -454,7 +453,7 @@ class TestSourcePackage(TestCaseWithFactory):
)
)
expected = {
- "user_can_change_product_series": True,
+ "user_can_change_product_series": False,
"user_can_change_branch": True,
"user_can_change_translation_usage": True,
"user_can_change_translations_autoimport_mode": True,
@@ -464,34 +463,26 @@ class TestSourcePackage(TestCaseWithFactory):
def test_getSharingDetailPermissions_change_product(self):
"""Test user_can_change_product_series.
- Until a Packaging is created, anyone can change product series.
- Afterward, random people cannot change product series.
+ Random people cannot create package link or change product series.
"""
sourcepackage = self.factory.makeSourcePackage()
- person1 = self.factory.makePerson(karma=100)
- person2 = self.factory.makePerson()
+ person = self.factory.makePerson()
def can_change_product_series():
return sourcepackage.getSharingDetailPermissions()[
"user_can_change_product_series"
]
- with person_logged_in(person1):
- self.assertTrue(can_change_product_series())
- with person_logged_in(person2):
- self.assertTrue(can_change_product_series())
- self.factory.makePackagingLink(
- sourcepackage=sourcepackage, owner=person1
- )
- with person_logged_in(person1):
- self.assertTrue(can_change_product_series())
- with person_logged_in(person2):
+ with person_logged_in(person):
+ self.assertFalse(can_change_product_series())
+ self.factory.makePackagingLink(sourcepackage=sourcepackage)
+ with person_logged_in(person):
self.assertFalse(can_change_product_series())
def test_getSharingDetailPermissions_no_product_series(self):
sourcepackage = self.factory.makeSourcePackage()
expected = {
- "user_can_change_product_series": True,
+ "user_can_change_product_series": False,
"user_can_change_branch": False,
"user_can_change_translation_usage": False,
"user_can_change_translations_autoimport_mode": False,
@@ -558,8 +549,12 @@ class TestSourcePackageWebService(WebServiceTestCase):
self.assertIs(None, sourcepackage.direct_packaging)
productseries = self.factory.makeProductSeries()
transaction.commit()
- ws_sourcepackage = self.wsObject(sourcepackage)
- ws_productseries = self.wsObject(productseries)
+ ws_sourcepackage = self.wsObject(
+ sourcepackage, user=sourcepackage.distribution.owner
+ )
+ ws_productseries = self.wsObject(
+ productseries, user=sourcepackage.distribution.owner
+ )
ws_sourcepackage.setPackaging(productseries=ws_productseries)
transaction.commit()
self.assertEqual(
@@ -568,11 +563,12 @@ class TestSourcePackageWebService(WebServiceTestCase):
def test_deletePackaging(self):
"""Deleting a packaging should work."""
- user = self.factory.makePerson(karma=200)
packaging = self.factory.makePackagingLink()
sourcepackage = packaging.sourcepackage
transaction.commit()
- self.wsObject(sourcepackage, user=user).deletePackaging()
+ self.wsObject(
+ sourcepackage, user=sourcepackage.distribution.owner
+ ).deletePackaging()
transaction.commit()
self.assertIs(None, sourcepackage.direct_packaging)
@@ -580,7 +576,9 @@ class TestSourcePackageWebService(WebServiceTestCase):
"""Deleting when there's no packaging should be a no-op."""
sourcepackage = self.factory.makeSourcePackage()
transaction.commit()
- self.wsObject(sourcepackage).deletePackaging()
+ self.wsObject(
+ sourcepackage, user=sourcepackage.distribution.owner
+ ).deletePackaging()
transaction.commit()
self.assertIs(None, sourcepackage.direct_packaging)
@@ -737,7 +735,9 @@ class TestSourcePackageViews(TestCaseWithFactory):
def test_editpackaging_obsolete_series_in_vocabulary(self):
# The sourcepackage's current product series is included in
# the vocabulary even if it is obsolete.
- self.package.setPackaging(self.obsolete_productseries, self.owner)
+ removeSecurityProxy(self.package).setPackaging(
+ self.obsolete_productseries, self.owner
+ )
form = {
"field.product": "bonkers",
"field.actions.continue": "Continue",
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
from lp.registry.model.commercialsubscription import CommercialSubscription
from lp.registry.model.karma import KarmaTotalCache
from lp.registry.model.milestone import Milestone
+from lp.registry.model.packaging import Packaging
from lp.registry.model.suitesourcepackage import SuiteSourcePackage
from lp.services.auth.interfaces import IAccessTokenSet
from lp.services.auth.utils import create_access_token_secret
@@ -1346,7 +1347,7 @@ class LaunchpadObjectFactory(ObjectFactory):
owner=None,
sourcepackage=None,
in_ubuntu=False,
- ):
+ ) -> Packaging:
assert sourcepackage is None or (
distroseries is None and sourcepackagename is None
), (