Merge lp:~cjwatson/launchpad/copy-allows-queue-admin into lp:launchpad

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: no longer in the source branch.
Merged at revision: 15712
Proposed branch: lp:~cjwatson/launchpad/copy-allows-queue-admin
Merge into: lp:launchpad
Diff against target: 218 lines (+114/-26)
5 files modified
lib/lp/security.py (+2/-13)
lib/lp/soyuz/interfaces/archive.py (+5/-4)
lib/lp/soyuz/model/archive.py (+13/-3)
lib/lp/soyuz/scripts/packagecopier.py (+11/-5)
lib/lp/soyuz/tests/test_archive.py (+83/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/copy-allows-queue-admin
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+116731@code.launchpad.net

Commit message

Allow queue admins to copy packages.

Description of the change

== Summary ==

Bug 1006917: people with queue admin permissions cannot necessarily copy packages; they require upload permissions too. This makes it hard to wean various automated scripts run by ubuntu-archive off direct database access and onto the API, because they have to be given blanket upload access, which makes me feel dirty. If nothing else, certain types of mistakes are harder to make when the permissions are narrower.

== Proposed fix ==

Many of the kinds of things we do with queue admin permissions are associated with copies (e.g. releasing stable updates), so it makes at least a degree of sense to allow those with queue admin permissions to copy packages. We trust all such people in Ubuntu anyway, and I don't think anyone else relies on queue admin permissions.

To attempt to minimise duplication, I beefed up Archive.canAdministerQueue to be able to handle multiple components, and re-expressed EditPackageUpload in terms of this. That makes it easy for check_copy_permissions to reuse this logic (although I had to break up the call to Archive.checkUpload into two pieces to distinguish "closed pocket" from "insufficient permissions"), and will support near-future work on per-pocket queue admin permissions.

== LOC Rationale ==

+88, but this is one of the remaining pieces needed to remove copy-package.py (once this or a similar branch lands, I'll be able to propose a fix for bug 1006871, which is the other piece needed for us to remove our last known reliance on it); and in any case this is more than offset by the -179 in https://code.launchpad.net/~cjwatson/launchpad/reorg-copy-package-tests-2/+merge/116729 which I just put up for review.

== Tests ==

bin/test -vvct archive.txt -t test_archivepermission -t archivepermission.txt -t browser.tests.test_queue -t xx-archive.txt -t test_archive.TestSyncSource

== Demo and Q/A ==

Try copying packages in each of the following cases:

 * person with appropriate upload permissions (yes)
 * person with appropriate queue admin permissions (yes)
 * person with queue admin permissions, but only to the wrong component (no)
 * person with neither type of permission (no)

== Lint ==

Just a pre-existing false positive due to pocketlint not understanding property setters:

./lib/lp/soyuz/model/archive.py
     346: redefinition of function 'private' from line 342

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

The branch looks good.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/security.py'
2--- lib/lp/security.py 2012-07-09 17:17:58 +0000
3+++ lib/lp/security.py 2012-07-25 19:29:20 +0000
4@@ -1730,19 +1730,8 @@
5 archive_append = AppendArchive(self.obj.archive)
6 return archive_append.checkAuthenticated(user)
7
8- permission_set = getUtility(IArchivePermissionSet)
9- permissions = permission_set.componentsForQueueAdmin(
10- self.obj.archive, user.person)
11- if permissions.count() == 0:
12- return False
13- allowed_components = set(
14- permission.component for permission in permissions)
15- existing_components = self.obj.components
16- # The intersection of allowed_components and
17- # existing_components must be equal to existing_components
18- # to allow the operation to go ahead.
19- return (allowed_components.intersection(existing_components)
20- == existing_components)
21+ return self.obj.archive.canAdministerQueue(
22+ user.person, self.obj.components)
23
24
25 class AdminByBuilddAdmin(AuthorizationBase):
26
27=== modified file 'lib/lp/soyuz/interfaces/archive.py'
28--- lib/lp/soyuz/interfaces/archive.py 2012-07-02 16:40:17 +0000
29+++ lib/lp/soyuz/interfaces/archive.py 2012-07-25 19:29:20 +0000
30@@ -680,14 +680,15 @@
31 None otherwise.
32 """
33
34- def canAdministerQueue(person, component):
35+ def canAdministerQueue(person, components):
36 """Check to see if person is allowed to administer queue items.
37
38- :param person: An `IPerson` whom should be checked for authenticate.
39- :param component: The context `IComponent` for the check.
40+ :param person: An `IPerson` who should be checked for authentication.
41+ :param components: The context `IComponent`(s) for the check.
42
43 :return: True if 'person' is allowed to administer the package upload
44- queue for items with 'component'.
45+ queue for all given 'components'. If 'components' is empty or None,
46+ check if 'person' has any queue admin permissions for this archive.
47 """
48
49 def getFileByName(filename):
50
51=== modified file 'lib/lp/soyuz/model/archive.py'
52--- lib/lp/soyuz/model/archive.py 2012-07-23 11:51:21 +0000
53+++ lib/lp/soyuz/model/archive.py 2012-07-25 19:29:20 +0000
54@@ -1347,10 +1347,20 @@
55
56 return None
57
58- def canAdministerQueue(self, user, component):
59+ def canAdministerQueue(self, user, components):
60 """See `IArchive`."""
61- return self._authenticate(
62- user, component, ArchivePermissionType.QUEUE_ADMIN)
63+ if components is None:
64+ components = []
65+ elif IComponent.providedBy(components):
66+ components = [components]
67+ permissions = self.getComponentsForQueueAdmin(user)
68+ if permissions.count() == 0:
69+ return False
70+ allowed_components = set(
71+ permission.component for permission in permissions)
72+ # The intersection of allowed_components and components must be
73+ # equal to components to allow the operation to go ahead.
74+ return allowed_components.intersection(components) == set(components)
75
76 def _authenticate(self, user, item, permission):
77 """Private helper method to check permissions."""
78
79=== modified file 'lib/lp/soyuz/scripts/packagecopier.py'
80--- lib/lp/soyuz/scripts/packagecopier.py 2012-07-05 09:43:58 +0000
81+++ lib/lp/soyuz/scripts/packagecopier.py 2012-07-25 19:29:20 +0000
82@@ -256,16 +256,22 @@
83 except IndexError:
84 destination_component = None
85
86+ # Is the destination pocket open at all?
87+ reason = archive.checkUploadToPocket(dest_series, pocket)
88+ if reason is not None:
89+ raise CannotCopy(reason)
90+
91 # If destination_component is not None, make sure the person
92 # has upload permission for this component. Otherwise, any
93 # upload permission on this archive will do.
94 strict_component = destination_component is not None
95- reason = archive.checkUpload(
96- person, dest_series, spn, destination_component, pocket,
97- strict_component=strict_component)
98-
99+ reason = archive.verifyUpload(
100+ person, spn, destination_component, dest_series,
101+ strict_component=strict_component, pocket=pocket)
102 if reason is not None:
103- raise CannotCopy(reason)
104+ # Queue admins are allowed to copy even if they can't upload.
105+ if not archive.canAdministerQueue(person, destination_component):
106+ raise CannotCopy(reason)
107
108
109 class CopyChecker:
110
111=== modified file 'lib/lp/soyuz/tests/test_archive.py'
112--- lib/lp/soyuz/tests/test_archive.py 2012-07-23 11:51:21 +0000
113+++ lib/lp/soyuz/tests/test_archive.py 2012-07-25 19:29:20 +0000
114@@ -2315,6 +2315,70 @@
115 to_pocket.name, to_series=to_series.name, include_binaries=False,
116 person=person)
117
118+ def test_copyPackage_allows_queue_admins_for_new_packages(self):
119+ # If a package does not exist in the target archive and series,
120+ # people with queue admin permissions to any component may copy it.
121+ (source, source_archive, source_name, target_archive, to_pocket,
122+ to_series, version) = self._setup_copy_data(
123+ target_purpose=ArchivePurpose.PRIMARY)
124+ person = self.factory.makePerson()
125+ with person_logged_in(target_archive.distribution.owner):
126+ target_archive.newQueueAdmin(person, "universe")
127+ target_archive.copyPackage(
128+ source_name, version, source_archive, to_pocket.name,
129+ to_series=to_series.name, include_binaries=False,
130+ person=person)
131+
132+ # There should be one copy job.
133+ job_source = getUtility(IPlainPackageCopyJobSource)
134+ copy_job = job_source.getActiveJobs(target_archive).one()
135+ self.assertEqual(target_archive, copy_job.target_archive)
136+
137+ def test_copyPackage_allows_queue_admins_for_correct_component(self):
138+ # If a package already exists in the target archive and series,
139+ # queue admins of its component may copy it.
140+ (source, source_archive, source_name, target_archive, to_pocket,
141+ to_series, version) = self._setup_copy_data(
142+ target_purpose=ArchivePurpose.PRIMARY)
143+ self.factory.makeSourcePackagePublishingHistory(
144+ distroseries=to_series, archive=target_archive,
145+ status=PackagePublishingStatus.PUBLISHED,
146+ sourcepackagename=source_name, version="%s~" % version,
147+ component="main")
148+ person = self.factory.makePerson()
149+ with person_logged_in(target_archive.distribution.owner):
150+ target_archive.newQueueAdmin(person, "main")
151+ target_archive.copyPackage(
152+ source_name, version, source_archive, to_pocket.name,
153+ to_series=to_series.name, include_binaries=False,
154+ person=person)
155+
156+ # There should be one copy job.
157+ job_source = getUtility(IPlainPackageCopyJobSource)
158+ copy_job = job_source.getActiveJobs(target_archive).one()
159+ self.assertEqual(target_archive, copy_job.target_archive)
160+
161+ def test_copyPackage_disallows_queue_admins_for_incorrect_component(self):
162+ # If a package already exists in the target archive and series,
163+ # people who only have queue admin permissions to some other
164+ # component may not copy it.
165+ (source, source_archive, source_name, target_archive, to_pocket,
166+ to_series, version) = self._setup_copy_data(
167+ target_purpose=ArchivePurpose.PRIMARY)
168+ self.factory.makeSourcePackagePublishingHistory(
169+ distroseries=to_series, archive=target_archive,
170+ status=PackagePublishingStatus.PUBLISHED,
171+ sourcepackagename=source_name, version="%s~" % version,
172+ component="main")
173+ person = self.factory.makePerson()
174+ with person_logged_in(target_archive.distribution.owner):
175+ target_archive.newQueueAdmin(person, "universe")
176+ self.assertRaises(
177+ CannotCopy,
178+ target_archive.copyPackage, source_name, version, source_archive,
179+ to_pocket.name, to_series=to_series.name, include_binaries=False,
180+ person=person)
181+
182 def test_copyPackage_disallows_non_release_target_pocket_for_PPA(self):
183 (source, source_archive, source_name, target_archive, to_pocket,
184 to_series, version) = self._setup_copy_data()
185@@ -2450,7 +2514,7 @@
186 self.assertEqual(target_archive, copy_job.target_archive)
187
188 def test_copyPackages_disallows_non_PPA_owners(self):
189- # Only people with launchpad.Append are allowed to call copyPackage.
190+ # Only people with launchpad.Append are allowed to call copyPackages.
191 (source, source_archive, source_name, target_archive, to_pocket,
192 to_series, version) = self._setup_copy_data()
193 person = self.factory.makePerson()
194@@ -2461,6 +2525,24 @@
195 to_pocket.name, to_series=to_series.name, include_binaries=False,
196 person=person)
197
198+ def test_copyPackages_allows_queue_admins(self):
199+ # Queue admins without upload permissions may still copy packages.
200+ (source, source_archive, source_name, target_archive, to_pocket,
201+ to_series, version) = self._setup_copy_data(
202+ target_purpose=ArchivePurpose.PRIMARY)
203+ person = self.factory.makePerson()
204+ with person_logged_in(target_archive.distribution.owner):
205+ target_archive.newQueueAdmin(person, "universe")
206+ target_archive.copyPackages(
207+ [source_name], source_archive, to_pocket.name,
208+ to_series=to_series.name, include_binaries=False,
209+ person=person)
210+
211+ # There should be one copy job.
212+ job_source = getUtility(IPlainPackageCopyJobSource)
213+ copy_job = job_source.getActiveJobs(target_archive).one()
214+ self.assertEqual(target_archive, copy_job.target_archive)
215+
216 def test_copyPackages_with_multiple_distroseries(self):
217 # The from_series parameter selects a source distroseries.
218 (source, source_archive, source_name, target_archive, to_pocket,