Merge ~pappacena/launchpad:snap-pillar-accesspolicy into launchpad:master
- Git
- lp:~pappacena/launchpad
- snap-pillar-accesspolicy
- Merge into 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) |
||||
Related bugs: |
|
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
Description of the change
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 : | # |
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
1 | diff --git a/lib/lp/registry/browser/pillar.py b/lib/lp/registry/browser/pillar.py |
2 | index 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)) |
14 | diff --git a/lib/lp/registry/interfaces/accesspolicy.py b/lib/lp/registry/interfaces/accesspolicy.py |
15 | index 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 | |
26 | diff --git a/lib/lp/registry/interfaces/sharingservice.py b/lib/lp/registry/interfaces/sharingservice.py |
27 | index 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 | |
115 | diff --git a/lib/lp/registry/model/accesspolicy.py b/lib/lp/registry/model/accesspolicy.py |
116 | index 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 |
186 | diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py |
187 | index 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: |
354 | diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py |
355 | index 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) |
367 | diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py |
368 | index 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.""" |
394 | diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py |
395 | index 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) |
601 | diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py |
602 | index 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. |
Pushed the requested changes.