Merge ~pappacena/launchpad:snap-pillar-ui into launchpad:master
- Git
- lp:~pappacena/launchpad
- snap-pillar-ui
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Thiago F. Pappacena | ||||
Approved revision: | 92b05132afbd563bf786c3204eacd2a96b10ab5b | ||||
Merge reported by: | Otto Co-Pilot | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~pappacena/launchpad:snap-pillar-ui | ||||
Merge into: | launchpad:master | ||||
Prerequisite: | ~pappacena/launchpad:snap-pillar | ||||
Diff against target: |
751 lines (+275/-68) 7 files modified
lib/lp/registry/personmerge.py (+1/-1) lib/lp/snappy/browser/snap.py (+69/-15) lib/lp/snappy/browser/tests/test_snap.py (+84/-3) lib/lp/snappy/interfaces/snap.py (+16/-7) lib/lp/snappy/model/snap.py (+83/-31) lib/lp/snappy/tests/test_snap.py (+12/-6) lib/lp/testing/factory.py (+10/-5) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+397529@code.launchpad.net |
Commit message
UI to associate a project pillar to Snaps on +admin view, and replacing "private" Snap flag with "information_type".
Description of the change
To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Needs Fixing
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
Revision history for this message
Thiago F. Pappacena (pappacena) : | # |
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/registry/personmerge.py b/lib/lp/registry/personmerge.py |
2 | index 50e0f91..949e8d9 100644 |
3 | --- a/lib/lp/registry/personmerge.py |
4 | +++ b/lib/lp/registry/personmerge.py |
5 | @@ -1,4 +1,4 @@ |
6 | -# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
7 | +# Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
8 | # GNU Affero General Public License version 3 (see the file LICENSE). |
9 | |
10 | """Person/team merger implementation.""" |
11 | diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py |
12 | index 3b06a89..9759e66 100644 |
13 | --- a/lib/lp/snappy/browser/snap.py |
14 | +++ b/lib/lp/snappy/browser/snap.py |
15 | @@ -1,4 +1,4 @@ |
16 | -# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
17 | +# Copyright 2015-2021 Canonical Ltd. This software is licensed under the |
18 | # GNU Affero General Public License version 3 (see the file LICENSE). |
19 | |
20 | """Snap views.""" |
21 | @@ -48,10 +48,12 @@ from lp.app.browser.tales import format_link |
22 | from lp.app.enums import PRIVATE_INFORMATION_TYPES |
23 | from lp.app.interfaces.informationtype import IInformationType |
24 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
25 | +from lp.app.vocabularies import InformationTypeVocabulary |
26 | from lp.app.widgets.itemswidgets import ( |
27 | LabeledMultiCheckBoxWidget, |
28 | LaunchpadDropdownWidget, |
29 | LaunchpadRadioWidget, |
30 | + LaunchpadRadioWidgetWithDescription, |
31 | ) |
32 | from lp.buildmaster.interfaces.processor import IProcessorSet |
33 | from lp.code.browser.widgets.gitref import GitRefWidget |
34 | @@ -343,7 +345,8 @@ class ISnapEditSchema(Interface): |
35 | use_template(ISnap, include=[ |
36 | 'owner', |
37 | 'name', |
38 | - 'private', |
39 | + 'information_type', |
40 | + 'project', |
41 | 'require_virtualized', |
42 | 'allow_internet', |
43 | 'build_source_tarball', |
44 | @@ -351,6 +354,7 @@ class ISnapEditSchema(Interface): |
45 | 'auto_build_channels', |
46 | 'store_upload', |
47 | ]) |
48 | + |
49 | store_distro_series = Choice( |
50 | vocabulary='SnappyDistroSeries', required=True, |
51 | title='Series') |
52 | @@ -520,8 +524,12 @@ class SnapAddView( |
53 | kwargs = {'git_ref': self.context} |
54 | else: |
55 | kwargs = {'branch': self.context} |
56 | - private = not getUtility( |
57 | - ISnapSet).isValidPrivacy(False, data['owner'], **kwargs) |
58 | + # XXX pappacena 2021-03-01: We should consider the pillar's branch |
59 | + # sharing policy when setting the information_type. |
60 | + # Once we move the information_type and pillar edition from the |
61 | + # admin view to the create/edit views, we should change this. |
62 | + information_type = getUtility(ISnapSet).getSnapSuggestedPrivacy( |
63 | + data['owner'], **kwargs) |
64 | if not data.get('auto_build', False): |
65 | data['auto_build_archive'] = None |
66 | data['auto_build_pocket'] = None |
67 | @@ -532,7 +540,8 @@ class SnapAddView( |
68 | auto_build_archive=data['auto_build_archive'], |
69 | auto_build_pocket=data['auto_build_pocket'], |
70 | auto_build_channels=data['auto_build_channels'], |
71 | - processors=data['processors'], private=private, |
72 | + information_type=information_type, |
73 | + processors=data['processors'], |
74 | build_source_tarball=data['build_source_tarball'], |
75 | store_upload=data['store_upload'], |
76 | store_series=data['store_distro_series'].snappy_series, |
77 | @@ -559,6 +568,16 @@ class BaseSnapEditView(LaunchpadEditFormView, SnapAuthorizeMixin): |
78 | |
79 | schema = ISnapEditSchema |
80 | |
81 | + def getInformationTypesToShow(self): |
82 | + """Get the information types to display on the edit form. |
83 | + |
84 | + We display a customised set of information types: anything allowed |
85 | + by the repository's model, plus the current type. |
86 | + """ |
87 | + allowed_types = set(self.context.getAllowedInformationTypes(self.user)) |
88 | + allowed_types.add(self.context.information_type) |
89 | + return allowed_types |
90 | + |
91 | @property |
92 | def cancel_url(self): |
93 | return canonical_url(self.context) |
94 | @@ -612,25 +631,36 @@ class BaseSnapEditView(LaunchpadEditFormView, SnapAuthorizeMixin): |
95 | |
96 | def validate(self, data): |
97 | super(BaseSnapEditView, self).validate(data) |
98 | - if data.get('private', self.context.private) is False: |
99 | - if 'private' in data or 'owner' in data: |
100 | + info_type = data.get('information_type', self.context.information_type) |
101 | + editing_info_type = 'information_type' in data |
102 | + private = info_type in PRIVATE_INFORMATION_TYPES |
103 | + if private is False: |
104 | + # These are the requirements for public snaps. |
105 | + if 'information_type' in data or 'owner' in data: |
106 | owner = data.get('owner', self.context.owner) |
107 | if owner is not None and owner.private: |
108 | self.setFieldError( |
109 | - 'private' if 'private' in data else 'owner', |
110 | + 'information_type' if editing_info_type else 'owner', |
111 | 'A public snap cannot have a private owner.') |
112 | - if 'private' in data or 'branch' in data: |
113 | + if 'information_type' in data or 'branch' in data: |
114 | branch = data.get('branch', self.context.branch) |
115 | if branch is not None and branch.private: |
116 | self.setFieldError( |
117 | - 'private' if 'private' in data else 'branch', |
118 | + 'information_type' if editing_info_type else 'branch', |
119 | 'A public snap cannot have a private branch.') |
120 | - if 'private' in data or 'git_ref' in data: |
121 | + if 'information_type' in data or 'git_ref' in data: |
122 | ref = data.get('git_ref', self.context.git_ref) |
123 | if ref is not None and ref.private: |
124 | self.setFieldError( |
125 | - 'private' if 'private' in data else 'git_ref', |
126 | + 'information_type' if editing_info_type else 'git_ref', |
127 | 'A public snap cannot have a private repository.') |
128 | + else: |
129 | + # Requirements for private snaps. |
130 | + project = data.get('project', self.context.project) |
131 | + if project is None: |
132 | + msg = ('Private snap recipes must be associated ' |
133 | + 'with a project.') |
134 | + self.setFieldError('project', msg) |
135 | |
136 | def _needStoreReauth(self, data): |
137 | """Does this change require reauthorizing to the store?""" |
138 | @@ -696,16 +726,40 @@ class SnapAdminView(BaseSnapEditView): |
139 | |
140 | page_title = 'Administer' |
141 | |
142 | - field_names = ['private', 'require_virtualized', 'allow_internet'] |
143 | + # XXX pappacena 2021-02-19: Once we have the whole privacy work in |
144 | + # place, we should move "project" and "information_type" from +admin |
145 | + # page to +edit, to allow common users to edit this. |
146 | + field_names = [ |
147 | + 'project', 'information_type', 'require_virtualized', 'allow_internet'] |
148 | + |
149 | + # See `setUpWidgets` method. |
150 | + custom_widget_information_type = CustomWidgetFactory( |
151 | + LaunchpadRadioWidgetWithDescription, |
152 | + vocabulary=InformationTypeVocabulary(types=[])) |
153 | + |
154 | + @property |
155 | + def initial_values(self): |
156 | + """Set initial values for the form.""" |
157 | + # XXX pappacena 2021-02-12: Until we back fill information_type |
158 | + # database column, it will be NULL, but snap.information_type |
159 | + # property has a fallback to check "private" property. This should |
160 | + # be removed once we back fill snap.information_type. |
161 | + return {'information_type': self.context.information_type} |
162 | + |
163 | + def setUpWidgets(self): |
164 | + super(SnapAdminView, self).setUpWidgets() |
165 | + info_type_widget = self.widgets['information_type'] |
166 | + info_type_widget.vocabulary = InformationTypeVocabulary( |
167 | + types=self.getInformationTypesToShow()) |
168 | |
169 | def validate(self, data): |
170 | super(SnapAdminView, self).validate(data) |
171 | # BaseSnapEditView.validate checks the rules for 'private' in |
172 | # combination with other attributes. |
173 | - if data.get('private', None) is True: |
174 | + if data.get('information_type', None) in PRIVATE_INFORMATION_TYPES: |
175 | if not getFeatureFlag(SNAP_PRIVATE_FEATURE_FLAG): |
176 | self.setFieldError( |
177 | - 'private', |
178 | + 'information_type', |
179 | 'You do not have permission to create private snaps.') |
180 | |
181 | |
182 | diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py |
183 | index 3e48162..5f33900 100644 |
184 | --- a/lib/lp/snappy/browser/tests/test_snap.py |
185 | +++ b/lib/lp/snappy/browser/tests/test_snap.py |
186 | @@ -2,7 +2,7 @@ |
187 | # NOTE: The first line above must stay first; do not move the copyright |
188 | # notice to the top. See http://www.python.org/dev/peps/pep-0263/. |
189 | # |
190 | -# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
191 | +# Copyright 2015-2021 Canonical Ltd. This software is licensed under the |
192 | # GNU Affero General Public License version 3 (see the file LICENSE). |
193 | |
194 | """Test snap package views.""" |
195 | @@ -85,6 +85,7 @@ from lp.testing import ( |
196 | admin_logged_in, |
197 | BrowserTestCase, |
198 | login, |
199 | + login_admin, |
200 | login_person, |
201 | person_logged_in, |
202 | TestCaseWithFactory, |
203 | @@ -400,6 +401,64 @@ class TestSnapAddView(BaseTestSnapView): |
204 | extract_text(find_tag_by_id(browser.contents, "privacy")) |
205 | ) |
206 | |
207 | + def test_create_new_snap_private_team_with_private_branch(self): |
208 | + # Creating snaps from private branch should make the snap follow its |
209 | + # privacy setting. |
210 | + self.useFixture(BranchHostingFixture(blob=b"")) |
211 | + login_person(self.person) |
212 | + private_team = self.factory.makeTeam( |
213 | + name='super-private', owner=self.person, |
214 | + visibility=PersonVisibility.PRIVATE) |
215 | + branch = self.factory.makeAnyBranch( |
216 | + owner=self.person, registrant=self.person, |
217 | + information_type=InformationType.PRIVATESECURITY) |
218 | + |
219 | + browser = self.getViewBrowser( |
220 | + branch, view_name="+new-snap", user=self.person) |
221 | + browser.getControl(name="field.name").value = "private-snap" |
222 | + browser.getControl("Owner").value = ['super-private'] |
223 | + browser.getControl("Create snap package").click() |
224 | + |
225 | + content = find_main_content(browser.contents) |
226 | + self.assertEqual("private-snap", extract_text(content.h1)) |
227 | + self.assertEqual( |
228 | + 'This snap contains Private information', |
229 | + extract_text(find_tag_by_id(browser.contents, "privacy")) |
230 | + ) |
231 | + login_admin() |
232 | + snap = getUtility(ISnapSet).getByName(private_team, 'private-snap') |
233 | + self.assertEqual( |
234 | + InformationType.PRIVATESECURITY, snap.information_type) |
235 | + |
236 | + def test_create_new_snap_private_team_with_private_git_repo(self): |
237 | + # Creating snaps from private repos should make the snap follow its |
238 | + # privacy setting. |
239 | + self.useFixture(BranchHostingFixture(blob=b"")) |
240 | + login_person(self.person) |
241 | + private_team = self.factory.makeTeam( |
242 | + name='super-private', owner=self.person, |
243 | + visibility=PersonVisibility.PRIVATE) |
244 | + [git_ref] = self.factory.makeGitRefs( |
245 | + owner=self.person, registrant=self.person, |
246 | + information_type=InformationType.PRIVATESECURITY) |
247 | + |
248 | + browser = self.getViewBrowser( |
249 | + git_ref, view_name="+new-snap", user=self.person) |
250 | + browser.getControl(name="field.name").value = "private-snap" |
251 | + browser.getControl("Owner").value = ['super-private'] |
252 | + browser.getControl("Create snap package").click() |
253 | + |
254 | + content = find_main_content(browser.contents) |
255 | + self.assertEqual("private-snap", extract_text(content.h1)) |
256 | + self.assertEqual( |
257 | + 'This snap contains Private information', |
258 | + extract_text(find_tag_by_id(browser.contents, "privacy")) |
259 | + ) |
260 | + login_admin() |
261 | + snap = getUtility(ISnapSet).getByName(private_team, 'private-snap') |
262 | + self.assertEqual( |
263 | + InformationType.PRIVATESECURITY, snap.information_type) |
264 | + |
265 | def test_create_new_snap_build_source_tarball(self): |
266 | # We can create a new snap and ask for it to build a source tarball. |
267 | self.useFixture(BranchHostingFixture(blob=b"")) |
268 | @@ -655,22 +714,43 @@ class TestSnapAdminView(BaseTestSnapView): |
269 | member_of=[getUtility(ILaunchpadCelebrities).commercial_admin]) |
270 | login_person(self.person) |
271 | snap = self.factory.makeSnap(registrant=self.person) |
272 | + project = self.factory.makeProduct(name='my-project') |
273 | self.assertTrue(snap.require_virtualized) |
274 | + self.assertIsNone(snap.project) |
275 | self.assertFalse(snap.private) |
276 | self.assertTrue(snap.allow_internet) |
277 | |
278 | + private = InformationType.PRIVATESECURITY.name |
279 | browser = self.getViewBrowser(snap, user=commercial_admin) |
280 | browser.getLink("Administer snap package").click() |
281 | + browser.getControl(name='field.project').value = "my-project" |
282 | browser.getControl("Require virtualized builders").selected = False |
283 | - browser.getControl("Private").selected = True |
284 | + browser.getControl(name="field.information_type").value = private |
285 | browser.getControl("Allow external network access").selected = False |
286 | browser.getControl("Update snap package").click() |
287 | |
288 | login_person(self.person) |
289 | + self.assertEqual(project, snap.project) |
290 | self.assertFalse(snap.require_virtualized) |
291 | self.assertTrue(snap.private) |
292 | self.assertFalse(snap.allow_internet) |
293 | |
294 | + def test_admin_snap_private_without_project(self): |
295 | + # Cannot make snap private if it doesn't have a project associated. |
296 | + login_person(self.person) |
297 | + snap = self.factory.makeSnap(registrant=self.person) |
298 | + commercial_admin = self.factory.makePerson( |
299 | + member_of=[getUtility(ILaunchpadCelebrities).commercial_admin]) |
300 | + private = InformationType.PRIVATESECURITY.name |
301 | + browser = self.getViewBrowser(snap, user=commercial_admin) |
302 | + browser.getLink("Administer snap package").click() |
303 | + browser.getControl(name='field.project').value = '' |
304 | + browser.getControl(name="field.information_type").value = private |
305 | + browser.getControl("Update snap package").click() |
306 | + self.assertEqual( |
307 | + 'Private snap recipes must be associated with a project.', |
308 | + extract_text(find_tags_by_class(browser.contents, "message")[1])) |
309 | + |
310 | def test_admin_snap_privacy_mismatch(self): |
311 | # Cannot make snap public if it still contains private information. |
312 | login_person(self.person) |
313 | @@ -682,9 +762,10 @@ class TestSnapAdminView(BaseTestSnapView): |
314 | # can reach this snap because it's owned by a private team. |
315 | commercial_admin = self.factory.makePerson( |
316 | member_of=[getUtility(ILaunchpadCelebrities).commercial_admin]) |
317 | + public = InformationType.PUBLIC.name |
318 | browser = self.getViewBrowser(snap, user=commercial_admin) |
319 | browser.getLink("Administer snap package").click() |
320 | - browser.getControl("Private").selected = False |
321 | + browser.getControl(name="field.information_type").value = public |
322 | browser.getControl("Update snap package").click() |
323 | self.assertEqual( |
324 | 'A public snap cannot have a private owner.', |
325 | diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py |
326 | index 9b19b1b..8ac5838 100644 |
327 | --- a/lib/lp/snappy/interfaces/snap.py |
328 | +++ b/lib/lp/snappy/interfaces/snap.py |
329 | @@ -67,6 +67,7 @@ from lazr.restful.fields import ( |
330 | Reference, |
331 | ReferenceChoice, |
332 | ) |
333 | +from lazr.restful.interface import copy_field |
334 | from six.moves import http_client |
335 | from zope.interface import ( |
336 | Attribute, |
337 | @@ -570,6 +571,12 @@ class ISnapView(Interface): |
338 | # Really ISnapBuild, patched in lp.snappy.interfaces.webservice. |
339 | value_type=Reference(schema=Interface), readonly=True))) |
340 | |
341 | + def getAllowedInformationTypes(user): |
342 | + """Get a list of acceptable `InformationType`s for this snap recipe. |
343 | + |
344 | + If the user is a Launchpad admin, any type is acceptable. |
345 | + """ |
346 | + |
347 | |
348 | class ISnapEdit(IWebhookTarget): |
349 | """`ISnap` methods that require launchpad.Edit permission.""" |
350 | @@ -672,7 +679,7 @@ class ISnapEditableAttributes(IHasOwner): |
351 | description=_("The owner of this snap package."))) |
352 | |
353 | project = ReferenceChoice( |
354 | - title=_('The project that this Snap is associated with.'), |
355 | + title=_('The project that this Snap is associated with'), |
356 | schema=IProduct, vocabulary='Product', |
357 | required=False, readonly=False) |
358 | |
359 | @@ -845,7 +852,7 @@ class ISnapAdminAttributes(Interface): |
360 | |
361 | information_type = exported(Choice( |
362 | title=_("Information type"), vocabulary=InformationType, |
363 | - required=True, readonly=True, default=InformationType.PUBLIC, |
364 | + required=True, readonly=False, default=InformationType.PUBLIC, |
365 | description=_( |
366 | "The type of information contained in this Snap recipe."))) |
367 | |
368 | @@ -884,6 +891,7 @@ class ISnapSet(Interface): |
369 | |
370 | @call_with(registrant=REQUEST_USER) |
371 | @operation_parameters( |
372 | + information_type=copy_field(ISnap["information_type"], required=False), |
373 | processors=List( |
374 | value_type=Reference(schema=IProcessor), required=False)) |
375 | @export_factory_operation( |
376 | @@ -891,15 +899,16 @@ class ISnapSet(Interface): |
377 | "owner", "distro_series", "name", "description", "branch", |
378 | "git_repository", "git_repository_url", "git_path", "git_ref", |
379 | "auto_build", "auto_build_archive", "auto_build_pocket", |
380 | - "private", "store_upload", "store_series", "store_name", |
381 | - "store_channels"]) |
382 | + "store_upload", "store_series", "store_name", "store_channels", |
383 | + "project"]) |
384 | @operation_for_version("devel") |
385 | def new(registrant, owner, distro_series, name, description=None, |
386 | branch=None, git_repository=None, git_repository_url=None, |
387 | git_path=None, git_ref=None, auto_build=False, |
388 | auto_build_archive=None, auto_build_pocket=None, |
389 | require_virtualized=True, processors=None, date_created=None, |
390 | - private=False, store_upload=False, store_series=None, |
391 | + information_type=InformationType.PUBLIC, store_upload=False, |
392 | + store_series=None, |
393 | store_name=None, store_secrets=None, store_channels=None, |
394 | project=None): |
395 | """Create an `ISnap`.""" |
396 | @@ -907,8 +916,8 @@ class ISnapSet(Interface): |
397 | def exists(owner, name): |
398 | """Check to see if a matching snap exists.""" |
399 | |
400 | - def isValidPrivacy(private, owner, branch=None, git_ref=None): |
401 | - """Whether or not the privacy context is valid.""" |
402 | + def getSnapSuggestedPrivacy(owner, branch=None, git_ref=None): |
403 | + """Which privacy a Snap should have based on its creation params.""" |
404 | |
405 | def isValidInformationType( |
406 | information_type, owner, branch=None, git_ref=None): |
407 | diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py |
408 | index 8ed9215..2c298fb 100644 |
409 | --- a/lib/lp/snappy/model/snap.py |
410 | +++ b/lib/lp/snappy/model/snap.py |
411 | @@ -29,6 +29,7 @@ from storm.expr import ( |
412 | Not, |
413 | Or, |
414 | Select, |
415 | + SQL, |
416 | ) |
417 | from storm.locals import ( |
418 | Bool, |
419 | @@ -58,11 +59,14 @@ from lp.app.browser.tales import ( |
420 | DateTimeFormatterAPI, |
421 | ) |
422 | from lp.app.enums import ( |
423 | + FREE_INFORMATION_TYPES, |
424 | InformationType, |
425 | PRIVATE_INFORMATION_TYPES, |
426 | + PUBLIC_INFORMATION_TYPES, |
427 | ) |
428 | from lp.app.errors import IncompatibleArguments |
429 | from lp.app.interfaces.security import IAuthorization |
430 | +from lp.app.interfaces.services import IService |
431 | from lp.buildmaster.enums import BuildStatus |
432 | from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet |
433 | from lp.buildmaster.model.builder import Builder |
434 | @@ -95,6 +99,10 @@ from lp.code.interfaces.gitrepository import ( |
435 | ) |
436 | from lp.code.model.branch import Branch |
437 | from lp.code.model.branchcollection import GenericBranchCollection |
438 | +from lp.code.model.branchnamespace import ( |
439 | + BRANCH_POLICY_ALLOWED_TYPES, |
440 | + BRANCH_POLICY_REQUIRED_GRANTS, |
441 | + ) |
442 | from lp.code.model.gitcollection import GenericGitCollection |
443 | from lp.code.model.gitrepository import GitRepository |
444 | from lp.registry.errors import PrivatePersonLinkageError |
445 | @@ -202,6 +210,17 @@ def snap_modified(snap, event): |
446 | removeSecurityProxy(snap).date_last_modified = UTC_NOW |
447 | |
448 | |
449 | +def user_has_special_snap_access(user): |
450 | + """Admins have special access. |
451 | + |
452 | + :param user: An `IPerson` or None. |
453 | + """ |
454 | + if user is None: |
455 | + return False |
456 | + roles = IPersonRoles(user) |
457 | + return roles.in_admin |
458 | + |
459 | + |
460 | @implementer(ISnapBuildRequest) |
461 | class SnapBuildRequest: |
462 | """See `ISnapBuildRequest`. |
463 | @@ -351,13 +370,7 @@ class Snap(Storm, WebhookTargetMixin): |
464 | |
465 | require_virtualized = Bool(name='require_virtualized') |
466 | |
467 | - def _validate_private(self, attr, value): |
468 | - if not getUtility(ISnapSet).isValidPrivacy( |
469 | - value, self.owner, self.branch, self.git_ref): |
470 | - raise SnapPrivacyMismatch |
471 | - return value |
472 | - |
473 | - private = Bool(name='private', validator=_validate_private) |
474 | + _private = Bool(name='private') |
475 | |
476 | def _valid_information_type(self, attr, value): |
477 | if not getUtility(ISnapSet).isValidInformationType( |
478 | @@ -389,20 +402,18 @@ class Snap(Storm, WebhookTargetMixin): |
479 | description=None, branch=None, git_ref=None, auto_build=False, |
480 | auto_build_archive=None, auto_build_pocket=None, |
481 | auto_build_channels=None, require_virtualized=True, |
482 | - date_created=DEFAULT, private=False, allow_internet=True, |
483 | - build_source_tarball=False, store_upload=False, |
484 | - store_series=None, store_name=None, store_secrets=None, |
485 | - store_channels=None, project=None): |
486 | + date_created=DEFAULT, information_type=InformationType.PUBLIC, |
487 | + allow_internet=True, build_source_tarball=False, |
488 | + store_upload=False, store_series=None, store_name=None, |
489 | + store_secrets=None, store_channels=None, project=None): |
490 | """Construct a `Snap`.""" |
491 | super(Snap, self).__init__() |
492 | |
493 | - # Set the private flag first so that other validators can perform |
494 | + # Set the information type first so that other validators can perform |
495 | # suitable privacy checks, but pillar should also be set, since it's |
496 | # mandatory for private snaps. |
497 | self.project = project |
498 | - self.private = private |
499 | - self.information_type = (InformationType.PROPRIETARY if private else |
500 | - InformationType.PUBLIC) |
501 | + self.information_type = information_type |
502 | |
503 | self.registrant = registrant |
504 | self.owner = owner |
505 | @@ -432,7 +443,7 @@ class Snap(Storm, WebhookTargetMixin): |
506 | @property |
507 | def information_type(self): |
508 | if self._information_type is None: |
509 | - return (InformationType.PROPRIETARY if self.private |
510 | + return (InformationType.PROPRIETARY if self._private |
511 | else InformationType.PUBLIC) |
512 | return self._information_type |
513 | |
514 | @@ -441,6 +452,10 @@ class Snap(Storm, WebhookTargetMixin): |
515 | self._information_type = information_type |
516 | |
517 | @property |
518 | + def private(self): |
519 | + return self.information_type not in PUBLIC_INFORMATION_TYPES |
520 | + |
521 | + @property |
522 | def valid_webhook_event_types(self): |
523 | return ["snap:build:0.1"] |
524 | |
525 | @@ -618,6 +633,24 @@ class Snap(Storm, WebhookTargetMixin): |
526 | def store_channels(self, value): |
527 | self._store_channels = value or None |
528 | |
529 | + def getAllowedInformationTypes(self, user): |
530 | + """See `ISnap`.""" |
531 | + if user_has_special_snap_access(user): |
532 | + # Admins can set any type. |
533 | + return set(PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES) |
534 | + if self.pillar is None: |
535 | + return set(FREE_INFORMATION_TYPES) |
536 | + required_grant = BRANCH_POLICY_REQUIRED_GRANTS[ |
537 | + self.project.branch_sharing_policy] |
538 | + if (required_grant is not None |
539 | + and not getUtility(IService, 'sharing').checkPillarAccess( |
540 | + [self.project], required_grant, self.owner) |
541 | + and (user is None |
542 | + or not getUtility(IService, 'sharing').checkPillarAccess( |
543 | + [self.project], required_grant, user))): |
544 | + return [] |
545 | + return BRANCH_POLICY_ALLOWED_TYPES[self.project.branch_sharing_policy] |
546 | + |
547 | @staticmethod |
548 | def extractSSOCaveats(macaroon): |
549 | locations = [ |
550 | @@ -1143,10 +1176,11 @@ class SnapSet: |
551 | git_path=None, git_ref=None, auto_build=False, |
552 | auto_build_archive=None, auto_build_pocket=None, |
553 | auto_build_channels=None, require_virtualized=True, |
554 | - processors=None, date_created=DEFAULT, private=False, |
555 | - allow_internet=True, build_source_tarball=False, |
556 | - store_upload=False, store_series=None, store_name=None, |
557 | - store_secrets=None, store_channels=None, project=None): |
558 | + processors=None, date_created=DEFAULT, |
559 | + information_type=InformationType.PUBLIC, allow_internet=True, |
560 | + build_source_tarball=False, store_upload=False, |
561 | + store_series=None, store_name=None, store_secrets=None, |
562 | + store_channels=None, project=None): |
563 | """See `ISnapSet`.""" |
564 | if not registrant.inTeam(owner): |
565 | if owner.is_team: |
566 | @@ -1183,7 +1217,8 @@ class SnapSet: |
567 | # IntegrityError due to exceptions being raised during object |
568 | # creation and to ensure that everything relevant is in the Storm |
569 | # cache. |
570 | - if not self.isValidPrivacy(private, owner, branch, git_ref): |
571 | + if not self.isValidInformationType( |
572 | + information_type, owner, branch, git_ref): |
573 | raise SnapPrivacyMismatch |
574 | |
575 | store = IMasterStore(Snap) |
576 | @@ -1194,7 +1229,7 @@ class SnapSet: |
577 | auto_build_pocket=auto_build_pocket, |
578 | auto_build_channels=auto_build_channels, |
579 | require_virtualized=require_virtualized, date_created=date_created, |
580 | - private=private, allow_internet=allow_internet, |
581 | + information_type=information_type, allow_internet=allow_internet, |
582 | build_source_tarball=build_source_tarball, |
583 | store_upload=store_upload, store_series=store_series, |
584 | store_name=store_name, store_secrets=store_secrets, |
585 | @@ -1208,9 +1243,24 @@ class SnapSet: |
586 | |
587 | return snap |
588 | |
589 | - def isValidPrivacy(self, private, owner, branch=None, git_ref=None): |
590 | + def getSnapSuggestedPrivacy(self, owner, branch=None, git_ref=None): |
591 | """See `ISnapSet`.""" |
592 | - # Private snaps may contain anything ... |
593 | + # Public snaps with private sources are not allowed. |
594 | + source = branch or git_ref |
595 | + if source is not None and source.private: |
596 | + return source.information_type |
597 | + |
598 | + # Public snaps owned by private teams are not allowed. |
599 | + if owner is not None and owner.private: |
600 | + return InformationType.PROPRIETARY |
601 | + |
602 | + # XXX pappacena 2021-03-02: We need to consider the pillar's branch |
603 | + # sharing policy here instead of suggesting PUBLIC. |
604 | + return InformationType.PUBLIC |
605 | + |
606 | + def isValidInformationType(self, information_type, owner, branch=None, |
607 | + git_ref=None): |
608 | + private = information_type not in PUBLIC_INFORMATION_TYPES |
609 | if private: |
610 | # If appropriately enabled via feature flag. |
611 | if not getFeatureFlag(SNAP_PRIVATE_FEATURE_FLAG): |
612 | @@ -1228,11 +1278,6 @@ class SnapSet: |
613 | |
614 | return True |
615 | |
616 | - def isValidInformationType(self, information_type, owner, branch=None, |
617 | - git_ref=None): |
618 | - private = information_type in PRIVATE_INFORMATION_TYPES |
619 | - return self.isValidPrivacy(private, owner, branch, git_ref) |
620 | - |
621 | def _getByName(self, owner, name): |
622 | return IStore(Snap).find( |
623 | Snap, Snap.owner == owner, Snap.name == name).one() |
624 | @@ -1333,15 +1378,22 @@ class SnapSet: |
625 | # XXX cjwatson 2016-11-25: This is in principle a poor query, but we |
626 | # don't yet have the access grant infrastructure to do better, and |
627 | # in any case the numbers involved should be very small. |
628 | + # XXX pappacena 2021-02-12: Once we do the migration to back fill |
629 | + # information_type, we should be able to change this. |
630 | + private_snap = SQL( |
631 | + "CASE information_type" |
632 | + " WHEN NULL THEN private" |
633 | + " ELSE information_type NOT IN ?" |
634 | + "END", params=[tuple(i.value for i in PUBLIC_INFORMATION_TYPES)]) |
635 | if visible_by_user is None: |
636 | - return Snap.private == False |
637 | + return private_snap == False |
638 | else: |
639 | roles = IPersonRoles(visible_by_user) |
640 | if roles.in_admin or roles.in_commercial_admin: |
641 | return True |
642 | else: |
643 | return Or( |
644 | - Snap.private == False, |
645 | + private_snap == False, |
646 | Snap.owner_id.is_in(Select( |
647 | TeamParticipation.teamID, |
648 | TeamParticipation.person == visible_by_user))) |
649 | diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py |
650 | index cdf8813..755851a 100644 |
651 | --- a/lib/lp/snappy/tests/test_snap.py |
652 | +++ b/lib/lp/snappy/tests/test_snap.py |
653 | @@ -166,7 +166,8 @@ class TestSnapFeatureFlag(TestCaseWithFactory): |
654 | self.assertRaises( |
655 | SnapPrivateFeatureDisabled, getUtility(ISnapSet).new, |
656 | person, person, None, None, |
657 | - branch=self.factory.makeAnyBranch(), private=True) |
658 | + branch=self.factory.makeAnyBranch(), |
659 | + information_type=InformationType.PROPRIETARY) |
660 | |
661 | |
662 | class TestSnap(TestCaseWithFactory): |
663 | @@ -1419,13 +1420,15 @@ class TestSnapSet(TestCaseWithFactory): |
664 | |
665 | def test_private_snap_information_type_compatibility(self): |
666 | login_admin() |
667 | + private = InformationType.PROPRIETARY |
668 | + public = InformationType.PUBLIC |
669 | private_snap = getUtility(ISnapSet).new( |
670 | - private=True, **self.makeSnapComponents()) |
671 | + information_type=private, **self.makeSnapComponents()) |
672 | self.assertEqual( |
673 | InformationType.PROPRIETARY, private_snap.information_type) |
674 | |
675 | public_snap = getUtility(ISnapSet).new( |
676 | - private=False, **self.makeSnapComponents()) |
677 | + information_type=public, **self.makeSnapComponents()) |
678 | self.assertEqual( |
679 | InformationType.PUBLIC, public_snap.information_type) |
680 | |
681 | @@ -1433,7 +1436,7 @@ class TestSnapSet(TestCaseWithFactory): |
682 | # Creating private snaps for public sources is allowed. |
683 | [ref] = self.factory.makeGitRefs() |
684 | components = self.makeSnapComponents(git_ref=ref) |
685 | - components['private'] = True |
686 | + components['information_type'] = InformationType.PROPRIETARY |
687 | components['project'] = self.factory.makeProduct() |
688 | snap = getUtility(ISnapSet).new(**components) |
689 | with person_logged_in(components['owner']): |
690 | @@ -2519,9 +2522,11 @@ class TestSnapWebservice(TestCaseWithFactory): |
691 | if auto_build_pocket is not None: |
692 | kwargs["auto_build_pocket"] = auto_build_pocket.title |
693 | logout() |
694 | + information_type = (InformationType.PROPRIETARY if private else |
695 | + InformationType.PUBLIC) |
696 | response = webservice.named_post( |
697 | "/+snaps", "new", owner=owner_url, distro_series=distroseries_url, |
698 | - name="mir", private=private, **kwargs) |
699 | + name="mir", information_type=information_type.title, **kwargs) |
700 | self.assertEqual(201, response.status) |
701 | return webservice.get(response.getHeader("Location")).jsonBody() |
702 | |
703 | @@ -2666,7 +2671,8 @@ class TestSnapWebservice(TestCaseWithFactory): |
704 | admin, permission=OAuthPermission.WRITE_PRIVATE) |
705 | admin_webservice.default_api_version = "devel" |
706 | response = admin_webservice.patch( |
707 | - snap_url, "application/json", json.dumps({"private": False})) |
708 | + snap_url, "application/json", |
709 | + json.dumps({"information_type": 'Public'})) |
710 | self.assertEqual(400, response.status) |
711 | self.assertEqual( |
712 | b"Snap recipe contains private information and cannot be public.", |
713 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
714 | index 9712bea..bc89da6 100644 |
715 | --- a/lib/lp/testing/factory.py |
716 | +++ b/lib/lp/testing/factory.py |
717 | @@ -4748,10 +4748,10 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
718 | auto_build_archive=None, auto_build_pocket=None, |
719 | auto_build_channels=None, is_stale=None, |
720 | require_virtualized=True, processors=None, |
721 | - date_created=DEFAULT, private=False, allow_internet=True, |
722 | - build_source_tarball=False, store_upload=False, |
723 | - store_series=None, store_name=None, store_secrets=None, |
724 | - store_channels=None, project=_DEFAULT): |
725 | + date_created=DEFAULT, private=False, information_type=None, |
726 | + allow_internet=True, build_source_tarball=False, |
727 | + store_upload=False, store_series=None, store_name=None, |
728 | + store_secrets=None, store_channels=None, project=_DEFAULT): |
729 | """Make a new Snap.""" |
730 | if registrant is None: |
731 | registrant = self.makePerson() |
732 | @@ -4775,13 +4775,18 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
733 | project = self.makeProduct() |
734 | if project is _DEFAULT: |
735 | project = None |
736 | + assert information_type is None or private is None |
737 | + if private is not None: |
738 | + information_type = (InformationType.PUBLIC if not private |
739 | + else InformationType.PROPRIETARY) |
740 | snap = getUtility(ISnapSet).new( |
741 | registrant, owner, distroseries, name, |
742 | require_virtualized=require_virtualized, processors=processors, |
743 | date_created=date_created, branch=branch, git_ref=git_ref, |
744 | auto_build=auto_build, auto_build_archive=auto_build_archive, |
745 | auto_build_pocket=auto_build_pocket, |
746 | - auto_build_channels=auto_build_channels, private=private, |
747 | + auto_build_channels=auto_build_channels, |
748 | + information_type=information_type, |
749 | allow_internet=allow_internet, |
750 | build_source_tarball=build_source_tarball, |
751 | store_upload=store_upload, store_series=store_series, |
Pushed the requested changes. This should be ready for another round of review.