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

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: bbc745ac1fd4d677d7c8235c9cfcacd9fb084fe8
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:snap-pillar-reconcile-access
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:snap-pillar-accesspolicy
Diff against target: 368 lines (+188/-13)
6 files modified
lib/lp/snappy/browser/snap.py (+7/-0)
lib/lp/snappy/browser/tests/test_snap.py (+14/-3)
lib/lp/snappy/interfaces/snap.py (+6/-0)
lib/lp/snappy/model/snap.py (+33/-2)
lib/lp/snappy/tests/test_snap.py (+112/-2)
lib/lp/testing/factory.py (+16/-6)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+397693@code.launchpad.net

Commit message

Adding reconcile for snap access policy when changing Snap privacy or project

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 changes.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
2index 2ac9785..f434bbf 100644
3--- a/lib/lp/snappy/browser/snap.py
4+++ b/lib/lp/snappy/browser/snap.py
5@@ -758,6 +758,13 @@ class SnapAdminView(BaseSnapEditView):
6 'information_type',
7 'You do not have permission to create private snaps.')
8
9+ def updateContextFromData(self, data, context=None, notify_modified=True):
10+ if 'project' in data:
11+ project = data.pop('project')
12+ self.context.setProject(project)
13+ super(SnapAdminView, self).updateContextFromData(
14+ data, context, notify_modified)
15+
16
17 class SnapEditView(BaseSnapEditView, EnableProcessorsMixin):
18 """View for editing snap packages."""
19diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
20index c5b2f2a..10caef9 100644
21--- a/lib/lp/snappy/browser/tests/test_snap.py
22+++ b/lib/lp/snappy/browser/tests/test_snap.py
23@@ -56,7 +56,10 @@ from lp.code.tests.helpers import (
24 BranchHostingFixture,
25 GitHostingFixture,
26 )
27-from lp.registry.enums import PersonVisibility
28+from lp.registry.enums import (
29+ BranchSharingPolicy,
30+ PersonVisibility,
31+ )
32 from lp.registry.interfaces.pocket import PackagePublishingPocket
33 from lp.registry.interfaces.series import SeriesStatus
34 from lp.services.config import config
35@@ -713,13 +716,17 @@ class TestSnapAdminView(BaseTestSnapView):
36 commercial_admin = self.factory.makePerson(
37 member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
38 login_person(self.person)
39+ project = self.factory.makeProduct(name="my-project")
40+ with person_logged_in(project.owner):
41+ project.information_type = InformationType.PROPRIETARY
42 snap = self.factory.makeSnap(registrant=self.person)
43- project = self.factory.makeProduct(name='my-project')
44 self.assertTrue(snap.require_virtualized)
45 self.assertIsNone(snap.project)
46 self.assertFalse(snap.private)
47 self.assertTrue(snap.allow_internet)
48
49+ self.factory.makeAccessPolicy(
50+ pillar=project, type=InformationType.PRIVATESECURITY)
51 private = InformationType.PRIVATESECURITY.name
52 browser = self.getViewBrowser(snap, user=commercial_admin)
53 browser.getLink("Administer snap package").click()
54@@ -756,8 +763,12 @@ class TestSnapAdminView(BaseTestSnapView):
55 login_person(self.person)
56 team = self.factory.makeTeam(
57 owner=self.person, visibility=PersonVisibility.PRIVATE)
58+ project = self.factory.makeProduct(
59+ information_type=InformationType.PUBLIC,
60+ branch_sharing_policy=BranchSharingPolicy.PUBLIC_OR_PROPRIETARY)
61 snap = self.factory.makeSnap(
62- registrant=self.person, owner=team, private=True)
63+ registrant=self.person, owner=team, project=project,
64+ information_type=InformationType.PRIVATESECURITY)
65 # Note that only LP admins or, in this case, commercial_admins
66 # can reach this snap because it's owned by a private team.
67 commercial_admin = self.factory.makePerson(
68diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
69index df95533..31708c3 100644
70--- a/lib/lp/snappy/interfaces/snap.py
71+++ b/lib/lp/snappy/interfaces/snap.py
72@@ -571,6 +571,9 @@ class ISnapView(Interface):
73 # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
74 value_type=Reference(schema=Interface), readonly=True)))
75
76+ def visibleByUser(user):
77+ """Can the specified user see this snap recipe?"""
78+
79 def getAllowedInformationTypes(user):
80 """Get a list of acceptable `InformationType`s for this snap recipe.
81
82@@ -839,6 +842,9 @@ class ISnapEditableAttributes(IHasOwner):
83 "'2.1/stable/fix-123', '2.1/stable', 'stable/fix-123', or "
84 "'stable'.")))
85
86+ def setProject(project):
87+ """Set the pillar project of this snap recipe."""
88+
89
90 class ISnapAdminAttributes(Interface):
91 """`ISnap` attributes that can be edited by admins.
92diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
93index 20dbf3d..52e2d33 100644
94--- a/lib/lp/snappy/model/snap.py
95+++ b/lib/lp/snappy/model/snap.py
96@@ -121,7 +121,10 @@ from lp.registry.interfaces.role import (
97 IHasOwner,
98 IPersonRoles,
99 )
100-from lp.registry.model.accesspolicy import AccessPolicyGrant
101+from lp.registry.model.accesspolicy import (
102+ AccessPolicyGrant,
103+ reconcile_access_for_artifact,
104+ )
105 from lp.registry.model.distroseries import DistroSeries
106 from lp.registry.model.series import ACTIVE_STATUSES
107 from lp.registry.model.teammembership import TeamParticipation
108@@ -420,8 +423,11 @@ class Snap(Storm, WebhookTargetMixin):
109 # Set the information type first so that other validators can perform
110 # suitable privacy checks, but pillar should also be set, since it's
111 # mandatory for private snaps.
112+ # Note that we set self._information_type (not self.information_type)
113+ # to avoid the call to self._reconcileAccess() while building the
114+ # Snap instance.
115 self.project = project
116- self.information_type = information_type
117+ self._information_type = information_type
118
119 self.registrant = registrant
120 self.owner = owner
121@@ -458,6 +464,7 @@ class Snap(Storm, WebhookTargetMixin):
122 @information_type.setter
123 def information_type(self, information_type):
124 self._information_type = information_type
125+ self._reconcileAccess()
126
127 @property
128 def private(self):
129@@ -1121,6 +1128,14 @@ class Snap(Storm, WebhookTargetMixin):
130 order_by = Desc(SnapBuild.id)
131 return self._getBuilds(filter_term, order_by)
132
133+ def visibleByUser(self, user):
134+ """See `ISnap`."""
135+ store = IStore(self)
136+ return not store.find(
137+ Snap,
138+ Snap.id == self.id,
139+ get_snap_privacy_filter(user)).is_empty()
140+
141 def subscribe(self, person, subscribed_by, ignore_permissions=False):
142 """See `ISnap`."""
143 # XXX pappacena 2021-02-05: We may need a "SnapSubscription" here.
144@@ -1136,6 +1151,21 @@ class Snap(Storm, WebhookTargetMixin):
145 self.pillar, person, unsubscribed_by, snaps=[self])
146 IStore(self).flush()
147
148+ def _reconcileAccess(self):
149+ """Reconcile the snap's sharing information.
150+
151+ Takes the privacy and pillar and makes the related AccessArtifact
152+ and AccessPolicyArtifacts match.
153+ """
154+ if self.project is None:
155+ return
156+ pillars = [self.project]
157+ reconcile_access_for_artifact(self, self.information_type, pillars)
158+
159+ def setProject(self, project):
160+ self.project = project
161+ self._reconcileAccess()
162+
163 def _deleteAccessGrants(self):
164 """Delete access grants for this snap recipe prior to deleting it."""
165 getUtility(IAccessArtifactSource).delete([self])
166@@ -1263,6 +1293,7 @@ class SnapSet:
167 store_name=store_name, store_secrets=store_secrets,
168 store_channels=store_channels, project=project)
169 store.add(snap)
170+ snap._reconcileAccess()
171
172 # Automatically subscribe the owner to the Snap.
173 snap.subscribe(snap.owner, registrant, ignore_permissions=True)
174diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
175index a68b116..e8ea61e 100644
176--- a/lib/lp/snappy/tests/test_snap.py
177+++ b/lib/lp/snappy/tests/test_snap.py
178@@ -42,6 +42,7 @@ from testtools.matchers import (
179 )
180 import transaction
181 from zope.component import getUtility
182+from zope.security.interfaces import Unauthorized
183 from zope.security.proxy import removeSecurityProxy
184
185 from lp.app.enums import InformationType
186@@ -64,10 +65,18 @@ from lp.code.tests.helpers import (
187 BranchHostingFixture,
188 GitHostingFixture,
189 )
190-from lp.registry.enums import PersonVisibility
191+from lp.registry.enums import (
192+ BranchSharingPolicy,
193+ PersonVisibility,
194+ )
195+from lp.registry.interfaces.accesspolicy import IAccessPolicySource
196 from lp.registry.interfaces.distribution import IDistributionSet
197 from lp.registry.interfaces.pocket import PackagePublishingPocket
198 from lp.registry.interfaces.series import SeriesStatus
199+from lp.registry.model.accesspolicy import (
200+ AccessArtifact,
201+ AccessArtifactGrant,
202+ )
203 from lp.services.config import config
204 from lp.services.crypto.interfaces import IEncryptedContainer
205 from lp.services.database.constants import (
206@@ -1327,6 +1336,105 @@ class TestSnapDeleteWithBuilds(TestCaseWithFactory):
207 self.assertRaises(LostObjectError, getattr, webhook, "target")
208
209
210+class TestSnapVisibility(TestCaseWithFactory):
211+
212+ layer = DatabaseFunctionalLayer
213+
214+ def setUp(self):
215+ super(TestSnapVisibility, self).setUp()
216+ self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
217+
218+ def getSnapGrants(self, snap, person=None):
219+ conditions = [AccessArtifact.snap == snap]
220+ if person is not None:
221+ conditions.append(AccessArtifactGrant.grantee == person)
222+ return IStore(AccessArtifactGrant).find(
223+ AccessArtifactGrant,
224+ AccessArtifactGrant.abstract_artifact_id == AccessArtifact.id,
225+ *conditions)
226+
227+ def test_only_owner_can_grant_access(self):
228+ owner = self.factory.makePerson()
229+ snap = self.factory.makeSnap(
230+ registrant=owner, owner=owner, private=True)
231+ other_person = self.factory.makePerson()
232+ with person_logged_in(owner):
233+ snap.subscribe(other_person, owner)
234+ with person_logged_in(other_person):
235+ self.assertRaises(Unauthorized, getattr, snap, 'subscribe')
236+
237+ def test_private_is_invisible_by_default(self):
238+ owner = self.factory.makePerson()
239+ person = self.factory.makePerson()
240+ snap = self.factory.makeSnap(
241+ registrant=owner, owner=owner, private=True)
242+ with person_logged_in(owner):
243+ self.assertFalse(snap.visibleByUser(person))
244+
245+ def test_private_is_visible_by_team_member(self):
246+ person = self.factory.makePerson()
247+ team = self.factory.makeTeam(members=[person])
248+ snap = self.factory.makeSnap(private=True, owner=team,
249+ registrant=person)
250+ with person_logged_in(team):
251+ self.assertTrue(snap.visibleByUser(person))
252+
253+ def test_subscribing_changes_visibility(self):
254+ person = self.factory.makePerson()
255+ owner = self.factory.makePerson()
256+ snap = self.factory.makeSnap(
257+ registrant=owner, owner=owner, private=True)
258+
259+ with person_logged_in(owner):
260+ self.assertFalse(snap.visibleByUser(person))
261+ snap.subscribe(person, snap.owner)
262+ # Calling again should be a no-op.
263+ snap.subscribe(person, snap.owner)
264+ self.assertTrue(snap.visibleByUser(person))
265+ snap.unsubscribe(person, snap.owner)
266+ self.assertFalse(snap.visibleByUser(person))
267+
268+ def test_reconcile_set_public(self):
269+ owner = self.factory.makePerson()
270+ snap = self.factory.makeSnap(
271+ registrant=owner, owner=owner, private=True)
272+ another_user = self.factory.makePerson()
273+ with admin_logged_in():
274+ snap.subscribe(another_user, snap.owner)
275+
276+ self.assertEqual(1, self.getSnapGrants(snap, another_user).count())
277+ with admin_logged_in():
278+ snap.information_type = InformationType.PUBLIC
279+ self.assertEqual(0, self.getSnapGrants(snap, another_user).count())
280+
281+ def test_reconcile_permissions_setting_project(self):
282+ owner = self.factory.makePerson()
283+ old_project = self.factory.makeProduct()
284+ getUtility(IAccessPolicySource).create(
285+ [(old_project, InformationType.PROPRIETARY)])
286+
287+ snap = self.factory.makeSnap(
288+ project=old_project, private=True, registrant=owner, owner=owner)
289+
290+ # Owner automatically gets a grant.
291+ with person_logged_in(owner):
292+ self.assertTrue(snap.visibleByUser(snap.owner))
293+ self.assertEqual(1, self.getSnapGrants(snap).count())
294+
295+ new_project = self.factory.makeProduct()
296+ getUtility(IAccessPolicySource).create(
297+ [(new_project, InformationType.PROPRIETARY)])
298+ another_person = self.factory.makePerson()
299+ with person_logged_in(owner):
300+ snap.subscribe(another_person, owner)
301+ self.assertTrue(snap.visibleByUser(another_person))
302+ self.assertEqual(2, self.getSnapGrants(snap).count())
303+
304+ snap.setProject(new_project)
305+ self.assertTrue(snap.visibleByUser(another_person))
306+ self.assertEqual(2, self.getSnapGrants(snap).count())
307+
308+
309 class TestSnapSet(TestCaseWithFactory):
310
311 layer = DatabaseFunctionalLayer
312@@ -1438,7 +1546,9 @@ class TestSnapSet(TestCaseWithFactory):
313 [ref] = self.factory.makeGitRefs()
314 components = self.makeSnapComponents(git_ref=ref)
315 components['information_type'] = InformationType.PROPRIETARY
316- components['project'] = self.factory.makeProduct()
317+ components['project'] = self.factory.makeProduct(
318+ information_type=InformationType.PROPRIETARY,
319+ branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
320 snap = getUtility(ISnapSet).new(**components)
321 with person_logged_in(components['owner']):
322 self.assertTrue(snap.private)
323diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
324index bc89da6..6948419 100644
325--- a/lib/lp/testing/factory.py
326+++ b/lib/lp/testing/factory.py
327@@ -4748,11 +4748,19 @@ class BareLaunchpadObjectFactory(ObjectFactory):
328 auto_build_archive=None, auto_build_pocket=None,
329 auto_build_channels=None, is_stale=None,
330 require_virtualized=True, processors=None,
331- date_created=DEFAULT, private=False, information_type=None,
332+ date_created=DEFAULT, private=None, information_type=None,
333 allow_internet=True, build_source_tarball=False,
334 store_upload=False, store_series=None, store_name=None,
335 store_secrets=None, store_channels=None, project=_DEFAULT):
336 """Make a new Snap."""
337+ assert information_type is None or private is None
338+ if information_type is None:
339+ # Defaults to public information type, unless "private" flag was
340+ # passed.
341+ information_type = (InformationType.PUBLIC if not private
342+ else InformationType.PROPRIETARY)
343+ if private is None:
344+ private = information_type not in PUBLIC_INFORMATION_TYPES
345 if registrant is None:
346 registrant = self.makePerson()
347 if owner is None:
348@@ -4772,13 +4780,15 @@ class BareLaunchpadObjectFactory(ObjectFactory):
349 if private and project is _DEFAULT:
350 # If we are creating a private snap and didn't explictly set a
351 # pillar for it, we must create a pillar.
352- project = self.makeProduct()
353+ branch_sharing = (
354+ BranchSharingPolicy.PUBLIC_OR_PROPRIETARY if not private
355+ else BranchSharingPolicy.PROPRIETARY)
356+ project = self.makeProduct(
357+ owner=registrant, registrant=registrant,
358+ information_type=information_type,
359+ branch_sharing_policy=branch_sharing)
360 if project is _DEFAULT:
361 project = None
362- assert information_type is None or private is None
363- if private is not None:
364- information_type = (InformationType.PUBLIC if not private
365- else InformationType.PROPRIETARY)
366 snap = getUtility(ISnapSet).new(
367 registrant, owner, distroseries, name,
368 require_virtualized=require_virtualized, processors=processors,