Merge lp:~rvb/launchpad/change-perm-sync2 into lp:launchpad

Proposed by Raphaël Badin
Status: Merged
Approved by: Raphaël Badin
Approved revision: no longer in the source branch.
Merged at revision: 12988
Proposed branch: lp:~rvb/launchpad/change-perm-sync2
Merge into: lp:launchpad
Diff against target: 1462 lines (+448/-239)
12 files modified
lib/lp/registry/browser/distroseries.py (+7/-2)
lib/lp/registry/browser/tests/test_distroseries.py (+218/-157)
lib/lp/registry/templates/distroseries-localdifferences.pt (+3/-2)
lib/lp/soyuz/browser/archive.py (+9/-3)
lib/lp/soyuz/doc/archive.txt (+23/-13)
lib/lp/soyuz/interfaces/archive.py (+23/-6)
lib/lp/soyuz/model/archive.py (+26/-8)
lib/lp/soyuz/model/packagecopyjob.py (+1/-1)
lib/lp/soyuz/scripts/packagecopier.py (+66/-23)
lib/lp/soyuz/scripts/tests/test_copypackage.py (+51/-23)
lib/lp/soyuz/stories/ppa/xx-ppa-files.txt (+2/-1)
lib/lp/soyuz/tests/test_archive.py (+19/-0)
To merge this branch: bzr merge lp:~rvb/launchpad/change-perm-sync2
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+60077@code.launchpad.net

Commit message

[r=jcsackett][bug=773261] Fix the permission check used to sync packages (It previously used lp.edit on the series and now checks upload permissions for every synced package).

Description of the change

This branch fixes the permission check used to sync packages. It previously used lp.edit on the series and now checks upload permissions for every synced package.

This is a follow-up branch on the reverted branch https://code.launchpad.net/~rvb/launchpad/change-perm-sync. The added code allow people with lp.Append on the series' main_archive to sync packages.

= Details =
The tricky part is that this perm check cannot be performed by a security adapter because the check needs more information than what is available inside a security adapter.

- Added the permission check inside checkCopy (lib/lp/soyuz/scripts/packagecopier.py). The person requesting the sync had to be passed along the call stack. Also, having lp.Append on the main_archive is enough to sync packages. This is used by the security team in ubuntu to copy any package in the Security pocket.
- Removed the perm check against lp.edit on the series.
- Added new tests and refactored existing ones in lib/lp/registry/browser/tests/test_distroseries.py to avoid code duplication.
- Drive-by documentation fix: added documentation for parameter strict_component of checkUpload (lib/lp/soyuz/interfaces/archive.py).
- Drive-by style fix in lib/lp/registry/browser/tests/test_distroseries.py: renamed a few local variables (that where overriding an import), removed unused variables.

== Pre-implementation notes ==
Spoke with William Grant.

= Tests =
(modified tests)
./bin/test -cvv test_distroseries test_sync_error_nothing_selected
./bin/test -cvv test_distroseries test_sync_notification_on_success
./bin/test -cvv test_distroseries test_sync_error_invalid_selection
(added tests)
./bin/test -cvv test_distroseries test_canPerformSync_anon
./bin/test -cvv test_distroseries test_canPerformSync_non_anon
./bin/test -cvv test_distroseries test_sync_error_no_perm_dest_archive
./bin/test -cvv test_distroseries test_sync_success_perm_component
./bin/test -cvv test_distroseries test_sync_error_no_perm_component
./bin/test -cvv test_distroseries test_sync_success_not_yet_in_derived_series
./bin/test -cvv test_distroseries test_sync_append_main_archive
./bin/test -cvv test_archive test_hasAnyPermission

= QA =
On DF: https://dogfood.launchpad.net/ubuntu/natty/+localpackagediffs
As anon user: the sync button should not be displayed.
As non privileged user: syncing packages should return an error.
As a user with perms on destination component A:
- syncing a package already existing in component B should fail.
- syncing a package already existing in component A should work.
- syncing a package not existing in the destination series should work.
  (https://dogfood.launchpad.net/ubuntu/natty/+missingpackages)
As a member of the security team: syncing package should work.

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :
Download full text (20.4 KiB)

Looks fine, I have a few comments in the diff below.

> === modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
> --- lib/lp/registry/browser/tests/test_distroseries.py 2011-05-05 05:22:20 +0000
> +++ lib/lp/registry/browser/tests/test_distroseries.py 2011-05-05 15:21:22 +0000
> @@ -27,10 +27,10 @@
> from zope.security.proxy import removeSecurityProxy
>
> from canonical.config import config
> -from canonical.database.constants import UTC_NOW
> from canonical.database.sqlbase import flush_database_caches
> from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
> from canonical.launchpad.testing.pages import find_tag_by_id
> +from canonical.launchpad.webapp.authorization import check_permission
> from canonical.launchpad.webapp.batching import BatchNavigator
> from canonical.launchpad.webapp.publisher import canonical_url
> from canonical.testing.layers import (
> @@ -48,8 +48,7 @@
> DistroSeriesDifferenceStatus,
> DistroSeriesDifferenceType,
> )
> -from lp.registry.interfaces.pocket import PackagePublishingPocket
> -from lp.registry.interfaces.series import SeriesStatus
> +from lp.registry.interfaces.person import IPersonSet
> from lp.services.features import (
> get_relevant_feature_controller,
> getFeatureFlag,
> @@ -61,15 +60,18 @@
> getFeatureStore,
> )
> from lp.soyuz.enums import (
> + ArchivePermissionType,
> PackagePublishingStatus,
> SourcePackageFormat,
> )
> +from lp.soyuz.interfaces.component import IComponentSet
> from lp.soyuz.interfaces.distributionjob import (
> IInitialiseDistroSeriesJobSource,
> )
> from lp.soyuz.interfaces.sourcepackageformat import (
> ISourcePackageFormatSelectionSet,
> )
> +from lp.soyuz.model.archivepermission import ArchivePermission
> from lp.testing import (
> celebrity_logged_in,
> feature_flags,
> @@ -113,7 +115,7 @@
> # Helper function to create a valid DSD.
> distroseries = self.factory.makeDistroSeries(
> parent_series=self.factory.makeDistroSeries())
> - ds_diff = self.factory.makeDistroSeriesDifference(
> + self.factory.makeDistroSeriesDifference(
> derived_series=distroseries, difference_type=difference_type)
> return create_initialized_view(distroseries, '+index')
>
> @@ -176,11 +178,11 @@
> derived_series,
> '+index',
> principal=self.simple_user)
> - html = view()
> + html_content = view()
>
> self.assertEqual(
> None, getFeatureFlag('soyuz.derived-series-ui.enabled'))
> - self.assertThat(html, Not(portlet_header))
> + self.assertThat(html_content, Not(portlet_header))
>
> def test_differences_portlet_all_differences(self):
> # The difference portlet shows the differences with the parent
> @@ -212,9 +214,9 @@
> # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
> # self.features to NullFeatureController.
> view.request.features = get_relevant_feature_controller()
> - html = view()
> + html_conte...

review: Approve
Revision history for this message
Raphaël Badin (rvb) wrote :

> derived_series, _, _ = self._setUpDSD() scans a little weirdly. You might try
> derived_series = self._setUpDSD()[0] or
> (derived_series, ignore1, ignore2) = self._setUpDSD()

Done.

> I think the period was actually the right punctuation here.

Done.

> It looks to me like syncSource isn't necessary; couldn't you just
> pass one source_name to syncSources? Obviously this isn't a problem
> in your branch, but it may be worth filing a bug to capture the issue?

I'll look into that. Thanks a lot for the review.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/distroseries.py'
2--- lib/lp/registry/browser/distroseries.py 2011-05-05 00:47:27 +0000
3+++ lib/lp/registry/browser/distroseries.py 2011-05-05 20:54:43 +0000
4@@ -784,7 +784,8 @@
5 if self.do_copy(
6 'selected_differences', sources, self.context.main_archive,
7 self.context, destination_pocket, include_binaries=False,
8- dest_url=series_url, dest_display_name=series_title):
9+ dest_url=series_url, dest_display_name=series_title,
10+ person=self.user):
11 # The copy worked so we can redirect back to the page to
12 # show the results.
13 self.next_url = self.request.URL
14@@ -803,7 +804,11 @@
15 This method is used as a condition for the above sync action, as
16 well as directly in the template.
17 """
18- return (check_permission('launchpad.Edit', self.context) and
19+ archive = self.context.main_archive
20+ has_perm = (self.user is not None and (
21+ archive.hasAnyPermission(self.user) or
22+ check_permission('launchpad.Append', archive)))
23+ return (has_perm and
24 self.cached_differences.batch.total() > 0)
25
26 @property
27
28=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
29--- lib/lp/registry/browser/tests/test_distroseries.py 2011-05-05 18:23:26 +0000
30+++ lib/lp/registry/browser/tests/test_distroseries.py 2011-05-05 20:54:43 +0000
31@@ -27,10 +27,10 @@
32 from zope.security.proxy import removeSecurityProxy
33
34 from canonical.config import config
35-from canonical.database.constants import UTC_NOW
36 from canonical.database.sqlbase import flush_database_caches
37 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
38 from canonical.launchpad.testing.pages import find_tag_by_id
39+from canonical.launchpad.webapp.authorization import check_permission
40 from canonical.launchpad.webapp.batching import BatchNavigator
41 from canonical.launchpad.webapp.publisher import canonical_url
42 from canonical.testing.layers import (
43@@ -48,8 +48,7 @@
44 DistroSeriesDifferenceStatus,
45 DistroSeriesDifferenceType,
46 )
47-from lp.registry.interfaces.pocket import PackagePublishingPocket
48-from lp.registry.interfaces.series import SeriesStatus
49+from lp.registry.interfaces.person import IPersonSet
50 from lp.services.features import (
51 get_relevant_feature_controller,
52 getFeatureFlag,
53@@ -61,15 +60,18 @@
54 getFeatureStore,
55 )
56 from lp.soyuz.enums import (
57+ ArchivePermissionType,
58 PackagePublishingStatus,
59 SourcePackageFormat,
60 )
61+from lp.soyuz.interfaces.component import IComponentSet
62 from lp.soyuz.interfaces.distributionjob import (
63 IInitialiseDistroSeriesJobSource,
64 )
65 from lp.soyuz.interfaces.sourcepackageformat import (
66 ISourcePackageFormatSelectionSet,
67 )
68+from lp.soyuz.model.archivepermission import ArchivePermission
69 from lp.testing import (
70 celebrity_logged_in,
71 feature_flags,
72@@ -176,11 +178,11 @@
73 derived_series,
74 '+index',
75 principal=self.simple_user)
76- html_text = view()
77+ html_content = view()
78
79 self.assertEqual(
80 None, getFeatureFlag('soyuz.derived-series-ui.enabled'))
81- self.assertThat(html_text, Not(portlet_header))
82+ self.assertThat(html_content, Not(portlet_header))
83
84 def test_differences_portlet_all_differences(self):
85 # The difference portlet shows the differences with the parent
86@@ -212,9 +214,9 @@
87 # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
88 # self.features to NullFeatureController.
89 view.request.features = get_relevant_feature_controller()
90- html_text = view()
91+ html_content = view()
92
93- self.assertThat(html_text, portlet_display)
94+ self.assertThat(html_content, portlet_display)
95
96 def test_differences_portlet_no_differences(self):
97 # The difference portlet displays 'No differences' if there is no
98@@ -238,9 +240,9 @@
99 # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
100 # self.features to NullFeatureController.
101 view.request.features = get_relevant_feature_controller()
102- html_text = view()
103+ html_content = view()
104
105- self.assertThat(html_text, portlet_display)
106+ self.assertThat(html_content, portlet_display)
107
108 def test_differences_portlet_initialising(self):
109 # The difference portlet displays 'The series is initialising.' if
110@@ -266,10 +268,10 @@
111 # XXX rvb 2011-04-12 bug=758649: LaunchpadTestRequest overrides
112 # self.features to NullFeatureController.
113 view.request.features = get_relevant_feature_controller()
114- html_text = view()
115+ html_content = view()
116
117 self.assertTrue(derived_series.is_initialising)
118- self.assertThat(html_text, portlet_display)
119+ self.assertThat(html_content, portlet_display)
120
121
122 class TestMilestoneBatchNavigatorAttribute(TestCaseWithFactory):
123@@ -404,7 +406,7 @@
124 class DistroSeriesDifferenceMixin:
125 """A helper class for testing differences pages"""
126
127- def _test_packagesets(self, html_text, packageset_text,
128+ def _test_packagesets(self, html_content, packageset_text,
129 packageset_class, msg_text):
130 parent_packagesets = soupmatchers.HTMLContains(
131 soupmatchers.Tag(
132@@ -412,7 +414,7 @@
133 attrs={'class': packageset_class},
134 text=packageset_text))
135
136- self.assertThat(html_text, parent_packagesets)
137+ self.assertThat(html_content, parent_packagesets)
138
139
140 class TestDistroSeriesLocalDifferences(
141@@ -474,11 +476,11 @@
142 ds_diff.derived_series,
143 '+localpackagediffs',
144 principal=self.simple_user)
145- html_text = view()
146+ html_content = view()
147
148 packageset_text = re.compile('\s*' + ps.name)
149 self._test_packagesets(
150- html_text, packageset_text, 'parent-packagesets',
151+ html_content, packageset_text, 'parent-packagesets',
152 'Parent packagesets')
153
154 def test_parent_packagesets_localpackagediffs_sorts(self):
155@@ -497,22 +499,25 @@
156 ds_diff.derived_series,
157 '+localpackagediffs',
158 principal=self.simple_user)
159- html_text = view()
160+ html = view()
161
162 packageset_text = re.compile(
163 '\s*' + ', '.join(sorted(unsorted_names)))
164 self._test_packagesets(
165- html_text, packageset_text, 'parent-packagesets',
166- 'Parent packagesets')
167+ html, packageset_text, 'parent-packagesets', 'Parent packagesets')
168
169 def test_queries(self):
170 # With no DistroSeriesDifferences the query count should be low and
171 # fairly static. However, with some DistroSeriesDifferences the query
172 # count will be higher, but it should remain the same no matter how
173 # many differences there are.
174- login_person(self.simple_user)
175 derived_series = self.factory.makeDistroSeries(
176 parent_series=self.factory.makeDistroSeries())
177+ ArchivePermission(
178+ archive=derived_series.main_archive, person=self.simple_user,
179+ component=getUtility(IComponentSet)["main"],
180+ permission=ArchivePermissionType.QUEUE_ADMIN)
181+ login_person(self.simple_user)
182
183 def add_differences(num):
184 for index in xrange(num):
185@@ -1007,12 +1012,14 @@
186 [resolved_diff], filtered_view.cached_differences.batch)
187
188 def _setUpDSD(self, src_name='src-name', versions=None,
189- difference_type=None):
190+ difference_type=None, distribution=None):
191 # Helper to create a derived series with fixed names and proper
192 # source package format selection along with a DSD.
193 parent_series = self.factory.makeDistroSeries(name='warty')
194+ if distribution == None:
195+ distribution = self.factory.makeDistribution('deribuntu')
196 derived_series = self.factory.makeDistroSeries(
197- distribution=self.factory.makeDistribution(name='deribuntu'),
198+ distribution=distribution,
199 name='derilucid', parent_series=parent_series)
200 self._set_source_selection(derived_series)
201 self.factory.makeDistroSeriesDifference(
202@@ -1024,33 +1031,45 @@
203 set_derived_series_ui_feature_flag(self)
204 return derived_series, parent_series, sourcepackagename
205
206- def test_canPerformSync_non_editor(self):
207- # Non-editors do not see options to sync.
208- derived_series = self.factory.makeDistroSeries(
209- name='derilucid', parent_series=self.factory.makeDistroSeries(
210- name='lucid'))
211- self.factory.makeDistroSeriesDifference(
212- derived_series=derived_series)
213-
214- set_derived_series_ui_feature_flag(self)
215+ def test_canPerformSync_anon(self):
216+ # Anonymous users cannot sync packages.
217+ derived_series = self._setUpDSD()[0]
218+ view = create_initialized_view(
219+ derived_series, '+localpackagediffs')
220+
221+ self.assertFalse(view.canPerformSync())
222+
223+ def test_canPerformSync_non_anon_no_perm_dest_archive(self):
224+ # Logged-in users with no permission on the destination archive
225+ # are not presented with options to perform syncs.
226+ derived_series = self._setUpDSD()[0]
227 with person_logged_in(self.factory.makePerson()):
228 view = create_initialized_view(
229 derived_series, '+localpackagediffs')
230
231- self.assertFalse(view.canPerformSync())
232-
233- def test_canPerformSync_editor(self):
234- # Editors are presented with options to perform syncs.
235- derived_series = self.factory.makeDistroSeries(
236- name='derilucid', parent_series=self.factory.makeDistroSeries(
237- name='lucid'))
238- self.factory.makeDistroSeriesDifference(
239- derived_series=derived_series)
240-
241- set_derived_series_ui_feature_flag(self)
242- with person_logged_in(derived_series.owner):
243+ self.assertFalse(view.canPerformSync())
244+
245+ def _setUpPersonWithPerm(self, derived_series):
246+ # Helper to create a person with an upload permission on the
247+ # series' archive.
248+ person = self.factory.makePerson()
249+ ArchivePermission(
250+ archive=derived_series.main_archive, person=person,
251+ component=getUtility(IComponentSet)["main"],
252+ permission=ArchivePermissionType.QUEUE_ADMIN)
253+ return person
254+
255+ def test_canPerformSync_non_anon(self):
256+ # Logged-in users with a permission on the destination archive
257+ # are presented with options to perform syncs.
258+ # Note that a more fine-grained perm check is done on each
259+ # synced package.
260+ derived_series = self._setUpDSD()[0]
261+ person = self._setUpPersonWithPerm(derived_series)
262+ with person_logged_in(person):
263 view = create_initialized_view(
264 derived_series, '+localpackagediffs')
265+
266 self.assertTrue(view.canPerformSync())
267
268 def _syncAndGetView(self, derived_series, person, sync_differences,
269@@ -1065,47 +1084,87 @@
270 })
271 return view
272
273- def test_sync_notification_on_success(self):
274- # Syncing one or more diffs results in a stub notification.
275- versions = {
276- 'base': '1.0',
277- 'derived': '1.0derived1',
278- 'parent': '1.0-1',
279- }
280- parent_series = self.factory.makeDistroSeries(name='warty')
281- derived_distro = self.factory.makeDistribution(name='deribuntu')
282- derived_series = self.factory.makeDistroSeries(
283- distribution=derived_distro, name='derilucid',
284- parent_series=parent_series)
285- self._set_source_selection(derived_series)
286- difference = self.factory.makeDistroSeriesDifference(
287- source_package_name_str='my-src-name',
288- derived_series=derived_series, versions=versions)
289-
290- # The inital state is that 1.0-1 is not in the derived series.
291- pubs = derived_series.main_archive.getPublishedSources(
292- name='my-src-name', version=versions['parent'],
293- distroseries=derived_series).any()
294- self.assertIs(None, pubs)
295-
296- # Now, sync the source from the parent using the form.
297- set_derived_series_ui_feature_flag(self)
298- with person_logged_in(derived_series.owner):
299- view = create_initialized_view(
300- derived_series, '+localpackagediffs',
301- method='POST', form={
302- 'field.selected_differences': [
303- difference.source_package_name.name,
304- ],
305- 'field.actions.sync': 'Sync',
306- })
307-
308- # The parent's version should now be in the derived series:
309- pub = derived_series.main_archive.getPublishedSources(
310- name='my-src-name', version=versions['parent'],
311- distroseries=derived_series).one()
312+ def test_sync_error_nothing_selected(self):
313+ # An error is raised when a sync is requested without any selection.
314+ derived_series = self._setUpDSD()[0]
315+ person = self._setUpPersonWithPerm(derived_series)
316+ view = self._syncAndGetView(derived_series, person, [])
317+
318+ self.assertEqual(1, len(view.errors))
319+ self.assertEqual(
320+ 'No differences selected.', view.errors[0])
321+
322+ def test_sync_error_invalid_selection(self):
323+ # An error is raised when an invalid difference is selected.
324+ derived_series = self._setUpDSD('my-src-name')[0]
325+ person = self._setUpPersonWithPerm(derived_series)
326+ view = self._syncAndGetView(
327+ derived_series, person, ['some-other-name'])
328+
329+ self.assertEqual(2, len(view.errors))
330+ self.assertEqual(
331+ 'No differences selected.', view.errors[0])
332+ self.assertEqual(
333+ 'Invalid value', view.errors[1].error_name)
334+
335+ def test_sync_error_no_perm_dest_archive(self):
336+ # A user without upload rights on the destination archive cannot
337+ # sync packages.
338+ derived_series = self._setUpDSD('my-src-name')[0]
339+ person = self._setUpPersonWithPerm(derived_series)
340+ view = self._syncAndGetView(
341+ derived_series, person, ['my-src-name'])
342+
343+ self.assertEqual(1, len(view.errors))
344+ self.assertTrue(
345+ "The signer of this package has no upload rights to this "
346+ "distribution's primary archive" in view.errors[0])
347+
348+ def makePersonWithComponentPermission(self, archive, component=None):
349+ person = self.factory.makePerson()
350+ if component is None:
351+ component = self.factory.makeComponent()
352+ removeSecurityProxy(archive).newComponentUploader(
353+ person, component)
354+ return person, component
355+
356+ def test_sync_success_perm_component(self):
357+ # A user with upload rights on the destination component
358+ # can sync packages.
359+ derived_series, parent_series, sourcepackagename = self._setUpDSD(
360+ 'my-src-name')
361+ person, _ = self.makePersonWithComponentPermission(
362+ derived_series.main_archive,
363+ derived_series.getSourcePackage(
364+ sourcepackagename).latest_published_component)
365+ view = self._syncAndGetView(
366+ derived_series, person, ['my-src-name'])
367+
368+ self.assertEqual(0, len(view.errors))
369+
370+ def test_sync_error_no_perm_component(self):
371+ # A user without upload rights on the destination component
372+ # will get an error when he syncs packages to this component.
373+ derived_series, parent_series, sourcepackagename = self._setUpDSD(
374+ 'my-src-name')
375+ person, another_component = self.makePersonWithComponentPermission(
376+ derived_series.main_archive)
377+ view = self._syncAndGetView(
378+ derived_series, person, ['my-src-name'])
379+
380+ self.assertEqual(1, len(view.errors))
381+ self.assertTrue(
382+ "Signer is not permitted to upload to the "
383+ "component" in view.errors[0])
384+
385+ def assertPackageCopied(self, series, src_name, version, view):
386+ # Helper to check that a package has been copied.
387+ # The new version should now be in the destination series.
388+ pub = series.main_archive.getPublishedSources(
389+ name=src_name, version=version,
390+ distroseries=series).one()
391 self.assertIsNot(None, pub)
392- self.assertEqual(versions['parent'], pub.sourcepackagerelease.version)
393+ self.assertEqual(version, pub.sourcepackagerelease.version)
394
395 # The view should show no errors, and the notification should
396 # confirm the sync worked.
397@@ -1114,87 +1173,90 @@
398 self.assertEqual(1, len(notifications))
399 self.assertEqual(
400 '<p>Packages copied to '
401- '<a href="http://launchpad.dev/deribuntu/derilucid"'
402+ '<a href="http://launchpad.dev/%s/%s"'
403 '>Derilucid</a>:</p>\n<ul>\n<li>my-src-name 1.0-1 in '
404- 'derilucid</li>\n</ul>',
405+ 'derilucid</li>\n</ul>' % (series.parent.name, series.name),
406 notifications[0].message)
407 # 302 is a redirect back to the same page.
408 self.assertEqual(302, view.request.response.getStatus())
409
410- def test_sync_error_nothing_selected(self):
411- # An error is raised when a sync is requested without any selection.
412- derived_series = self.factory.makeDistroSeries(
413- name='derilucid', parent_series=self.factory.makeDistroSeries(
414- name='lucid'))
415- self.factory.makeDistroSeriesDifference(
416- source_package_name_str='my-src-name',
417- derived_series=derived_series)
418-
419- set_derived_series_ui_feature_flag(self)
420- with person_logged_in(derived_series.owner):
421- view = create_initialized_view(
422- derived_series, '+localpackagediffs',
423- method='POST', form={
424- 'field.selected_differences': [],
425- 'field.actions.sync': 'Sync',
426- })
427-
428- self.assertEqual(1, len(view.errors))
429- self.assertEqual(
430- 'No differences selected.', view.errors[0])
431-
432- def test_sync_error_invalid_selection(self):
433- # An error is raised when an invalid difference is selected.
434- derived_series = self.factory.makeDistroSeries(
435- name='derilucid', parent_series=self.factory.makeDistroSeries(
436- name='lucid'))
437- self._set_source_selection(derived_series)
438- self.factory.makeDistroSeriesDifference(
439- source_package_name_str='my-src-name',
440- derived_series=derived_series)
441-
442- set_derived_series_ui_feature_flag(self)
443- with person_logged_in(derived_series.owner):
444- view = create_initialized_view(
445- derived_series, '+localpackagediffs',
446- method='POST', form={
447- 'field.selected_differences': ['some-other-name'],
448- 'field.actions.sync': 'Sync',
449- })
450-
451- self.assertEqual(2, len(view.errors))
452- self.assertEqual(
453- 'No differences selected.', view.errors[0])
454- self.assertEqual(
455- 'Invalid value', view.errors[1].error_name)
456-
457- def test_sync_in_released_series_in_updates(self):
458- # If the destination series is released, the sync packages end
459- # up in the updates pocket.
460+ def test_sync_notification_on_success(self):
461+ # A user with upload rights on the destination archive can
462+ # sync packages. Notifications about the synced packages are
463+ # displayed and the packages are copied inside the destination
464+ # series.
465 versions = {
466+ 'base': '1.0',
467+ 'derived': '1.0derived1',
468 'parent': '1.0-1',
469 }
470 derived_series, parent_series, sourcepackagename = self._setUpDSD(
471 'my-src-name', versions=versions)
472
473- # Update destination series status to current and update
474- # daterelease.
475- with celebrity_logged_in('admin'):
476- derived_series.status = SeriesStatus.CURRENT
477- derived_series.datereleased = UTC_NOW
478-
479- self._syncAndGetView(
480- derived_series, derived_series.owner, ['my-src-name'])
481-
482- parent_pub = parent_series.main_archive.getPublishedSources(
483- name='my-src-name', version=versions['parent'],
484- distroseries=parent_series).one()
485- pub = derived_series.main_archive.getPublishedSources(
486- name='my-src-name', version=versions['parent'],
487- distroseries=derived_series).one()
488-
489- self.assertEqual(self.factory.getAnyPocket(), parent_pub.pocket)
490- self.assertEqual(PackagePublishingPocket.UPDATES, pub.pocket)
491+ # Setup a user with upload rights.
492+ person = self.factory.makePerson()
493+ removeSecurityProxy(derived_series.main_archive).newPackageUploader(
494+ person, sourcepackagename)
495+
496+ # The inital state is that 1.0-1 is not in the derived series.
497+ pubs = derived_series.main_archive.getPublishedSources(
498+ name='my-src-name', version=versions['parent'],
499+ distroseries=derived_series).any()
500+ self.assertIs(None, pubs)
501+
502+ # Now, sync the source from the parent using the form.
503+ view = self._syncAndGetView(
504+ derived_series, person, ['my-src-name'])
505+
506+ # The parent's version should now be in the derived series and
507+ # the notifications displayed:
508+ self.assertPackageCopied(
509+ derived_series, 'my-src-name', versions['parent'], view)
510+
511+ def test_sync_success_not_yet_in_derived_series(self):
512+ # If the package to sync does not exist yet in the derived series,
513+ # upload right to any component inside the destination series will be
514+ # enough to sync the package.
515+ versions = {
516+ 'parent': '1.0-1',
517+ }
518+ missing = DistroSeriesDifferenceType.MISSING_FROM_DERIVED_SERIES
519+ derived_series, parent_series, sourcepackagename = self._setUpDSD(
520+ 'my-src-name', difference_type=missing, versions=versions)
521+ person, another_component = self.makePersonWithComponentPermission(
522+ derived_series.main_archive)
523+ view = self._syncAndGetView(
524+ derived_series, person, ['my-src-name'],
525+ view_name='+missingpackages')
526+
527+ self.assertPackageCopied(
528+ derived_series, 'my-src-name', versions['parent'], view)
529+
530+ def test_sync_append_main_archive(self):
531+ # A user with lp.Append on the main archive (e.g. members of
532+ # ubuntu-security on an ubuntu series) can sync packages.
533+ # XXX: rvb 2011-05-05 bug=777911: This check should be refactored
534+ # and moved to lib/lp/soyuz/scripts/tests/test_copypackage.py.
535+ versions = {
536+ 'base': '1.0',
537+ 'derived': '1.0derived1',
538+ 'parent': '1.0-1',
539+ }
540+ ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
541+ derived_series, parent_series, sourcepackagename = self._setUpDSD(
542+ 'my-src-name', distribution=ubuntu, versions=versions)
543+ ubuntu_security = getUtility(IPersonSet).getByName('ubuntu-security')
544+ with person_logged_in(ubuntu_security):
545+ self.assertTrue(
546+ check_permission(
547+ 'launchpad.Append', derived_series.main_archive))
548+ view = self._syncAndGetView(
549+ derived_series, ubuntu_security, ['my-src-name'])
550+
551+ self.assertTrue(view.canPerformSync())
552+
553+ self.assertPackageCopied(
554+ derived_series, 'my-src-name', versions['parent'], view)
555
556
557 class TestDistroSeriesNeedsPackagesView(TestCaseWithFactory):
558@@ -1291,12 +1353,11 @@
559 self.ds_diff.derived_series,
560 '+missingpackages',
561 principal=self.simple_user)
562- html_text = view()
563+ html = view()
564
565 packageset_text = re.compile('\s*' + ps.name)
566 self._test_packagesets(
567- html_text, packageset_text, 'parent-packagesets',
568- 'Parent packagesets')
569+ html, packageset_text, 'parent-packagesets', 'Parent packagesets')
570
571
572 class DistroSerieUniquePackageDiffsTestCase(TestCaseWithFactory):
573@@ -1378,8 +1439,8 @@
574 self.ds_diff.derived_series,
575 '+uniquepackages',
576 principal=self.simple_user)
577- html_text = view()
578+ html = view()
579
580 packageset_text = re.compile('\s*' + ps.name)
581 self._test_packagesets(
582- html_text, packageset_text, 'packagesets', 'Packagesets')
583+ html, packageset_text, 'packagesets', 'Packagesets')
584
585=== modified file 'lib/lp/registry/templates/distroseries-localdifferences.pt'
586--- lib/lp/registry/templates/distroseries-localdifferences.pt 2011-05-05 00:47:27 +0000
587+++ lib/lp/registry/templates/distroseries-localdifferences.pt 2011-05-05 20:54:43 +0000
588@@ -23,7 +23,8 @@
589 <div class="top-portlet" metal:fill-slot="main"
590 tal:define="differences view/cached_differences;
591 series_name context/displayname;
592- parent_name context/parent_series/displayname;">
593+ parent_name context/parent_series/displayname;
594+ can_perform_sync view/canPerformSync;">
595 <p><tal:replace replace="structure view/explanation/escapedtext" /></p>
596
597 <tal:distroseries_localdiff_search_form
598@@ -64,7 +65,7 @@
599 tal:attributes="class src_name">
600
601 <td>
602- <input tal:condition="view/canPerformSync"
603+ <input tal:condition="can_perform_sync"
604 name="field.selected_differences" type="checkbox"
605 tal:attributes="
606 value src_name;
607
608=== modified file 'lib/lp/soyuz/browser/archive.py'
609--- lib/lp/soyuz/browser/archive.py 2011-05-05 00:47:27 +0000
610+++ lib/lp/soyuz/browser/archive.py 2011-05-05 20:54:43 +0000
611@@ -1219,7 +1219,8 @@
612
613 def do_copy(self, sources_field_name, source_pubs, dest_archive,
614 dest_series, dest_pocket, include_binaries,
615- dest_url=None, dest_display_name=None):
616+ dest_url=None, dest_display_name=None, person=None,
617+ check_permissions=True):
618 """Copy packages and add appropriate feedback to the browser page.
619
620 :param sources_field_name: The name of the form field to set errors
621@@ -1236,13 +1237,17 @@
622 :param dest_display_name: The text to use for the dest_url link.
623 Defaults to the target archive's display name and will be
624 automatically escaped for inclusion in the output.
625+ :param person: The person requesting the copy.
626+ :param: check_permissions: boolean indicating whether or not the
627+ requester's permissions to copy should be checked.
628
629 :return: True if the copying worked, False otherwise.
630 """
631 try:
632 copies = do_copy(
633 source_pubs, dest_archive, dest_series,
634- dest_pocket, include_binaries)
635+ dest_pocket, include_binaries, allow_delayed_copies=True,
636+ person=person, check_permissions=check_permissions)
637 except CannotCopy, error:
638 messages = []
639 error_lines = str(error).splitlines()
640@@ -1462,7 +1467,8 @@
641 # setting up on-page notifications.
642 if self.do_copy(
643 'selected_sources', selected_sources, destination_archive,
644- destination_series, destination_pocket, include_binaries):
645+ destination_series, destination_pocket, include_binaries,
646+ person=self.user):
647 # The copy worked so we can redirect back to the page to
648 # show the result.
649 self.setNextURL()
650
651=== modified file 'lib/lp/soyuz/doc/archive.txt'
652--- lib/lp/soyuz/doc/archive.txt 2011-05-05 00:47:27 +0000
653+++ lib/lp/soyuz/doc/archive.txt 2011-05-05 20:54:43 +0000
654@@ -2275,7 +2275,8 @@
655 release pocket, but to do so we must have edit permissions on the archive.
656
657 >>> sources = ["package1", "package2"]
658- >>> mark.archive.syncSources(sources, cprov.archive, "release")
659+ >>> mark.archive.syncSources(
660+ ... sources, cprov.archive, "release", person=None)
661 Traceback (most recent call last):
662 ...
663 Unauthorized...
664@@ -2284,7 +2285,8 @@
665
666 >>> login("mark@example.com")
667
668- >>> mark.archive.syncSources(sources, cprov.archive, "release")
669+ >>> mark.archive.syncSources(
670+ ... sources, cprov.archive, "release", person=mark)
671
672 >>> mark_one = mark.archive.getPublishedSources(name="package1").one()
673 >>> print mark_one.sourcepackagerelease.version
674@@ -2298,7 +2300,8 @@
675
676 Repeating this source copy gives an error:
677
678- >>> mark.archive.syncSources(sources, cprov.archive, "release")
679+ >>> mark.archive.syncSources(
680+ ... sources, cprov.archive, "release", person=mark)
681 Traceback (most recent call last):
682 ...
683 CannotCopy:
684@@ -2310,7 +2313,8 @@
685 Repeating this copy with binaries also gives an error:
686
687 >>> mark.archive.syncSources(
688- ... sources, cprov.archive, "release", include_binaries=True)
689+ ... sources, cprov.archive, "release", include_binaries=True,
690+ ... person=mark)
691 Traceback (most recent call last):
692 ...
693 CannotCopy:
694@@ -2320,18 +2324,21 @@
695 Specifying non-existent source names, pocket names or distroseries names
696 all result in a NotFound exception:
697
698- >>> mark.archive.syncSources(["bogus"], cprov.archive, "release")
699+ >>> mark.archive.syncSources(["bogus"], cprov.archive, "release",
700+ ... person=mark)
701 Traceback (most recent call last):
702 ...
703 NoSuchSourcePackageName: No such source package: 'bogus'.
704
705- >>> mark.archive.syncSources(sources, cprov.archive, "badpocket")
706+ >>> mark.archive.syncSources(sources, cprov.archive, "badpocket",
707+ ... person=mark)
708 Traceback (most recent call last):
709 ...
710 PocketNotFound: 'BADPOCKET'
711
712 >>> mark.archive.syncSources(
713- ... sources, cprov.archive, "release", to_series="badseries")
714+ ... sources, cprov.archive, "release", to_series="badseries",
715+ ... person=mark)
716 Traceback (most recent call last):
717 ...
718 DistroSeriesNotFound: badseries
719@@ -2360,7 +2367,8 @@
720 As with syncSources() you need to have edit permission on the archive.
721
722 >>> login(ANONYMOUS)
723- >>> mark.archive.syncSource("pack", "1.0", cprov.archive, "release")
724+ >>> mark.archive.syncSource("pack", "1.0", cprov.archive, "release",
725+ ... person=None)
726 Traceback (most recent call last):
727 ...
728 Unauthorized...
729@@ -2368,7 +2376,8 @@
730 Login as mark to continue.
731
732 >>> login("mark@example.com")
733- >>> mark.archive.syncSource("pack", "1.0", cprov.archive, "release")
734+ >>> mark.archive.syncSource("pack", "1.0", cprov.archive, "release",
735+ ... person=mark)
736 >>> pack = mark.archive.getPublishedSources(
737 ... name="pack", exact_match=True).one()
738 >>> print pack.sourcepackagerelease.version
739@@ -2376,7 +2385,8 @@
740
741 Copy package3 1.0 explicitly:
742
743- >>> mark.archive.syncSource("package3", "1.0", cprov.archive, "release")
744+ >>> mark.archive.syncSource("package3", "1.0", cprov.archive,
745+ ... "release", person=mark)
746 >>> mark_three = mark.archive.getPublishedSources(name="package3").one()
747 >>> print mark_three.sourcepackagerelease.version
748 1.0
749@@ -2403,7 +2413,7 @@
750
751 >>> mark.archive.syncSource(
752 ... "built-source", "1.0", cprov.archive, "release",
753- ... include_binaries=True)
754+ ... include_binaries=True, person=mark)
755
756 Now, Mark's PPA has 'built-source' source and it's 2 binaries.
757
758@@ -2415,7 +2425,7 @@
759 or a CannotCopy exception is thrown.
760
761 >>> mark.archive.syncSource(
762- ... "package3", "1.2", cprov.archive, "updates")
763+ ... "package3", "1.2", cprov.archive, "updates", person=mark)
764 Traceback (most recent call last):
765 ...
766 CannotCopy: Destination pocket must be 'release' for a PPA.
767@@ -2446,7 +2456,7 @@
768
769 >>> mark.archive.syncSource(
770 ... source_name='overridden', version='1.0',
771- ... from_archive=source_archive, to_pocket='release')
772+ ... from_archive=source_archive, to_pocket='release', person=mark)
773
774 >>> copy = mark.archive.getPublishedSources(name="overridden").one()
775 >>> print copy.section.name
776
777=== modified file 'lib/lp/soyuz/interfaces/archive.py'
778--- lib/lp/soyuz/interfaces/archive.py 2011-05-05 00:47:27 +0000
779+++ lib/lp/soyuz/interfaces/archive.py 2011-05-05 20:54:43 +0000
780@@ -616,6 +616,9 @@
781 :param component: The `Component` being uploaded to.
782 :param pocket: The `PackagePublishingPocket` of 'distroseries' being
783 uploaded to.
784+ :param strict_component: True if access to the specific component for
785+ the package is needed to upload to it. If False, then access to
786+ any component will do.
787 :return: The reason for not being able to upload, None otherwise.
788 """
789
790@@ -632,7 +635,7 @@
791 :param distroseries: The upload's target distro series.
792 :param strict_component: True if access to the specific component for
793 the package is needed to upload to it. If False, then access to
794- any package will do.
795+ any component will do.
796 :return: CannotUploadToArchive if 'person' cannot upload to the
797 archive,
798 None otherwise.
799@@ -1173,12 +1176,22 @@
800 @operation_returns_collection_of(Interface)
801 @export_read_operation()
802 def getComponentsForQueueAdmin(person):
803- """Return `IArchivePermission` for the person's queue admin components
804+ """Return `IArchivePermission` for the person's queue admin
805+ components.
806
807- :param person: An `IPerson`
808+ :param person: An `IPerson`.
809 :return: A list of `IArchivePermission` records.
810 """
811
812+ def hasAnyPermission(person):
813+ """Whether or not this person has any permission at all on this
814+ archive.
815+
816+ :param person: The `IPerson` for whom the check is performed.
817+ :return: A boolean indicating if the person has any permission on this
818+ archive at all.
819+ """
820+
821 def getPackageDownloadCount(bpr, day, country):
822 """Get the `IBinaryPackageDownloadCount` with the given key."""
823
824@@ -1212,6 +1225,7 @@
825 class IArchiveAppend(Interface):
826 """Archive interface for operations restricted by append privilege."""
827
828+ @call_with(person=REQUEST_USER)
829 @operation_parameters(
830 source_names=List(
831 title=_("Source package names"),
832@@ -1227,8 +1241,8 @@
833 @export_write_operation()
834 # Source_names is a string because exporting a SourcePackageName is
835 # rather nonsensical as it only has id and name columns.
836- def syncSources(source_names, from_archive, to_pocket,
837- to_series=None, include_binaries=False):
838+ def syncSources(source_names, from_archive, to_pocket, to_series=None,
839+ include_binaries=False, person=None):
840 """Synchronise (copy) named sources into this archive from another.
841
842 It will copy the most recent PUBLISHED versions of the named
843@@ -1246,6 +1260,7 @@
844 :param include_binaries: optional boolean, controls whether or not
845 the published binaries for each given source should also be
846 copied along with the source.
847+ :param person: the `IPerson` who requests the sync.
848
849 :raises NoSuchSourcePackageName: if the source name is invalid
850 :raises PocketNotFound: if the pocket name is invalid
851@@ -1253,6 +1268,7 @@
852 :raises CannotCopy: if there is a problem copying.
853 """
854
855+ @call_with(person=REQUEST_USER)
856 @operation_parameters(
857 source_name=TextLine(title=_("Source package name")),
858 version=TextLine(title=_("Version")),
859@@ -1271,7 +1287,7 @@
860 # we should consider either changing this method or adding a new one
861 # that takes that object instead.
862 def syncSource(source_name, version, from_archive, to_pocket,
863- to_series=None, include_binaries=False):
864+ to_series=None, include_binaries=False, person=None):
865 """Synchronise (copy) a single named source into this archive.
866
867 Copy a specific version of a named source to the destination
868@@ -1285,6 +1301,7 @@
869 :param include_binaries: optional boolean, controls whether or not
870 the published binaries for each given source should also be
871 copied along with the source.
872+ :param person: the `IPerson` who requests the sync.
873
874 :raises NoSuchSourcePackageName: if the source name is invalid
875 :raises PocketNotFound: if the pocket name is invalid
876
877=== modified file 'lib/lp/soyuz/model/archive.py'
878--- lib/lp/soyuz/model/archive.py 2011-05-05 00:47:27 +0000
879+++ lib/lp/soyuz/model/archive.py 2011-05-05 20:54:43 +0000
880@@ -28,8 +28,8 @@
881 Desc,
882 Or,
883 Select,
884+ SQL,
885 Sum,
886- SQL,
887 )
888 from storm.locals import (
889 Count,
890@@ -1001,6 +1001,19 @@
891 permission_set = getUtility(IArchivePermissionSet)
892 return permission_set.componentsForQueueAdmin(self, person)
893
894+ def hasAnyPermission(self, person):
895+ """See `IArchive`."""
896+ # Avoiding circular imports.
897+ from lp.soyuz.model.archivepermission import ArchivePermission
898+
899+ any_perm_on_archive = Store.of(self).find(
900+ TeamParticipation,
901+ ArchivePermission.archive == self.id,
902+ TeamParticipation.person == person.id,
903+ TeamParticipation.teamID == ArchivePermission.personID,
904+ )
905+ return not any_perm_on_archive.is_empty()
906+
907 def getBuildCounters(self, include_needsbuild=True):
908 """See `IArchiveSet`."""
909
910@@ -1176,7 +1189,7 @@
911 strict_component)
912
913 def verifyUpload(self, person, sourcepackagename, component,
914- distroseries, strict_component=True):
915+ distroseries, strict_component=True):
916 """See `IArchive`."""
917 if not self.enabled:
918 return ArchiveDisabled(self.displayname)
919@@ -1435,15 +1448,17 @@
920 reason)
921
922 def syncSources(self, source_names, from_archive, to_pocket,
923- to_series=None, include_binaries=False):
924+ to_series=None, include_binaries=False, person=None):
925 """See `IArchive`."""
926 # Find and validate the source package names in source_names.
927 sources = self._collectLatestPublishedSources(
928 from_archive, source_names)
929- self._copySources(sources, to_pocket, to_series, include_binaries)
930+ self._copySources(
931+ sources, to_pocket, to_series, include_binaries,
932+ person=person)
933
934 def syncSource(self, source_name, version, from_archive, to_pocket,
935- to_series=None, include_binaries=False):
936+ to_series=None, include_binaries=False, person=None):
937 """See `IArchive`."""
938 # Check to see if the source package exists, and raise a useful error
939 # if it doesn't.
940@@ -1452,7 +1467,9 @@
941 source = from_archive.getPublishedSources(
942 name=source_name, version=version, exact_match=True).first()
943
944- self._copySources([source], to_pocket, to_series, include_binaries)
945+ self._copySources(
946+ [source], to_pocket, to_series, include_binaries,
947+ person=person)
948
949 def _collectLatestPublishedSources(self, from_archive, source_names):
950 """Private helper to collect the latest published sources for an
951@@ -1478,7 +1495,7 @@
952 return sources
953
954 def _copySources(self, sources, to_pocket, to_series=None,
955- include_binaries=False):
956+ include_binaries=False, person=None):
957 """Private helper function to copy sources to this archive.
958
959 It takes a list of SourcePackagePublishingHistory but the other args
960@@ -1507,7 +1524,8 @@
961 series = None
962
963 # Perform the copy, may raise CannotCopy.
964- do_copy(sources, self, series, pocket, include_binaries)
965+ do_copy(
966+ sources, self, series, pocket, include_binaries, person=person)
967
968 def getAuthToken(self, person):
969 """See `IArchive`."""
970
971=== modified file 'lib/lp/soyuz/model/packagecopyjob.py'
972--- lib/lp/soyuz/model/packagecopyjob.py 2011-05-05 00:47:27 +0000
973+++ lib/lp/soyuz/model/packagecopyjob.py 2011-05-05 20:54:43 +0000
974@@ -127,4 +127,4 @@
975 do_copy(
976 sources=source_packages, archive=self.target_archive,
977 series=self.target_distroseries, pocket=self.target_pocket,
978- include_binaries=self.include_binaries)
979+ include_binaries=self.include_binaries, check_permissions=False)
980
981=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
982--- lib/lp/soyuz/scripts/packagecopier.py 2011-05-05 00:47:27 +0000
983+++ lib/lp/soyuz/scripts/packagecopier.py 2011-05-05 20:54:43 +0000
984@@ -24,14 +24,16 @@
985 from zope.component import getUtility
986
987 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
988+from canonical.launchpad.webapp.authorization import check_permission
989 from canonical.librarian.utils import copy_and_close
990 from lp.app.errors import NotFoundError
991 from lp.buildmaster.enums import BuildStatus
992 from lp.soyuz.adapters.packagelocation import build_package_location
993-from lp.soyuz.enums import ArchivePurpose
994-from lp.soyuz.interfaces.archive import (
995- CannotCopy,
996+from lp.soyuz.enums import (
997+ ArchivePurpose,
998+ SourcePackageFormat,
999 )
1000+from lp.soyuz.interfaces.archive import CannotCopy
1001 from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
1002 from lp.soyuz.interfaces.publishing import (
1003 active_publishing_status,
1004@@ -44,7 +46,6 @@
1005 IPackageUploadCustom,
1006 IPackageUploadSet,
1007 )
1008-from lp.soyuz.enums import SourcePackageFormat
1009 from lp.soyuz.scripts.ftpmasterbase import (
1010 SoyuzScript,
1011 SoyuzScriptError,
1012@@ -374,7 +375,8 @@
1013 "%s already exists in destination archive with "
1014 "different contents." % lf.libraryfile.filename)
1015
1016- def checkCopy(self, source, series, pocket):
1017+ def checkCopy(self, source, series, pocket, person=None,
1018+ check_permissions=True):
1019 """Check if the source can be copied to the given location.
1020
1021 Check possible conflicting publications in the destination archive.
1022@@ -384,13 +386,49 @@
1023 higher than any version of the same source present in the
1024 destination suite (series + pocket).
1025
1026+ If person is not None, check that this person has upload rights to
1027+ the destination (archive, component, pocket).
1028+
1029 :param source: copy candidate, `ISourcePackagePublishingHistory`.
1030 :param series: destination `IDistroSeries`.
1031 :param pocket: destination `PackagePublishingPocket`.
1032+ :param person: requester `IPerson`.
1033+ :param check_permissions: boolean indicating whether or not the
1034+ requester's permissions to copy should be checked.
1035
1036 :raise CannotCopy when a copy is not allowed to be performed
1037 containing the reason of the error.
1038 """
1039+ # If there is a requester, check that he has upload permission
1040+ # into the destination (archive, component, pocket). This check
1041+ # is done here rather than in the security adapter because it
1042+ # requires more info than is available in the security adapter.
1043+ if check_permissions:
1044+ if person is None:
1045+ raise CannotCopy(
1046+ 'Cannot check copy permissions (no requester).')
1047+ else:
1048+ sourcepackagerelease = source.sourcepackagerelease
1049+ sourcepackagename = sourcepackagerelease.sourcepackagename
1050+ destination_component = series.getSourcePackage(
1051+ sourcepackagename).latest_published_component
1052+ # If destination_component is not None, make sure the person
1053+ # has upload permission for this component. Otherwise, any
1054+ # upload permission on this archive will do.
1055+ strict_component = destination_component is not None
1056+ reason = self.archive.checkUpload(
1057+ person, series, sourcepackagename, destination_component,
1058+ pocket, strict_component=strict_component)
1059+ if reason is not None:
1060+ # launchpad.Append on the main archive is sufficient
1061+ # to copy arbitrary packages. This allows for
1062+ # ubuntu's security team to sync sources into the
1063+ # primary archive (bypassing the queue and
1064+ # annoncements).
1065+ if not check_permission(
1066+ 'launchpad.Append', series.main_archive):
1067+ raise CannotCopy(reason)
1068+
1069 if series not in self.archive.distribution.series:
1070 raise CannotCopy(
1071 "No such distro series %s in distribution %s." %
1072@@ -459,7 +497,7 @@
1073
1074
1075 def do_copy(sources, archive, series, pocket, include_binaries=False,
1076- allow_delayed_copies=True):
1077+ allow_delayed_copies=True, person=None, check_permissions=True):
1078 """Perform the complete copy of the given sources incrementally.
1079
1080 Verifies if each copy can be performed using `CopyChecker` and
1081@@ -470,17 +508,20 @@
1082
1083 Wrapper for `do_direct_copy`.
1084
1085- :param: sources: a list of `ISourcePackagePublishingHistory`.
1086- :param: archive: the target `IArchive`.
1087- :param: series: the target `IDistroSeries`, if None is given the same
1088+ :param sources: a list of `ISourcePackagePublishingHistory`.
1089+ :param archive: the target `IArchive`.
1090+ :param series: the target `IDistroSeries`, if None is given the same
1091 current source distroseries will be used as destination.
1092- :param: pocket: the target `PackagePublishingPocket`.
1093- :param: include_binaries: optional boolean, controls whether or
1094+ :param pocket: the target `PackagePublishingPocket`.
1095+ :param include_binaries: optional boolean, controls whether or
1096 not the published binaries for each given source should be also
1097 copied along with the source.
1098 :param allow_delayed_copies: boolean indicating whether or not private
1099 sources can be copied to public archives using delayed_copies.
1100 Defaults to True, only set as False in the UnembargoPackage context.
1101+ :param person: the requester `IPerson`.
1102+ :param check_permissions: boolean indicating whether or not the
1103+ requester's permissions to copy should be checked.
1104
1105 :raise CannotCopy when one or more copies were not allowed. The error
1106 will contain the reason why each copy was denied.
1107@@ -500,7 +541,8 @@
1108 else:
1109 destination_series = series
1110 try:
1111- copy_checker.checkCopy(source, destination_series, pocket)
1112+ copy_checker.checkCopy(
1113+ source, destination_series, pocket, person, check_permissions)
1114 except CannotCopy, reason:
1115 errors.append("%s (%s)" % (source.displayname, reason))
1116 continue
1117@@ -536,12 +578,12 @@
1118 Also copy published binaries for each source if requested to. Again,
1119 only copy binaries that were not yet copied before.
1120
1121- :param: source: an `ISourcePackagePublishingHistory`.
1122- :param: archive: the target `IArchive`.
1123- :param: series: the target `IDistroSeries`, if None is given the same
1124+ :param source: an `ISourcePackagePublishingHistory`.
1125+ :param archive: the target `IArchive`.
1126+ :param series: the target `IDistroSeries`, if None is given the same
1127 current source distroseries will be used as destination.
1128- :param: pocket: the target `PackagePublishingPocket`.
1129- :param: include_binaries: optional boolean, controls whether or
1130+ :param pocket: the target `PackagePublishingPocket`.
1131+ :param include_binaries: optional boolean, controls whether or
1132 not the published binaries for each given source should be also
1133 copied along with the source.
1134
1135@@ -608,11 +650,11 @@
1136
1137 Also include published builds for each source if requested to.
1138
1139- :param: source: an `ISourcePackagePublishingHistory`.
1140- :param: archive: the target `IArchive`.
1141- :param: series: the target `IDistroSeries`.
1142- :param: pocket: the target `PackagePublishingPocket`.
1143- :param: include_binaries: optional boolean, controls whether or
1144+ :param source: an `ISourcePackagePublishingHistory`.
1145+ :param archive: the target `IArchive`.
1146+ :param series: the target `IDistroSeries`.
1147+ :param pocket: the target `PackagePublishingPocket`.
1148+ :param include_binaries: optional boolean, controls whether or
1149 not the published binaries for each given source should be also
1150 copied along with the source.
1151
1152@@ -778,7 +820,8 @@
1153 copies = do_copy(
1154 sources, self.destination.archive,
1155 self.destination.distroseries, self.destination.pocket,
1156- self.options.include_binaries, self.allow_delayed_copies)
1157+ self.options.include_binaries, self.allow_delayed_copies,
1158+ check_permissions=False)
1159 except CannotCopy, error:
1160 self.logger.error(str(error))
1161 return []
1162
1163=== modified file 'lib/lp/soyuz/scripts/tests/test_copypackage.py'
1164--- lib/lp/soyuz/scripts/tests/test_copypackage.py 2011-05-05 00:47:27 +0000
1165+++ lib/lp/soyuz/scripts/tests/test_copypackage.py 2011-05-05 20:54:43 +0000
1166@@ -450,7 +450,9 @@
1167 copy_checker = CopyChecker(self.archive, include_binaries=False)
1168 self.assertIs(
1169 None,
1170- copy_checker.checkCopy(self.source, self.series, self.pocket))
1171+ copy_checker.checkCopy(
1172+ self.source, self.series, self.pocket,
1173+ check_permissions=False))
1174 checked_copies = list(copy_checker.getCheckedCopies())
1175 self.assertEquals(1, len(checked_copies))
1176 [checked_copy] = checked_copies
1177@@ -476,7 +478,9 @@
1178 copy_checker = CopyChecker(self.archive, include_binaries=True)
1179 self.assertIs(
1180 None,
1181- copy_checker.checkCopy(self.source, self.series, self.pocket))
1182+ copy_checker.checkCopy(
1183+ self.source, self.series, self.pocket,
1184+ check_permissions=False))
1185 checked_copies = list(copy_checker.getCheckedCopies())
1186 self.assertEquals(1, len(checked_copies))
1187 [checked_copy] = checked_copies
1188@@ -485,7 +489,8 @@
1189 BuildSetStatus.FULLYBUILT_PENDING)
1190 self.assertEquals(delayed, checked_copy.delayed)
1191
1192- def assertCannotCopySourceOnly(self, msg):
1193+ def assertCannotCopySourceOnly(self, msg, person=None,
1194+ check_permissions=False):
1195 """`CopyChecker.checkCopy()` for source-only copy raises CannotCopy.
1196
1197 No `CheckedCopy` is stored.
1198@@ -493,7 +498,8 @@
1199 copy_checker = CopyChecker(self.archive, include_binaries=False)
1200 self.assertRaisesWithContent(
1201 CannotCopy, msg,
1202- copy_checker.checkCopy, self.source, self.series, self.pocket)
1203+ copy_checker.checkCopy, self.source, self.series, self.pocket,
1204+ person, check_permissions)
1205 checked_copies = list(copy_checker.getCheckedCopies())
1206 self.assertEquals(0, len(checked_copies))
1207
1208@@ -505,7 +511,8 @@
1209 copy_checker = CopyChecker(self.archive, include_binaries=True)
1210 self.assertRaisesWithContent(
1211 CannotCopy, msg,
1212- copy_checker.checkCopy, self.source, self.series, self.pocket)
1213+ copy_checker.checkCopy, self.source, self.series, self.pocket,
1214+ None, False)
1215 checked_copies = list(copy_checker.getCheckedCopies())
1216 self.assertEquals(0, len(checked_copies))
1217
1218@@ -514,6 +521,14 @@
1219 self.assertCannotCopyBinaries(
1220 'source has no binaries to be copied')
1221
1222+ def test_cannot_copy_check_perm_no_person(self):
1223+ # If check_permissions=True and person=None is passed to
1224+ # checkCopy, raise an error (cannot check permissions for a
1225+ # 'None' person).
1226+ self.assertCannotCopySourceOnly(
1227+ 'Cannot check copy permissions (no requester).',
1228+ person=None, check_permissions=True)
1229+
1230 def test_cannot_copy_binaries_from_FTBFS(self):
1231 [build] = self.source.createMissingBuilds()
1232 build.status = BuildStatus.FAILEDTOBUILD
1233@@ -699,10 +714,14 @@
1234 # At this point copy is allowed with or without binaries.
1235 copy_checker = CopyChecker(archive, include_binaries=False)
1236 self.assertIs(
1237- None, copy_checker.checkCopy(source, series, pocket))
1238+ None,
1239+ copy_checker.checkCopy(
1240+ source, series, pocket, check_permissions=False))
1241 copy_checker = CopyChecker(archive, include_binaries=True)
1242 self.assertIs(
1243- None, copy_checker.checkCopy(source, series, pocket))
1244+ None,
1245+ copy_checker.checkCopy(
1246+ source, series, pocket, check_permissions=False))
1247
1248 # Set the expiration date of one of the testing binary files.
1249 utc = pytz.timezone('UTC')
1250@@ -713,14 +732,15 @@
1251 # Now source-only copies are allowed.
1252 copy_checker = CopyChecker(archive, include_binaries=False)
1253 self.assertIs(
1254- None, copy_checker.checkCopy(source, series, pocket))
1255+ None, copy_checker.checkCopy(
1256+ source, series, pocket, check_permissions=False))
1257
1258 # Copies with binaries are denied.
1259 copy_checker = CopyChecker(archive, include_binaries=True)
1260 self.assertRaisesWithContent(
1261 CannotCopy,
1262 'source has expired binaries',
1263- copy_checker.checkCopy, source, series, pocket)
1264+ copy_checker.checkCopy, source, series, pocket, None, False)
1265
1266 def test_checkCopy_cannot_copy_expired_sources(self):
1267 # checkCopy() raises CannotCopy if the copy requested includes
1268@@ -745,7 +765,7 @@
1269 self.assertRaisesWithContent(
1270 CannotCopy,
1271 'source contains expired files',
1272- copy_checker.checkCopy, source, series, pocket)
1273+ copy_checker.checkCopy, source, series, pocket, None, False)
1274
1275 def test_checkCopy_allows_copies_from_other_distributions(self):
1276 # It is possible to copy packages between distributions,
1277@@ -768,7 +788,8 @@
1278 # Copy of sources to series in another distribution can be
1279 # performed.
1280 copy_checker = CopyChecker(archive, include_binaries=False)
1281- copy_checker.checkCopy(source, series, pocket)
1282+ copy_checker.checkCopy(
1283+ source, series, pocket, check_permissions=False)
1284
1285 def test_checkCopy_forbids_copies_to_unknown_distroseries(self):
1286 # We currently deny copies to series that are not for the Archive
1287@@ -794,7 +815,7 @@
1288 self.assertRaisesWithContent(
1289 CannotCopy,
1290 'No such distro series sid in distribution debian.',
1291- copy_checker.checkCopy, source, sid, pocket)
1292+ copy_checker.checkCopy, source, sid, pocket, None, False)
1293
1294 def test_checkCopy_respects_sourceformatselection(self):
1295 # A source copy should be denied if the source's dsc_format is
1296@@ -820,7 +841,8 @@
1297 self.assertRaisesWithContent(
1298 CannotCopy,
1299 "Source format '3.0 (quilt)' not supported by target series "
1300- "warty.", copy_checker.checkCopy, source, series, pocket)
1301+ "warty.", copy_checker.checkCopy, source, series, pocket, None,
1302+ False)
1303
1304 def test_checkCopy_identifies_conflicting_copy_candidates(self):
1305 # checkCopy() is able to identify conflicting candidates within
1306@@ -850,7 +872,8 @@
1307 self.assertIs(
1308 None,
1309 copy_checker.checkCopy(
1310- source, source.distroseries, source.pocket))
1311+ source, source.distroseries, source.pocket,
1312+ check_permissions=False))
1313
1314 # The second source-only copy, for hoary-test, fails, since it
1315 # conflicts with the just-approved copy.
1316@@ -859,7 +882,8 @@
1317 'same version already building in the destination archive '
1318 'for Breezy Badger Autotest',
1319 copy_checker.checkCopy,
1320- copied_source, copied_source.distroseries, copied_source.pocket)
1321+ copied_source, copied_source.distroseries, copied_source.pocket,
1322+ None, False)
1323
1324 def test_checkCopy_identifies_delayed_copies_conflicts(self):
1325 # checkCopy() detects copy conflicts in the upload queue for
1326@@ -882,14 +906,16 @@
1327 # Commit so the just-created files are accessible and perform
1328 # the delayed-copy.
1329 self.layer.txn.commit()
1330- do_copy([source], archive, series, pocket, include_binaries=False)
1331+ do_copy(
1332+ [source], archive, series, pocket, include_binaries=False,
1333+ check_permissions=False)
1334
1335 # Repeating the copy is denied.
1336 copy_checker = CopyChecker(archive, include_binaries=False)
1337 self.assertRaisesWithContent(
1338 CannotCopy,
1339 'same version already uploaded and waiting in ACCEPTED queue',
1340- copy_checker.checkCopy, source, series, pocket)
1341+ copy_checker.checkCopy, source, series, pocket, None, False)
1342
1343 def test_checkCopy_suppressing_delayed_copies(self):
1344 # `CopyChecker` by default will request delayed-copies when it's
1345@@ -917,7 +943,8 @@
1346 # this operation, since restricted files are being copied to
1347 # public archives.
1348 copy_checker = CopyChecker(archive, include_binaries=False)
1349- copy_checker.checkCopy(source, series, pocket)
1350+ copy_checker.checkCopy(
1351+ source, series, pocket, check_permissions=False)
1352 [checked_copy] = list(copy_checker.getCheckedCopies())
1353 self.assertTrue(checked_copy.delayed)
1354
1355@@ -925,7 +952,8 @@
1356 # scheduled.
1357 copy_checker = CopyChecker(
1358 archive, include_binaries=False, allow_delayed_copies=False)
1359- copy_checker.checkCopy(source, series, pocket)
1360+ copy_checker.checkCopy(
1361+ source, series, pocket, check_permissions=False)
1362 [checked_copy] = list(copy_checker.getCheckedCopies())
1363 self.assertFalse(checked_copy.delayed)
1364
1365@@ -2683,7 +2711,7 @@
1366 self.assertIs(
1367 None,
1368 checker.checkCopy(proposed_source, warty,
1369- PackagePublishingPocket.UPDATES))
1370+ PackagePublishingPocket.UPDATES, check_permissions=False))
1371
1372 def testCopySourceWithConflictingFilesInPPAs(self):
1373 """We can copy source if the source files match, both in name and
1374@@ -2728,7 +2756,7 @@
1375 "test-source_1.0.orig.tar.gz already exists in destination "
1376 "archive with different contents.",
1377 checker.checkCopy, test2_source, warty,
1378- PackagePublishingPocket.RELEASE)
1379+ PackagePublishingPocket.RELEASE, None, False)
1380
1381 def testCopySourceWithSameFilenames(self):
1382 """We can copy source if the source files match, both in name and
1383@@ -2771,7 +2799,7 @@
1384 self.assertIs(
1385 None,
1386 checker.checkCopy(test2_source, warty,
1387- PackagePublishingPocket.RELEASE))
1388+ PackagePublishingPocket.RELEASE, check_permissions=False))
1389
1390 def testCopySourceWithExpiredSourcesInDestination(self):
1391 """We can also copy sources if the destination archive has expired
1392@@ -2820,4 +2848,4 @@
1393 self.assertIs(
1394 None,
1395 checker.checkCopy(test2_source, warty,
1396- PackagePublishingPocket.RELEASE))
1397+ PackagePublishingPocket.RELEASE, check_permissions=False))
1398
1399=== modified file 'lib/lp/soyuz/stories/ppa/xx-ppa-files.txt'
1400--- lib/lp/soyuz/stories/ppa/xx-ppa-files.txt 2011-05-05 00:47:27 +0000
1401+++ lib/lp/soyuz/stories/ppa/xx-ppa-files.txt 2011-05-05 20:54:43 +0000
1402@@ -268,7 +268,8 @@
1403 ... no_priv_private_ppa.getPublishedSources(name='test-pkg'),
1404 ... no_priv.archive, series=ubuntu['warty'],
1405 ... pocket=PackagePublishingPocket.RELEASE,
1406- ... include_binaries=True, allow_delayed_copies=False)
1407+ ... include_binaries=True, allow_delayed_copies=False,
1408+ ... person=no_priv)
1409
1410 >>> from zope.security.proxy import removeSecurityProxy
1411 >>> removeSecurityProxy(dsc_file).restricted = False
1412
1413=== modified file 'lib/lp/soyuz/tests/test_archive.py'
1414--- lib/lp/soyuz/tests/test_archive.py 2011-05-05 00:47:27 +0000
1415+++ lib/lp/soyuz/tests/test_archive.py 2011-05-05 20:54:43 +0000
1416@@ -37,6 +37,7 @@
1417 from lp.services.propertycache import clear_property_cache
1418 from lp.services.worlddata.interfaces.country import ICountrySet
1419 from lp.soyuz.enums import (
1420+ ArchivePermissionType,
1421 ArchivePurpose,
1422 ArchiveStatus,
1423 PackagePublishingStatus,
1424@@ -60,6 +61,7 @@
1425 from lp.soyuz.interfaces.component import IComponentSet
1426 from lp.soyuz.interfaces.processor import IProcessorFamilySet
1427 from lp.soyuz.model.archive import Archive
1428+from lp.soyuz.model.archivepermission import ArchivePermission
1429 from lp.soyuz.model.binarypackagerelease import (
1430 BinaryPackageReleaseDownloadCount,
1431 )
1432@@ -67,6 +69,7 @@
1433 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
1434 from lp.testing import (
1435 ANONYMOUS,
1436+ celebrity_logged_in,
1437 login,
1438 login_person,
1439 person_logged_in,
1440@@ -824,6 +827,22 @@
1441 False,
1442 archive.canUploadSuiteSourcePackage(person, suitesourcepackage))
1443
1444+ def test_hasAnyPermission(self):
1445+ # hasAnyPermission returns true if the person is the member of a
1446+ # team with any kind of permission on the archive.
1447+ archive = self.factory.makeArchive()
1448+ person = self.factory.makePerson()
1449+ team = self.factory.makeTeam()
1450+ main = getUtility(IComponentSet)["main"]
1451+ ArchivePermission(
1452+ archive=archive, person=team, component=main,
1453+ permission=ArchivePermissionType.UPLOAD)
1454+
1455+ self.assertFalse(archive.hasAnyPermission(person))
1456+ with celebrity_logged_in('admin'):
1457+ team.addMember(person, team.teamowner)
1458+ self.assertTrue(archive.hasAnyPermission(person))
1459+
1460
1461 class TestUpdatePackageDownloadCount(TestCaseWithFactory):
1462 """Ensure that updatePackageDownloadCount works as expected."""