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

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: de8de18f0a804ec918fa0d246e66a5937b14f40b
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:snap-pillar-accesspolicy
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:snap-pillar-ui
Diff against target: 652 lines (+205/-56)
9 files modified
lib/lp/registry/browser/pillar.py (+1/-1)
lib/lp/registry/interfaces/accesspolicy.py (+1/-0)
lib/lp/registry/interfaces/sharingservice.py (+19/-6)
lib/lp/registry/model/accesspolicy.py (+16/-8)
lib/lp/registry/services/sharingservice.py (+42/-13)
lib/lp/registry/services/tests/test_sharingservice.py (+1/-1)
lib/lp/snappy/interfaces/snap.py (+9/-0)
lib/lp/snappy/model/snap.py (+82/-27)
lib/lp/snappy/tests/test_snap.py (+34/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+397692@code.launchpad.net

Commit message

Adding Snap as an artifact for sharing services

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/registry/browser/pillar.py b/lib/lp/registry/browser/pillar.py
2index dd83648..52b1554 100644
3--- a/lib/lp/registry/browser/pillar.py
4+++ b/lib/lp/registry/browser/pillar.py
5@@ -439,7 +439,7 @@ class PillarPersonSharingView(LaunchpadView):
6 def _loadSharedArtifacts(self):
7 # As a concrete can by linked via more than one policy, we use sets to
8 # filter out dupes.
9- (self.bugtasks, self.branches, self.gitrepositories,
10+ (self.bugtasks, self.branches, self.gitrepositories, self.snaps,
11 self.specifications) = (
12 self.sharing_service.getSharedArtifacts(
13 self.pillar, self.person, self.user))
14diff --git a/lib/lp/registry/interfaces/accesspolicy.py b/lib/lp/registry/interfaces/accesspolicy.py
15index 2c454a7..0e2c8c8 100644
16--- a/lib/lp/registry/interfaces/accesspolicy.py
17+++ b/lib/lp/registry/interfaces/accesspolicy.py
18@@ -36,6 +36,7 @@ class IAccessArtifact(Interface):
19 bug_id = Attribute("bug_id")
20 branch_id = Attribute("branch_id")
21 gitrepository_id = Attribute("gitrepository_id")
22+ snap_id = Attribute("snap_id")
23 specification_id = Attribute("specification_id")
24
25
26diff --git a/lib/lp/registry/interfaces/sharingservice.py b/lib/lp/registry/interfaces/sharingservice.py
27index 386fef4..f6c39fb 100644
28--- a/lib/lp/registry/interfaces/sharingservice.py
29+++ b/lib/lp/registry/interfaces/sharingservice.py
30@@ -1,4 +1,4 @@
31-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
32+# Copyright 2012-2021 Canonical Ltd. This software is licensed under the
33 # GNU Affero General Public License version 3 (see the file LICENSE).
34
35 """Interfaces for sharing service."""
36@@ -45,7 +45,7 @@ from lp.registry.interfaces.distribution import IDistribution
37 from lp.registry.interfaces.person import IPerson
38 from lp.registry.interfaces.pillar import IPillar
39 from lp.registry.interfaces.product import IProduct
40-
41+from lp.snappy.interfaces.snap import ISnap
42
43 # XXX 2012-02-24 wallyworld bug 939910
44 # Need to export for version 'beta' even though we only want to use it in
45@@ -177,7 +177,8 @@ class ISharingService(IService):
46 """
47
48 def getVisibleArtifacts(person, bugs=None, branches=None,
49- gitrepositories=None, specifications=None):
50+ gitrepositories=None, snaps=None,
51+ specifications=None):
52 """Return the artifacts shared with person.
53
54 Given lists of artifacts, return those a person has access to either
55@@ -188,6 +189,7 @@ class ISharingService(IService):
56 :param branches: the branches to check for which a person has access.
57 :param gitrepositories: the Git repositories to check for which a
58 person has access.
59+ :param snaps: the snap recipes to check for which a person has access.
60 :param specifications: the specifications to check for which a
61 person has access.
62 :return: a collection of artifacts the person can see.
63@@ -328,12 +330,16 @@ class ISharingService(IService):
64 gitrepositories=List(
65 Reference(schema=IGitRepository),
66 title=_('Git repositories'), required=False),
67+ snaps=List(
68+ Reference(schema=ISnap),
69+ title=_('Snap recipes'), required=False),
70 specifications=List(
71 Reference(schema=ISpecification), title=_('Specifications'),
72 required=False))
73 @operation_for_version('devel')
74 def revokeAccessGrants(pillar, grantee, user, bugs=None, branches=None,
75- gitrepositories=None, specifications=None):
76+ gitrepositories=None, snaps=None,
77+ specifications=None):
78 """Remove a grantee's access to the specified artifacts.
79
80 :param pillar: the pillar from which to remove access
81@@ -342,6 +348,7 @@ class ISharingService(IService):
82 :param bugs: the bugs for which to revoke access
83 :param branches: the branches for which to revoke access
84 :param gitrepositories: the Git repositories for which to revoke access
85+ :param snaps: The snap recipes for which to revoke access
86 :param specifications: the specifications for which to revoke access
87 """
88
89@@ -357,10 +364,15 @@ class ISharingService(IService):
90 Reference(schema=IBranch), title=_('Branches'), required=False),
91 gitrepositories=List(
92 Reference(schema=IGitRepository),
93- title=_('Git repositories'), required=False))
94+ title=_('Git repositories'), required=False),
95+ snaps=List(
96+ Reference(schema=ISnap),
97+ title=_('Snap recipes'), required=False)
98+ )
99 @operation_for_version('devel')
100 def ensureAccessGrants(grantees, user, bugs=None, branches=None,
101- gitrepositories=None, specifications=None):
102+ gitrepositories=None, snaps=None,
103+ specifications=None):
104 """Ensure a grantee has an access grant to the specified artifacts.
105
106 :param grantees: the people or teams for whom to grant access
107@@ -368,6 +380,7 @@ class ISharingService(IService):
108 :param bugs: the bugs for which to grant access
109 :param branches: the branches for which to grant access
110 :param gitrepositories: the Git repositories for which to grant access
111+ :param snaps: the snap recipes for which to grant access
112 :param specifications: the specifications for which to grant access
113 """
114
115diff --git a/lib/lp/registry/model/accesspolicy.py b/lib/lp/registry/model/accesspolicy.py
116index 2e8fddf..7bb5e9f 100644
117--- a/lib/lp/registry/model/accesspolicy.py
118+++ b/lib/lp/registry/model/accesspolicy.py
119@@ -1,4 +1,4 @@
120-# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
121+# Copyright 2011-2021 Canonical Ltd. This software is licensed under the
122 # GNU Affero General Public License version 3 (see the file LICENSE).
123
124 """Model classes for pillar and artifact access policies."""
125@@ -98,6 +98,8 @@ class AccessArtifact(StormBase):
126 branch = Reference(branch_id, 'Branch.id')
127 gitrepository_id = Int(name='gitrepository')
128 gitrepository = Reference(gitrepository_id, 'GitRepository.id')
129+ snap_id = Int(name="snap")
130+ snap = Reference(snap_id, 'Snap.id')
131 specification_id = Int(name='specification')
132 specification = Reference(specification_id, 'Specification.id')
133
134@@ -114,12 +116,15 @@ class AccessArtifact(StormBase):
135 from lp.bugs.interfaces.bug import IBug
136 from lp.code.interfaces.branch import IBranch
137 from lp.code.interfaces.gitrepository import IGitRepository
138+ from lp.snappy.interfaces.snap import ISnap
139 if IBug.providedBy(concrete_artifact):
140 col = cls.bug
141 elif IBranch.providedBy(concrete_artifact):
142 col = cls.branch
143 elif IGitRepository.providedBy(concrete_artifact):
144 col = cls.gitrepository
145+ elif ISnap.providedBy(concrete_artifact):
146+ col = cls.snap
147 elif ISpecification.providedBy(concrete_artifact):
148 col = cls.specification
149 else:
150@@ -143,6 +148,7 @@ class AccessArtifact(StormBase):
151 from lp.bugs.interfaces.bug import IBug
152 from lp.code.interfaces.branch import IBranch
153 from lp.code.interfaces.gitrepository import IGitRepository
154+ from lp.snappy.interfaces.snap import ISnap
155
156 existing = list(cls.find(concrete_artifacts))
157 if len(existing) == len(concrete_artifacts):
158@@ -156,18 +162,20 @@ class AccessArtifact(StormBase):
159 insert_values = []
160 for concrete in needed:
161 if IBug.providedBy(concrete):
162- insert_values.append((concrete, None, None, None))
163+ insert_values.append((concrete, None, None, None, None))
164 elif IBranch.providedBy(concrete):
165- insert_values.append((None, concrete, None, None))
166+ insert_values.append((None, concrete, None, None, None))
167 elif IGitRepository.providedBy(concrete):
168- insert_values.append((None, None, concrete, None))
169+ insert_values.append((None, None, concrete, None, None))
170+ elif ISnap.providedBy(concrete):
171+ insert_values.append((None, None, None, concrete, None))
172 elif ISpecification.providedBy(concrete):
173- insert_values.append((None, None, None, concrete))
174+ insert_values.append((None, None, None, None, concrete))
175 else:
176 raise ValueError("%r is not a supported artifact" % concrete)
177- new = create(
178- (cls.bug, cls.branch, cls.gitrepository, cls.specification),
179- insert_values, get_objects=True)
180+ columns = (cls.bug, cls.branch, cls.gitrepository, cls.snap,
181+ cls.specification)
182+ new = create(columns, insert_values, get_objects=True)
183 return list(existing) + new
184
185 @classmethod
186diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
187index 09744ee..01e90da 100644
188--- a/lib/lp/registry/services/sharingservice.py
189+++ b/lib/lp/registry/services/sharingservice.py
190@@ -1,4 +1,4 @@
191-# Copyright 2012-2015 Canonical Ltd. This software is licensed under the
192+# Copyright 2012-2021 Canonical Ltd. This software is licensed under the
193 # GNU Affero General Public License version 3 (see the file LICENSE).
194
195 """Classes for pillar and artifact sharing service."""
196@@ -81,6 +81,10 @@ from lp.services.webapp.authorization import (
197 available_with_permission,
198 check_permission,
199 )
200+from lp.snappy.interfaces.snap import (
201+ ISnap,
202+ ISnapSet,
203+ )
204
205
206 @implementer(ISharingService)
207@@ -197,11 +201,12 @@ class SharingService:
208 @available_with_permission('launchpad.Driver', 'pillar')
209 def getSharedArtifacts(self, pillar, person, user, include_bugs=True,
210 include_branches=True, include_gitrepositories=True,
211- include_specifications=True):
212+ include_snaps=True, include_specifications=True):
213 """See `ISharingService`."""
214 bug_ids = set()
215 branch_ids = set()
216 gitrepository_ids = set()
217+ snap_ids = set()
218 specification_ids = set()
219 for artifact in self.getArtifactGrantsForPersonOnPillar(
220 pillar, person):
221@@ -211,6 +216,8 @@ class SharingService:
222 branch_ids.add(artifact.branch_id)
223 elif artifact.gitrepository_id and include_gitrepositories:
224 gitrepository_ids.add(artifact.gitrepository_id)
225+ elif artifact.snap_id and include_snaps:
226+ snap_ids.add(artifact.snap_id)
227 elif artifact.specification_id and include_specifications:
228 specification_ids.add(artifact.specification_id)
229
230@@ -234,16 +241,20 @@ class SharingService:
231 wanted_gitrepositories = all_gitrepositories.visibleByUser(
232 user).withIds(*gitrepository_ids)
233 gitrepositories = list(wanted_gitrepositories.getRepositories())
234+ snaps = []
235+ if snap_ids:
236+ all_snaps = getUtility(ISnapSet)
237+ snaps = all_snaps.findByIds(snap_ids)
238 specifications = []
239 if specification_ids:
240 specifications = load(Specification, specification_ids)
241
242- return bugtasks, branches, gitrepositories, specifications
243+ return bugtasks, branches, gitrepositories, snaps, specifications
244
245 @available_with_permission('launchpad.Driver', 'pillar')
246 def getSharedBugs(self, pillar, person, user):
247 """See `ISharingService`."""
248- bugtasks, _, _, _ = self.getSharedArtifacts(
249+ bugtasks, _, _, _, _ = self.getSharedArtifacts(
250 pillar, person, user, include_branches=False,
251 include_gitrepositories=False, include_specifications=False)
252 return bugtasks
253@@ -251,7 +262,7 @@ class SharingService:
254 @available_with_permission('launchpad.Driver', 'pillar')
255 def getSharedBranches(self, pillar, person, user):
256 """See `ISharingService`."""
257- _, branches, _, _ = self.getSharedArtifacts(
258+ _, branches, _, _, _ = self.getSharedArtifacts(
259 pillar, person, user, include_bugs=False,
260 include_gitrepositories=False, include_specifications=False)
261 return branches
262@@ -259,15 +270,23 @@ class SharingService:
263 @available_with_permission('launchpad.Driver', 'pillar')
264 def getSharedGitRepositories(self, pillar, person, user):
265 """See `ISharingService`."""
266- _, _, gitrepositories, _ = self.getSharedArtifacts(
267+ _, _, gitrepositories, _, _ = self.getSharedArtifacts(
268 pillar, person, user, include_bugs=False, include_branches=False,
269 include_specifications=False)
270 return gitrepositories
271
272 @available_with_permission('launchpad.Driver', 'pillar')
273+ def getSharedSnaps(self, pillar, person, user):
274+ """See `ISharingService`."""
275+ _, _, _, snaps, _ = self.getSharedArtifacts(
276+ pillar, person, user, include_bugs=False, include_branches=False,
277+ include_gitrepositories=False)
278+ return snaps
279+
280+ @available_with_permission('launchpad.Driver', 'pillar')
281 def getSharedSpecifications(self, pillar, person, user):
282 """See `ISharingService`."""
283- _, _, _, specifications = self.getSharedArtifacts(
284+ _, _, _, _, specifications = self.getSharedArtifacts(
285 pillar, person, user, include_bugs=False, include_branches=False,
286 include_gitrepositories=False)
287 return specifications
288@@ -307,8 +326,8 @@ class SharingService:
289 In(Specification.id, spec_ids)))
290
291 def getVisibleArtifacts(self, person, bugs=None, branches=None,
292- gitrepositories=None, specifications=None,
293- ignore_permissions=False):
294+ gitrepositories=None, snaps=None,
295+ specifications=None, ignore_permissions=False):
296 """See `ISharingService`."""
297 bug_ids = []
298 branch_ids = []
299@@ -752,11 +771,11 @@ class SharingService:
300
301 @available_with_permission('launchpad.Edit', 'pillar')
302 def revokeAccessGrants(self, pillar, grantee, user, bugs=None,
303- branches=None, gitrepositories=None,
304+ branches=None, gitrepositories=None, snaps=None,
305 specifications=None):
306 """See `ISharingService`."""
307
308- if (not bugs and not branches and not gitrepositories and
309+ if (not bugs and not branches and not gitrepositories and not snaps and
310 not specifications):
311 raise ValueError(
312 "Either bugs, branches, gitrepositories, or specifications "
313@@ -769,6 +788,8 @@ class SharingService:
314 artifacts.extend(branches)
315 if gitrepositories:
316 artifacts.extend(gitrepositories)
317+ if snaps:
318+ artifacts.extend(snaps)
319 if specifications:
320 artifacts.extend(specifications)
321 # Find the access artifacts associated with the bugs, branches, Git
322@@ -779,14 +800,20 @@ class SharingService:
323 getUtility(IAccessArtifactGrantSource).revokeByArtifact(
324 artifacts_to_delete, [grantee])
325
326+ # XXX: Pappacena 2021-02-05: snaps should not trigger this job,
327+ # since we do not have a "SnapSubscription" yet.
328+ artifacts = [i for i in artifacts if not ISnap.providedBy(i)]
329+ if not artifacts:
330+ return
331+
332 # Create a job to remove subscriptions for artifacts the grantee can no
333 # longer see.
334 return getUtility(IRemoveArtifactSubscriptionsJobSource).create(
335 user, artifacts, grantee=grantee, pillar=pillar)
336
337 def ensureAccessGrants(self, grantees, user, bugs=None, branches=None,
338- gitrepositories=None, specifications=None,
339- ignore_permissions=False):
340+ gitrepositories=None, snaps=None,
341+ specifications=None, ignore_permissions=False):
342 """See `ISharingService`."""
343
344 artifacts = []
345@@ -796,6 +823,8 @@ class SharingService:
346 artifacts.extend(branches)
347 if gitrepositories:
348 artifacts.extend(gitrepositories)
349+ if snaps:
350+ artifacts.extend(snaps)
351 if specifications:
352 artifacts.extend(specifications)
353 if not ignore_permissions:
354diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py
355index 50ce8c6..9d0e07c 100644
356--- a/lib/lp/registry/services/tests/test_sharingservice.py
357+++ b/lib/lp/registry/services/tests/test_sharingservice.py
358@@ -1459,7 +1459,7 @@ class TestSharingService(TestCaseWithFactory):
359
360 # Check the results.
361 (shared_bugtasks, shared_branches, shared_gitrepositories,
362- shared_specs) = (
363+ shared_snaps, shared_specs) = (
364 self.service.getSharedArtifacts(product, grantee, user))
365 self.assertContentEqual(bug_tasks[:9], shared_bugtasks)
366 self.assertContentEqual(branches[:9], shared_branches)
367diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
368index 8ac5838..df95533 100644
369--- a/lib/lp/snappy/interfaces/snap.py
370+++ b/lib/lp/snappy/interfaces/snap.py
371@@ -874,6 +874,12 @@ class ISnapAdminAttributes(Interface):
372 "Allow access to external network resources via a proxy. "
373 "Resources hosted on Launchpad itself are always allowed.")))
374
375+ def subscribe(person, subscribed_by):
376+ """Subscribe a person to this snap recipe."""
377+
378+ def unsubscribe(person, unsubscribed_by):
379+ """Unsubscribe a person to this snap recipe."""
380+
381
382 # XXX cjwatson 2015-07-17 bug=760849: "beta" is a lie to get WADL
383 # generation working. Individual attributes must set their version to
384@@ -919,6 +925,9 @@ class ISnapSet(Interface):
385 def getSnapSuggestedPrivacy(owner, branch=None, git_ref=None):
386 """Which privacy a Snap should have based on its creation params."""
387
388+ def findByIds(snap_ids):
389+ """Return all snap packages with the given ids."""
390+
391 def isValidInformationType(
392 information_type, owner, branch=None, git_ref=None):
393 """Whether or not the information type context is valid."""
394diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
395index b66bb75..2d06276 100644
396--- a/lib/lp/snappy/model/snap.py
397+++ b/lib/lp/snappy/model/snap.py
398@@ -24,7 +24,9 @@ import six
399 from six.moves.urllib.parse import urlsplit
400 from storm.expr import (
401 And,
402+ Coalesce,
403 Desc,
404+ Join,
405 LeftJoin,
406 Not,
407 Or,
408@@ -67,6 +69,7 @@ from lp.app.enums import (
409 )
410 from lp.app.errors import IncompatibleArguments
411 from lp.app.interfaces.security import IAuthorization
412+from lp.app.interfaces.services import IService
413 from lp.buildmaster.enums import BuildStatus
414 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
415 from lp.buildmaster.model.builder import Builder
416@@ -106,6 +109,7 @@ from lp.code.model.branchnamespace import (
417 from lp.code.model.gitcollection import GenericGitCollection
418 from lp.code.model.gitrepository import GitRepository
419 from lp.registry.errors import PrivatePersonLinkageError
420+from lp.registry.interfaces.accesspolicy import IAccessArtifactSource
421 from lp.registry.interfaces.person import (
422 IPerson,
423 IPersonSet,
424@@ -117,6 +121,7 @@ from lp.registry.interfaces.role import (
425 IHasOwner,
426 IPersonRoles,
427 )
428+from lp.registry.model.accesspolicy import AccessPolicyGrant
429 from lp.registry.model.distroseries import DistroSeries
430 from lp.registry.model.series import ACTIVE_STATUSES
431 from lp.registry.model.teammembership import TeamParticipation
432@@ -135,6 +140,9 @@ from lp.services.database.interfaces import (
433 IStore,
434 )
435 from lp.services.database.stormexpr import (
436+ Array,
437+ ArrayAgg,
438+ ArrayIntersects,
439 Greatest,
440 IsDistinctFrom,
441 NullsLast,
442@@ -1113,6 +1121,25 @@ class Snap(Storm, WebhookTargetMixin):
443 order_by = Desc(SnapBuild.id)
444 return self._getBuilds(filter_term, order_by)
445
446+ def subscribe(self, person, subscribed_by, ignore_permissions=False):
447+ """See `ISnap`."""
448+ # XXX pappacena 2021-02-05: We may need a "SnapSubscription" here.
449+ service = getUtility(IService, "sharing")
450+ service.ensureAccessGrants(
451+ [person], subscribed_by, snaps=[self],
452+ ignore_permissions=ignore_permissions)
453+
454+ def unsubscribe(self, person, unsubscribed_by):
455+ """See `ISnap`."""
456+ service = getUtility(IService, "sharing")
457+ service.revokeAccessGrants(
458+ self.pillar, person, unsubscribed_by, snaps=[self])
459+ IStore(self).flush()
460+
461+ def _deleteAccessGrants(self):
462+ """Delete access grants for this snap recipe prior to deleting it."""
463+ getUtility(IAccessArtifactSource).delete([self])
464+
465 def destroySelf(self):
466 """See `ISnap`."""
467 store = IStore(Snap)
468@@ -1149,6 +1176,7 @@ class Snap(Storm, WebhookTargetMixin):
469 [SnapJob.job_id], And(SnapJob.job == Job.id, SnapJob.snap == self))
470 store.find(Job, Job.id.is_in(affected_jobs)).remove()
471 getUtility(IWebhookSet).delete(self.webhooks)
472+ self._deleteAccessGrants()
473 store.remove(self)
474 store.find(
475 BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)).remove()
476@@ -1236,6 +1264,9 @@ class SnapSet:
477 store_channels=store_channels, project=project)
478 store.add(snap)
479
480+ # Automatically subscribe the owner to the Snap.
481+ snap.subscribe(snap.owner, registrant, ignore_permissions=True)
482+
483 if processors is None:
484 processors = [
485 p for p in snap.available_processors if p.build_by_default]
486@@ -1303,6 +1334,10 @@ class SnapSet:
487 expressions.append(Snap.owner == owner)
488 return IStore(Snap).find(Snap, *expressions)
489
490+ def findByIds(self, snap_ids):
491+ """See `ISnapSet`."""
492+ return IStore(ISnap).find(Snap, Snap.id.is_in(snap_ids))
493+
494 def findByOwner(self, owner):
495 """See `ISnapSet`."""
496 return IStore(Snap).find(Snap, Snap.owner == owner)
497@@ -1372,36 +1407,12 @@ class SnapSet:
498 snaps.order_by(Desc(Snap.date_last_modified))
499 return snaps
500
501- def _findSnapVisibilityClause(self, visible_by_user):
502- # XXX cjwatson 2016-11-25: This is in principle a poor query, but we
503- # don't yet have the access grant infrastructure to do better, and
504- # in any case the numbers involved should be very small.
505- # XXX pappacena 2021-02-12: Once we do the migration to back fill
506- # information_type, we should be able to change this.
507- private_snap = SQL(
508- "CASE information_type"
509- " WHEN NULL THEN private"
510- " ELSE information_type NOT IN ?"
511- "END", params=[tuple(i.value for i in PUBLIC_INFORMATION_TYPES)])
512- if visible_by_user is None:
513- return private_snap == False
514- else:
515- roles = IPersonRoles(visible_by_user)
516- if roles.in_admin or roles.in_commercial_admin:
517- return True
518- else:
519- return Or(
520- private_snap == False,
521- Snap.owner_id.is_in(Select(
522- TeamParticipation.teamID,
523- TeamParticipation.person == visible_by_user)))
524-
525 def findByURL(self, url, owner=None, visible_by_user=None):
526 """See `ISnapSet`."""
527 clauses = [Snap.git_repository_url == url]
528 if owner is not None:
529 clauses.append(Snap.owner == owner)
530- clauses.append(self._findSnapVisibilityClause(visible_by_user))
531+ clauses.append(get_snap_privacy_filter(visible_by_user))
532 return IStore(Snap).find(Snap, *clauses)
533
534 def findByURLPrefix(self, url_prefix, owner=None, visible_by_user=None):
535@@ -1418,7 +1429,7 @@ class SnapSet:
536 clauses = [Or(*prefix_clauses)]
537 if owner is not None:
538 clauses.append(Snap.owner == owner)
539- clauses.append(self._findSnapVisibilityClause(visible_by_user))
540+ clauses.append(get_snap_privacy_filter(visible_by_user))
541 return IStore(Snap).find(Snap, *clauses)
542
543 def findByStoreName(self, store_name, owner=None, visible_by_user=None):
544@@ -1426,7 +1437,7 @@ class SnapSet:
545 clauses = [Snap.store_name == store_name]
546 if owner is not None:
547 clauses.append(Snap.owner == owner)
548- clauses.append(self._findSnapVisibilityClause(visible_by_user))
549+ clauses.append(get_snap_privacy_filter(visible_by_user))
550 return IStore(Snap).find(Snap, *clauses)
551
552 def preloadDataForSnaps(self, snaps, user=None):
553@@ -1591,3 +1602,47 @@ class SnapStoreSecretsEncryptedContainer(NaClEncryptedContainerBase):
554 config.snappy.store_secrets_private_key.encode("UTF-8"))
555 else:
556 return None
557+
558+
559+def get_snap_privacy_filter(user):
560+ """Returns the filter for all private Snaps that the given user is
561+ subscribed to (that is, has access without being directly an owner).
562+
563+ :return: A storm condition.
564+ """
565+ # XXX pappacena 2021-02-12: Once we do the migration to back fill
566+ # information_type, we should be able to change this.
567+ private_snap = SQL(
568+ "CASE information_type"
569+ " WHEN NULL THEN private"
570+ " ELSE information_type NOT IN ?"
571+ "END", params=[tuple(i.value for i in PUBLIC_INFORMATION_TYPES)])
572+ if user is None:
573+ return private_snap == False
574+
575+ roles = IPersonRoles(user)
576+ if roles.in_admin or roles.in_commercial_admin:
577+ return True
578+
579+ artifact_grant_query = Coalesce(
580+ ArrayIntersects(
581+ SQL("%s.access_grants" % Snap.__storm_table__),
582+ Select(
583+ ArrayAgg(TeamParticipation.teamID),
584+ tables=TeamParticipation,
585+ where=(TeamParticipation.person == user)
586+ )), False)
587+
588+ policy_grant_query = Coalesce(
589+ ArrayIntersects(
590+ Array(SQL("%s.access_policy" % Snap.__storm_table__)),
591+ Select(
592+ ArrayAgg(AccessPolicyGrant.policy_id),
593+ tables=(AccessPolicyGrant,
594+ Join(TeamParticipation,
595+ TeamParticipation.teamID ==
596+ AccessPolicyGrant.grantee_id)),
597+ where=(TeamParticipation.person == user)
598+ )), False)
599+
600+ return Or(private_snap == False, artifact_grant_query, policy_grant_query)
601diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
602index 58a2a95..d8956db 100644
603--- a/lib/lp/snappy/tests/test_snap.py
604+++ b/lib/lp/snappy/tests/test_snap.py
605@@ -120,6 +120,7 @@ from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource
606 from lp.snappy.interfaces.snapjob import ISnapRequestBuildsJobSource
607 from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
608 from lp.snappy.model.snap import (
609+ get_snap_privacy_filter,
610 Snap,
611 SnapSet,
612 )
613@@ -1535,6 +1536,39 @@ class TestSnapSet(TestCaseWithFactory):
614 self.assertContentEqual(snaps[:3], snap_set.findByPerson(owners[0]))
615 self.assertContentEqual(snaps[3:], snap_set.findByPerson(owners[1]))
616
617+ def test_get_snap_privacy_filter_includes_grants(self):
618+ grantee, creator = [self.factory.makePerson() for i in range(2)]
619+ # All snaps are owned by "creator", and "grantee" will later have
620+ # access granted using sharing service.
621+ snap_data = dict(registrant=creator, owner=creator, private=True)
622+ private_snaps = [self.factory.makeSnap(**snap_data) for _ in range(2)]
623+ shared_snaps = [self.factory.makeSnap(**snap_data) for _ in range(2)]
624+ snap_data["private"] = False
625+ public_snaps = [self.factory.makeSnap(**snap_data) for _ in range(3)]
626+
627+ with admin_logged_in():
628+ for snap in shared_snaps:
629+ snap.subscribe(grantee, creator)
630+
631+ def all_snaps_visible_by(person):
632+ return IStore(Snap).find(
633+ Snap, get_snap_privacy_filter(person))
634+
635+ # Creator should get all snaps.
636+ self.assertContentEqual(
637+ public_snaps + private_snaps + shared_snaps,
638+ all_snaps_visible_by(creator))
639+
640+ # Grantee should get public and shared snaps.
641+ self.assertContentEqual(
642+ public_snaps + shared_snaps, all_snaps_visible_by(grantee))
643+
644+ with admin_logged_in():
645+ # After revoking, Grantee should have no access to the shared ones.
646+ for snap in shared_snaps:
647+ snap.unsubscribe(grantee, creator)
648+ self.assertContentEqual(public_snaps, all_snaps_visible_by(grantee))
649+
650 def test_findByProject(self):
651 # ISnapSet.findByProject returns all Snaps based on branches or
652 # repositories for the given project.