Merge lp:~wgrant/launchpad/dedupe-bugnotification-canApprove into lp:launchpad

Proposed by William Grant
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 13011
Proposed branch: lp:~wgrant/launchpad/dedupe-bugnotification-canApprove
Merge into: lp:launchpad
Diff against target: 429 lines (+168/-151)
6 files modified
database/schema/security.cfg (+5/-0)
lib/lp/bugs/doc/bug-nomination.txt (+0/-125)
lib/lp/bugs/model/bugnomination.py (+16/-19)
lib/lp/bugs/tests/test_bugnomination.py (+116/-1)
lib/lp/soyuz/model/component.py (+3/-2)
lib/lp/testing/factory.py (+28/-4)
To merge this branch: bzr merge lp:~wgrant/launchpad/dedupe-bugnotification-canApprove
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+60369@code.launchpad.net

Commit message

[r=benji][bug=451390] BugNomination.canApprove now uses Archive.verifyUpload, allowing it to respect Packageset upload permissions.

Description of the change

BugNomination.canApprove has long duplicated logic from archiveuploader and elsewhere to allow people to approve series nominations for packages they can upload. This has lead to a number of deficiencies, but most recently a lack of support for packageset permissions.

This branch changes BugNomination.canApprove to use Archive.verifyUpload (which checks against the package, its component, and any relevant packagesets) instead of Archive.checkArchivePermission (which just checks the package or component that it is given).

I replaced the canApprove doctest with unittests, and added a new one to ensure that packageset permissions are respected.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good.

I do have one question: I haven't seen "with
celebrity_logged_in('admin'):" used in non-test code before (and a grep
through the LP code doesn't turn any up). That doesn't mean it's wrong,
but perhaps something that warrants some thought. I'm a little worried
that logging or other audit trail might be polluted by faking the active
user. Perhaps judicious use of removeSecurityProxy is an alternative.

review: Approve (code)
Revision history for this message
William Grant (wgrant) wrote :

That's in lp.testing.factory.

Revision history for this message
Benji York (benji) wrote :

On Mon, May 9, 2011 at 7:04 PM, William Grant <email address hidden> wrote:
> That's in lp.testing.factory.

Ha! Right you are.
--
Benji York

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2011-05-04 17:14:18 +0000
3+++ database/schema/security.cfg 2011-05-10 04:03:36 +0000
4@@ -1652,6 +1652,7 @@
5 public.distroseries = SELECT
6 public.emailaddress = SELECT
7 public.faq = SELECT
8+public.flatpackagesetinclusion = SELECT
9 public.gpgkey = SELECT
10 public.job = SELECT, INSERT, UPDATE
11 public.karma = SELECT, INSERT
12@@ -1664,6 +1665,10 @@
13 public.messagechunk = SELECT, INSERT
14 public.milestone = SELECT
15 public.packagebugsupervisor = SELECT
16+public.packageset = SELECT
17+public.packagesetgroup = SELECT
18+public.packagesetinclusion = SELECT
19+public.packagesetsources = SELECT
20 public.person = SELECT, UPDATE
21 public.personlanguage = SELECT
22 public.personsettings = SELECT
23
24=== modified file 'lib/lp/bugs/doc/bug-nomination.txt'
25--- lib/lp/bugs/doc/bug-nomination.txt 2011-03-28 20:54:50 +0000
26+++ lib/lp/bugs/doc/bug-nomination.txt 2011-05-10 04:03:36 +0000
27@@ -352,131 +352,6 @@
28 >>> ubuntu_warty.driver = None
29
30
31-=== Permissions to approve a nomination ===
32-
33-Since approving a nomination can affect release planning, not everyone
34-is allowed to do it. For large projects, like distributions, it's not
35-suitable to restrict approving nominations to only release managers,
36-instead anyone that can upload a package may approve a nomination. The
37-package upload permission system is a bit complex. Someone can be
38-allowed to either upload a single source package, or all packages in a
39-component, or all packages in a packageset.
40-
41- >>> from lp.soyuz.interfaces.archivepermission import (
42- ... IArchivePermissionSet)
43- >>> from lp.soyuz.model.component import Component
44- >>> permission_set = getUtility(IArchivePermissionSet)
45- >>> main = Component.byName('main')
46- >>> main_uploader = factory.makePerson()
47- >>> permission_set.newComponentUploader(
48- ... ubuntu.main_archive, main_uploader, main)
49- <ArchivePermission at ...>
50-
51- >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
52- >>> ubuntu_evolution = ubuntu.getSourcePackage('evolution')
53- >>> evolution_uploader = factory.makePerson()
54- >>> getUtility(IArchivePermissionSet).newPackageUploader(
55- ... ubuntu.main_archive, evolution_uploader,
56- ... ubuntu_evolution.sourcepackagename)
57- <ArchivePermission at ...>
58- >>> ubuntu_cnews = ubuntu.getSourcePackage('cnews')
59- >>> cnews_uploader = factory.makePerson()
60- >>> getUtility(IArchivePermissionSet).newPackageUploader(
61- ... ubuntu.main_archive, cnews_uploader,
62- ... ubuntu_cnews.sourcepackagename)
63- <ArchivePermission at ...>
64-
65-For a bug nominated to a package from the 'main' component in Ubuntu,
66-people that are allowed to upload to 'main' are allowed to approve the
67-nomination.
68-
69- >>> ubuntu_evolution = ubuntu.getSourcePackage('evolution')
70- >>> main_bugtask = factory.makeBugTask(target=ubuntu_evolution)
71- >>> main_bug = main_bugtask.bug
72- >>> ubuntu_hoary = ubuntu.getSeries('hoary')
73- >>> hoary_evolution = ubuntu_hoary.getSourcePackage('evolution')
74- >>> hoary_evolution.latest_published_component.name
75- u'main'
76- >>> nominator = factory.makePerson()
77- >>> hoary_main_nomination = main_bug.addNomination(
78- ... nominator, ubuntu_hoary)
79-
80- >>> hoary_main_nomination.canApprove(nominator)
81- False
82- >>> hoary_main_nomination.canApprove(main_uploader)
83- True
84-
85-People that have been granted rights to upload that specific package are
86-also allowed.
87-
88- >>> hoary_main_nomination.canApprove(evolution_uploader)
89- True
90- >>> hoary_main_nomination.canApprove(cnews_uploader)
91- False
92-
93-Only uploaders to the relevant component are allowed to approve it. For
94-example, people who are allowed only to upload to 'universe' may not
95-approve 'main' nominations.
96-
97- >>> universe_uploader = factory.makePerson()
98- >>> universe = Component.byName('universe')
99- >>> permission_set.newComponentUploader(
100- ... ubuntu.main_archive, universe_uploader, universe)
101- <ArchivePermission at ...>
102-
103- >>> hoary_main_nomination.canApprove(universe_uploader)
104- False
105-
106-If the bug is targeted to source packages in more than one component,
107-anyone that can upload to any of the components or packages may approve
108-the nomination. This is because a nomination is tied to a release only,
109-not to source packages.
110-
111- >>> hoary_cnews = ubuntu_hoary.getSourcePackage('cnews')
112- >>> hoary_cnews.latest_published_component.name
113- u'universe'
114- >>> universe_task = factory.makeBugTask(
115- ... bug=main_bug, target=hoary_cnews)
116- >>> hoary_main_nomination.canApprove(universe_uploader)
117- True
118- >>> hoary_main_nomination.canApprove(main_uploader)
119- True
120- >>> hoary_main_nomination.canApprove(evolution_uploader)
121- True
122- >>> hoary_main_nomination.canApprove(cnews_uploader)
123- True
124-
125-Drivers may approve nominations as well.
126-
127- >>> hoary_driver = factory.makePerson()
128- >>> ubuntu_hoary.driver = hoary_driver
129- >>> hoary_main_nomination.canApprove(hoary_driver)
130- True
131-
132- >>> ubuntu_driver = factory.makePerson()
133- >>> ubuntu.driver = ubuntu_driver
134- >>> hoary_main_nomination.canApprove(ubuntu_driver)
135- True
136-
137-If a bug isn't targeted to a specific source package, any component
138-uploader can approve a nomination.
139-
140- >>> ubuntu_bugtask = factory.makeBugTask(target=ubuntu)
141- >>> bug = ubuntu_bugtask.bug
142- >>> hoary_nomination = bug.addNomination(nominator, ubuntu_hoary)
143- >>> hoary_nomination.canApprove(nominator)
144- False
145-
146- >>> hoary_nomination.canApprove(main_uploader)
147- True
148- >>> hoary_nomination.canApprove(universe_uploader)
149- True
150- >>> hoary_nomination.canApprove(evolution_uploader)
151- False
152- >>> hoary_nomination.canApprove(cnews_uploader)
153- False
154-
155-
156 == Declining a nomination ==
157
158 Declining a nomination simply sets its status to DECLINED. No tasks are
159
160=== modified file 'lib/lp/bugs/model/bugnomination.py'
161--- lib/lp/bugs/model/bugnomination.py 2010-08-20 20:31:18 +0000
162+++ lib/lp/bugs/model/bugnomination.py 2011-05-10 04:03:36 +0000
163@@ -127,33 +127,30 @@
164 return True
165
166 if self.distroseries is not None:
167- # For distributions anyone that can upload to the
168- # distribution may approve nominations.
169- bug_packagenames_and_components = set()
170 distribution = self.distroseries.distribution
171+ # An uploader to any of the packages can approve the
172+ # nomination. Compile a list of possibilities, and check
173+ # them all.
174+ package_names = []
175 for bugtask in self.bug.bugtasks:
176 if (bugtask.distribution == distribution
177 and bugtask.sourcepackagename is not None):
178- source_package = self.distroseries.getSourcePackage(
179- bugtask.sourcepackagename)
180- bug_packagenames_and_components.add(
181- bugtask.sourcepackagename)
182- if source_package.latest_published_component is not None:
183- bug_packagenames_and_components.add(
184- source_package.latest_published_component)
185- if len(bug_packagenames_and_components) == 0:
186+ package_names.append(bugtask.sourcepackagename)
187+ if len(package_names) == 0:
188 # If the bug isn't targeted to a source package, allow
189- # any uploader to approve the nomination.
190- bug_packagenames_and_components = set(
191- upload_component.component
192- for upload_component in distribution.uploaders)
193- for packagename_or_component in bug_packagenames_and_components:
194- if distribution.main_archive.checkArchivePermission(
195- person, packagename_or_component):
196+ # any component uploader to approve the nomination, like
197+ # a new package.
198+ return distribution.main_archive.verifyUpload(
199+ person, None, None, None, strict_component=False) is None
200+ for name in package_names:
201+ component = self.distroseries.getSourcePackage(
202+ name).latest_published_component
203+ if distribution.main_archive.verifyUpload(
204+ person, name, component, self.distroseries) is None:
205 return True
206-
207 return False
208
209+
210 class BugNominationSet:
211 """See IBugNominationSet."""
212 implements(IBugNominationSet)
213
214=== modified file 'lib/lp/bugs/tests/test_bugnomination.py'
215--- lib/lp/bugs/tests/test_bugnomination.py 2011-03-23 16:28:51 +0000
216+++ lib/lp/bugs/tests/test_bugnomination.py 2011-05-10 04:03:36 +0000
217@@ -10,7 +10,11 @@
218 logout,
219 )
220 from canonical.testing.layers import DatabaseFunctionalLayer
221-from lp.testing import TestCaseWithFactory
222+from lp.soyuz.interfaces.publishing import PackagePublishingStatus
223+from lp.testing import (
224+ celebrity_logged_in,
225+ TestCaseWithFactory,
226+ )
227
228
229 class CanBeNominatedForTestMixin:
230@@ -101,3 +105,114 @@
231 sp_bug.addTask(
232 self.eric, self.series.distribution.getSourcePackage(spn))
233 self.assertTrue(sp_bug.canBeNominatedFor(self.series))
234+
235+
236+class TestCanApprove(TestCaseWithFactory):
237+
238+ layer = DatabaseFunctionalLayer
239+
240+ def test_normal_user_cannot_approve(self):
241+ nomination = self.factory.makeBugNomination(
242+ target=self.factory.makeProductSeries())
243+ self.assertFalse(nomination.canApprove(self.factory.makePerson()))
244+
245+ def test_driver_can_approve(self):
246+ product = self.factory.makeProduct(driver=self.factory.makePerson())
247+ nomination = self.factory.makeBugNomination(
248+ target=self.factory.makeProductSeries(product=product))
249+ self.assertTrue(nomination.canApprove(product.driver))
250+
251+ def publishSource(self, series, sourcepackagename, component):
252+ return self.factory.makeSourcePackagePublishingHistory(
253+ archive=series.main_archive,
254+ distroseries=series,
255+ sourcepackagename=sourcepackagename,
256+ component=component,
257+ status=PackagePublishingStatus.PUBLISHED)
258+
259+ def test_component_uploader_can_approve(self):
260+ # A component uploader can approve a nomination for a package in
261+ # that component, but not those in other components
262+ series = self.factory.makeDistroSeries()
263+ package_name = self.factory.makeSourcePackageName()
264+ with celebrity_logged_in('admin'):
265+ perm = series.main_archive.newComponentUploader(
266+ self.factory.makePerson(), self.factory.makeComponent())
267+ other_perm = series.main_archive.newComponentUploader(
268+ self.factory.makePerson(), self.factory.makeComponent())
269+ nomination = self.factory.makeBugNomination(
270+ target=series.getSourcePackage(package_name))
271+
272+ # Publish the package in one of the uploaders' components. The
273+ # uploader for the other component cannot approve the nomination.
274+ self.publishSource(series, package_name, perm.component)
275+ self.assertFalse(nomination.canApprove(other_perm.person))
276+ self.assertTrue(nomination.canApprove(perm.person))
277+
278+ def test_any_component_uploader_can_approve_for_no_package(self):
279+ # An uploader for any component can approve a nomination without
280+ # a package.
281+ series = self.factory.makeDistroSeries()
282+ with celebrity_logged_in('admin'):
283+ perm = series.main_archive.newComponentUploader(
284+ self.factory.makePerson(), self.factory.makeComponent())
285+ nomination = self.factory.makeBugNomination(target=series)
286+
287+ self.assertFalse(nomination.canApprove(self.factory.makePerson()))
288+ self.assertTrue(nomination.canApprove(perm.person))
289+
290+ def test_package_uploader_can_approve(self):
291+ # A package uploader can approve a nomination for that package,
292+ # but not others.
293+ series = self.factory.makeDistroSeries()
294+ package_name = self.factory.makeSourcePackageName()
295+ with celebrity_logged_in('admin'):
296+ perm = series.main_archive.newPackageUploader(
297+ self.factory.makePerson(), package_name)
298+ other_perm = series.main_archive.newPackageUploader(
299+ self.factory.makePerson(),
300+ self.factory.makeSourcePackageName())
301+ nomination = self.factory.makeBugNomination(
302+ target=series.getSourcePackage(package_name))
303+
304+ self.assertFalse(nomination.canApprove(other_perm.person))
305+ self.assertTrue(nomination.canApprove(perm.person))
306+
307+ def test_packageset_uploader_can_approve(self):
308+ # A packageset uploader can approve a nomination for anything in
309+ # that packageset.
310+ series = self.factory.makeDistroSeries()
311+ package_name = self.factory.makeSourcePackageName()
312+ ps = self.factory.makePackageset(
313+ distroseries=series, packages=[package_name])
314+ with celebrity_logged_in('admin'):
315+ perm = series.main_archive.newPackagesetUploader(
316+ self.factory.makePerson(), ps)
317+ nomination = self.factory.makeBugNomination(
318+ target=series.getSourcePackage(package_name))
319+
320+ self.assertFalse(nomination.canApprove(self.factory.makePerson()))
321+ self.assertTrue(nomination.canApprove(perm.person))
322+
323+ def test_any_uploader_can_approve(self):
324+ # If there are multiple tasks for a distribution, an uploader to
325+ # any of the involved packages or components can approve the
326+ # nomination.
327+ series = self.factory.makeDistroSeries()
328+ package_name = self.factory.makeSourcePackageName()
329+ comp_package_name = self.factory.makeSourcePackageName()
330+ with celebrity_logged_in('admin'):
331+ package_perm = series.main_archive.newPackageUploader(
332+ self.factory.makePerson(), package_name)
333+ comp_perm = series.main_archive.newComponentUploader(
334+ self.factory.makePerson(), self.factory.makeComponent())
335+ nomination = self.factory.makeBugNomination(
336+ target=series.getSourcePackage(package_name))
337+ self.factory.makeBugTask(
338+ bug=nomination.bug,
339+ target=series.distribution.getSourcePackage(comp_package_name))
340+
341+ self.publishSource(series, package_name, comp_perm.component)
342+ self.assertFalse(nomination.canApprove(self.factory.makePerson()))
343+ self.assertTrue(nomination.canApprove(package_perm.person))
344+ self.assertTrue(nomination.canApprove(comp_perm.person))
345
346=== modified file 'lib/lp/soyuz/model/component.py'
347--- lib/lp/soyuz/model/component.py 2010-08-20 20:31:18 +0000
348+++ lib/lp/soyuz/model/component.py 2011-05-10 04:03:36 +0000
349@@ -34,6 +34,9 @@
350
351 name = StringCol(notNull=True, alternateID=True)
352
353+ def __repr__(self):
354+ return "<%s '%s'>" % (self.__class__.__name__, self.name)
355+
356
357 class ComponentSelection(SQLBase):
358 """See IComponentSelection."""
359@@ -75,8 +78,6 @@
360 return component
361 return self.new(name)
362
363-
364 def new(self, name):
365 """See IComponentSet."""
366 return Component(name=name)
367-
368
369=== modified file 'lib/lp/testing/factory.py'
370--- lib/lp/testing/factory.py 2011-05-09 22:53:33 +0000
371+++ lib/lp/testing/factory.py 2011-05-10 04:03:36 +0000
372@@ -920,7 +920,8 @@
373 self, name=None, project=None, displayname=None,
374 licenses=None, owner=None, registrant=None,
375 title=None, summary=None, official_malone=None,
376- translations_usage=None, bug_supervisor=None):
377+ translations_usage=None, bug_supervisor=None,
378+ driver=None):
379 """Create and return a new, arbitrary Product."""
380 if owner is None:
381 owner = self.makePerson()
382@@ -947,14 +948,15 @@
383 licenses=licenses,
384 project=project,
385 registrant=registrant)
386+ naked_product = removeSecurityProxy(product)
387 if official_malone is not None:
388- removeSecurityProxy(product).official_malone = official_malone
389+ naked_product.official_malone = official_malone
390 if translations_usage is not None:
391- naked_product = removeSecurityProxy(product)
392 naked_product.translations_usage = translations_usage
393 if bug_supervisor is not None:
394- naked_product = removeSecurityProxy(product)
395 naked_product.bug_supervisor = bug_supervisor
396+ if driver is not None:
397+ naked_product.driver = driver
398 return product
399
400 def makeProductSeries(self, product=None, name=None, owner=None,
401@@ -1713,6 +1715,28 @@
402
403 return removeSecurityProxy(bug).addTask(owner, target)
404
405+ def makeBugNomination(self, bug=None, target=None):
406+ """Create and return a BugNomination.
407+
408+ Will create a non-series task if it does not already exist.
409+
410+ :param bug: The `IBug` the nomination should be for. If None,
411+ one will be created.
412+ :param target: The `IProductSeries`, `IDistroSeries` or
413+ `ISourcePackage` to nominate for.
414+ """
415+ if ISourcePackage.providedBy(target):
416+ non_series = target.distribution_sourcepackage
417+ series = target.distroseries
418+ else:
419+ non_series = target.parent
420+ series = target
421+ with celebrity_logged_in('admin'):
422+ bug = self.makeBugTask(bug=bug, target=non_series).bug
423+ nomination = bug.addNomination(
424+ getUtility(ILaunchpadCelebrities).admin, series)
425+ return nomination
426+
427 def makeBugTracker(self, base_url=None, bugtrackertype=None, title=None,
428 name=None):
429 """Make a new bug tracker."""