Merge lp:~cjwatson/launchpad/archive-getallpermissions into lp:launchpad

Proposed by Colin Watson on 2012-08-01
Status: Merged
Approved by: William Grant on 2012-08-10
Approved revision: no longer in the source branch.
Merged at revision: 15789
Proposed branch: lp:~cjwatson/launchpad/archive-getallpermissions
Merge into: lp:launchpad
Diff against target: 259 lines (+98/-14)
7 files modified
lib/lp/_schema_circular_imports.py (+1/-0)
lib/lp/soyuz/browser/tests/test_archive_webservice.py (+27/-3)
lib/lp/soyuz/interfaces/archive.py (+10/-0)
lib/lp/soyuz/interfaces/archivepermission.py (+7/-0)
lib/lp/soyuz/model/archive.py (+5/-0)
lib/lp/soyuz/model/archivepermission.py (+13/-0)
lib/lp/soyuz/stories/webservice/xx-archive.txt (+35/-11)
To merge this branch: bzr merge lp:~cjwatson/launchpad/archive-getallpermissions
Reviewer Review Type Date Requested Status
William Grant code Approve on 2012-08-10
Steve Kowalik (community) code 2012-08-01 Approve on 2012-08-08
Review via email: mp+117606@code.launchpad.net

Commit Message

Add exported Archive.getAllPermissions method.

Description of the Change

== Summary ==

Bug 1030936: The Ubuntu Developer Membership Board would like to be able to correlate the list of permissions granted on the Ubuntu archive against the membership of the ~ubuntu-dev team. Unfortunately, due to the variety of possible permission types and targets, it's prohibitively expensive to get such a list using the current API.

== Proposed fix ==

Add an exported Archive.getAllPermissions method, amounting to ArchivePermission.selectBy(archive=archive).

== LOC Rationale ==

+50. I have 3911 lines of credit; this should come out in the wash.

== Tests ==

bin/test -vvct lib/lp/soyuz/stories/webservice/xx-archive.txt

== Demo and Q/A ==

Look through the contents of lp.distributions["ubuntu"].main_archive.getAllPermissions() on qastaging.

== Lint ==

One pre-existing false positive and some pre-existing doctest cruft which I don't think is worth attempting to fix here:

./lib/lp/soyuz/model/archive.py
     346: redefinition of function 'private' from line 342
./lib/lp/soyuz/stories/webservice/xx-archive.txt
      43: want exceeds 78 characters.
      47: want exceeds 78 characters.
     173: want exceeds 78 characters.
     190: want exceeds 78 characters.
     207: want exceeds 78 characters.
     224: want exceeds 78 characters.
     397: want exceeds 78 characters.
     458: want exceeds 78 characters.
     589: want exceeds 78 characters.
     648: want exceeds 78 characters.

To post a comment you must log in.
Robert Collins (lifeless) wrote :

This has a hidden late evaluation case, which if you're dealing with a
large data set will punish you: you are returning team/person objects,
but they are not always visible to the user, so you need to eager
evaluate visibility (or push it down into your query).

Robert Collins (lifeless) wrote :

(Unless you intend to always return permissions even if the person
calling the API can't see the involved teams [not a problem for
Ubuntu, but its implicit in the model...] - that would require a
different approach.)

Steve Kowalik (stevenk) wrote :

I don't think we need to worry about visibility for this case -- you can't create ArchivePermissions for PPAs, and the governance around adding permissions for the Ubuntu primary archive would quickly quash a private team, so I think it's fine.

review: Approve (code)
William Grant (wgrant) wrote :

ArchivePermissions can be created for PPAs, and private archives (PPA or distribution) will probably want private teams in future. We need to either restrict private teams from participating here, or preload permissions.

review: Needs Fixing (code)
William Grant (wgrant) wrote :

Doesn't fix the private team permission check case, but it's unlikely to be a problem right now, and we don't have a truly good solution yet.

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/_schema_circular_imports.py'
2--- lib/lp/_schema_circular_imports.py 2012-08-08 12:05:10 +0000
3+++ lib/lp/_schema_circular_imports.py 2012-08-10 17:20:25 +0000
4@@ -393,6 +393,7 @@
5 patch_collection_property(IArchive, 'dependencies', IArchiveDependency)
6 patch_collection_property(
7 IArchive, 'enabled_restricted_families', IProcessorFamily)
8+patch_collection_return_type(IArchive, 'getAllPermissions', IArchivePermission)
9 patch_collection_return_type(
10 IArchive, 'getPermissionsForPerson', IArchivePermission)
11 patch_collection_return_type(
12
13=== modified file 'lib/lp/soyuz/browser/tests/test_archive_webservice.py'
14--- lib/lp/soyuz/browser/tests/test_archive_webservice.py 2012-07-23 11:51:21 +0000
15+++ lib/lp/soyuz/browser/tests/test_archive_webservice.py 2012-08-10 17:20:25 +0000
16@@ -12,25 +12,31 @@
17 Unauthorized as LRUnauthorized,
18 )
19 from testtools import ExpectedException
20+from testtools.matchers import Equals
21 import transaction
22 from zope.component import getUtility
23
24 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
25 from lp.registry.interfaces.pocket import PackagePublishingPocket
26 from lp.soyuz.enums import (
27+ ArchivePermissionType,
28 ArchivePurpose,
29 PackagePublishingStatus,
30 )
31+from lp.soyuz.interfaces.component import IComponentSet
32 from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
33 from lp.soyuz.interfaces.processor import IProcessorFamilySet
34+from lp.soyuz.model.archivepermission import ArchivePermission
35 from lp.testing import (
36 celebrity_logged_in,
37 launchpadlib_for,
38 person_logged_in,
39+ StormStatementRecorder,
40 TestCaseWithFactory,
41 WebServiceTestCase,
42 )
43 from lp.testing.layers import DatabaseFunctionalLayer
44+from lp.testing.matchers import HasQueryCount
45 from lp.testing.pages import LaunchpadWebServiceCaller
46
47
48@@ -40,12 +46,12 @@
49 def setUp(self):
50 TestCaseWithFactory.setUp(self)
51 with celebrity_logged_in('admin'):
52- archive = self.factory.makeArchive(
53+ self.archive = self.factory.makeArchive(
54 purpose=ArchivePurpose.PRIMARY)
55 distroseries = self.factory.makeDistroSeries(
56- distribution=archive.distribution)
57+ distribution=self.archive.distribution)
58 person = self.factory.makePerson()
59- distro_name = archive.distribution.name
60+ distro_name = self.archive.distribution.name
61 distroseries_name = distroseries.name
62 person_name = person.name
63
64@@ -83,6 +89,24 @@
65 "Not permitted to upload to the UPDATES pocket in a series "
66 "in the 'DEVELOPMENT' state.", e.content)
67
68+ def test_getAllPermissions_constant_query_count(self):
69+ # getAllPermissions has a query count constant in the number of
70+ # permissions and people.
71+ with celebrity_logged_in('admin'):
72+ main = getUtility(IComponentSet)["main"]
73+ ArchivePermission(
74+ archive=self.archive, person=self.factory.makePerson(),
75+ component=main, permission=ArchivePermissionType.UPLOAD)
76+ with StormStatementRecorder() as recorder_one:
77+ list(self.main_archive.getAllPermissions())
78+ with celebrity_logged_in('admin'):
79+ ArchivePermission(
80+ archive=self.archive, person=self.factory.makePerson(),
81+ component=main, permission=ArchivePermissionType.UPLOAD)
82+ with StormStatementRecorder() as recorder:
83+ list(self.main_archive.getAllPermissions())
84+ self.assertThat(recorder, HasQueryCount(Equals(recorder_one.count)))
85+
86
87 class TestExternalDependencies(WebServiceTestCase):
88
89
90=== modified file 'lib/lp/soyuz/interfaces/archive.py'
91--- lib/lp/soyuz/interfaces/archive.py 2012-08-08 16:34:39 +0000
92+++ lib/lp/soyuz/interfaces/archive.py 2012-08-10 17:20:25 +0000
93@@ -1160,6 +1160,16 @@
94 could not be found.
95 """
96
97+ # Really IArchivePermission, set below to avoid circular import.
98+ @operation_returns_collection_of(Interface)
99+ @export_read_operation()
100+ @operation_for_version("devel")
101+ def getAllPermissions():
102+ """Return all `IArchivePermission` records for this archive.
103+
104+ :return: A list of `IArchivePermission` records.
105+ """
106+
107 @operation_parameters(person=Reference(schema=IPerson))
108 # Really IArchivePermission, set below to avoid circular import.
109 @operation_returns_collection_of(Interface)
110
111=== modified file 'lib/lp/soyuz/interfaces/archivepermission.py'
112--- lib/lp/soyuz/interfaces/archivepermission.py 2012-08-01 04:18:50 +0000
113+++ lib/lp/soyuz/interfaces/archivepermission.py 2012-08-10 17:20:25 +0000
114@@ -63,6 +63,7 @@
115 title=_("The permission type being granted."),
116 values=ArchivePermissionType, readonly=False, required=True))
117
118+ personID = Attribute("DB ID for person.")
119 person = exported(
120 PublicPersonChoice(
121 title=_("Person"),
122@@ -170,6 +171,12 @@
123 authenticated in that context.
124 """
125
126+ def permissionsForArchive(archive):
127+ """All `ArchivePermission` records for the archive.
128+
129+ :param archive: An `IArchive`.
130+ """
131+
132 def permissionsForPerson(person, archive):
133 """All `ArchivePermission` records for the person.
134
135
136=== modified file 'lib/lp/soyuz/model/archive.py'
137--- lib/lp/soyuz/model/archive.py 2012-08-08 16:34:39 +0000
138+++ lib/lp/soyuz/model/archive.py 2012-08-10 17:20:25 +0000
139@@ -1058,6 +1058,11 @@
140 return permission_set.checkAuthenticated(
141 user, self, perm_type, item, distroseries=distroseries)
142
143+ def getAllPermissions(self):
144+ """See `IArchive`."""
145+ permission_set = getUtility(IArchivePermissionSet)
146+ return permission_set.permissionsForArchive(self)
147+
148 def getPermissionsForPerson(self, person):
149 """See `IArchive`."""
150 permission_set = getUtility(IArchivePermissionSet)
151
152=== modified file 'lib/lp/soyuz/model/archivepermission.py'
153--- lib/lp/soyuz/model/archivepermission.py 2012-08-01 04:18:50 +0000
154+++ lib/lp/soyuz/model/archivepermission.py 2012-08-10 17:20:25 +0000
155@@ -10,6 +10,8 @@
156 'ArchivePermissionSet',
157 ]
158
159+from operator import attrgetter
160+
161 from lazr.enum import DBItem
162 from sqlobject import (
163 BoolCol,
164@@ -30,6 +32,7 @@
165
166 from lp.app.errors import NotFoundError
167 from lp.registry.interfaces.distribution import IDistributionSet
168+from lp.registry.interfaces.person import IPersonSet
169 from lp.registry.interfaces.pocket import PackagePublishingPocket
170 from lp.registry.interfaces.sourcepackagename import (
171 ISourcePackageName,
172@@ -224,6 +227,16 @@
173 ISourcePackageNameSet)[sourcepackagename]
174 return sourcepackagename
175
176+ def _precachePersonsForPermissions(self, permissions):
177+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
178+ set(map(attrgetter("personID"), permissions))))
179+ return permissions
180+
181+ def permissionsForArchive(self, archive):
182+ """See `IArchivePermissionSet`."""
183+ return self._precachePersonsForPermissions(
184+ ArchivePermission.selectBy(archive=archive))
185+
186 def permissionsForPerson(self, archive, person):
187 """See `IArchivePermissionSet`."""
188 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
189
190=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
191--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2012-08-08 16:34:39 +0000
192+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2012-08-10 17:20:25 +0000
193@@ -229,14 +229,7 @@
194 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
195
196 Permission collections can be retrieved with custom operations on the
197-archive.
198-
199-`getPermissionsForPerson` returns all the permissions that a user has.
200-
201- >>> ubuntu_team = user_webservice.get("/~ubuntu-team").jsonBody()
202- >>> permissions = user_webservice.named_get(
203- ... ubuntutest['main_archive_link'], 'getPermissionsForPerson',
204- ... person=ubuntu_team['self_link']).jsonBody()
205+archive. First, define some general helper functions.
206
207 >>> def permission_entry_sort_key(entry):
208 ... return (entry['permission'],
209@@ -255,6 +248,40 @@
210 ... print entry['pocket']
211 ... print entry['distroseries_link']
212
213+`getAllPermissions` returns all permissions on the archive.
214+
215+ >>> ubuntu_devel = user_webservice.get(
216+ ... '/distros', api_version='devel').jsonBody()['entries'][0]
217+
218+ >>> def show_all_permissions(archive):
219+ ... permissions = user_webservice.get(
220+ ... '%s?ws.op=getAllPermissions&ws.size=50' % archive,
221+ ... api_version='devel').jsonBody()
222+ ... show_permission_entries(permissions)
223+
224+ >>> show_all_permissions(ubuntu_devel['main_archive_link'])
225+ Archive Upload Rights ...~carlos None mozilla-firefox None None
226+ Archive Upload Rights ...~ubuntu-team main None None None
227+ Archive Upload Rights ...~ubuntu-team restricted None None None
228+ Archive Upload Rights ...~ubuntu-team universe None None None
229+ Queue Administration Rights ...~name12 main None None None
230+ Queue Administration Rights ...~name12 multiverse None None None
231+ Queue Administration Rights ...~name12 restricted None None None
232+ Queue Administration Rights ...~name12 universe None None None
233+ Queue Administration Rights ...~no-team-memberships multiverse None None None
234+ Queue Administration Rights ...~no-team-memberships universe None None None
235+ Queue Administration Rights ...~ubuntu-team main None None None
236+ Queue Administration Rights ...~ubuntu-team partner None None None
237+ Queue Administration Rights ...~ubuntu-team restricted None None None
238+ Queue Administration Rights ...~ubuntu-team universe None None None
239+
240+`getPermissionsForPerson` returns all the permissions that a user has.
241+
242+ >>> ubuntu_team = user_webservice.get("/~ubuntu-team").jsonBody()
243+ >>> permissions = user_webservice.named_get(
244+ ... ubuntutest['main_archive_link'], 'getPermissionsForPerson',
245+ ... person=ubuntu_team['self_link']).jsonBody()
246+
247 >>> show_permission_entries(permissions)
248 Archive Upload Rights ...~ubuntu-team main None None None
249 Archive Upload Rights ...~ubuntu-team universe None None None
250@@ -589,9 +616,6 @@
251 getUploadersForPocket returns all the permissions where someone can upload
252 to a particular pocket:
253
254- >>> ubuntu_devel = webservice.get(
255- ... '/distros', api_version='devel').jsonBody()['entries'][0]
256-
257 >>> def show_pocket_permissions(pocket):
258 ... permissions = user_webservice.named_get(
259 ... ubuntu_devel['main_archive_link'], 'getUploadersForPocket',