Merge ~pappacena/launchpad:snap-pillar-subscribe-removal-job into launchpad:master

Proposed by Thiago F. Pappacena
Status: Superseded
Proposed branch: ~pappacena/launchpad:snap-pillar-subscribe-removal-job
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:snap-pillar-subscribe
Diff against target: 890 lines (+199/-85)
16 files modified
database/schema/security.cfg (+2/-0)
lib/lp/blueprints/model/specification.py (+2/-2)
lib/lp/blueprints/tests/test_specification.py (+4/-4)
lib/lp/bugs/model/bug.py (+3/-3)
lib/lp/code/browser/branchsubscription.py (+2/-2)
lib/lp/code/browser/gitsubscription.py (+2/-2)
lib/lp/code/model/branch.py (+2/-2)
lib/lp/code/model/gitrepository.py (+1/-1)
lib/lp/code/model/tests/test_branchsubscription.py (+3/-3)
lib/lp/registry/model/sharingjob.py (+36/-1)
lib/lp/registry/services/sharingservice.py (+15/-12)
lib/lp/registry/services/tests/test_sharingservice.py (+11/-7)
lib/lp/registry/tests/test_sharingjob.py (+56/-10)
lib/lp/snappy/interfaces/snap.py (+4/-0)
lib/lp/snappy/model/snap.py (+36/-12)
lib/lp/snappy/tests/test_snap.py (+20/-24)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+398318@code.launchpad.net

This proposal has been superseded by a proposal from 2021-03-11.

Commit message

Remove dangling SnapSubscription when changing Snap pillar's sharing policy

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

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

96846ab... by Thiago F. Pappacena

Fixing snap privacy filter usage

be29706... by Thiago F. Pappacena

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

0efaed4... by Thiago F. Pappacena

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

d256e68... by Thiago F. Pappacena

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

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

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

e857291... by Thiago F. Pappacena

Refactoring and setting proper db permission for job

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

Pushed the requested changes, and added a comment about `get_snap_privacy_filter` method.

Revision history for this message
Colin Watson (cjwatson) :
920b88c... by Thiago F. Pappacena

Not granting commercial admin view permisson on every snap

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

Pushed the changes for admin visibility after talking to wgrant and cjwatson.

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

Merge branch 'master' into snap-pillar-subscribe-removal-job

5f729ee... by Thiago F. Pappacena

Removing unecessary shortcut for admin permission on snaps

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

Unmerged commits

5f729ee... by Thiago F. Pappacena

Removing unecessary shortcut for admin permission on snaps

df76888... by Thiago F. Pappacena

Merge branch 'master' into snap-pillar-subscribe-removal-job

920b88c... by Thiago F. Pappacena

Not granting commercial admin view permisson on every snap

e857291... by Thiago F. Pappacena

Refactoring and setting proper db permission for job

76b720d... by Thiago F. Pappacena

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

d256e68... by Thiago F. Pappacena

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

0efaed4... by Thiago F. Pappacena

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

be29706... by Thiago F. Pappacena

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

96846ab... by Thiago F. Pappacena

Fixing snap privacy filter usage

6ead4b4... by Thiago F. Pappacena

Merge branch 'snap-pillar-subscribe' into snap-pillar-subscribe-removal-job

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 e9dcc62..2f4ffdf 100644
3--- a/database/schema/security.cfg
4+++ b/database/schema/security.cfg
5@@ -2111,6 +2111,8 @@ public.job = SELECT, INSERT, UPDATE
6 public.person = SELECT
7 public.product = SELECT
8 public.sharingjob = SELECT, INSERT, UPDATE
9+public.snap = SELECT
10+public.snapsubscription = SELECT, UPDATE, DELETE
11 public.specification = SELECT
12 public.specificationsubscription = SELECT, DELETE
13 public.teamparticipation = SELECT
14diff --git a/lib/lp/blueprints/model/specification.py b/lib/lp/blueprints/model/specification.py
15index e0d4001..55ce9d5 100644
16--- a/lib/lp/blueprints/model/specification.py
17+++ b/lib/lp/blueprints/model/specification.py
18@@ -1,4 +1,4 @@
19-# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
20+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
21 # GNU Affero General Public License version 3 (see the file LICENSE).
22
23 __metaclass__ = type
24@@ -755,7 +755,7 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
25 # Grant the subscriber access if they can't see the
26 # specification.
27 service = getUtility(IService, 'sharing')
28- _, _, _, shared_specs = service.getVisibleArtifacts(
29+ _, _, _, _, shared_specs = service.getVisibleArtifacts(
30 person, specifications=[self], ignore_permissions=True)
31 if not shared_specs:
32 service.ensureAccessGrants(
33diff --git a/lib/lp/blueprints/tests/test_specification.py b/lib/lp/blueprints/tests/test_specification.py
34index 8de08c9..86c4e4a 100644
35--- a/lib/lp/blueprints/tests/test_specification.py
36+++ b/lib/lp/blueprints/tests/test_specification.py
37@@ -1,4 +1,4 @@
38-# Copyright 2010-2015 Canonical Ltd. This software is licensed under the
39+# Copyright 2010-2021 Canonical Ltd. This software is licensed under the
40 # GNU Affero General Public License version 3 (see the file LICENSE).
41
42 """Unit tests for Specification."""
43@@ -492,7 +492,7 @@ class SpecificationTests(TestCaseWithFactory):
44 product=product, information_type=InformationType.PROPRIETARY)
45 spec.subscribe(user, subscribed_by=owner)
46 service = getUtility(IService, 'sharing')
47- _, _, _, shared_specs = service.getVisibleArtifacts(
48+ _, _, _, _, shared_specs = service.getVisibleArtifacts(
49 user, specifications=[spec])
50 self.assertEqual([spec], shared_specs)
51 # The spec is also returned by getSharedSpecifications(),
52@@ -509,7 +509,7 @@ class SpecificationTests(TestCaseWithFactory):
53 service.sharePillarInformation(
54 product, user_2, owner, permissions)
55 spec.subscribe(user_2, subscribed_by=owner)
56- _, _, _, shared_specs = service.getVisibleArtifacts(
57+ _, _, _, _, shared_specs = service.getVisibleArtifacts(
58 user_2, specifications=[spec])
59 self.assertEqual([spec], shared_specs)
60 self.assertEqual(
61@@ -529,7 +529,7 @@ class SpecificationTests(TestCaseWithFactory):
62 spec.subscribe(user, subscribed_by=owner)
63 spec.unsubscribe(user, unsubscribed_by=owner)
64 service = getUtility(IService, 'sharing')
65- _, _, _, shared_specs = service.getVisibleArtifacts(
66+ _, _, _, _, shared_specs = service.getVisibleArtifacts(
67 user, specifications=[spec])
68 self.assertEqual([], shared_specs)
69
70diff --git a/lib/lp/bugs/model/bug.py b/lib/lp/bugs/model/bug.py
71index a9b177c..f7ebc86 100644
72--- a/lib/lp/bugs/model/bug.py
73+++ b/lib/lp/bugs/model/bug.py
74@@ -1,4 +1,4 @@
75-# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
76+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
77 # GNU Affero General Public License version 3 (see the file LICENSE).
78
79 """Launchpad bug-related database table classes."""
80@@ -875,7 +875,7 @@ class Bug(SQLBase, InformationTypeMixin):
81 # there is at least one bugtask for which access can be checked.
82 if self.default_bugtask:
83 service = getUtility(IService, 'sharing')
84- bugs, _, _, _ = service.getVisibleArtifacts(
85+ bugs, _, _, _, _ = service.getVisibleArtifacts(
86 person, bugs=[self], ignore_permissions=True)
87 if not bugs:
88 service.ensureAccessGrants(
89@@ -1819,7 +1819,7 @@ class Bug(SQLBase, InformationTypeMixin):
90 if information_type in PRIVATE_INFORMATION_TYPES:
91 service = getUtility(IService, 'sharing')
92 for person in (who, self.owner):
93- bugs, _, _, _ = service.getVisibleArtifacts(
94+ bugs, _, _, _, _ = service.getVisibleArtifacts(
95 person, bugs=[self], ignore_permissions=True)
96 if not bugs:
97 # subscribe() isn't sufficient if a subscription
98diff --git a/lib/lp/code/browser/branchsubscription.py b/lib/lp/code/browser/branchsubscription.py
99index fbd98bc..cee4504 100644
100--- a/lib/lp/code/browser/branchsubscription.py
101+++ b/lib/lp/code/browser/branchsubscription.py
102@@ -1,4 +1,4 @@
103-# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
104+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
105 # GNU Affero General Public License version 3 (see the file LICENSE).
106
107 __metaclass__ = type
108@@ -276,7 +276,7 @@ class BranchSubscriptionEditView(LaunchpadEditFormView):
109 url = canonical_url(self.branch)
110 # If the subscriber can no longer see the branch, redirect them away.
111 service = getUtility(IService, 'sharing')
112- _, branches, _, _ = service.getVisibleArtifacts(
113+ _, branches, _, _, _ = service.getVisibleArtifacts(
114 self.person, branches=[self.branch], ignore_permissions=True)
115 if not branches:
116 url = canonical_url(self.branch.target)
117diff --git a/lib/lp/code/browser/gitsubscription.py b/lib/lp/code/browser/gitsubscription.py
118index 3eda78c..a18d593 100644
119--- a/lib/lp/code/browser/gitsubscription.py
120+++ b/lib/lp/code/browser/gitsubscription.py
121@@ -1,4 +1,4 @@
122-# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
123+# Copyright 2015-2021 Canonical Ltd. This software is licensed under the
124 # GNU Affero General Public License version 3 (see the file LICENSE).
125
126 __metaclass__ = type
127@@ -280,7 +280,7 @@ class GitSubscriptionEditView(LaunchpadEditFormView):
128 # If the subscriber can no longer see the repository, redirect them
129 # away.
130 service = getUtility(IService, "sharing")
131- _, _, repositories, _ = service.getVisibleArtifacts(
132+ _, _, repositories, _, _ = service.getVisibleArtifacts(
133 self.person, gitrepositories=[self.repository],
134 ignore_permissions=True)
135 if not repositories:
136diff --git a/lib/lp/code/model/branch.py b/lib/lp/code/model/branch.py
137index 947aaf1..278db40 100644
138--- a/lib/lp/code/model/branch.py
139+++ b/lib/lp/code/model/branch.py
140@@ -1,4 +1,4 @@
141-# Copyright 2009-2020 Canonical Ltd. This software is licensed under the
142+# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
143 # GNU Affero General Public License version 3 (see the file LICENSE).
144
145 __metaclass__ = type
146@@ -1041,7 +1041,7 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
147 subscription.review_level = code_review_level
148 # Grant the subscriber access if they can't see the branch.
149 service = getUtility(IService, 'sharing')
150- _, branches, _, _ = service.getVisibleArtifacts(
151+ _, branches, _, _, _ = service.getVisibleArtifacts(
152 person, branches=[self], ignore_permissions=True)
153 if not branches:
154 service.ensureAccessGrants(
155diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
156index 74d94ec..589dcbf 100644
157--- a/lib/lp/code/model/gitrepository.py
158+++ b/lib/lp/code/model/gitrepository.py
159@@ -1002,7 +1002,7 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
160 subscription.review_level = code_review_level
161 # Grant the subscriber access if they can't see the repository.
162 service = getUtility(IService, "sharing")
163- _, _, repositories, _ = service.getVisibleArtifacts(
164+ _, _, repositories, _, _ = service.getVisibleArtifacts(
165 person, gitrepositories=[self], ignore_permissions=True)
166 if not repositories:
167 service.ensureAccessGrants(
168diff --git a/lib/lp/code/model/tests/test_branchsubscription.py b/lib/lp/code/model/tests/test_branchsubscription.py
169index 6a15cd0..b5b234e 100644
170--- a/lib/lp/code/model/tests/test_branchsubscription.py
171+++ b/lib/lp/code/model/tests/test_branchsubscription.py
172@@ -1,4 +1,4 @@
173-# Copyright 2010-2017 Canonical Ltd. This software is licensed under the
174+# Copyright 2010-2021 Canonical Ltd. This software is licensed under the
175 # GNU Affero General Public License version 3 (see the file LICENSE).
176
177 """Tests for the BranchSubscription model object."""
178@@ -134,7 +134,7 @@ class TestBranchSubscriptions(TestCaseWithFactory):
179 None, CodeReviewNotificationLevel.NOEMAIL, owner)
180 # The stacked on branch should be visible.
181 service = getUtility(IService, 'sharing')
182- _, visible_branches, _, _ = service.getVisibleArtifacts(
183+ _, visible_branches, _, _, _ = service.getVisibleArtifacts(
184 grantee, branches=[private_stacked_on_branch])
185 self.assertContentEqual(
186 [private_stacked_on_branch], visible_branches)
187@@ -162,7 +162,7 @@ class TestBranchSubscriptions(TestCaseWithFactory):
188 grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
189 None, CodeReviewNotificationLevel.NOEMAIL, owner)
190 # The stacked on branch should not be visible.
191- _, visible_branches, _, _ = service.getVisibleArtifacts(
192+ _, visible_branches, _, _, _ = service.getVisibleArtifacts(
193 grantee, branches=[private_stacked_on_branch])
194 self.assertContentEqual([], visible_branches)
195 self.assertIn(
196diff --git a/lib/lp/registry/model/sharingjob.py b/lib/lp/registry/model/sharingjob.py
197index 7d10fa9..81e6a9e 100644
198--- a/lib/lp/registry/model/sharingjob.py
199+++ b/lib/lp/registry/model/sharingjob.py
200@@ -1,4 +1,4 @@
201-# Copyright 2012-2020 Canonical Ltd. This software is licensed under the
202+# Copyright 2012-2021 Canonical Ltd. This software is licensed under the
203 # GNU Affero General Public License version 3 (see the file LICENSE).
204
205 """Job classes related to the sharing feature are in here."""
206@@ -91,6 +91,12 @@ from lp.services.job.model.job import (
207 )
208 from lp.services.job.runner import BaseRunnableJob
209 from lp.services.mail.sendmail import format_address_for_person
210+from lp.snappy.interfaces.snap import ISnap
211+from lp.snappy.model.snap import (
212+ get_snap_privacy_filter,
213+ Snap,
214+ )
215+from lp.snappy.model.snapsubscription import SnapSubscription
216
217
218 class SharingJobType(DBEnumeratedType):
219@@ -263,6 +269,7 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
220 bug_ids = []
221 branch_ids = []
222 gitrepository_ids = []
223+ snap_ids = []
224 specification_ids = []
225 if artifacts:
226 for artifact in artifacts:
227@@ -272,6 +279,8 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
228 branch_ids.append(artifact.id)
229 elif IGitRepository.providedBy(artifact):
230 gitrepository_ids.append(artifact.id)
231+ elif ISnap.providedBy(artifact):
232+ snap_ids.append(artifact.id)
233 elif ISpecification.providedBy(artifact):
234 specification_ids.append(artifact.id)
235 else:
236@@ -284,6 +293,7 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
237 'bug_ids': bug_ids,
238 'branch_ids': branch_ids,
239 'gitrepository_ids': gitrepository_ids,
240+ 'snap_ids': snap_ids,
241 'specification_ids': specification_ids,
242 'information_types': information_types,
243 'requestor.id': requestor.id
244@@ -320,6 +330,10 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
245 return self.metadata.get('gitrepository_ids', [])
246
247 @property
248+ def snap_ids(self):
249+ return self.metadata.get('snap_ids', [])
250+
251+ @property
252 def specification_ids(self):
253 return self.metadata.get('specification_ids', [])
254
255@@ -349,6 +363,7 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
256 'bug_ids': self.bug_ids,
257 'branch_ids': self.branch_ids,
258 'gitrepository_ids': self.gitrepository_ids,
259+ 'snap_ids': self.snap_ids,
260 'specification_ids': self.specification_ids,
261 'pillar': getattr(self.pillar, 'name', None),
262 'grantee': getattr(self.grantee, 'name', None)
263@@ -365,6 +380,7 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
264 bug_filters = []
265 branch_filters = []
266 gitrepository_filters = []
267+ snap_filters = []
268 specification_filters = []
269
270 if self.branch_ids:
271@@ -372,6 +388,8 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
272 if self.gitrepository_ids:
273 gitrepository_filters.append(GitRepository.id.is_in(
274 self.gitrepository_ids))
275+ if self.snap_ids:
276+ snap_filters.append(Snap.id.is_in(self.snap_ids))
277 if self.specification_ids:
278 specification_filters.append(Specification.id.is_in(
279 self.specification_ids))
280@@ -387,6 +405,8 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
281 gitrepository_filters.append(
282 GitRepository.information_type.is_in(
283 self.information_types))
284+ snap_filters.append(Snap._information_type.is_in(
285+ self.information_types))
286 specification_filters.append(
287 Specification.information_type.is_in(
288 self.information_types))
289@@ -423,6 +443,11 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
290 Select(
291 TeamParticipation.personID,
292 where=TeamParticipation.team == self.grantee)))
293+ snap_filters.append(
294+ In(SnapSubscription.person_id,
295+ Select(
296+ TeamParticipation.personID,
297+ where=TeamParticipation.team == self.grantee)))
298 specification_filters.append(
299 In(SpecificationSubscription.person_id,
300 Select(
301@@ -466,6 +491,16 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
302 for sub in gitrepository_subscriptions:
303 sub.repository.unsubscribe(
304 sub.person, self.requestor, ignore_permissions=True)
305+ if snap_filters:
306+ snap_filters.append(
307+ Not(get_snap_privacy_filter(SnapSubscription.person_id)))
308+ snap_subscriptions = IStore(SnapSubscription).using(
309+ SnapSubscription,
310+ Join(Snap, Snap.id == SnapSubscription.snap_id)
311+ ).find(SnapSubscription, *snap_filters).config(distinct=True)
312+ for sub in snap_subscriptions:
313+ sub.snap.unsubscribe(
314+ sub.person, self.requestor, ignore_permissions=True)
315 if specification_filters:
316 specification_filters.append(Not(*get_specification_privacy_filter(
317 SpecificationSubscription.person_id)))
318diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
319index 01e90da..4909604 100644
320--- a/lib/lp/registry/services/sharingservice.py
321+++ b/lib/lp/registry/services/sharingservice.py
322@@ -81,10 +81,7 @@ from lp.services.webapp.authorization import (
323 available_with_permission,
324 check_permission,
325 )
326-from lp.snappy.interfaces.snap import (
327- ISnap,
328- ISnapSet,
329- )
330+from lp.snappy.interfaces.snap import ISnapSet
331
332
333 @implementer(ISharingService)
334@@ -332,6 +329,7 @@ class SharingService:
335 bug_ids = []
336 branch_ids = []
337 gitrepository_ids = []
338+ snap_ids = []
339 for bug in bugs or []:
340 if (not ignore_permissions
341 and not check_permission('launchpad.View', bug)):
342@@ -344,9 +342,14 @@ class SharingService:
343 branch_ids.append(branch.id)
344 for gitrepository in gitrepositories or []:
345 if (not ignore_permissions
346- and not check_permission('launchpad.View', gitrepository)):
347+ and not check_permission('launchpad.View', gitrepository)):
348 raise Unauthorized
349 gitrepository_ids.append(gitrepository.id)
350+ for snap in snaps or []:
351+ if (not ignore_permissions
352+ and not check_permission('launchpad.View', snap)):
353+ raise Unauthorized
354+ snap_ids.append(snap.id)
355 for spec in specifications or []:
356 if (not ignore_permissions
357 and not check_permission('launchpad.View', spec)):
358@@ -376,6 +379,12 @@ class SharingService:
359 visible_gitrepositories = list(
360 wanted_gitrepositories.getRepositories())
361
362+ # Load the Snaps.
363+ visible_snaps = []
364+ if snap_ids:
365+ visible_snaps = list(getUtility(ISnapSet).findByIds(
366+ snap_ids, visible_by_user=person))
367+
368 # Load the specifications.
369 visible_specs = []
370 if specifications:
371@@ -387,7 +396,7 @@ class SharingService:
372
373 return (
374 visible_bugs, visible_branches, visible_gitrepositories,
375- visible_specs)
376+ visible_snaps, visible_specs)
377
378 def getInvisibleArtifacts(self, person, bugs=None, branches=None,
379 gitrepositories=None):
380@@ -800,12 +809,6 @@ class SharingService:
381 getUtility(IAccessArtifactGrantSource).revokeByArtifact(
382 artifacts_to_delete, [grantee])
383
384- # XXX: Pappacena 2021-02-05: snaps should not trigger this job,
385- # since we do not have a "SnapSubscription" yet.
386- artifacts = [i for i in artifacts if not ISnap.providedBy(i)]
387- if not artifacts:
388- return
389-
390 # Create a job to remove subscriptions for artifacts the grantee can no
391 # longer see.
392 return getUtility(IRemoveArtifactSubscriptionsJobSource).create(
393diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py
394index 9d0e07c..8d94723 100644
395--- a/lib/lp/registry/services/tests/test_sharingservice.py
396+++ b/lib/lp/registry/services/tests/test_sharingservice.py
397@@ -1,4 +1,4 @@
398-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
399+# Copyright 2012-2021 Canonical Ltd. This software is licensed under the
400 # GNU Affero General Public License version 3 (see the file LICENSE).
401
402 __metaclass__ = type
403@@ -1101,7 +1101,7 @@ class TestSharingService(TestCaseWithFactory):
404 # Check that grantees have expected access grants and subscriptions.
405 for person in [team_grantee, person_grantee]:
406 (visible_bugs, visible_branches, visible_gitrepositories,
407- visible_specs) = (
408+ visible_snaps, visible_specs) = (
409 self.service.getVisibleArtifacts(
410 person, bugs=bugs, branches=branches,
411 gitrepositories=gitrepositories,
412@@ -1133,7 +1133,7 @@ class TestSharingService(TestCaseWithFactory):
413 for bug in bugs or []:
414 self.assertNotIn(person, bug.getDirectSubscribers())
415 (visible_bugs, visible_branches, visible_gitrepositories,
416- visible_specs) = (
417+ visible_snaps, visible_specs) = (
418 self.service.getVisibleArtifacts(
419 person, bugs=bugs, branches=branches,
420 gitrepositories=gitrepositories))
421@@ -1783,7 +1783,8 @@ class TestSharingService(TestCaseWithFactory):
422 grantee, ignore, bugs, branches, gitrepositories, specs = (
423 self._make_Artifacts())
424 # Check the results.
425- shared_bugs, shared_branches, shared_gitrepositories, shared_specs = (
426+ (shared_bugs, shared_branches, shared_gitrepositories,
427+ shared_snaps, shared_specs) = (
428 self.service.getVisibleArtifacts(
429 grantee, bugs=bugs, branches=branches,
430 gitrepositories=gitrepositories, specifications=specs))
431@@ -1797,7 +1798,8 @@ class TestSharingService(TestCaseWithFactory):
432 # user has a policy grant for the pillar of the specification.
433 _, owner, bugs, branches, gitrepositories, specs = (
434 self._make_Artifacts())
435- shared_bugs, shared_branches, shared_gitrepositories, shared_specs = (
436+ (shared_bugs, shared_branches, shared_gitrepositories,
437+ shared_snaps, shared_specs) = (
438 self.service.getVisibleArtifacts(
439 owner, bugs=bugs, branches=branches,
440 gitrepositories=gitrepositories, specifications=specs))
441@@ -1840,7 +1842,8 @@ class TestSharingService(TestCaseWithFactory):
442 information_type=InformationType.USERDATA)
443 bugs.append(bug)
444
445- shared_bugs, shared_branches, shared_gitrepositories, shared_specs = (
446+ (shared_bugs, shared_branches, shared_gitrepositories,
447+ visible_snaps, shared_specs) = (
448 self.service.getVisibleArtifacts(grantee, bugs=bugs))
449 self.assertContentEqual(bugs, shared_bugs)
450
451@@ -1848,7 +1851,8 @@ class TestSharingService(TestCaseWithFactory):
452 for x in range(0, 5):
453 change_callback(bugs[x], owner)
454 # Check the results.
455- shared_bugs, shared_branches, shared_gitrepositories, shared_specs = (
456+ (shared_bugs, shared_branches, shared_gitrepositories,
457+ visible_snaps, shared_specs) = (
458 self.service.getVisibleArtifacts(grantee, bugs=bugs))
459 self.assertContentEqual(bugs[5:], shared_bugs)
460
461diff --git a/lib/lp/registry/tests/test_sharingjob.py b/lib/lp/registry/tests/test_sharingjob.py
462index 58fa5c1..32aec0f 100644
463--- a/lib/lp/registry/tests/test_sharingjob.py
464+++ b/lib/lp/registry/tests/test_sharingjob.py
465@@ -1,4 +1,4 @@
466-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
467+# Copyright 2012-2021 Canonical Ltd. This software is licensed under the
468 # GNU Affero General Public License version 3 (see the file LICENSE).
469
470 """Tests for SharingJobs."""
471@@ -39,6 +39,7 @@ from lp.services.features.testing import FeatureFixture
472 from lp.services.job.interfaces.job import JobStatus
473 from lp.services.job.tests import block_on_job
474 from lp.services.mail.sendmail import format_address_for_person
475+from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
476 from lp.testing import (
477 login_person,
478 person_logged_in,
479@@ -127,6 +128,16 @@ class SharingJobDerivedTestCase(TestCaseWithFactory):
480 'for gitrepository_ids=[%d], requestor=%s>'
481 % (gitrepository.id, requestor.name), repr(job))
482
483+ def test_repr_snaps(self):
484+ requestor = self.factory.makePerson()
485+ snap = self.factory.makeSnap()
486+ job = getUtility(IRemoveArtifactSubscriptionsJobSource).create(
487+ requestor, artifacts=[snap])
488+ self.assertEqual(
489+ '<REMOVE_ARTIFACT_SUBSCRIPTIONS job reconciling subscriptions '
490+ 'for requestor=%s, snap_ids=[%d]>'
491+ % (requestor.name, snap.id), repr(job))
492+
493 def test_repr_specifications(self):
494 requestor = self.factory.makePerson()
495 specification = self.factory.makeSpecification()
496@@ -241,9 +252,11 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
497 layer = CeleryJobLayer
498
499 def setUp(self):
500- self.useFixture(FeatureFixture({
501+ features = {
502 'jobs.celery.enabled_classes': 'RemoveArtifactSubscriptionsJob',
503- }))
504+ }
505+ features.update(SNAP_TESTING_FLAGS)
506+ self.useFixture(FeatureFixture(features))
507 super(RemoveArtifactSubscriptionsJobTestCase, self).setUp()
508
509 def test_create(self):
510@@ -315,6 +328,9 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
511 gitrepository = self.factory.makeGitRepository(
512 owner=owner, target=product,
513 information_type=InformationType.USERDATA)
514+ snap = self.factory.makeSnap(
515+ owner=owner, registrant=owner, project=product,
516+ information_type=InformationType.USERDATA)
517 specification = self.factory.makeSpecification(
518 owner=owner, product=product,
519 information_type=InformationType.PROPRIETARY)
520@@ -332,6 +348,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
521 gitrepository.subscribe(artifact_indirect_grantee,
522 BranchSubscriptionNotificationLevel.NOEMAIL, None,
523 CodeReviewNotificationLevel.NOEMAIL, owner)
524+ snap.subscribe(artifact_indirect_grantee, owner)
525 # Subscribing somebody to a specification does not automatically
526 # create an artifact grant.
527 spec_artifact = self.factory.makeAccessArtifact(specification)
528@@ -341,10 +358,11 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
529 specification.subscribe(artifact_indirect_grantee, owner)
530
531 # pick one of the concrete artifacts (bug, branch, Git repository,
532- # or spec) and subscribe the teams and persons.
533+ # snap, or spec) and subscribe the teams and persons.
534 concrete_artifact, get_pillars, get_subscribers = configure_test(
535- bug, branch, gitrepository, specification, policy_team_grantee,
536- policy_indirect_grantee, artifact_team_grantee, owner)
537+ bug, branch, gitrepository, snap, specification,
538+ policy_team_grantee, policy_indirect_grantee,
539+ artifact_team_grantee, owner)
540
541 # Subscribing policy_team_grantee has created an artifact grant so we
542 # need to revoke that to test the job.
543@@ -377,6 +395,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
544 self.assertIn(artifact_indirect_grantee, bug.getDirectSubscribers())
545 self.assertIn(artifact_indirect_grantee, branch.subscribers)
546 self.assertIn(artifact_indirect_grantee, gitrepository.subscribers)
547+ self.assertIn(artifact_indirect_grantee, snap.subscribers)
548 self.assertIn(artifact_indirect_grantee,
549 removeSecurityProxy(specification).subscribers)
550
551@@ -389,7 +408,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
552 return removeSecurityProxy(
553 concrete_artifact).getDirectSubscribers()
554
555- def configure_test(bug, branch, gitrepository, specification,
556+ def configure_test(bug, branch, gitrepository, snap, specification,
557 policy_team_grantee, policy_indirect_grantee,
558 artifact_team_grantee, owner):
559 concrete_artifact = bug
560@@ -409,7 +428,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
561 def get_subscribers(concrete_artifact):
562 return concrete_artifact.subscribers
563
564- def configure_test(bug, branch, gitrepository, specification,
565+ def configure_test(bug, branch, gitrepository, snap, specification,
566 policy_team_grantee, policy_indirect_grantee,
567 artifact_team_grantee, owner):
568 concrete_artifact = branch
569@@ -438,7 +457,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
570 def get_subscribers(concrete_artifact):
571 return concrete_artifact.subscribers
572
573- def configure_test(bug, branch, gitrepository, specification,
574+ def configure_test(bug, branch, gitrepository, snap, specification,
575 policy_team_grantee, policy_indirect_grantee,
576 artifact_team_grantee, owner):
577 concrete_artifact = gitrepository
578@@ -459,6 +478,26 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
579 self._assert_artifact_change_unsubscribes(
580 change_callback, configure_test)
581
582+ def _assert_snap_change_unsubscribes(self, change_callback):
583+
584+ def get_pillars(concrete_artifact):
585+ return [concrete_artifact.project]
586+
587+ def get_subscribers(concrete_artifact):
588+ return concrete_artifact.subscribers
589+
590+ def configure_test(bug, branch, gitrepository, snap, specification,
591+ policy_team_grantee, policy_indirect_grantee,
592+ artifact_team_grantee, owner):
593+ concrete_artifact = snap
594+ snap.subscribe(policy_team_grantee, owner)
595+ snap.subscribe(policy_indirect_grantee, owner)
596+ snap.subscribe(artifact_team_grantee, owner)
597+ return concrete_artifact, get_pillars, get_subscribers
598+
599+ self._assert_artifact_change_unsubscribes(
600+ change_callback, configure_test)
601+
602 def _assert_specification_change_unsubscribes(self, change_callback):
603
604 def get_pillars(concrete_artifact):
605@@ -467,7 +506,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
606 def get_subscribers(concrete_artifact):
607 return concrete_artifact.subscribers
608
609- def configure_test(bug, branch, gitrepository, specification,
610+ def configure_test(bug, branch, gitrepository, snap, specification,
611 policy_team_grantee, policy_indirect_grantee,
612 artifact_team_grantee, owner):
613 naked_spec = removeSecurityProxy(specification)
614@@ -496,6 +535,13 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
615
616 self._assert_gitrepository_change_unsubscribes(change_information_type)
617
618+ def test_change_information_type_snap(self):
619+ def change_information_type(snap):
620+ removeSecurityProxy(snap).information_type = (
621+ InformationType.PRIVATESECURITY)
622+
623+ self._assert_snap_change_unsubscribes(change_information_type)
624+
625 def test_change_information_type_specification(self):
626 def change_information_type(specification):
627 removeSecurityProxy(specification).information_type = (
628diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
629index 31708c3..71f534b 100644
630--- a/lib/lp/snappy/interfaces/snap.py
631+++ b/lib/lp/snappy/interfaces/snap.py
632@@ -571,6 +571,10 @@ class ISnapView(Interface):
633 # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
634 value_type=Reference(schema=Interface), readonly=True)))
635
636+ subscribers = CollectionField(
637+ title=_("Persons subscribed to this snap recipe."),
638+ readonly=True, value_type=Reference(IPerson))
639+
640 def visibleByUser(user):
641 """Can the specified user see this snap recipe?"""
642
643diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
644index 141f27a..ba89613 100644
645--- a/lib/lp/snappy/model/snap.py
646+++ b/lib/lp/snappy/model/snap.py
647@@ -5,6 +5,7 @@ from __future__ import absolute_import, print_function, unicode_literals
648
649 __metaclass__ = type
650 __all__ = [
651+ 'get_snap_privacy_filter',
652 'Snap',
653 ]
654
655@@ -26,6 +27,7 @@ from storm.expr import (
656 And,
657 Coalesce,
658 Desc,
659+ Exists,
660 Join,
661 LeftJoin,
662 Not,
663@@ -71,6 +73,7 @@ from lp.app.errors import (
664 SubscriptionPrivacyViolation,
665 UserCannotUnsubscribePerson,
666 )
667+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
668 from lp.app.interfaces.security import IAuthorization
669 from lp.app.interfaces.services import IService
670 from lp.buildmaster.enums import BuildStatus
671@@ -132,6 +135,7 @@ from lp.registry.model.accesspolicy import (
672 reconcile_access_for_artifact,
673 )
674 from lp.registry.model.distroseries import DistroSeries
675+from lp.registry.model.person import Person
676 from lp.registry.model.series import ACTIVE_STATUSES
677 from lp.registry.model.teammembership import TeamParticipation
678 from lp.services.config import config
679@@ -1165,6 +1169,13 @@ class Snap(Storm, WebhookTargetMixin):
680 person.is_team and
681 person.anyone_can_join())
682
683+ @property
684+ def subscribers(self):
685+ return Store.of(self).find(
686+ Person,
687+ SnapSubscription.person_id == Person.id,
688+ SnapSubscription.snap == self)
689+
690 def subscribe(self, person, subscribed_by, ignore_permissions=False):
691 """See `ISnap`."""
692 if not self._userCanBeSubscribed(person):
693@@ -1177,9 +1188,12 @@ class Snap(Storm, WebhookTargetMixin):
694 person=person, snap=self, subscribed_by=subscribed_by)
695 Store.of(subscription).flush()
696 service = getUtility(IService, "sharing")
697- service.ensureAccessGrants(
698- [person], subscribed_by, snaps=[self],
699- ignore_permissions=ignore_permissions)
700+ _, _, _, snaps, _ = service.getVisibleArtifacts(
701+ person, snaps=[self], ignore_permissions=True)
702+ if not snaps:
703+ service.ensureAccessGrants(
704+ [person], subscribed_by, snaps=[self],
705+ ignore_permissions=ignore_permissions)
706
707 def unsubscribe(self, person, unsubscribed_by, ignore_permissions=False):
708 """See `ISnap`."""
709@@ -1421,9 +1435,12 @@ class SnapSet:
710 expressions.append(Snap.owner == owner)
711 return IStore(Snap).find(Snap, *expressions)
712
713- def findByIds(self, snap_ids):
714+ def findByIds(self, snap_ids, visible_by_user=None):
715 """See `ISnapSet`."""
716- return IStore(ISnap).find(Snap, Snap.id.is_in(snap_ids))
717+ clauses = [Snap.id.is_in(snap_ids)]
718+ if visible_by_user is not None:
719+ clauses.append(get_snap_privacy_filter(visible_by_user))
720+ return IStore(Snap).find(Snap, *clauses)
721
722 def findByOwner(self, owner):
723 """See `ISnapSet`."""
724@@ -1694,9 +1711,11 @@ class SnapStoreSecretsEncryptedContainer(NaClEncryptedContainerBase):
725
726
727 def get_snap_privacy_filter(user):
728- """Returns the filter for all private Snaps that the given user is
729- subscribed to (that is, has access without being directly an owner).
730+ """Returns the filter for all Snaps that the given user has access to,
731+ including private snaps where the user has proper permission.
732
733+ :param user: An IPerson, or a class attribute that references an IPerson
734+ in the database.
735 :return: A storm condition.
736 """
737 # XXX pappacena 2021-02-12: Once we do the migration to back fill
738@@ -1707,10 +1726,6 @@ def get_snap_privacy_filter(user):
739 if user is None:
740 return private_snap == False
741
742- roles = IPersonRoles(user)
743- if roles.in_admin or roles.in_commercial_admin:
744- return True
745-
746 artifact_grant_query = Coalesce(
747 ArrayIntersects(
748 SQL("%s.access_grants" % Snap.__storm_table__),
749@@ -1732,4 +1747,13 @@ def get_snap_privacy_filter(user):
750 where=(TeamParticipation.person == user)
751 )), False)
752
753- return Or(private_snap == False, artifact_grant_query, policy_grant_query)
754+ admin_team_id = getUtility(ILaunchpadCelebrities).admin.id
755+ user_is_admin = Exists(Select(
756+ TeamParticipation.personID,
757+ tables=[TeamParticipation],
758+ where=And(
759+ TeamParticipation.teamID == admin_team_id,
760+ TeamParticipation.person == user)))
761+ return Or(
762+ private_snap == False, artifact_grant_query, policy_grant_query,
763+ user_is_admin)
764diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
765index 39362b3..04ed945 100644
766--- a/lib/lp/snappy/tests/test_snap.py
767+++ b/lib/lp/snappy/tests/test_snap.py
768@@ -3102,8 +3102,7 @@ class TestSnapWebservice(TestCaseWithFactory):
769 ws_snaps = [
770 self.webservice.getAbsoluteUrl(api_url(snap))
771 for snap in snaps]
772- commercial_admin = (
773- getUtility(ILaunchpadCelebrities).commercial_admin.teamowner)
774+ admin = getUtility(ILaunchpadCelebrities).admin.teamowner
775 logout()
776
777 # Anonymous requests can only see public snaps.
778@@ -3141,15 +3140,15 @@ class TestSnapWebservice(TestCaseWithFactory):
779 [entry["self_link"] for entry in response.jsonBody()["entries"]])
780
781 # Admins can see all snaps with this URL.
782- commercial_admin_webservice = webservice_for_person(
783- commercial_admin, permission=OAuthPermission.READ_PRIVATE)
784- response = commercial_admin_webservice.named_get(
785+ admin_webservice = webservice_for_person(
786+ admin, permission=OAuthPermission.READ_PRIVATE)
787+ response = admin_webservice.named_get(
788 "/+snaps", "findByURL", url=urls[0], api_version="devel")
789 self.assertEqual(200, response.status)
790 self.assertContentEqual(
791 ws_snaps[:4],
792 [entry["self_link"] for entry in response.jsonBody()["entries"]])
793- response = commercial_admin_webservice.named_get(
794+ response = admin_webservice.named_get(
795 "/+snaps", "findByURL", url=urls[0], owner=person_urls[0],
796 api_version="devel")
797 self.assertEqual(200, response.status)
798@@ -3180,8 +3179,7 @@ class TestSnapWebservice(TestCaseWithFactory):
799 ws_snaps = [
800 self.webservice.getAbsoluteUrl(api_url(snap))
801 for snap in snaps]
802- commercial_admin = (
803- getUtility(ILaunchpadCelebrities).commercial_admin.teamowner)
804+ admin = getUtility(ILaunchpadCelebrities).admin.teamowner
805 logout()
806 prefix = "https://git.example.org/foo/"
807
808@@ -3222,16 +3220,16 @@ class TestSnapWebservice(TestCaseWithFactory):
809 [entry["self_link"] for entry in response.jsonBody()["entries"]])
810
811 # Admins can see all snaps with this URL prefix.
812- commercial_admin_webservice = webservice_for_person(
813- commercial_admin, permission=OAuthPermission.READ_PRIVATE)
814- response = commercial_admin_webservice.named_get(
815+ admin_webservice = webservice_for_person(
816+ admin, permission=OAuthPermission.READ_PRIVATE)
817+ response = admin_webservice.named_get(
818 "/+snaps", "findByURLPrefix", url_prefix=prefix,
819 api_version="devel")
820 self.assertEqual(200, response.status)
821 self.assertContentEqual(
822 ws_snaps[:8],
823 [entry["self_link"] for entry in response.jsonBody()["entries"]])
824- response = commercial_admin_webservice.named_get(
825+ response = admin_webservice.named_get(
826 "/+snaps", "findByURLPrefix", url_prefix=prefix,
827 owner=person_urls[0], api_version="devel")
828 self.assertEqual(200, response.status)
829@@ -3264,8 +3262,7 @@ class TestSnapWebservice(TestCaseWithFactory):
830 ws_snaps = [
831 self.webservice.getAbsoluteUrl(api_url(snap))
832 for snap in snaps]
833- commercial_admin = (
834- getUtility(ILaunchpadCelebrities).commercial_admin.teamowner)
835+ admin = getUtility(ILaunchpadCelebrities).admin.teamowner
836 logout()
837 prefixes = [
838 "https://git.example.org/foo/", "https://git.example.org/bar/"]
839@@ -3307,16 +3304,16 @@ class TestSnapWebservice(TestCaseWithFactory):
840 [entry["self_link"] for entry in response.jsonBody()["entries"]])
841
842 # Admins can see all snaps with any of these URL prefixes.
843- commercial_admin_webservice = webservice_for_person(
844- commercial_admin, permission=OAuthPermission.READ_PRIVATE)
845- response = commercial_admin_webservice.named_get(
846+ admin_webservice = webservice_for_person(
847+ admin, permission=OAuthPermission.READ_PRIVATE)
848+ response = admin_webservice.named_get(
849 "/+snaps", "findByURLPrefixes", url_prefixes=prefixes,
850 api_version="devel")
851 self.assertEqual(200, response.status)
852 self.assertContentEqual(
853 ws_snaps[:16],
854 [entry["self_link"] for entry in response.jsonBody()["entries"]])
855- response = commercial_admin_webservice.named_get(
856+ response = admin_webservice.named_get(
857 "/+snaps", "findByURLPrefixes", url_prefixes=prefixes,
858 owner=person_urls[0], api_version="devel")
859 self.assertEqual(200, response.status)
860@@ -3341,8 +3338,7 @@ class TestSnapWebservice(TestCaseWithFactory):
861 ws_snaps = [
862 self.webservice.getAbsoluteUrl(api_url(snap))
863 for snap in snaps]
864- commercial_admin = (
865- getUtility(ILaunchpadCelebrities).commercial_admin.teamowner)
866+ admin = getUtility(ILaunchpadCelebrities).admin.teamowner
867 logout()
868
869 # Anonymous requests can only see public snaps.
870@@ -3382,16 +3378,16 @@ class TestSnapWebservice(TestCaseWithFactory):
871 [entry["self_link"] for entry in response.jsonBody()["entries"]])
872
873 # Admins can see all snaps with this store name.
874- commercial_admin_webservice = webservice_for_person(
875- commercial_admin, permission=OAuthPermission.READ_PRIVATE)
876- response = commercial_admin_webservice.named_get(
877+ admin_webservice = webservice_for_person(
878+ admin, permission=OAuthPermission.READ_PRIVATE)
879+ response = admin_webservice.named_get(
880 "/+snaps", "findByStoreName", store_name=store_names[0],
881 api_version="devel")
882 self.assertEqual(200, response.status)
883 self.assertContentEqual(
884 ws_snaps[:4],
885 [entry["self_link"] for entry in response.jsonBody()["entries"]])
886- response = commercial_admin_webservice.named_get(
887+ response = admin_webservice.named_get(
888 "/+snaps", "findByStoreName", store_name=store_names[0],
889 owner=person_urls[0], api_version="devel")
890 self.assertEqual(200, response.status)