Merge ~pappacena/launchpad:allow-edit-codeimport into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 9c46f4f8c43e101a3394533006a29095a11ba804
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:allow-edit-codeimport
Merge into: launchpad:master
Diff against target: 316 lines (+105/-23)
6 files modified
lib/lp/code/browser/codeimport.py (+29/-14)
lib/lp/code/browser/tests/test_codeimport.py (+45/-3)
lib/lp/code/configure.zcml (+5/-2)
lib/lp/code/stories/codeimport/xx-create-codeimport.txt (+8/-0)
lib/lp/code/stories/codeimport/xx-edit-codeimport.txt (+1/-2)
lib/lp/security.py (+17/-2)
Reviewer Review Type Date Requested Status
Tom Wardill (community) Approve
Colin Watson (community) Approve
Review via email: mp+384586@code.launchpad.net

Commit message

Allow users to edit their own code imports.

Description of the change

As we can see by the question https://answers.launchpad.net/launchpad/+question/690961, users cannot edit their own code imports. The only way to change it today is deleting the old repository, and editing the project to create a new code import. This is a long and non-intuitive path for users.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

This is basically fine by me, but I'd suggest first checking with William for more details on his IRC comment on Tuesday:

  <wgrant> With old-style imports it required extreme care. But only CVS still uses that system.

... and whether we need to do anything about that here.

review: Approve
33bd03e... by Thiago F. Pappacena

Inlining security check for launchpad.Edit on ICodeImport

9c46f4f... by Thiago F. Pappacena

Adding launchpad.Moderate permission for code imports

By adding this new permission, we move what is currently launchpad.Edit
to the new launchpad.Moderate permission, and allow for launchpad.Edit
only the permission to set the code import URL. This way, users can
change their code import url, but not manage the code import status.

Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

cjwatson, after a quick discussion with wgrant, I have pushed some changes here.

I'll quote wgrant's message on IRC, which is a good explanation of what I've done in the latest commit:

"I think the URL thing is fine, but we should ensure they don't have access to the suspend bits, or the whiteboard if that still exists, etc. It might make sense to move everything that is lp.Edit to lp.Moderate, then move the URL and any other targeted bits to lp.Edit."

Since I have changed quite a bit, it would be interesting to have another review round.

Revision history for this message
Tom Wardill (twom) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/code/browser/codeimport.py b/lib/lp/code/browser/codeimport.py
2index 635347d..8516afe 100644
3--- a/lib/lp/code/browser/codeimport.py
4+++ b/lib/lp/code/browser/codeimport.py
5@@ -1,4 +1,4 @@
6-# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
7+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
8 # GNU Affero General Public License version 3 (see the file LICENSE).
9
10 """Browser views for CodeImports."""
11@@ -88,7 +88,6 @@ from lp.code.interfaces.gitnamespace import (
12 IGitNamespacePolicy,
13 )
14 from lp.registry.interfaces.product import IProduct
15-from lp.registry.interfaces.role import IPersonRoles
16 from lp.services.beautifulsoup import BeautifulSoup
17 from lp.services.fields import URIField
18 from lp.services.propertycache import cachedproperty
19@@ -98,6 +97,7 @@ from lp.services.webapp import (
20 Navigation,
21 stepto,
22 )
23+from lp.services.webapp.authorization import check_permission
24 from lp.services.webapp.batching import BatchNavigator
25 from lp.services.webapp.breadcrumb import Breadcrumb
26 from lp.services.webapp.escaping import structured
27@@ -189,10 +189,14 @@ class CodeImportBaseView(LaunchpadFormView):
28 custom_widget_url = CustomWidgetFactory(URIWidget, displayWidth=50)
29
30 @cachedproperty
31- def _super_user(self):
32- """Is the user an admin or member of vcs-imports?"""
33- role = IPersonRoles(self.user)
34- return role.in_admin or role.in_vcs_imports
35+ def _is_edit_user(self):
36+ """Can this user edit specific fields?"""
37+ return check_permission("launchpad.Edit", self.code_import)
38+
39+ @cachedproperty
40+ def _is_moderator_user(self):
41+ """Is a moderator of code imports?"""
42+ return check_permission("launchpad.Moderate", self.code_import)
43
44 def showOptionalMarker(self, field_name):
45 """Don't show the optional marker for rcs locations."""
46@@ -555,12 +559,22 @@ def _makeEditAction(label, status, text):
47
48 def success(self, action, data):
49 """Make the requested status change."""
50- if status is not None:
51- data['review_status'] = status
52- event = self.code_import.updateFromData(data, self.user)
53- if event is not None:
54- self.request.response.addNotification(
55- 'The code import has been ' + text + '.')
56+ if self._is_moderator_user:
57+ # Moderators can change everything in code import, including its
58+ # status.
59+ if status is not None:
60+ data['review_status'] = status
61+ event = self.code_import.updateFromData(data, self.user)
62+ if event is not None:
63+ self.request.response.addNotification(
64+ 'The code import has been ' + text + '.')
65+ elif self._is_edit_user and "url" in data:
66+ # Edit users can only change URL
67+ new_url = data["url"]
68+ if self.code_import.url != new_url:
69+ self.code_import.url = new_url
70+ self.request.response.addNotification(
71+ 'The code import URL has been updated.')
72 else:
73 self.request.response.addNotification('No changes made.')
74 name = label.lower().replace(' ', '_')
75@@ -600,7 +614,7 @@ class CodeImportEditView(CodeImportBaseView):
76 self.code_import = self.context.code_import
77 if self.code_import is None:
78 raise NotFoundError
79- if not self._super_user:
80+ if not self._is_edit_user and not self._is_moderator_user:
81 raise Unauthorized
82 # The next and cancel location is the target details page.
83 self.cancel_url = self.next_url = canonical_url(self.context)
84@@ -629,7 +643,8 @@ class CodeImportEditView(CodeImportBaseView):
85
86 def _showButtonForStatus(self, status):
87 """If the status is different, and the user is super, show button."""
88- return self._super_user and self.code_import.review_status != status
89+ return (self._is_moderator_user and
90+ self.code_import.review_status != status)
91
92 actions = form.Actions(
93 _makeEditAction(_('Update'), None, 'updated'),
94diff --git a/lib/lp/code/browser/tests/test_codeimport.py b/lib/lp/code/browser/tests/test_codeimport.py
95index 97f1799..68b8b2e 100644
96--- a/lib/lp/code/browser/tests/test_codeimport.py
97+++ b/lib/lp/code/browser/tests/test_codeimport.py
98@@ -1,4 +1,4 @@
99-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
100+# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
101 # GNU Affero General Public License version 3 (see the file LICENSE).
102
103 """Tests for the code import browser code."""
104@@ -13,12 +13,14 @@ from testtools.matchers import StartsWith
105 from zope.security.interfaces import Unauthorized
106
107 from lp.code.enums import (
108+ CodeImportReviewStatus,
109 RevisionControlSystems,
110 TargetRevisionControlSystems,
111 )
112 from lp.code.tests.helpers import GitHostingFixture
113 from lp.services.webapp import canonical_url
114 from lp.testing import (
115+ admin_logged_in,
116 person_logged_in,
117 TestCaseWithFactory,
118 )
119@@ -72,10 +74,50 @@ class TestImportDetails(TestCaseWithFactory):
120 code_import, 'git-import-details',
121 'This repository is an import of the Git repository')
122
123- def test_branch_owner_of_import_forbidden(self):
124+ def test_other_users_are_forbidden_to_change_codeimport(self):
125 # Unauthorized users are forbidden to edit an import.
126 cimport = self.factory.makeCodeImport()
127- with person_logged_in(cimport.branch.owner):
128+ another_person = self.factory.makePerson()
129+ with person_logged_in(another_person):
130 self.assertRaises(
131 Unauthorized, create_initialized_view, cimport.branch,
132 '+edit-import')
133+
134+ def test_branch_owner_of_import_can_edit_it(self):
135+ # Owners are allowed to edit code import.
136+ cimport = self.factory.makeCodeImport()
137+ with person_logged_in(cimport.branch.owner):
138+ view = create_initialized_view(
139+ cimport.branch, '+edit-import', form={
140+ "field.actions.update": "update",
141+ "field.url": "http://foo.test"
142+ })
143+ self.assertEqual([], view.errors)
144+ self.assertEqual('http://foo.test', cimport.url)
145+
146+ def test_branch_owner_of_import_cannot_change_status(self):
147+ # Owners are allowed to edit code import.
148+ cimport = self.factory.makeCodeImport()
149+ original_url = cimport.url
150+ with person_logged_in(cimport.branch.owner):
151+ view = create_initialized_view(
152+ cimport.branch, '+edit-import', form={
153+ "field.actions.suspend": "Suspend",
154+ "field.url": "http://foo.test"
155+ })
156+ self.assertEqual([], view.errors)
157+ self.assertEqual(original_url, cimport.url)
158+
159+ def test_admin_can_change_code_import_status(self):
160+ # Owners are allowed to edit code import.
161+ cimport = self.factory.makeCodeImport()
162+ with admin_logged_in():
163+ view = create_initialized_view(
164+ cimport.branch, '+edit-import', form={
165+ "field.actions.suspend": "Suspend",
166+ "field.url": "http://foo.test"
167+ })
168+ self.assertEqual([], view.errors)
169+ self.assertEqual("http://foo.test", cimport.url)
170+ self.assertEqual(
171+ CodeImportReviewStatus.SUSPENDED, cimport.review_status)
172diff --git a/lib/lp/code/configure.zcml b/lib/lp/code/configure.zcml
173index ce409aa..09d5011 100644
174--- a/lib/lp/code/configure.zcml
175+++ b/lib/lp/code/configure.zcml
176@@ -1,4 +1,4 @@
177-<!-- Copyright 2009-2019 Canonical Ltd. This software is licensed under the
178+<!-- Copyright 2009-2020 Canonical Ltd. This software is licensed under the
179 GNU Affero General Public License version 3 (see the file LICENSE).
180 -->
181
182@@ -657,11 +657,14 @@
183 consecutive_failure_count
184 getImportDetailsForDisplay"/>
185 <require
186+ permission="launchpad.Edit"
187+ set_attributes="url"/>
188+ <require
189 permission="launchpad.AnyPerson"
190 attributes="tryFailingImportAgain
191 requestImport"/>
192 <require
193- permission="launchpad.Edit"
194+ permission="launchpad.Moderate"
195 attributes="updateFromData"/>
196
197 <!-- ICodeImport has no set_schema, because all modifications should be
198diff --git a/lib/lp/code/stories/codeimport/xx-create-codeimport.txt b/lib/lp/code/stories/codeimport/xx-create-codeimport.txt
199index c8cb7a1..99dda4a 100644
200--- a/lib/lp/code/stories/codeimport/xx-create-codeimport.txt
201+++ b/lib/lp/code/stories/codeimport/xx-create-codeimport.txt
202@@ -83,6 +83,7 @@ When the user clicks continue, the import branch is created
203 at http://bzr.example.com/firefox/trunk.
204 The next import is scheduled to run
205 as soon as possible.
206+ Edit import source or review import
207 >>> browser.getLink("http://bzr.example.com/firefox/trunk")
208 <Link text='http://bzr.example.com/firefox/trunk'
209 url='http://bzr.example.com/firefox/trunk'>
210@@ -102,6 +103,7 @@ URL.
211 at http://user:password@bzr.example.com/firefox/trunk.
212 The next import is scheduled to run
213 as soon as possible.
214+ Edit import source or review import
215
216 Specifying a Launchpad URL results in an error.
217
218@@ -130,6 +132,7 @@ But a Launchpad Git URL is OK.
219 This branch is an import of the HEAD branch of the Git repository at
220 git://git.launchpad.net/firefox.git.
221 The next import is scheduled to run as soon as possible.
222+ Edit import source or review import
223
224 Requesting a Subversion import
225 ==============================
226@@ -153,6 +156,7 @@ When the user clicks continue, the import branch is created
227 from http://svn.example.com/firefox/trunk.
228 The next import is scheduled to run
229 as soon as possible.
230+ Edit import source or review import
231 >>> browser.getLink("http://svn.example.com/firefox/trunk")
232 <Link text='http://svn.example.com/firefox/trunk'
233 url='http://svn.example.com/firefox/trunk'>
234@@ -182,6 +186,7 @@ URL.
235 from http://user:password@svn.example.com/firefox/trunk.
236 The next import is scheduled to run
237 as soon as possible.
238+ Edit import source or review import
239
240
241 Requesting a Git-to-Bazaar import
242@@ -205,6 +210,7 @@ When the user clicks continue, the approved import branch is created.
243 This branch is an import of the HEAD branch of the Git repository at
244 git://example.com/firefox.git.
245 The next import is scheduled to run as soon as possible.
246+ Edit import source or review import
247
248
249 Requesting a Git-to-Git import
250@@ -233,6 +239,7 @@ When the user clicks continue, the approved import repository is created.
251 This repository is an import of the Git repository at
252 git://example.com/firefox.git.
253 The next import is scheduled to run as soon as possible.
254+ Edit import source or review import
255
256
257 Requesting a CVS import
258@@ -256,6 +263,7 @@ to identify the CVS branch. A project and branch name are also required.
259 :pserver:anonymous@cvs.example.com:/mozilla/cvs.
260 The next import is scheduled to run
261 as soon as possible.
262+ Edit import source or review import
263
264 Requesting a CVS import with invalid information
265 ================================================
266diff --git a/lib/lp/code/stories/codeimport/xx-edit-codeimport.txt b/lib/lp/code/stories/codeimport/xx-edit-codeimport.txt
267index 2f65697..c7aebb5 100644
268--- a/lib/lp/code/stories/codeimport/xx-edit-codeimport.txt
269+++ b/lib/lp/code/stories/codeimport/xx-edit-codeimport.txt
270@@ -76,8 +76,7 @@ and a 404 if the branch doesn't have an import.
271
272 >>> import_browser.open(svn_import_location)
273 >>> import_browser.getLink('Edit import source or review import')
274- Traceback (most recent call last):
275- LinkNotFoundError
276+ <Link text='Edit import source or review import' url='.../+edit-import'>
277
278
279 == Import details for a import that has been imported successfully ==
280diff --git a/lib/lp/security.py b/lib/lp/security.py
281index f7d7bf8..005c52f 100644
282--- a/lib/lp/security.py
283+++ b/lib/lp/security.py
284@@ -1574,15 +1574,30 @@ class OnlyVcsImportsAndAdmins(AuthorizationBase):
285 return user.in_admin or user.in_vcs_imports
286
287
288-class EditCodeImport(OnlyVcsImportsAndAdmins):
289+class EditCodeImport(AuthorizationBase):
290 """Control who can edit the object view of a CodeImport.
291
292 Currently, we restrict the visibility of the new code import
293- system to members of ~vcs-imports and Launchpad admins.
294+ system to owners, members of ~vcs-imports and Launchpad admins.
295 """
296 permission = 'launchpad.Edit'
297 usedfor = ICodeImport
298
299+ def checkAuthenticated(self, user):
300+ return (user.inTeam(self.obj.owner) or
301+ user.in_admin or
302+ user.in_vcs_imports)
303+
304+
305+class ModerateCodeImport(OnlyVcsImportsAndAdmins):
306+ """Control who can moderate a CodeImport.
307+
308+ Currently, we restrict the visibility of code import moderation
309+ system to members of ~vcs-imports and Launchpad admins.
310+ """
311+ permission = 'launchpad.Moderate'
312+ usedfor = ICodeImport
313+
314
315 class SeeCodeImportJobSet(OnlyVcsImportsAndAdmins):
316 """Control who can see the CodeImportJobSet utility.