Merge lp:~sinzui/launchpad/project-branch-permissions into lp:launchpad

Proposed by Curtis Hovey on 2012-08-17
Status: Merged
Approved by: Curtis Hovey on 2012-08-20
Approved revision: no longer in the source branch.
Merged at revision: 15831
Proposed branch: lp:~sinzui/launchpad/project-branch-permissions
Merge into: lp:launchpad
Diff against target: 219 lines (+103/-33)
4 files modified
lib/lp/code/configure.zcml (+5/-0)
lib/lp/code/interfaces/branch.py (+42/-33)
lib/lp/code/model/tests/test_branch.py (+43/-0)
lib/lp/security.py (+13/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/project-branch-permissions
Reviewer Review Type Date Requested Status
Ian Booth (community) 2012-08-17 Approve on 2012-08-20
Review via email: mp+120219@code.launchpad.net

Commit Message

Allow project maintainers to change branch information to prevent disclosure.

Description of the Change

Project maintainers cannot update branches owned by contributors to
secure the project.

--------------------------------------------------------------------

RULES

    Pre-implementation: jcsackett
    * Several attributes on IBranchEditableAttributes and IBranchEdit
      can be moved to IBranchModerate.
    * Add a security checker for launchpad.Moderate on IBranchModerate
      that permits the anyone with launchpad.Edit + the product.owner
      and commercial admins permission to change the data.

QA

    * Visit https://code.qastaging.launchpad.net/lp-dev-utils
    * Verify the branches owned by former Canonical employees are Private
    * Run the test api script
    * Verify all the branches are Private branches are now Proprietary

    {{{
    import logging
    from launchpadlib.launchpad import Launchpad

    logging.basicConfig()
    log = logging.getLogger(SCRIPT_NAME)
    log.setLevel(logging.INFO)

    lp = Launchpad.login_with(
        'testing', service_root='https://api.qastaging.launchpad.net',
        version='devel')
    project = lp.projects['lp-dev-utils']
    statuses = [
        'Experimental', 'Development', 'Mature', 'Merged', 'Abandoned']
    for branch in project.getBranches(status=statuses):
        if branch.information_type == 'Private':
            try:
                log.info(
                    'Changing %s information type to Proprietary',
                    branch.unique_name)
                branch.transitionToInformationType(
                    information_type='Proprietary')
            except:
                log.info(
                    'Failed to change %s information type.',
                    branch.unique_name)
    }}

LINT

    lib/lp/security.py
    lib/lp/code/configure.zcml
    lib/lp/code/interfaces/branch.py
    lib/lp/code/model/tests/test_branch.py

TEST

    ./bin/test -vvc -t BranchModerateTestCase lp.code.model.tests.test_branch

IMPLEMENTATION

Added a security checker for launchpad.Moderate on branches. Anyone with
launchpad.Edit plus the product.owner and commercial admins have
permission.
    lib/lp/security.py

Created IBranchModerateAttributes and moved name, description, reviewer,
and lifecycle_status from IBranchEditableAttributes. Created
IBranchModerate and moved transitionToInformationType() from
IBranchEditable.
    lib/lp/code/configure.zcml
    lib/lp/code/interfaces/branch.py
    lib/lp/code/model/tests/test_branch.py

To post a comment you must log in.
Ian Booth (wallyworld) wrote :

Looks great. A few quibbles:

Small typos:

35 + """IBranch attributes that can be edited by a more than one community."""
56 + """IBranch methods that can be edited by a more than one community."""

I think this was unintentional:

195 + """Tests for `Branch.commitsFornDays`."""

With the BranchModerateTestCase tests, we check access is granted for product owner and commercial admin. Could you please add in checks for branch owner to be complete.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/configure.zcml'
2--- lib/lp/code/configure.zcml 2012-04-27 16:22:05 +0000
3+++ lib/lp/code/configure.zcml 2012-08-20 14:05:35 +0000
4@@ -446,11 +446,16 @@
5 permission="launchpad.View"
6 interface="lp.app.interfaces.launchpad.IPrivacy
7 lp.code.interfaces.branch.IBranchAnyone
8+ lp.code.interfaces.branch.IBranchModerateAttributes
9 lp.code.interfaces.branch.IBranchEditableAttributes
10 lp.code.interfaces.branch.IBranchPublic
11 lp.code.interfaces.branch.IBranchView"
12 attributes="merge_queue merge_queue_config"/>
13 <require
14+ permission="launchpad.Moderate"
15+ interface="lp.code.interfaces.branch.IBranchModerate"
16+ set_schema="lp.code.interfaces.branch.IBranchModerateAttributes" />
17+ <require
18 permission="launchpad.Edit"
19 interface="lp.code.interfaces.branch.IBranchEdit"
20 set_schema="lp.code.interfaces.branch.IBranchEditableAttributes"
21
22=== modified file 'lib/lp/code/interfaces/branch.py'
23--- lib/lp/code/interfaces/branch.py 2012-08-16 20:13:14 +0000
24+++ lib/lp/code/interfaces/branch.py 2012-08-20 14:05:35 +0000
25@@ -981,11 +981,8 @@
26 """
27
28
29-class IBranchEditableAttributes(Interface):
30- """IBranch attributes that can be edited.
31-
32- These attributes need launchpad.View to see, and launchpad.Edit to change.
33- """
34+class IBranchModerateAttributes(Interface):
35+ """IBranch attributes that can be edited by more than one community."""
36
37 name = exported(
38 TextLine(
39@@ -1004,6 +1001,44 @@
40 "that is responsible for reviewing proposals and "
41 "merging into this branch.")))
42
43+ description = exported(
44+ Text(
45+ title=_('Description'), required=False,
46+ description=_(
47+ 'A short description of the changes in this branch.')))
48+
49+ lifecycle_status = exported(
50+ Choice(
51+ title=_('Status'), vocabulary=BranchLifecycleStatus,
52+ default=BranchLifecycleStatus.DEVELOPMENT))
53+
54+
55+class IBranchModerate(Interface):
56+ """IBranch methods that can be edited by more than one community."""
57+
58+ @operation_parameters(
59+ information_type=copy_field(IBranchPublic['information_type']),
60+ )
61+ @call_with(who=REQUEST_USER, verify_policy=True)
62+ @export_write_operation()
63+ @operation_for_version("devel")
64+ def transitionToInformationType(information_type, who,
65+ verify_policy=True):
66+ """Set the information type for this branch.
67+
68+ :param information_type: The `InformationType` to transition to.
69+ :param who: The `IPerson` who is making the change.
70+ :param verify_policy: Check if the new information type complies
71+ with the `IBranchNamespacePolicy`.
72+ """
73+
74+
75+class IBranchEditableAttributes(Interface):
76+ """IBranch attributes that can be edited.
77+
78+ These attributes need launchpad.View to see, and launchpad.Edit to change.
79+ """
80+
81 url = exported(
82 BranchURIField(
83 title=_('Branch URL'), required=False,
84@@ -1027,17 +1062,6 @@
85 title=_("Branch Type"), required=True, readonly=True,
86 vocabulary=BranchType))
87
88- description = exported(
89- Text(
90- title=_('Description'), required=False,
91- description=_(
92- 'A short description of the changes in this branch.')))
93-
94- lifecycle_status = exported(
95- Choice(
96- title=_('Status'), vocabulary=BranchLifecycleStatus,
97- default=BranchLifecycleStatus.DEVELOPMENT))
98-
99 branch_format = exported(
100 Choice(
101 title=_("Branch Format"),
102@@ -1132,22 +1156,6 @@
103 :raise: CannotDeleteBranch if the branch cannot be deleted.
104 """
105
106- @operation_parameters(
107- information_type=copy_field(IBranchPublic['information_type']),
108- )
109- @call_with(who=REQUEST_USER, verify_policy=True)
110- @export_write_operation()
111- @operation_for_version("devel")
112- def transitionToInformationType(information_type, who,
113- verify_policy=True):
114- """Set the information type for this branch.
115-
116- :param information_type: The `InformationType` to transition to.
117- :param who: The `IPerson` who is making the change.
118- :param verify_policy: Check if the new information type complies
119- with the `IBranchNamespacePolicy`.
120- """
121-
122
123 class IMergeQueueable(Interface):
124 """An interface for branches that can be queued."""
125@@ -1197,7 +1205,8 @@
126
127
128 class IBranch(IBranchPublic, IBranchView, IBranchEdit,
129- IBranchEditableAttributes, IBranchAnyone, IMergeQueueable):
130+ IBranchEditableAttributes, IBranchModerate,
131+ IBranchModerateAttributes, IBranchAnyone, IMergeQueueable):
132 """A Bazaar branch."""
133
134 # Mark branches as exported entries for the Launchpad API.
135
136=== modified file 'lib/lp/code/model/tests/test_branch.py'
137--- lib/lp/code/model/tests/test_branch.py 2012-08-17 05:05:37 +0000
138+++ lib/lp/code/model/tests/test_branch.py 2012-08-20 14:05:35 +0000
139@@ -138,6 +138,7 @@
140 )
141 from lp.services.osutils import override_environ
142 from lp.services.propertycache import clear_property_cache
143+from lp.services.webapp.authorization import check_permission
144 from lp.services.webapp.interfaces import IOpenLaunchBag
145 from lp.testing import (
146 admin_logged_in,
147@@ -2582,6 +2583,48 @@
148 InformationType.PRIVATESECURITY, branch.information_type)
149
150
151+class BranchModerateTestCase(TestCaseWithFactory):
152+ """Test that product owners and commercial admins can moderate branches."""
153+
154+ layer = DatabaseFunctionalLayer
155+
156+ def test_moderate_permission(self):
157+ # Test the ModerateBranch security checker.
158+ branch = self.factory.makeProductBranch()
159+ with person_logged_in(branch.product.owner):
160+ self.assertTrue(
161+ check_permission('launchpad.Moderate', branch))
162+ with celebrity_logged_in('commercial_admin'):
163+ self.assertTrue(
164+ check_permission('launchpad.Moderate', branch))
165+
166+ def test_methods_smoketest(self):
167+ # Users with launchpad.Moderate can call transitionToInformationType.
168+ branch = self.factory.makeProductBranch()
169+ with celebrity_logged_in('commercial_admin') as admin:
170+ branch.product.setBranchSharingPolicy(
171+ BranchSharingPolicy.PUBLIC, admin)
172+ with person_logged_in(branch.product.owner):
173+ branch.transitionToInformationType(
174+ InformationType.PRIVATESECURITY, branch.product.owner)
175+ self.assertEqual(
176+ InformationType.PRIVATESECURITY, branch.information_type)
177+
178+ def test_attribute_smoketest(self):
179+ # Users with launchpad.Moderate can set attrs.
180+ branch = self.factory.makeProductBranch()
181+ with person_logged_in(branch.product.owner):
182+ branch.name = 'not-secret'
183+ branch.description = 'redacted'
184+ branch.reviewer = branch.product.owner
185+ branch.lifecycle_status = BranchLifecycleStatus.EXPERIMENTAL
186+ self.assertEqual('not-secret', branch.name)
187+ self.assertEqual('redacted', branch.description)
188+ self.assertEqual(branch.product.owner, branch.reviewer)
189+ self.assertEqual(
190+ BranchLifecycleStatus.EXPERIMENTAL, branch.lifecycle_status)
191+
192+
193 class TestBranchCommitsForDays(TestCaseWithFactory):
194 """Tests for `Branch.commitsForDays`."""
195
196
197=== modified file 'lib/lp/security.py'
198--- lib/lp/security.py 2012-08-14 23:27:07 +0000
199+++ lib/lp/security.py 2012-08-20 14:05:35 +0000
200@@ -2095,6 +2095,19 @@
201 and user.inTeam(code_import.registrant)))
202
203
204+class ModerateBranch(EditBranch):
205+ """The owners, product owners, and admins can moderate branches."""
206+ permission = 'launchpad.Moderate'
207+
208+ def checkAuthenticated(self, user):
209+ if super(ModerateBranch, self).checkAuthenticated(user):
210+ return True
211+ branch = self.obj
212+ if branch.product is not None and user.inTeam(branch.product.owner):
213+ return True
214+ return user.in_commercial_admin
215+
216+
217 def can_upload_linked_package(person_role, branch):
218 """True if person may upload the package linked to `branch`."""
219 # No associated `ISuiteSourcePackage` data -> not an official branch.