launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19410
[Merge] lp:~cjwatson/launchpad/processors-for-everyone into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/processors-for-everyone into lp:launchpad.
Commit message:
Allow archive owners to enable/disable unrestricted processors.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/processors-for-everyone/+merge/272093
Allow archive owners to enable/disable unrestricted processors.
Some things to note:
* Archive.setProcessors moves to launchpad.View and has manual permission checks. This is necessary because commercial admins have launchpad.Admin on archives but not necessarily launchpad.Edit.
* Setting the Archive.processors property involves no security checks. This only affects Archive:+edit, which now uses Archive.setProcessors.
* Processor selection moves from Archive:+admin to Archive:+edit.
* Processors that are unavailable in any of an archive's distribution's series are omitted entirely, since showing users greyed-out lpia or whatever is unlikely to achieve much except confusion.
* Restricted but available processors are shown, but greyed out so that you have an indication that you can ask for them.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/processors-for-everyone into lp:launchpad.
=== modified file 'lib/lp/app/widgets/itemswidgets.py'
--- lib/lp/app/widgets/itemswidgets.py 2013-04-10 08:09:05 +0000
+++ lib/lp/app/widgets/itemswidgets.py 2015-09-23 12:20:46 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2012 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Widgets dealing with a choice of options."""
@@ -38,7 +38,7 @@
class PlainMultiCheckBoxWidget(MultiCheckBoxWidget):
- """MultiCheckBoxWidget that copes with CustomWidgetFacotry."""
+ """MultiCheckBoxWidget that copes with CustomWidgetFactory."""
_joinButtonToMessageTemplate = u'%s %s '
@@ -48,12 +48,26 @@
if IChoice.providedBy(vocabulary):
vocabulary = vocabulary.vocabulary
MultiCheckBoxWidget.__init__(self, field, vocabulary, request)
+ self._disabled_items = []
+
+ @property
+ def disabled_items(self):
+ return self._disabled_items
+
+ @disabled_items.setter
+ def disabled_items(self, items):
+ if items is None:
+ items = []
+ self._disabled_items = [
+ self.vocabulary.getTerm(item).token for item in items]
def _renderItem(self, index, text, value, name, cssClass, checked=False):
- """Render a checkbox and text without without label."""
+ """Render a checkbox and text without a label."""
kw = {}
if checked:
kw['checked'] = 'checked'
+ if value in self.disabled_items:
+ kw['disabled'] = 'disabled'
value = html_escape(value)
text = html_escape(text)
id = '%s.%s' % (name, index)
@@ -76,6 +90,8 @@
kw = {}
if checked:
kw['checked'] = 'checked'
+ if value in self.disabled_items:
+ kw['disabled'] = 'disabled'
value = html_escape(value)
text = html_escape(text)
id = '%s.%s' % (name, index)
=== modified file 'lib/lp/app/widgets/tests/test_itemswidgets.py'
--- lib/lp/app/widgets/tests/test_itemswidgets.py 2012-11-26 08:40:20 +0000
+++ lib/lp/app/widgets/tests/test_itemswidgets.py 2015-09-23 12:20:46 +0000
@@ -92,6 +92,17 @@
expected = '<input ... /> <unsafe> &nbsp; title '
self.assertRenderItem(expected, self.UNSAFE_TERM, checked=False)
+ def test__renderItem_disabled(self):
+ # Render item in disabled state.
+ self.widget.disabled_items = ['object-1']
+ expected = (
+ '<input ... disabled="disabled" ... /> Safe title')
+ self.assertRenderItem(expected, self.SAFE_TERM)
+ expected = (
+ '<input class="checkboxType" id="test_field.1" name="test_field" '
+ 'type="checkbox" value="token-2" /> <unsafe> ...')
+ self.assertRenderItem(expected, self.UNSAFE_TERM)
+
class TestLabeledMultiCheckBoxWidget(ItemWidgetTestCase):
"""Test the LabeledMultiCheckBoxWidget class."""
@@ -118,6 +129,19 @@
expected = '<label .../> <unsafe> &nbsp; title</label>'
self.assertRenderItem(expected, self.UNSAFE_TERM, checked=False)
+ def test__renderItem_disabled(self):
+ # Render item in disabled state.
+ self.widget.disabled_items = ['object-1']
+ expected = (
+ '<label ...><input ... disabled="disabled" ... /> '
+ 'Safe title</label>')
+ self.assertRenderItem(expected, self.SAFE_TERM)
+ expected = (
+ '<label for="field.test_field.1" style="font-weight: normal">'
+ '<input class="checkboxType" id="test_field.1" name="test_field" '
+ 'type="checkbox" value="token-2" /> <unsafe> ...')
+ self.assertRenderItem(expected, self.UNSAFE_TERM)
+
class TestLaunchpadRadioWidget(ItemWidgetTestCase):
"""Test the LaunchpadRadioWidget class."""
=== modified file 'lib/lp/registry/browser/distribution.py'
--- lib/lp/registry/browser/distribution.py 2015-07-08 16:05:11 +0000
+++ lib/lp/registry/browser/distribution.py 2015-09-23 12:20:46 +0000
@@ -857,6 +857,7 @@
LaunchpadFormView.setUpFields(self)
self.form_fields += self.createRequireVirtualized()
self.form_fields += self.createEnabledProcessors(
+ getUtility(IProcessorSet).getAll(),
u"The architectures on which the distribution's main archive can "
u"build.")
@@ -920,6 +921,7 @@
RegistryEditFormView.setUpFields(self)
self.form_fields += self.createRequireVirtualized()
self.form_fields += self.createEnabledProcessors(
+ getUtility(IProcessorSet).getAll(),
u"The architectures on which the distribution's main archive can "
u"build.")
=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2015-09-21 19:21:57 +0000
+++ lib/lp/soyuz/browser/archive.py 2015-09-23 12:20:46 +0000
@@ -41,6 +41,7 @@
from storm.expr import Desc
from zope.component import getUtility
from zope.formlib import form
+from zope.formlib.widget import CustomWidgetFactory
from zope.formlib.widgets import TextAreaWidget
from zope.interface import (
implementer,
@@ -81,7 +82,6 @@
)
from lp.app.widgets.textwidgets import StrippedTextWidget
from lp.buildmaster.enums import BuildStatus
-from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.code.interfaces.sourcepackagerecipebuild import (
ISourcePackageRecipeBuildSource,
)
@@ -1992,6 +1992,12 @@
# IArchive.enabled is a read-only property that cannot be set
# directly.
del(data['enabled'])
+ new_processors = data.get('processors')
+ if new_processors is not None:
+ if set(self.context.processors) != set(new_processors):
+ self.context.setProcessors(
+ new_processors, check_permissions=True, user=self.user)
+ del data['processors']
self.updateContextFromData(data)
self.next_url = canonical_url(self.context)
@@ -2010,37 +2016,26 @@
"enabled", "Deleted PPAs can't be enabled.")
-class ArchiveEditView(BaseArchiveEditView):
-
- field_names = [
- 'displayname',
- 'description',
- 'enabled',
- 'publish',
- 'build_debug_symbols',
- 'publish_debug_symbols',
- ]
- custom_widget(
- 'description', TextAreaWidget, height=10, width=30)
- page_title = 'Change details'
-
- @property
- def label(self):
- return 'Edit %s' % self.context.displayname
-
-
class EnableProcessorsMixin:
"""A mixin that provides processors field support"""
- def createEnabledProcessors(self, description=None):
+ def createEnabledProcessors(self, available_processors, description=None):
"""Creates the 'processors' field."""
terms = []
- for processor in sorted(
- getUtility(IProcessorSet).getAll(), key=attrgetter('name')):
+ disabled = []
+ if check_permission('launchpad.Admin', self.context):
+ can_modify = lambda proc: True
+ else:
+ can_modify = lambda proc: not proc.restricted
+ for processor in sorted(available_processors, key=attrgetter('name')):
terms.append(SimpleTerm(
processor, token=processor.name,
title="%s (%s)" % (processor.title, processor.name)))
+ if not can_modify(processor):
+ disabled.append(processor)
old_field = IArchive['processors']
+ widget = CustomWidgetFactory(
+ LabeledMultiCheckBoxWidget, disabled_items=disabled)
return form.Fields(
List(__name__=old_field.__name__,
title=old_field.title,
@@ -2048,7 +2043,54 @@
required=False,
description=old_field.description if description is None
else description),
- render_context=self.render_context)
+ render_context=self.render_context, custom_widget=widget)
+
+
+class ArchiveEditView(BaseArchiveEditView, EnableProcessorsMixin):
+
+ field_names = [
+ 'displayname',
+ 'description',
+ 'enabled',
+ 'publish',
+ 'build_debug_symbols',
+ 'publish_debug_symbols',
+ ]
+ custom_widget(
+ 'description', TextAreaWidget, height=10, width=30)
+ page_title = 'Change details'
+
+ @property
+ def label(self):
+ return 'Edit %s' % self.context.displayname
+
+ @property
+ def initial_values(self):
+ return {
+ 'processors': self.context.processors,
+ }
+
+ def setUpFields(self):
+ """Override `LaunchpadEditFormView`.
+
+ See `createEnabledProcessors` method.
+ """
+ super(ArchiveEditView, self).setUpFields()
+ self.form_fields += self.createEnabledProcessors(
+ self.context.available_processors,
+ u"The architectures on which the archive can build. Some "
+ u"architectures are restricted and may only be enabled or "
+ u"disabled by administrators.")
+
+ def validate(self, data):
+ if 'processors' in data:
+ available_processors = set(self.context.available_processors)
+ for processor in self.context.processors:
+ if (processor not in available_processors and
+ processor not in data['processors']):
+ # This processor is not currently available for
+ # selection, but is enabled. Leave it untouched.
+ data['processors'].append(processor)
class ArchiveAdminView(BaseArchiveEditView, EnableProcessorsMixin):
@@ -2064,7 +2106,6 @@
'external_dependencies',
]
custom_widget('external_dependencies', TextAreaWidget, height=3)
- custom_widget('processors', LabeledMultiCheckBoxWidget)
page_title = 'Administer'
@property
@@ -2105,20 +2146,6 @@
"""
return self.context.owner.visibility == PersonVisibility.PRIVATE
- @property
- def initial_values(self):
- return {
- 'processors': self.context.processors,
- }
-
- def setUpFields(self):
- """Override `LaunchpadEditFormView`.
-
- See `createEnabledProcessors` method.
- """
- super(ArchiveAdminView, self).setUpFields()
- self.form_fields += self.createEnabledProcessors()
-
class ArchiveDeleteView(LaunchpadFormView):
"""View class for deleting `IArchive`s"""
=== modified file 'lib/lp/soyuz/browser/tests/test_archive.py'
--- lib/lp/soyuz/browser/tests/test_archive.py 2014-06-30 13:51:15 +0000
+++ lib/lp/soyuz/browser/tests/test_archive.py 2015-09-23 12:20:46 +0000
@@ -1,21 +1,142 @@
-# Copyright 2014 Canonical Ltd. This software is licensed under the
+# Copyright 2014-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
-from testtools.matchers import Equals
+from fixtures import FakeLogger
+from testtools.matchers import (
+ Equals,
+ MatchesSetwise,
+ MatchesStructure,
+ )
+from zope.component import getUtility
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.buildmaster.interfaces.processor import IProcessorSet
+from lp.services.webapp import canonical_url
+from lp.soyuz.interfaces.archive import CannotModifyArchiveProcessor
from lp.testing import (
admin_logged_in,
login_person,
record_two_runs,
TestCaseWithFactory,
)
-from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.layers import (
+ DatabaseFunctionalLayer,
+ LaunchpadFunctionalLayer,
+ )
from lp.testing.matchers import HasQueryCount
+from lp.testing.pages import extract_text
from lp.testing.views import create_initialized_view
+class TestArchiveEditView(TestCaseWithFactory):
+
+ layer = LaunchpadFunctionalLayer
+
+ def setUp(self):
+ super(TestArchiveEditView, self).setUp()
+ # None of the Ubuntu series in sampledata have amd64. Add it to
+ # breezy so that it shows up in the list of available processors.
+ self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
+ proc_amd64 = getUtility(IProcessorSet).getByName("amd64")
+ self.factory.makeDistroArchSeries(
+ distroseries=self.ubuntu.getSeries("breezy-autotest"),
+ architecturetag="amd64", processor=proc_amd64)
+
+ def test_display_processors(self):
+ ppa = self.factory.makeArchive()
+ owner = login_person(ppa.owner)
+ browser = self.getUserBrowser(
+ canonical_url(ppa) + "/+edit", user=owner)
+ processors = browser.getControl(name="field.processors")
+ self.assertContentEqual(
+ ["Intel 386 (386)", "AMD 64bit (amd64)", "HPPA Processor (hppa)"],
+ [extract_text(option) for option in processors.displayOptions])
+ self.assertContentEqual(["386", "amd64", "hppa"], processors.options)
+
+ def test_edit_processors(self):
+ ppa = self.factory.makeArchive()
+ owner = login_person(ppa.owner)
+ self.assertContentEqual(
+ ["386", "amd64", "hppa"],
+ [processor.name for processor in ppa.processors])
+ browser = self.getUserBrowser(
+ canonical_url(ppa) + "/+edit", user=owner)
+ processors = browser.getControl(name="field.processors")
+ self.assertContentEqual(["386", "amd64", "hppa"], processors.value)
+ processors.value = ["386", "amd64"]
+ browser.getControl("Save").click()
+ login_person(ppa.owner)
+ self.assertContentEqual(
+ ["386", "amd64"],
+ [processor.name for processor in ppa.processors])
+
+ def test_edit_with_invisible_processor(self):
+ # It's possible for existing archives to have an enabled processor
+ # that's no longer usable with any non-obsolete distroseries, which
+ # will mean it's hidden from the UI, but the non-admin
+ # Archive.setProcessors isn't allowed to disable it. Editing the
+ # processor list of such an archive leaves the invisible processor
+ # intact.
+ proc_386 = getUtility(IProcessorSet).getByName("386")
+ proc_amd64 = getUtility(IProcessorSet).getByName("amd64")
+ proc_armel = self.factory.makeProcessor(
+ name="armel", restricted=True, build_by_default=False)
+ ppa = self.factory.makeArchive()
+ with admin_logged_in():
+ ppa.processors = [proc_386, proc_amd64, proc_armel]
+ owner = login_person(ppa.owner)
+ browser = self.getUserBrowser(
+ canonical_url(ppa) + "/+edit", user=owner)
+ processors = browser.getControl(name="field.processors")
+ self.assertContentEqual(["386", "amd64"], processors.value)
+ processors.value = ["amd64"]
+ browser.getControl("Save").click()
+ login_person(ppa.owner)
+ self.assertContentEqual(
+ ["amd64", "armel"],
+ [processor.name for processor in ppa.processors])
+
+ def test_edit_processors_restricted(self):
+ # A restricted processor is shown disabled in the UI and cannot be
+ # enabled.
+ self.useFixture(FakeLogger())
+ proc_armhf = self.factory.makeProcessor(
+ name="armhf", restricted=True, build_by_default=False)
+ self.factory.makeDistroArchSeries(
+ distroseries=self.ubuntu.getSeries("breezy-autotest"),
+ architecturetag="armhf", processor=proc_armhf)
+ ppa = self.factory.makeArchive()
+ owner = login_person(ppa.owner)
+ self.assertContentEqual(
+ ["386", "amd64", "hppa"],
+ [processor.name for processor in ppa.processors])
+ browser = self.getUserBrowser(
+ canonical_url(ppa) + "/+edit", user=owner)
+ processors = browser.getControl(name="field.processors")
+ self.assertContentEqual(["386", "amd64", "hppa"], processors.value)
+ self.assertThat(
+ processors.controls, MatchesSetwise(
+ MatchesStructure.byEquality(
+ optionValue="386", disabled=False),
+ MatchesStructure.byEquality(
+ optionValue="amd64", disabled=False),
+ MatchesStructure.byEquality(
+ optionValue="armhf", disabled=True),
+ MatchesStructure.byEquality(
+ optionValue="hppa", disabled=False),
+ ))
+ # Even if the user works around the disabled checkbox and forcibly
+ # enables it, they can't enable the restricted processor.
+ for control in processors.controls:
+ if control.optionValue == "armhf":
+ control.mech_item.disabled = False
+ processors.value = ["386", "amd64", "armhf"]
+ self.assertRaises(
+ CannotModifyArchiveProcessor, browser.getControl("Save").click)
+
+
class TestArchiveCopyPackagesView(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
=== modified file 'lib/lp/soyuz/browser/tests/test_archive_admin_view.py'
--- lib/lp/soyuz/browser/tests/test_archive_admin_view.py 2015-06-19 05:47:43 +0000
+++ lib/lp/soyuz/browser/tests/test_archive_admin_view.py 2015-09-23 12:20:46 +0000
@@ -3,17 +3,14 @@
__metaclass__ = type
-from lp.services.webapp import canonical_url
from lp.services.webapp.servers import LaunchpadTestRequest
from lp.soyuz.browser.archive import ArchiveAdminView
from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
from lp.testing import (
login,
- login_celebrity,
TestCaseWithFactory,
)
from lp.testing.layers import LaunchpadFunctionalLayer
-from lp.testing.pages import extract_text
class TestArchivePrivacySwitchingView(TestCaseWithFactory):
@@ -91,36 +88,3 @@
'This archive already has published sources. '
'It is not possible to switch the privacy.',
view.errors[0])
-
-
-class TestArchiveAdminView(TestCaseWithFactory):
-
- layer = LaunchpadFunctionalLayer
-
- def test_display_processors(self):
- ppa = self.factory.makeArchive()
- admin = login_celebrity("admin")
- browser = self.getUserBrowser(
- canonical_url(ppa) + "/+admin", user=admin)
- processors = browser.getControl(name="field.processors")
- self.assertContentEqual(
- ["Intel 386 (386)", "AMD 64bit (amd64)", "HPPA Processor (hppa)"],
- [extract_text(option) for option in processors.displayOptions])
- self.assertContentEqual(["386", "amd64", "hppa"], processors.options)
-
- def test_edit_processors(self):
- ppa = self.factory.makeArchive()
- admin = login_celebrity("admin")
- self.assertEqual(
- ["386", "amd64", "hppa"],
- [processor.name for processor in ppa.processors])
- browser = self.getUserBrowser(
- canonical_url(ppa) + "/+admin", user=admin)
- processors = browser.getControl(name="field.processors")
- self.assertContentEqual(["386", "amd64", "hppa"], processors.value)
- processors.value = ["386", "amd64"]
- browser.getControl("Save").click()
- login_celebrity("admin")
- self.assertEqual(
- ["386", "amd64"],
- [processor.name for processor in ppa.processors])
=== modified file 'lib/lp/soyuz/browser/tests/test_archive_webservice.py'
--- lib/lp/soyuz/browser/tests/test_archive_webservice.py 2015-09-16 06:53:39 +0000
+++ lib/lp/soyuz/browser/tests/test_archive_webservice.py 2015-09-23 12:20:46 +0000
@@ -1,4 +1,4 @@
-# Copyright 2010-2013 Canonical Ltd. This software is licensed under the
+# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -304,47 +304,83 @@
self.assertEqual('New ARM Title', ws_proc.title)
self.assertEqual('New ARM Description', ws_proc.description)
- def test_setProcessors(self):
- """A new processor can be added to the enabled restricted set."""
+ def setProcessors(self, user, archive_url, names):
+ ws = webservice_for_person(
+ user, permission=OAuthPermission.WRITE_PUBLIC)
+ return ws.named_post(
+ archive_url, 'setProcessors',
+ processors=['/+processors/%s' % name for name in names],
+ api_version='devel')
+
+ def assertProcessors(self, user, archive_url, names):
+ body = webservice_for_person(user).get(
+ archive_url + '/processors', api_version='devel').jsonBody()
+ self.assertContentEqual(
+ names, [entry['name'] for entry in body['entries']])
+
+ def test_setProcessors_admin(self):
+ """An admin can add a new processor to the enabled restricted set."""
commercial = getUtility(ILaunchpadCelebrities).commercial_admin
commercial_admin = self.factory.makePerson(member_of=[commercial])
self.factory.makeProcessor(
'arm', 'ARM', 'ARM', restricted=True, build_by_default=False)
ppa_url = api_url(self.factory.makeArchive(purpose=ArchivePurpose.PPA))
-
- body = webservice_for_person(commercial_admin).get(
- ppa_url + '/processors', api_version='devel').jsonBody()
- self.assertContentEqual(
- ['386', 'hppa', 'amd64'],
- [entry['name'] for entry in body['entries']])
-
- response = webservice_for_person(
- commercial_admin,
- permission=OAuthPermission.WRITE_PUBLIC).named_post(
- ppa_url, 'setProcessors',
- processors=['/+processors/386', '/+processors/arm'],
- api_version='devel')
+ self.assertProcessors(
+ commercial_admin, ppa_url, ['386', 'hppa', 'amd64'])
+
+ response = self.setProcessors(
+ commercial_admin, ppa_url, ['386', 'arm'])
self.assertEqual(200, response.status)
-
- body = webservice_for_person(commercial_admin).get(
- ppa_url + '/processors', api_version='devel').jsonBody()
- self.assertContentEqual(
- ['386', 'arm'], [entry['name'] for entry in body['entries']])
-
- def test_setProcessors_owner_forbidden(self):
- """Only commercial admins can call setProcessors."""
+ self.assertProcessors(commercial_admin, ppa_url, ['386', 'arm'])
+
+ def test_setProcessors_non_owner_forbidden(self):
+ """Only commercial admins and archive owners can call setProcessors."""
self.factory.makeProcessor(
- 'arm', 'ARM', 'ARM', restricted=True, build_by_default=False)
- archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
- ppa_url = api_url(archive)
- owner = archive.owner
+ 'unrestricted', 'Unrestricted', 'Unrestricted', restricted=False,
+ build_by_default=False)
+ ppa_url = api_url(self.factory.makeArchive(purpose=ArchivePurpose.PPA))
- response = webservice_for_person(owner).named_post(
- ppa_url, 'setProcessors',
- processors=['/+processors/386', '/+processors/arm'],
- api_version='devel')
+ response = self.setProcessors(
+ self.factory.makePerson(), ppa_url, ['386', 'unrestricted'])
self.assertEqual(401, response.status)
+ def test_setProcessors_owner(self):
+ """The archive owner can enable/disable unrestricted processors."""
+ archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
+ ppa_url = api_url(archive)
+ owner = archive.owner
+ self.assertProcessors(owner, ppa_url, ['386', 'hppa', 'amd64'])
+
+ response = self.setProcessors(owner, ppa_url, ['386'])
+ self.assertEqual(200, response.status)
+ self.assertProcessors(owner, ppa_url, ['386'])
+
+ response = self.setProcessors(owner, ppa_url, ['386', 'amd64'])
+ self.assertEqual(200, response.status)
+ self.assertProcessors(owner, ppa_url, ['386', 'amd64'])
+
+ def test_setProcessors_owner_restricted_forbidden(self):
+ """The archive owner cannot enable/disable restricted processors."""
+ commercial = getUtility(ILaunchpadCelebrities).commercial_admin
+ commercial_admin = self.factory.makePerson(member_of=[commercial])
+ self.factory.makeProcessor(
+ 'arm', 'ARM', 'ARM', restricted=True, build_by_default=False)
+ archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
+ ppa_url = api_url(archive)
+ owner = archive.owner
+
+ response = self.setProcessors(owner, ppa_url, ['386', 'arm'])
+ self.assertEqual(403, response.status)
+
+ # If a commercial admin enables arm, the owner cannot disable it.
+ response = self.setProcessors(
+ commercial_admin, ppa_url, ['386', 'arm'])
+ self.assertEqual(200, response.status)
+ self.assertProcessors(owner, ppa_url, ['386', 'arm'])
+
+ response = self.setProcessors(owner, ppa_url, ['386'])
+ self.assertEqual(403, response.status)
+
def test_enableRestrictedProcessor(self):
"""A new processor can be added to the enabled restricted set."""
self.ws_version = 'devel'
=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml 2015-09-21 19:21:57 +0000
+++ lib/lp/soyuz/configure.zcml 2015-09-23 12:20:46 +0000
@@ -365,7 +365,7 @@
permission="launchpad.Edit"
interface="lp.soyuz.interfaces.archive.IArchiveEdit"
set_attributes="build_debug_symbols description displayname
- publish publish_debug_symbols status
+ processors publish publish_debug_symbols status
suppress_subscription_notifications"/>
<!--
NOTE: The 'private' permission controls who can turn a public
@@ -380,7 +380,7 @@
enabled_restricted_processors
external_dependencies name
permit_obsolete_series_uploads
- private processors require_virtualized"/>
+ private require_virtualized"/>
<require
permission="launchpad.Moderate"
set_schema="lp.soyuz.interfaces.archive.IArchiveRestricted"/>
=== modified file 'lib/lp/soyuz/interfaces/archive.py'
--- lib/lp/soyuz/interfaces/archive.py 2015-09-16 06:53:39 +0000
+++ lib/lp/soyuz/interfaces/archive.py 2015-09-23 12:20:46 +0000
@@ -14,6 +14,7 @@
'CannotCopy',
'CannotSwitchPrivacy',
'ComponentNotFound',
+ 'CannotModifyArchiveProcessor',
'CannotUploadToArchive',
'CannotUploadToPPA',
'CannotUploadToPocket',
@@ -297,6 +298,19 @@
self.errors = errors
+@error_status(httplib.FORBIDDEN)
+class CannotModifyArchiveProcessor(Exception):
+ """Tried to enable or disable a restricted processor on an archive."""
+
+ _fmt = (
+ '%(processor)s is restricted, and may only be enabled or disabled '
+ 'by administrators.')
+
+ def __init__(self, processor):
+ super(CannotModifyArchiveProcessor, self).__init__(
+ self._fmt % {'processor': processor.name})
+
+
class IArchivePublic(IPrivacy, IHasOwner):
"""An Archive interface for publicly available operations."""
# Most of this stuff should really be on View, but it's needed for
@@ -650,6 +664,20 @@
readonly=True),
as_of='devel')
+ available_processors = Attribute(
+ "The architectures that are available to be enabled or disabled for "
+ "this archive.")
+
+ @call_with(check_permissions=True, user=REQUEST_USER)
+ @operation_parameters(
+ processors=List(
+ value_type=Reference(schema=IProcessor), required=True),
+ )
+ @export_write_operation()
+ @operation_for_version('devel')
+ def setProcessors(processors, check_permissions=False, user=None):
+ """Set the architectures on which the archive can build."""
+
def getSourcesForDeletion(name=None, status=None, distroseries=None):
"""All `ISourcePackagePublishingHistory` available for deletion.
@@ -2050,15 +2078,6 @@
"""Archive interface for operations restricted by commercial."""
@operation_parameters(
- processors=List(
- value_type=Reference(schema=IProcessor), required=True),
- )
- @export_write_operation()
- @operation_for_version('devel')
- def setProcessors(processors):
- """Set the architectures on which the archive can build."""
-
- @operation_parameters(
processor=Reference(schema=IProcessor, required=True),
)
@export_write_operation()
=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py 2015-09-16 06:53:39 +0000
+++ lib/lp/soyuz/model/archive.py 2015-09-23 12:20:46 +0000
@@ -48,6 +48,7 @@
alsoProvides,
implementer,
)
+from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
from lp.app.errors import NotFoundError
@@ -139,6 +140,7 @@
ArchiveDisabled,
ArchiveNotPrivate,
CannotCopy,
+ CannotModifyArchiveProcessor,
CannotSwitchPrivacy,
CannotUploadToPocket,
CannotUploadToPPA,
@@ -2129,23 +2131,60 @@
"""See `IArchive`."""
self.processors = set(self.processors + [processor])
+ @property
+ def available_processors(self):
+ """See `IArchive`."""
+ # Circular imports.
+ from lp.registry.model.distroseries import DistroSeries
+ from lp.soyuz.model.distroarchseries import DistroArchSeries
+
+ clauses = [
+ Processor.id == DistroArchSeries.processor_id,
+ DistroArchSeries.distroseriesID == DistroSeries.id,
+ DistroSeries.distribution == self.distribution,
+ ]
+ if not self.permit_obsolete_series_uploads:
+ clauses.append(DistroSeries.status != SeriesStatus.OBSOLETE)
+ return Store.of(self).find(Processor, *clauses).config(
+ distinct=(Processor.id,))
+
def _getProcessors(self):
return list(Store.of(self).find(
Processor,
Processor.id == ArchiveArch.processor_id,
ArchiveArch.archive == self))
- def setProcessors(self, processors):
+ def setProcessors(self, processors, check_permissions=False, user=None):
"""See `IArchive`."""
+ if check_permissions:
+ can_modify = None
+ if user is not None:
+ roles = IPersonRoles(user)
+ authz = lambda perm: getAdapter(self, IAuthorization, perm)
+ if authz('launchpad.Admin').checkAuthenticated(roles):
+ can_modify = lambda proc: True
+ elif authz('launchpad.Edit').checkAuthenticated(roles):
+ can_modify = lambda proc: not proc.restricted
+ if can_modify is None:
+ raise Unauthorized(
+ 'Permission launchpad.Admin or launchpad.Edit required '
+ 'on %s.' % self)
+ else:
+ can_modify = lambda proc: True
+
enablements = dict(Store.of(self).find(
(Processor, ArchiveArch),
Processor.id == ArchiveArch.processor_id,
ArchiveArch.archive == self))
for proc in enablements:
if proc not in processors:
+ if not can_modify(proc):
+ raise CannotModifyArchiveProcessor(proc)
Store.of(self).remove(enablements[proc])
for proc in processors:
if proc not in self.processors:
+ if not can_modify(proc):
+ raise CannotModifyArchiveProcessor(proc)
archivearch = ArchiveArch()
archivearch.archive = self
archivearch.processor = proc
=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py 2015-09-21 19:21:57 +0000
+++ lib/lp/soyuz/tests/test_archive.py 2015-09-23 12:20:46 +0000
@@ -63,6 +63,7 @@
ArchiveDependencyError,
ArchiveDisabled,
CannotCopy,
+ CannotModifyArchiveProcessor,
CannotUploadToPocket,
CannotUploadToPPA,
CannotUploadToSeries,
@@ -1077,11 +1078,11 @@
"""Ensure that restricted architectures builds can be allowed and
disallowed correctly."""
- layer = LaunchpadZopelessLayer
+ layer = LaunchpadFunctionalLayer
def setUp(self):
"""Setup an archive with relevant publications."""
- super(TestProcessors, self).setUp()
+ super(TestProcessors, self).setUp(user='foo.bar@xxxxxxxxxxxxx')
self.publisher = SoyuzTestPublisher()
self.publisher.prepareBreezyAutotest()
self.archive = self.factory.makeArchive()
@@ -1139,6 +1140,43 @@
self.archive.processors = []
self.assertContentEqual([], self.archive.processors)
+ def test_set_non_admin(self):
+ """Non-admins can only enable or disable unrestricted processors."""
+ self.archive.setProcessors(self.default_procs)
+ self.assertContentEqual(self.default_procs, self.archive.processors)
+ with person_logged_in(self.archive.owner) as owner:
+ # Adding arm is forbidden ...
+ self.assertRaises(
+ CannotModifyArchiveProcessor, self.archive.setProcessors,
+ [self.default_procs[0], self.arm],
+ check_permissions=True, user=owner)
+ # ... but removing amd64 is OK.
+ self.archive.setProcessors(
+ [self.default_procs[0]], check_permissions=True, user=owner)
+ self.assertContentEqual(
+ [self.default_procs[0]], self.archive.processors)
+ with admin_logged_in() as admin:
+ self.archive.setProcessors(
+ [self.default_procs[0], self.arm],
+ check_permissions=True, user=admin)
+ self.assertContentEqual(
+ [self.default_procs[0], self.arm], self.archive.processors)
+ with person_logged_in(self.archive.owner) as owner:
+ hppa = getUtility(IProcessorSet).getByName("hppa")
+ self.assertFalse(hppa.restricted)
+ # Adding hppa while removing arm is forbidden ...
+ self.assertRaises(
+ CannotModifyArchiveProcessor, self.archive.setProcessors,
+ [self.default_procs[0], hppa],
+ check_permissions=True, user=owner)
+ # ... but adding hppa while retaining arm is OK.
+ self.archive.setProcessors(
+ [self.default_procs[0], self.arm, hppa],
+ check_permissions=True, user=owner)
+ self.assertContentEqual(
+ [self.default_procs[0], self.arm, hppa],
+ self.archive.processors)
+
def test_set_enabled_restricted_processors(self):
"""The deprecated enabled_restricted_processors property still works.
Follow ups