Merge ~pappacena/launchpad:snap-pillar-ui into launchpad: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)
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".

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 :

Pushed the requested changes. This should be ready for another round of review.

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
1diff --git a/lib/lp/registry/personmerge.py b/lib/lp/registry/personmerge.py
2index 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."""
11diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
12index 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
182diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
183index 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.',
325diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
326index 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):
407diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
408index 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)))
649diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
650index 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.",
713diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
714index 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,