Merge ~pappacena/launchpad:snap-pillar into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: c7955b9825801193bb479a9d3528abff9a661144
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:snap-pillar
Merge into: launchpad:master
Diff against target: 372 lines (+119/-14)
5 files modified
database/schema/security.cfg (+3/-0)
lib/lp/snappy/interfaces/snap.py (+33/-4)
lib/lp/snappy/model/snap.py (+58/-5)
lib/lp/snappy/tests/test_snap.py (+16/-2)
lib/lp/testing/factory.py (+9/-3)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+397458@code.launchpad.net

Commit message

Adding Snap.project to be the (optional) pillar of snaps

Description of the change

This adds a project to be the optional pillar of Snaps (mandatory for private Snaps). Once we create the UI to set this, we should start validating and only allowing private Snaps that belongs to a given pillar (so we can use the pillar's sharing options to control Snap's privacy).

Database patch is available here: https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/397459.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) wrote :

Pushed the requested change.

I've also removed the XXX comment about validating (project + private) attributes at model level. It would break old snaps if we've added that validation in the future.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Thiago F. Pappacena (pappacena) :
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/database/schema/security.cfg b/database/schema/security.cfg
2index bf4b81c..e343a5f 100644
3--- a/database/schema/security.cfg
4+++ b/database/schema/security.cfg
5@@ -302,6 +302,7 @@ public.snapbuild = SELECT, INSERT, UPDATE, DELETE
6 public.snapbuildjob = SELECT, INSERT, UPDATE, DELETE
7 public.snapfile = SELECT, INSERT, UPDATE, DELETE
8 public.snapjob = SELECT, INSERT, UPDATE, DELETE
9+public.snapsubscription = SELECT, INSERT, UPDATE, DELETE
10 public.snappydistroseries = SELECT, INSERT, UPDATE, DELETE
11 public.snappyseries = SELECT, INSERT, UPDATE, DELETE
12 public.sourcepackageformatselection = SELECT
13@@ -2246,6 +2247,7 @@ type=user
14
15 [person-merge-job]
16 groups=script
17+public.accesspolicyartifact = SELECT
18 public.accessartifactgrant = SELECT, UPDATE, DELETE
19 public.accesspolicy = SELECT, UPDATE, DELETE
20 public.accesspolicygrant = SELECT, UPDATE, DELETE
21@@ -2363,6 +2365,7 @@ public.signedcodeofconduct = SELECT, UPDATE
22 public.snap = SELECT, UPDATE
23 public.snapbase = SELECT, UPDATE
24 public.snapbuild = SELECT, UPDATE
25+public.snapsubscription = SELECT, UPDATE, DELETE
26 public.snappyseries = SELECT, UPDATE
27 public.sourcepackagename = SELECT
28 public.sourcepackagepublishinghistory = SELECT, UPDATE
29diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
30index 3835b23..9b19b1b 100644
31--- a/lib/lp/snappy/interfaces/snap.py
32+++ b/lib/lp/snappy/interfaces/snap.py
33@@ -1,4 +1,4 @@
34-# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
35+# Copyright 2015-2021 Canonical Ltd. This software is licensed under the
36 # GNU Affero General Public License version 3 (see the file LICENSE).
37
38 """Snap package interfaces."""
39@@ -36,6 +36,7 @@ __all__ = [
40 'SnapBuildRequestStatus',
41 'SnapNotOwner',
42 'SnapPrivacyMismatch',
43+ 'SnapPrivacyPillarError',
44 'SnapPrivateFeatureDisabled',
45 ]
46
47@@ -88,7 +89,9 @@ from zope.security.interfaces import (
48 )
49
50 from lp import _
51+from lp.app.enums import InformationType
52 from lp.app.errors import NameLookupFailed
53+from lp.app.interfaces.informationtype import IInformationType
54 from lp.app.interfaces.launchpad import IPrivacy
55 from lp.app.validators.name import name_validator
56 from lp.buildmaster.interfaces.processor import IProcessor
57@@ -98,6 +101,7 @@ from lp.code.interfaces.gitrepository import IGitRepository
58 from lp.registry.interfaces.distroseries import IDistroSeries
59 from lp.registry.interfaces.person import IPerson
60 from lp.registry.interfaces.pocket import PackagePublishingPocket
61+from lp.registry.interfaces.product import IProduct
62 from lp.registry.interfaces.role import IHasOwner
63 from lp.services.fields import (
64 PersonChoice,
65@@ -212,7 +216,16 @@ class SnapPrivacyMismatch(Exception):
66 def __init__(self, message=None):
67 super(SnapPrivacyMismatch, self).__init__(
68 message or
69- "Snap contains private information and cannot be public.")
70+ "Snap recipe contains private information and cannot be public.")
71+
72+
73+@error_status(http_client.BAD_REQUEST)
74+class SnapPrivacyPillarError(Exception):
75+ """Private Snaps should be based in a pillar."""
76+
77+ def __init__(self, message=None):
78+ super(SnapPrivacyPillarError, self).__init__(
79+ message or "Private Snap recipes should have a pillar.")
80
81
82 class BadSnapSearchContext(Exception):
83@@ -658,6 +671,11 @@ class ISnapEditableAttributes(IHasOwner):
84 vocabulary="AllUserTeamsParticipationPlusSelf",
85 description=_("The owner of this snap package.")))
86
87+ project = ReferenceChoice(
88+ title=_('The project that this Snap is associated with.'),
89+ schema=IProduct, vocabulary='Product',
90+ required=False, readonly=False)
91+
92 distro_series = exported(Reference(
93 IDistroSeries, title=_("Distro Series"),
94 required=False, readonly=False,
95@@ -825,6 +843,12 @@ class ISnapAdminAttributes(Interface):
96 title=_("Private"), required=False, readonly=False,
97 description=_("Whether or not this snap is private.")))
98
99+ information_type = exported(Choice(
100+ title=_("Information type"), vocabulary=InformationType,
101+ required=True, readonly=True, default=InformationType.PUBLIC,
102+ description=_(
103+ "The type of information contained in this Snap recipe.")))
104+
105 require_virtualized = exported(Bool(
106 title=_("Require virtualized builders"), required=True, readonly=False,
107 description=_("Only build this snap package on virtual builders.")))
108@@ -850,7 +874,7 @@ class ISnapAdminAttributes(Interface):
109 @exported_as_webservice_entry(as_of="beta")
110 class ISnap(
111 ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes,
112- IPrivacy):
113+ IPrivacy, IInformationType):
114 """A buildable snap package."""
115
116
117@@ -876,7 +900,8 @@ class ISnapSet(Interface):
118 auto_build_archive=None, auto_build_pocket=None,
119 require_virtualized=True, processors=None, date_created=None,
120 private=False, store_upload=False, store_series=None,
121- store_name=None, store_secrets=None, store_channels=None):
122+ store_name=None, store_secrets=None, store_channels=None,
123+ project=None):
124 """Create an `ISnap`."""
125
126 def exists(owner, name):
127@@ -885,6 +910,10 @@ class ISnapSet(Interface):
128 def isValidPrivacy(private, owner, branch=None, git_ref=None):
129 """Whether or not the privacy context is valid."""
130
131+ def isValidInformationType(
132+ information_type, owner, branch=None, git_ref=None):
133+ """Whether or not the information type context is valid."""
134+
135 @operation_parameters(
136 owner=Reference(IPerson, title=_("Owner"), required=True),
137 name=TextLine(title=_("Snap name"), required=True))
138diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
139index 7e3d14c..b298a86 100644
140--- a/lib/lp/snappy/model/snap.py
141+++ b/lib/lp/snappy/model/snap.py
142@@ -1,4 +1,4 @@
143-# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
144+# Copyright 2015-2021 Canonical Ltd. This software is licensed under the
145 # GNU Affero General Public License version 3 (see the file LICENSE).
146
147 from __future__ import absolute_import, print_function, unicode_literals
148@@ -57,6 +57,10 @@ from lp.app.browser.tales import (
149 ArchiveFormatterAPI,
150 DateTimeFormatterAPI,
151 )
152+from lp.app.enums import (
153+ InformationType,
154+ PRIVATE_INFORMATION_TYPES,
155+ )
156 from lp.app.errors import IncompatibleArguments
157 from lp.app.interfaces.security import IAuthorization
158 from lp.buildmaster.enums import BuildStatus
159@@ -298,6 +302,9 @@ class Snap(Storm, WebhookTargetMixin):
160 owner_id = Int(name='owner', allow_none=False, validator=_validate_owner)
161 owner = Reference(owner_id, 'Person.id')
162
163+ project_id = Int(name='project', allow_none=True)
164+ project = Reference(project_id, 'Product.id')
165+
166 distro_series_id = Int(name='distro_series', allow_none=True)
167 distro_series = Reference(distro_series_id, 'DistroSeries.id')
168
169@@ -352,6 +359,17 @@ class Snap(Storm, WebhookTargetMixin):
170
171 private = Bool(name='private', validator=_validate_private)
172
173+ def _valid_information_type(self, attr, value):
174+ if not getUtility(ISnapSet).isValidInformationType(
175+ value, self.owner, self.branch, self.git_ref):
176+ raise SnapPrivacyMismatch
177+ return value
178+
179+ _information_type = DBEnum(
180+ enum=InformationType, default=InformationType.PUBLIC,
181+ name="information_type",
182+ validator=_valid_information_type)
183+
184 allow_internet = Bool(name='allow_internet', allow_none=False)
185
186 build_source_tarball = Bool(name='build_source_tarball', allow_none=False)
187@@ -374,13 +392,17 @@ class Snap(Storm, WebhookTargetMixin):
188 date_created=DEFAULT, private=False, allow_internet=True,
189 build_source_tarball=False, store_upload=False,
190 store_series=None, store_name=None, store_secrets=None,
191- store_channels=None):
192+ store_channels=None, project=None):
193 """Construct a `Snap`."""
194 super(Snap, self).__init__()
195
196 # Set the private flag first so that other validators can perform
197- # suitable privacy checks.
198+ # suitable privacy checks, but pillar should also be set, since it's
199+ # mandatory for private snaps.
200+ self.project = project
201 self.private = private
202+ self.information_type = (InformationType.PROPRIETARY if private else
203+ InformationType.PUBLIC)
204
205 self.registrant = registrant
206 self.owner = owner
207@@ -408,6 +430,17 @@ class Snap(Storm, WebhookTargetMixin):
208 return "<Snap ~%s/+snap/%s>" % (self.owner.name, self.name)
209
210 @property
211+ def information_type(self):
212+ if self._information_type is None:
213+ return (InformationType.PROPRIETARY if self.private
214+ else InformationType.PUBLIC)
215+ return self._information_type
216+
217+ @information_type.setter
218+ def information_type(self, information_type):
219+ self._information_type = information_type
220+
221+ @property
222 def valid_webhook_event_types(self):
223 return ["snap:build:0.1"]
224
225@@ -461,6 +494,21 @@ class Snap(Storm, WebhookTargetMixin):
226 return None
227
228 @property
229+ def pillar(self):
230+ """See `ISnap`."""
231+ return self.project
232+
233+ @pillar.setter
234+ def pillar(self, pillar):
235+ if pillar is None:
236+ self.project = None
237+ elif IProduct.providedBy(pillar):
238+ self.project = pillar
239+ else:
240+ raise ValueError(
241+ 'The pillar of a Snap must be an IProduct instance.')
242+
243+ @property
244 def available_processors(self):
245 """See `ISnap`."""
246 clauses = [Processor.id == DistroArchSeries.processor_id]
247@@ -1098,7 +1146,7 @@ class SnapSet:
248 processors=None, date_created=DEFAULT, private=False,
249 allow_internet=True, build_source_tarball=False,
250 store_upload=False, store_series=None, store_name=None,
251- store_secrets=None, store_channels=None):
252+ store_secrets=None, store_channels=None, project=None):
253 """See `ISnapSet`."""
254 if not registrant.inTeam(owner):
255 if owner.is_team:
256@@ -1150,7 +1198,7 @@ class SnapSet:
257 build_source_tarball=build_source_tarball,
258 store_upload=store_upload, store_series=store_series,
259 store_name=store_name, store_secrets=store_secrets,
260- store_channels=store_channels)
261+ store_channels=store_channels, project=project)
262 store.add(snap)
263
264 if processors is None:
265@@ -1180,6 +1228,11 @@ class SnapSet:
266
267 return True
268
269+ def isValidInformationType(self, information_type, owner, branch=None,
270+ git_ref=None):
271+ private = information_type in PRIVATE_INFORMATION_TYPES
272+ return self.isValidPrivacy(private, owner, branch, git_ref)
273+
274 def _getByName(self, owner, name):
275 return IStore(Snap).find(
276 Snap, Snap.owner == owner, Snap.name == name).one()
277diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
278index 706255a..9c2fda4 100644
279--- a/lib/lp/snappy/tests/test_snap.py
280+++ b/lib/lp/snappy/tests/test_snap.py
281@@ -1,4 +1,4 @@
282-# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
283+# Copyright 2015-2021 Canonical Ltd. This software is licensed under the
284 # GNU Affero General Public License version 3 (see the file LICENSE).
285
286 """Test snap packages."""
287@@ -131,6 +131,7 @@ from lp.testing import (
288 ANONYMOUS,
289 api_url,
290 login,
291+ login_admin,
292 logout,
293 person_logged_in,
294 record_two_runs,
295@@ -1416,11 +1417,24 @@ class TestSnapSet(TestCaseWithFactory):
296 self.assertEqual(ref.path, snap.git_path)
297 self.assertEqual(ref, snap.git_ref)
298
299+ def test_private_snap_information_type_compatibility(self):
300+ login_admin()
301+ private_snap = getUtility(ISnapSet).new(
302+ private=True, **self.makeSnapComponents())
303+ self.assertEqual(
304+ InformationType.PROPRIETARY, private_snap.information_type)
305+
306+ public_snap = getUtility(ISnapSet).new(
307+ private=False, **self.makeSnapComponents())
308+ self.assertEqual(
309+ InformationType.PUBLIC, public_snap.information_type)
310+
311 def test_private_snap_for_public_sources(self):
312 # Creating private snaps for public sources is allowed.
313 [ref] = self.factory.makeGitRefs()
314 components = self.makeSnapComponents(git_ref=ref)
315 components['private'] = True
316+ components['project'] = self.factory.makeProduct()
317 snap = getUtility(ISnapSet).new(**components)
318 with person_logged_in(components['owner']):
319 self.assertTrue(snap.private)
320@@ -2646,7 +2660,7 @@ class TestSnapWebservice(TestCaseWithFactory):
321 snap_url, "application/json", json.dumps({"private": False}))
322 self.assertEqual(400, response.status)
323 self.assertEqual(
324- b"Snap contains private information and cannot be public.",
325+ b"Snap recipe contains private information and cannot be public.",
326 response.body)
327
328 def test_cannot_set_private_components_of_public_snap(self):
329diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
330index c833ddf..9914cc8 100644
331--- a/lib/lp/testing/factory.py
332+++ b/lib/lp/testing/factory.py
333@@ -2,7 +2,7 @@
334 # NOTE: The first line above must stay first; do not move the copyright
335 # notice to the top. See http://www.python.org/dev/peps/pep-0263/.
336 #
337-# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
338+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
339 # GNU Affero General Public License version 3 (see the file LICENSE).
340
341 """Testing infrastructure for the Launchpad application.
342@@ -4754,7 +4754,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
343 date_created=DEFAULT, private=False, allow_internet=True,
344 build_source_tarball=False, store_upload=False,
345 store_series=None, store_name=None, store_secrets=None,
346- store_channels=None):
347+ store_channels=None, project=_DEFAULT):
348 """Make a new Snap."""
349 if registrant is None:
350 registrant = self.makePerson()
351@@ -4772,6 +4772,12 @@ class BareLaunchpadObjectFactory(ObjectFactory):
352 distribution=distroseries.distribution, owner=owner)
353 if auto_build_pocket is None:
354 auto_build_pocket = PackagePublishingPocket.UPDATES
355+ if private and project is _DEFAULT:
356+ # If we are creating a private snap and didn't explictly set a
357+ # pillar for it, we must create a pillar.
358+ project = self.makeProduct()
359+ if project is _DEFAULT:
360+ project = None
361 snap = getUtility(ISnapSet).new(
362 registrant, owner, distroseries, name,
363 require_virtualized=require_virtualized, processors=processors,
364@@ -4783,7 +4789,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
365 build_source_tarball=build_source_tarball,
366 store_upload=store_upload, store_series=store_series,
367 store_name=store_name, store_secrets=store_secrets,
368- store_channels=store_channels)
369+ store_channels=store_channels, project=project)
370 if is_stale is not None:
371 removeSecurityProxy(snap).is_stale = is_stale
372 IStore(snap).flush()