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
=== modified file 'lib/lp/_schema_circular_imports.py'
--- lib/lp/_schema_circular_imports.py 2012-05-14 03:12:44 +0000
+++ lib/lp/_schema_circular_imports.py 2012-06-11 09:21:23 +0000
@@ -118,6 +118,7 @@
118 )118 )
119from lp.registry.interfaces.person import (119from lp.registry.interfaces.person import (
120 IPerson,120 IPerson,
121 IPersonEditRestricted,
121 IPersonLimitedView,122 IPersonLimitedView,
122 IPersonViewRestricted,123 IPersonViewRestricted,
123 ITeam,124 ITeam,
@@ -309,7 +310,7 @@
309patch_reference_property(IPersonViewRestricted, 'archive', IArchive)310patch_reference_property(IPersonViewRestricted, 'archive', IArchive)
310patch_collection_property(IPersonViewRestricted, 'ppas', IArchive)311patch_collection_property(IPersonViewRestricted, 'ppas', IArchive)
311patch_entry_return_type(IPersonLimitedView, 'getPPAByName', IArchive)312patch_entry_return_type(IPersonLimitedView, 'getPPAByName', IArchive)
312patch_entry_return_type(IPersonViewRestricted, 'createPPA', IArchive)313patch_entry_return_type(IPersonEditRestricted, 'createPPA', IArchive)
313314
314IHasBuildRecords['getBuildRecords'].queryTaggedValue(315IHasBuildRecords['getBuildRecords'].queryTaggedValue(
315 LAZR_WEBSERVICE_EXPORTED)[316 LAZR_WEBSERVICE_EXPORTED)[
316317
=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2012-05-31 03:54:13 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2012-06-11 09:21:23 +0000
@@ -39,6 +39,7 @@
39from lp.registry.interfaces.person import TeamSubscriptionPolicy39from lp.registry.interfaces.person import TeamSubscriptionPolicy
40from lp.registry.interfaces.pocket import PackagePublishingPocket40from lp.registry.interfaces.pocket import PackagePublishingPocket
41from lp.registry.interfaces.series import SeriesStatus41from lp.registry.interfaces.series import SeriesStatus
42from lp.registry.interfaces.teammembership import TeamMembershipStatus
42from lp.services.database.constants import UTC_NOW43from lp.services.database.constants import UTC_NOW
43from lp.services.propertycache import clear_property_cache44from lp.services.propertycache import clear_property_cache
44from lp.services.webapp import canonical_url45from lp.services.webapp import canonical_url
@@ -670,7 +671,8 @@
670 # name specifed an error is shown.671 # name specifed an error is shown.
671 self.user = self.factory.makePerson(name='eric')672 self.user = self.factory.makePerson(name='eric')
672 # Make a PPA called 'ppa' using the default.673 # Make a PPA called 'ppa' using the default.
673 self.user.createPPA(name='foo')674 with person_logged_in(self.user):
675 self.user.createPPA(name='foo')
674 branch = self.factory.makeAnyBranch()676 branch = self.factory.makeAnyBranch()
675677
676 # A new recipe can be created from the branch page.678 # A new recipe can be created from the branch page.
@@ -710,6 +712,9 @@
710 team = self.factory.makeTeam(712 team = self.factory.makeTeam(
711 name='vikings', members=[self.user],713 name='vikings', members=[self.user],
712 subscription_policy=TeamSubscriptionPolicy.MODERATED)714 subscription_policy=TeamSubscriptionPolicy.MODERATED)
715 with person_logged_in(team.teamowner):
716 team.setMembershipData(
717 self.user, TeamMembershipStatus.ADMIN, team.teamowner)
713 branch = self.factory.makeAnyBranch(owner=team)718 branch = self.factory.makeAnyBranch(owner=team)
714719
715 # A new recipe can be created from the branch page.720 # A new recipe can be created from the branch page.
716721
=== modified file 'lib/lp/registry/browser/tests/test_team.py'
--- lib/lp/registry/browser/tests/test_team.py 2012-05-02 05:25:11 +0000
+++ lib/lp/registry/browser/tests/test_team.py 2012-06-11 09:21:23 +0000
@@ -344,7 +344,8 @@
344 # the team has any ppas.344 # the team has any ppas.
345345
346 def setup_team(team):346 def setup_team(team):
347 team.createPPA()347 with person_logged_in(team.teamowner):
348 team.createPPA()
348349
349 self._test_edit_team_view_expected_subscription_vocab(350 self._test_edit_team_view_expected_subscription_vocab(
350 setup_team, CLOSED_TEAM_POLICY)351 setup_team, CLOSED_TEAM_POLICY)
351352
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2012-05-28 09:17:25 +0000
+++ lib/lp/registry/interfaces/person.py 2012-06-11 09:21:23 +0000
@@ -17,6 +17,7 @@
17 'IObjectReassignment',17 'IObjectReassignment',
18 'IPerson',18 'IPerson',
19 'IPersonClaim',19 'IPersonClaim',
20 'IPersonEditRestricted',
20 'IPersonPublic',21 'IPersonPublic',
21 'IPersonSet',22 'IPersonSet',
22 'IPersonSettings',23 'IPersonSettings',
@@ -1482,33 +1483,6 @@
1482 :return: True if the user was subscribed, false if they weren't.1483 :return: True if the user was subscribed, false if they weren't.
1483 """1484 """
14841485
1485 @operation_parameters(
1486 name=TextLine(required=True, constraint=name_validator),
1487 displayname=TextLine(required=False),
1488 description=TextLine(required=False),
1489 private=Bool(required=False),
1490 suppress_subscription_notifications=Bool(required=False),
1491 )
1492 @export_factory_operation(Interface, []) # Really IArchive.
1493 @operation_for_version("beta")
1494 def createPPA(name=None, displayname=None, description=None,
1495 private=False, suppress_subscription_notifications=False):
1496 """Create a PPA.
1497
1498 :param name: A string with the name of the new PPA to create. If
1499 not specified, defaults to 'ppa'.
1500 :param displayname: The displayname for the new PPA.
1501 :param description: The description for the new PPA.
1502 :param private: Whether or not to create a private PPA. Defaults to
1503 False, which means the PPA will be public.
1504 :param suppress_subscription_notifications: Whether or not to suppress
1505 emails to new subscribers about their subscriptions. Only
1506 meaningful for private PPAs.
1507 :raises: `PPACreationError` if an error is encountered
1508
1509 :return: a PPA `IArchive` record.
1510 """
1511
1512 def checkRename():1486 def checkRename():
1513 """Check if a person or team can be renamed.1487 """Check if a person or team can be renamed.
15141488
@@ -1826,6 +1800,33 @@
1826 about the change.1800 about the change.
1827 """1801 """
18281802
1803 @operation_parameters(
1804 name=TextLine(required=True, constraint=name_validator),
1805 displayname=TextLine(required=False),
1806 description=TextLine(required=False),
1807 private=Bool(required=False),
1808 suppress_subscription_notifications=Bool(required=False),
1809 )
1810 @export_factory_operation(Interface, []) # Really IArchive.
1811 @operation_for_version("beta")
1812 def createPPA(name=None, displayname=None, description=None,
1813 private=False, suppress_subscription_notifications=False):
1814 """Create a PPA.
1815
1816 :param name: A string with the name of the new PPA to create. If
1817 not specified, defaults to 'ppa'.
1818 :param displayname: The displayname for the new PPA.
1819 :param description: The description for the new PPA.
1820 :param private: Whether or not to create a private PPA. Defaults to
1821 False, which means the PPA will be public.
1822 :param suppress_subscription_notifications: Whether or not to suppress
1823 emails to new subscribers about their subscriptions. Only
1824 meaningful for private PPAs.
1825 :raises: `PPACreationError` if an error is encountered
1826
1827 :return: a PPA `IArchive` record.
1828 """
1829
18291830
1830class IPersonSpecialRestricted(Interface):1831class IPersonSpecialRestricted(Interface):
1831 """IPerson methods that require launchpad.Special permission to use."""1832 """IPerson methods that require launchpad.Special permission to use."""
18321833
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-06-09 16:26:32 +0000
+++ lib/lp/registry/model/person.py 2012-06-11 09:21:23 +0000
@@ -2978,11 +2978,6 @@
2978 def createPPA(self, name=None, displayname=None, description=None,2978 def createPPA(self, name=None, displayname=None, description=None,
2979 private=False, suppress_subscription_notifications=False):2979 private=False, suppress_subscription_notifications=False):
2980 """See `IPerson`."""2980 """See `IPerson`."""
2981 # XXX: We pass through the Person on whom the PPA is being created,
2982 # but validate_ppa assumes that that Person is also the one creating
2983 # the PPA. This is not true in general, and particularly not for
2984 # teams. Instead, both the acting user and the target of the PPA
2985 # creation ought to be passed through.
2986 errors = validate_ppa(self, name, private)2981 errors = validate_ppa(self, name, private)
2987 if errors:2982 if errors:
2988 raise PPACreationError(errors)2983 raise PPACreationError(errors)
29892984
=== modified file 'lib/lp/registry/tests/test_team.py'
--- lib/lp/registry/tests/test_team.py 2012-05-02 05:25:11 +0000
+++ lib/lp/registry/tests/test_team.py 2012-06-11 09:21:23 +0000
@@ -500,7 +500,8 @@
500 # made to set an illegal open subscription policy on a team.500 # made to set an illegal open subscription policy on a team.
501 team = self.factory.makeTeam(501 team = self.factory.makeTeam(
502 subscription_policy=TeamSubscriptionPolicy.RESTRICTED)502 subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
503 team.createPPA()503 with person_logged_in(team.teamowner):
504 team.createPPA()
504 for policy in OPEN_TEAM_POLICY:505 for policy in OPEN_TEAM_POLICY:
505 self.assertRaises(506 self.assertRaises(
506 TeamSubscriptionPolicyError,507 TeamSubscriptionPolicyError,
507508
=== modified file 'lib/lp/soyuz/browser/archive.py'
--- lib/lp/soyuz/browser/archive.py 2012-06-09 16:26:32 +0000
+++ lib/lp/soyuz/browser/archive.py 2012-06-11 09:21:23 +0000
@@ -1994,6 +1994,7 @@
1994 # PPA name.1994 # PPA name.
1995 name = data.get('name', None)1995 name = data.get('name', None)
19961996
1997 # XXX: jml: I think this ought to call createPPA.
1997 # XXX cprov 2009-03-27 bug=188564: We currently only create PPAs1998 # XXX cprov 2009-03-27 bug=188564: We currently only create PPAs
1998 # for Ubuntu distribution. PPA creation should be revisited when we1999 # for Ubuntu distribution. PPA creation should be revisited when we
1999 # start supporting other distribution (debian, mainly).2000 # start supporting other distribution (debian, mainly).
20002001
=== modified file 'lib/lp/soyuz/model/archive.py'
--- lib/lp/soyuz/model/archive.py 2012-06-09 16:26:32 +0000
+++ lib/lp/soyuz/model/archive.py 2012-06-11 09:21:23 +0000
@@ -105,6 +105,7 @@
105from lp.services.webapp.authorization import check_permission105from lp.services.webapp.authorization import check_permission
106from lp.services.webapp.interfaces import (106from lp.services.webapp.interfaces import (
107 DEFAULT_FLAVOR,107 DEFAULT_FLAVOR,
108 ILaunchBag,
108 IStoreSelector,109 IStoreSelector,
109 MAIN_STORE,110 MAIN_STORE,
110 )111 )
@@ -2054,7 +2055,14 @@
2054 job.destroySelf()2055 job.destroySelf()
20552056
20562057
2057def validate_ppa(person, proposed_name, private=False):2058def validate_ppa(owner, proposed_name, private=False):
2059 """Can 'person' create a PPA called 'proposed_name'?
2060
2061 :param owner: The proposed owner of the PPA.
2062 :param proposed_name: The proposed name.
2063 :param private: Whether or not to make it private.
2064 """
2065 creator = getUtility(ILaunchBag).user
2058 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu2066 ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
2059 if private:2067 if private:
2060 # NOTE: This duplicates the policy in lp/soyuz/configure.zcml2068 # NOTE: This duplicates the policy in lp/soyuz/configure.zcml
@@ -2062,11 +2070,11 @@
2062 # set 'private', and the logic in `AdminByCommercialTeamOrAdmins`2070 # set 'private', and the logic in `AdminByCommercialTeamOrAdmins`
2063 # which determines who is granted launchpad.Commercial2071 # which determines who is granted launchpad.Commercial
2064 # permissions.2072 # permissions.
2065 role = IPersonRoles(person)2073 role = IPersonRoles(creator)
2066 if not (role.in_admin or role.in_commercial_admin):2074 if not (role.in_admin or role.in_commercial_admin):
2067 return '%s is not allowed to make private PPAs' % person.name2075 return '%s is not allowed to make private PPAs' % creator.name
2068 if person.is_team and (2076 if owner.is_team and (
2069 person.subscriptionpolicy in OPEN_TEAM_POLICY):2077 owner.subscriptionpolicy in OPEN_TEAM_POLICY):
2070 return "Open teams cannot have PPAs."2078 return "Open teams cannot have PPAs."
2071 if proposed_name is not None and proposed_name == ubuntu.name:2079 if proposed_name is not None and proposed_name == ubuntu.name:
2072 return (2080 return (
@@ -2074,14 +2082,14 @@
2074 if proposed_name is None:2082 if proposed_name is None:
2075 proposed_name = 'ppa'2083 proposed_name = 'ppa'
2076 try:2084 try:
2077 person.getPPAByName(proposed_name)2085 owner.getPPAByName(proposed_name)
2078 except NoSuchPPA:2086 except NoSuchPPA:
2079 return None2087 return None
2080 else:2088 else:
2081 text = "You already have a PPA named '%s'." % proposed_name2089 text = "You already have a PPA named '%s'." % proposed_name
2082 if person.is_team:2090 if owner.is_team:
2083 text = "%s already has a PPA named '%s'." % (2091 text = "%s already has a PPA named '%s'." % (
2084 person.displayname, proposed_name)2092 owner.displayname, proposed_name)
2085 return text2093 return text
20862094
20872095
20882096
=== modified file 'lib/lp/soyuz/tests/test_archive.py'
--- lib/lp/soyuz/tests/test_archive.py 2012-06-09 16:26:32 +0000
+++ lib/lp/soyuz/tests/test_archive.py 2012-06-11 09:21:23 +0000
@@ -1666,30 +1666,35 @@
16661666
1667 def test_private_ppa_non_commercial_admin(self):1667 def test_private_ppa_non_commercial_admin(self):
1668 ppa_owner = self.factory.makePerson()1668 ppa_owner = self.factory.makePerson()
1669 with person_logged_in(ppa_owner):
1670 errors = validate_ppa(
1671 ppa_owner, self.factory.getUniqueString(), private=True)
1669 self.assertEqual(1672 self.assertEqual(
1670 '%s is not allowed to make private PPAs' % (ppa_owner.name,),1673 '%s is not allowed to make private PPAs' % (ppa_owner.name,),
1671 validate_ppa(1674 errors)
1672 ppa_owner, self.factory.getUniqueString(), private=True))
16731675
1674 def test_private_ppa_commercial_admin(self):1676 def test_private_ppa_commercial_admin(self):
1675 ppa_owner = self.factory.makePerson()1677 ppa_owner = self.factory.makePerson()
1676 with celebrity_logged_in('admin'):1678 with celebrity_logged_in('admin'):
1677 comm = getUtility(ILaunchpadCelebrities).commercial_admin1679 comm = getUtility(ILaunchpadCelebrities).commercial_admin
1678 comm.addMember(ppa_owner, comm.teamowner)1680 comm.addMember(ppa_owner, comm.teamowner)
1679 self.assertIsNone(1681 with person_logged_in(ppa_owner):
1680 validate_ppa(1682 self.assertIsNone(
1681 ppa_owner, self.factory.getUniqueString(), private=True))1683 validate_ppa(
1684 ppa_owner, self.factory.getUniqueString(), private=True))
16821685
1683 def test_private_ppa_admin(self):1686 def test_private_ppa_admin(self):
1684 ppa_owner = self.factory.makeAdministrator()1687 ppa_owner = self.factory.makeAdministrator()
1685 self.assertIsNone(1688 with person_logged_in(ppa_owner):
1686 validate_ppa(1689 self.assertIsNone(
1687 ppa_owner, self.factory.getUniqueString(), private=True))1690 validate_ppa(
1691 ppa_owner, self.factory.getUniqueString(), private=True))
16881692
1689 def test_two_ppas(self):1693 def test_two_ppas(self):
1690 ppa = self.factory.makeArchive(name='ppa')1694 ppa = self.factory.makeArchive(name='ppa')
1691 self.assertEqual(1695 self.assertEqual(
1692 "You already have a PPA named 'ppa'.", validate_ppa(ppa.owner, 'ppa'))1696 "You already have a PPA named 'ppa'.",
1697 validate_ppa(ppa.owner, 'ppa'))
16931698
1694 def test_two_ppas_with_team(self):1699 def test_two_ppas_with_team(self):
1695 team = self.factory.makeTeam(1700 team = self.factory.makeTeam(
16961701
=== modified file 'lib/lp/soyuz/tests/test_person_createppa.py'
--- lib/lp/soyuz/tests/test_person_createppa.py 2012-05-25 22:14:21 +0000
+++ lib/lp/soyuz/tests/test_person_createppa.py 2012-06-11 09:21:23 +0000
@@ -5,6 +5,7 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8from zope.security.interfaces import Unauthorized
8from lp.registry.errors import PPACreationError9from lp.registry.errors import PPACreationError
9from lp.testing import (10from lp.testing import (
10 celebrity_logged_in,11 celebrity_logged_in,
@@ -21,13 +22,14 @@
2122
22 def test_default_name(self):23 def test_default_name(self):
23 person = self.factory.makePerson()24 person = self.factory.makePerson()
24 ppa = person.createPPA()25 with person_logged_in(person):
26 ppa = person.createPPA()
25 self.assertEqual(ppa.name, 'ppa')27 self.assertEqual(ppa.name, 'ppa')
2628
27 def test_private(self):29 def test_private(self):
28 with celebrity_logged_in('commercial_admin') as person:30 with celebrity_logged_in('commercial_admin') as person:
29 ppa = person.createPPA(private=True)31 ppa = person.createPPA(private=True)
30 self.assertEqual(True, ppa.private)32 self.assertEqual(True, ppa.private)
3133
32 def test_private_without_permission(self):34 def test_private_without_permission(self):
33 person = self.factory.makePerson()35 person = self.factory.makePerson()
@@ -35,7 +37,15 @@
35 self.assertRaises(37 self.assertRaises(
36 PPACreationError, person.createPPA, private=True)38 PPACreationError, person.createPPA, private=True)
3739
40 def test_different_person(self):
41 # You cannot make a PPA on another person.
42 owner = self.factory.makePerson()
43 creator = self.factory.makePerson()
44 with person_logged_in(creator):
45 self.assertRaises(Unauthorized, getattr, owner, 'createPPA')
46
38 def test_suppress_subscription_notifications(self):47 def test_suppress_subscription_notifications(self):
39 person = self.factory.makePerson()48 person = self.factory.makePerson()
40 ppa = person.createPPA(suppress_subscription_notifications=True)49 with person_logged_in(person):
50 ppa = person.createPPA(suppress_subscription_notifications=True)
41 self.assertEqual(True, ppa.suppress_subscription_notifications)51 self.assertEqual(True, ppa.suppress_subscription_notifications)