Merge lp:~jml/launchpad/validate-ppa-owner into lp:launchpad

Proposed by Jonathan Lange
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 15390
Proposed branch: lp:~jml/launchpad/validate-ppa-owner
Merge into: lp:launchpad
Prerequisite: lp:~jml/launchpad/validate-ppa-function
Diff against target: 345 lines (+84/-56)
10 files modified
lib/lp/_schema_circular_imports.py (+2/-1)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+6/-1)
lib/lp/registry/browser/tests/test_team.py (+2/-1)
lib/lp/registry/interfaces/person.py (+28/-27)
lib/lp/registry/model/person.py (+0/-5)
lib/lp/registry/tests/test_team.py (+2/-1)
lib/lp/soyuz/browser/archive.py (+1/-0)
lib/lp/soyuz/model/archive.py (+16/-8)
lib/lp/soyuz/tests/test_archive.py (+14/-9)
lib/lp/soyuz/tests/test_person_createppa.py (+13/-3)
To merge this branch: bzr merge lp:~jml/launchpad/validate-ppa-owner
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Launchpad code reviewers Pending
Review via email: mp+109529@code.launchpad.net

Commit message

Restrict IPerson.createPPA to those with launchpad.Edit permissions.

Description of the change

I noticed that createPPA can be called by anyone on anyone, since it's only got ViewRestricted permissions. I don't think this is a major vulnerability, since it doesn't grant upload permissions to that created PPA. Thus, I'm submitting this in the clear. I hope that's OK.

This branch changes createPPA to be on IPersonEditRestricted, thus requiring launchpad.Edit permissions on the team (person) that the PPA is being created for. This matches the behaviour in the web UI, which only shows PPA creation screens for users with launchpad.Edit on a team (person).

Curtis was concerned that this might break behaviour for key users: CA, OEM, OpenStack. He suggested that we do an audit first. However, this was when we believed that only team members could create PPAs via API, rather than the more restrictive web UI, which only allows team admins. Now that our understanding has changed, I think this patch should be landed now.

It also changes the web UI code to also use createPPA. This leaves Launchpad with one consistent way to create PPAs, which certainly makes it easier for *me* to understand.

This add 24 lines to the code base, including the one added by the dependent branch. I'm fairly sure that the CA PPA arc is in credit. Will get exact numbers soon.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank's jml. You discovered and fixed a disclosure feature mistake (and we might think of the LOC point to also be in the disclosure domain.). This is good to land.

review: Approve (code)
Revision history for this message
Jonathan Lange (jml) wrote :

Thanks. I'd like to keep track of it in our own arc, if you don't mind.

https://docs.google.com/a/canonical.com/spreadsheet/ccc?key=0AsE2-aW-RE8adG5vbW1nN3dJV0o0WUVKSElFRE9UUFE#gid=0 says this will put us on a net of zero.

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-05-14 03:12:44 +0000
3+++ lib/lp/_schema_circular_imports.py 2012-06-11 09:21:23 +0000
4@@ -118,6 +118,7 @@
5 )
6 from lp.registry.interfaces.person import (
7 IPerson,
8+ IPersonEditRestricted,
9 IPersonLimitedView,
10 IPersonViewRestricted,
11 ITeam,
12@@ -309,7 +310,7 @@
13 patch_reference_property(IPersonViewRestricted, 'archive', IArchive)
14 patch_collection_property(IPersonViewRestricted, 'ppas', IArchive)
15 patch_entry_return_type(IPersonLimitedView, 'getPPAByName', IArchive)
16-patch_entry_return_type(IPersonViewRestricted, 'createPPA', IArchive)
17+patch_entry_return_type(IPersonEditRestricted, 'createPPA', IArchive)
18
19 IHasBuildRecords['getBuildRecords'].queryTaggedValue(
20 LAZR_WEBSERVICE_EXPORTED)[
21
22=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
23--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2012-05-31 03:54:13 +0000
24+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2012-06-11 09:21:23 +0000
25@@ -39,6 +39,7 @@
26 from lp.registry.interfaces.person import TeamSubscriptionPolicy
27 from lp.registry.interfaces.pocket import PackagePublishingPocket
28 from lp.registry.interfaces.series import SeriesStatus
29+from lp.registry.interfaces.teammembership import TeamMembershipStatus
30 from lp.services.database.constants import UTC_NOW
31 from lp.services.propertycache import clear_property_cache
32 from lp.services.webapp import canonical_url
33@@ -670,7 +671,8 @@
34 # name specifed an error is shown.
35 self.user = self.factory.makePerson(name='eric')
36 # Make a PPA called 'ppa' using the default.
37- self.user.createPPA(name='foo')
38+ with person_logged_in(self.user):
39+ self.user.createPPA(name='foo')
40 branch = self.factory.makeAnyBranch()
41
42 # A new recipe can be created from the branch page.
43@@ -710,6 +712,9 @@
44 team = self.factory.makeTeam(
45 name='vikings', members=[self.user],
46 subscription_policy=TeamSubscriptionPolicy.MODERATED)
47+ with person_logged_in(team.teamowner):
48+ team.setMembershipData(
49+ self.user, TeamMembershipStatus.ADMIN, team.teamowner)
50 branch = self.factory.makeAnyBranch(owner=team)
51
52 # A new recipe can be created from the branch page.
53
54=== modified file 'lib/lp/registry/browser/tests/test_team.py'
55--- lib/lp/registry/browser/tests/test_team.py 2012-05-02 05:25:11 +0000
56+++ lib/lp/registry/browser/tests/test_team.py 2012-06-11 09:21:23 +0000
57@@ -344,7 +344,8 @@
58 # the team has any ppas.
59
60 def setup_team(team):
61- team.createPPA()
62+ with person_logged_in(team.teamowner):
63+ team.createPPA()
64
65 self._test_edit_team_view_expected_subscription_vocab(
66 setup_team, CLOSED_TEAM_POLICY)
67
68=== modified file 'lib/lp/registry/interfaces/person.py'
69--- lib/lp/registry/interfaces/person.py 2012-05-28 09:17:25 +0000
70+++ lib/lp/registry/interfaces/person.py 2012-06-11 09:21:23 +0000
71@@ -17,6 +17,7 @@
72 'IObjectReassignment',
73 'IPerson',
74 'IPersonClaim',
75+ 'IPersonEditRestricted',
76 'IPersonPublic',
77 'IPersonSet',
78 'IPersonSettings',
79@@ -1482,33 +1483,6 @@
80 :return: True if the user was subscribed, false if they weren't.
81 """
82
83- @operation_parameters(
84- name=TextLine(required=True, constraint=name_validator),
85- displayname=TextLine(required=False),
86- description=TextLine(required=False),
87- private=Bool(required=False),
88- suppress_subscription_notifications=Bool(required=False),
89- )
90- @export_factory_operation(Interface, []) # Really IArchive.
91- @operation_for_version("beta")
92- def createPPA(name=None, displayname=None, description=None,
93- private=False, suppress_subscription_notifications=False):
94- """Create a PPA.
95-
96- :param name: A string with the name of the new PPA to create. If
97- not specified, defaults to 'ppa'.
98- :param displayname: The displayname for the new PPA.
99- :param description: The description for the new PPA.
100- :param private: Whether or not to create a private PPA. Defaults to
101- False, which means the PPA will be public.
102- :param suppress_subscription_notifications: Whether or not to suppress
103- emails to new subscribers about their subscriptions. Only
104- meaningful for private PPAs.
105- :raises: `PPACreationError` if an error is encountered
106-
107- :return: a PPA `IArchive` record.
108- """
109-
110 def checkRename():
111 """Check if a person or team can be renamed.
112
113@@ -1826,6 +1800,33 @@
114 about the change.
115 """
116
117+ @operation_parameters(
118+ name=TextLine(required=True, constraint=name_validator),
119+ displayname=TextLine(required=False),
120+ description=TextLine(required=False),
121+ private=Bool(required=False),
122+ suppress_subscription_notifications=Bool(required=False),
123+ )
124+ @export_factory_operation(Interface, []) # Really IArchive.
125+ @operation_for_version("beta")
126+ def createPPA(name=None, displayname=None, description=None,
127+ private=False, suppress_subscription_notifications=False):
128+ """Create a PPA.
129+
130+ :param name: A string with the name of the new PPA to create. If
131+ not specified, defaults to 'ppa'.
132+ :param displayname: The displayname for the new PPA.
133+ :param description: The description for the new PPA.
134+ :param private: Whether or not to create a private PPA. Defaults to
135+ False, which means the PPA will be public.
136+ :param suppress_subscription_notifications: Whether or not to suppress
137+ emails to new subscribers about their subscriptions. Only
138+ meaningful for private PPAs.
139+ :raises: `PPACreationError` if an error is encountered
140+
141+ :return: a PPA `IArchive` record.
142+ """
143+
144
145 class IPersonSpecialRestricted(Interface):
146 """IPerson methods that require launchpad.Special permission to use."""
147
148=== modified file 'lib/lp/registry/model/person.py'
149--- lib/lp/registry/model/person.py 2012-06-09 16:26:32 +0000
150+++ lib/lp/registry/model/person.py 2012-06-11 09:21:23 +0000
151@@ -2978,11 +2978,6 @@
152 def createPPA(self, name=None, displayname=None, description=None,
153 private=False, suppress_subscription_notifications=False):
154 """See `IPerson`."""
155- # XXX: We pass through the Person on whom the PPA is being created,
156- # but validate_ppa assumes that that Person is also the one creating
157- # the PPA. This is not true in general, and particularly not for
158- # teams. Instead, both the acting user and the target of the PPA
159- # creation ought to be passed through.
160 errors = validate_ppa(self, name, private)
161 if errors:
162 raise PPACreationError(errors)
163
164=== modified file 'lib/lp/registry/tests/test_team.py'
165--- lib/lp/registry/tests/test_team.py 2012-05-02 05:25:11 +0000
166+++ lib/lp/registry/tests/test_team.py 2012-06-11 09:21:23 +0000
167@@ -500,7 +500,8 @@
168 # made to set an illegal open subscription policy on a team.
169 team = self.factory.makeTeam(
170 subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
171- team.createPPA()
172+ with person_logged_in(team.teamowner):
173+ team.createPPA()
174 for policy in OPEN_TEAM_POLICY:
175 self.assertRaises(
176 TeamSubscriptionPolicyError,
177
178=== modified file 'lib/lp/soyuz/browser/archive.py'
179--- lib/lp/soyuz/browser/archive.py 2012-06-09 16:26:32 +0000
180+++ lib/lp/soyuz/browser/archive.py 2012-06-11 09:21:23 +0000
181@@ -1994,6 +1994,7 @@
182 # PPA name.
183 name = data.get('name', None)
184
185+ # XXX: jml: I think this ought to call createPPA.
186 # XXX cprov 2009-03-27 bug=188564: We currently only create PPAs
187 # for Ubuntu distribution. PPA creation should be revisited when we
188 # start supporting other distribution (debian, mainly).
189
190=== modified file 'lib/lp/soyuz/model/archive.py'
191--- lib/lp/soyuz/model/archive.py 2012-06-09 16:26:32 +0000
192+++ lib/lp/soyuz/model/archive.py 2012-06-11 09:21:23 +0000
193@@ -105,6 +105,7 @@
194 from lp.services.webapp.authorization import check_permission
195 from lp.services.webapp.interfaces import (
196 DEFAULT_FLAVOR,
197+ ILaunchBag,
198 IStoreSelector,
199 MAIN_STORE,
200 )
201@@ -2054,7 +2055,14 @@
202 job.destroySelf()
203
204
205-def validate_ppa(person, proposed_name, private=False):
206+def validate_ppa(owner, proposed_name, private=False):
207+ """Can 'person' create a PPA called 'proposed_name'?
208+
209+ :param owner: The proposed owner of the PPA.
210+ :param proposed_name: The proposed name.
211+ :param private: Whether or not to make it private.
212+ """
213+ creator = getUtility(ILaunchBag).user
214 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
215 if private:
216 # NOTE: This duplicates the policy in lp/soyuz/configure.zcml
217@@ -2062,11 +2070,11 @@
218 # set 'private', and the logic in `AdminByCommercialTeamOrAdmins`
219 # which determines who is granted launchpad.Commercial
220 # permissions.
221- role = IPersonRoles(person)
222+ role = IPersonRoles(creator)
223 if not (role.in_admin or role.in_commercial_admin):
224- return '%s is not allowed to make private PPAs' % person.name
225- if person.is_team and (
226- person.subscriptionpolicy in OPEN_TEAM_POLICY):
227+ return '%s is not allowed to make private PPAs' % creator.name
228+ if owner.is_team and (
229+ owner.subscriptionpolicy in OPEN_TEAM_POLICY):
230 return "Open teams cannot have PPAs."
231 if proposed_name is not None and proposed_name == ubuntu.name:
232 return (
233@@ -2074,14 +2082,14 @@
234 if proposed_name is None:
235 proposed_name = 'ppa'
236 try:
237- person.getPPAByName(proposed_name)
238+ owner.getPPAByName(proposed_name)
239 except NoSuchPPA:
240 return None
241 else:
242 text = "You already have a PPA named '%s'." % proposed_name
243- if person.is_team:
244+ if owner.is_team:
245 text = "%s already has a PPA named '%s'." % (
246- person.displayname, proposed_name)
247+ owner.displayname, proposed_name)
248 return text
249
250
251
252=== modified file 'lib/lp/soyuz/tests/test_archive.py'
253--- lib/lp/soyuz/tests/test_archive.py 2012-06-09 16:26:32 +0000
254+++ lib/lp/soyuz/tests/test_archive.py 2012-06-11 09:21:23 +0000
255@@ -1666,30 +1666,35 @@
256
257 def test_private_ppa_non_commercial_admin(self):
258 ppa_owner = self.factory.makePerson()
259+ with person_logged_in(ppa_owner):
260+ errors = validate_ppa(
261+ ppa_owner, self.factory.getUniqueString(), private=True)
262 self.assertEqual(
263 '%s is not allowed to make private PPAs' % (ppa_owner.name,),
264- validate_ppa(
265- ppa_owner, self.factory.getUniqueString(), private=True))
266+ errors)
267
268 def test_private_ppa_commercial_admin(self):
269 ppa_owner = self.factory.makePerson()
270 with celebrity_logged_in('admin'):
271 comm = getUtility(ILaunchpadCelebrities).commercial_admin
272 comm.addMember(ppa_owner, comm.teamowner)
273- self.assertIsNone(
274- validate_ppa(
275- ppa_owner, self.factory.getUniqueString(), private=True))
276+ with person_logged_in(ppa_owner):
277+ self.assertIsNone(
278+ validate_ppa(
279+ ppa_owner, self.factory.getUniqueString(), private=True))
280
281 def test_private_ppa_admin(self):
282 ppa_owner = self.factory.makeAdministrator()
283- self.assertIsNone(
284- validate_ppa(
285- ppa_owner, self.factory.getUniqueString(), private=True))
286+ with person_logged_in(ppa_owner):
287+ self.assertIsNone(
288+ validate_ppa(
289+ ppa_owner, self.factory.getUniqueString(), private=True))
290
291 def test_two_ppas(self):
292 ppa = self.factory.makeArchive(name='ppa')
293 self.assertEqual(
294- "You already have a PPA named 'ppa'.", validate_ppa(ppa.owner, 'ppa'))
295+ "You already have a PPA named 'ppa'.",
296+ validate_ppa(ppa.owner, 'ppa'))
297
298 def test_two_ppas_with_team(self):
299 team = self.factory.makeTeam(
300
301=== modified file 'lib/lp/soyuz/tests/test_person_createppa.py'
302--- lib/lp/soyuz/tests/test_person_createppa.py 2012-05-25 22:14:21 +0000
303+++ lib/lp/soyuz/tests/test_person_createppa.py 2012-06-11 09:21:23 +0000
304@@ -5,6 +5,7 @@
305
306 __metaclass__ = type
307
308+from zope.security.interfaces import Unauthorized
309 from lp.registry.errors import PPACreationError
310 from lp.testing import (
311 celebrity_logged_in,
312@@ -21,13 +22,14 @@
313
314 def test_default_name(self):
315 person = self.factory.makePerson()
316- ppa = person.createPPA()
317+ with person_logged_in(person):
318+ ppa = person.createPPA()
319 self.assertEqual(ppa.name, 'ppa')
320
321 def test_private(self):
322 with celebrity_logged_in('commercial_admin') as person:
323 ppa = person.createPPA(private=True)
324- self.assertEqual(True, ppa.private)
325+ self.assertEqual(True, ppa.private)
326
327 def test_private_without_permission(self):
328 person = self.factory.makePerson()
329@@ -35,7 +37,15 @@
330 self.assertRaises(
331 PPACreationError, person.createPPA, private=True)
332
333+ def test_different_person(self):
334+ # You cannot make a PPA on another person.
335+ owner = self.factory.makePerson()
336+ creator = self.factory.makePerson()
337+ with person_logged_in(creator):
338+ self.assertRaises(Unauthorized, getattr, owner, 'createPPA')
339+
340 def test_suppress_subscription_notifications(self):
341 person = self.factory.makePerson()
342- ppa = person.createPPA(suppress_subscription_notifications=True)
343+ with person_logged_in(person):
344+ ppa = person.createPPA(suppress_subscription_notifications=True)
345 self.assertEqual(True, ppa.suppress_subscription_notifications)