Merge ~pappacena/launchpad:snap-pillar-subscribe into launchpad:master
- Git
- lp:~pappacena/launchpad
- snap-pillar-subscribe
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Thiago F. Pappacena | ||||
Approved revision: | f5f27aab0499911221fd8cee9311f783ef1b9c97 | ||||
Merge reported by: | Otto Co-Pilot | ||||
Merged at revision: | not available | ||||
Proposed branch: | ~pappacena/launchpad:snap-pillar-subscribe | ||||
Merge into: | launchpad:master | ||||
Prerequisite: | ~pappacena/launchpad:snap-pillar-reconcile-access | ||||
Diff against target: |
666 lines (+324/-24) 9 files modified
lib/lp/registry/personmerge.py (+21/-4) lib/lp/registry/tests/test_personmerge.py (+37/-2) lib/lp/snappy/browser/tests/test_snap.py (+5/-0) lib/lp/snappy/interfaces/snapsubscription.py (+42/-0) lib/lp/snappy/model/snap.py (+62/-7) lib/lp/snappy/model/snapsubscription.py (+62/-0) lib/lp/snappy/tests/test_snap.py (+79/-7) lib/lp/snappy/tests/test_snapbuild.py (+7/-2) lib/lp/testing/factory.py (+9/-2) |
||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+397755@code.launchpad.net |
Commit message
Adding SnapSubscription model
Description of the change
To post a comment you must log in.
- f3387eb... by Thiago F. Pappacena
-
Merge branch 'snap-pillar-
reconcile- access' into snap-pillar- subscribe - 7f125c7... by Thiago F. Pappacena
-
Merge branch 'snap-pillar-
reconcile- access' into snap-pillar- subscribe - 984531e... by Thiago F. Pappacena
-
Fixing personmerge test
- 4dacf01... by Thiago F. Pappacena
-
Removing SnapSubscriptions when deleting a Snap
- 6612dca... by Thiago F. Pappacena
-
Test fix: private snaps cannot be associated with non-moderated teams
- a273e7a... by Thiago F. Pappacena
-
Fixing Snap.unsubscribe and improving performance on Snap.visibleByUser
- 858eaf1... by Thiago F. Pappacena
-
Merge branch 'snap-pillar-
reconcile- access' into snap-pillar- subscribe - 92bebbd... by Thiago F. Pappacena
-
Merge branch 'master' into snap-pillar-
subscribe
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
- f5f27aa... by Thiago F. Pappacena
-
Allowing snap owner to unsubscribe anyone and few minor adjustments
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/personmerge.py b/lib/lp/registry/personmerge.py |
2 | index 949e8d9..3e51005 100644 |
3 | --- a/lib/lp/registry/personmerge.py |
4 | +++ b/lib/lp/registry/personmerge.py |
5 | @@ -660,18 +660,36 @@ def _mergeSnap(cur, from_person, to_person): |
6 | existing_names = [ |
7 | s.name for s in getUtility(ISnapSet).findByOwner(to_person)] |
8 | for snap in snaps: |
9 | - new_name = snap.name |
10 | + naked_snap = removeSecurityProxy(snap) |
11 | + new_name = naked_snap.name |
12 | count = 1 |
13 | while new_name in existing_names: |
14 | new_name = '%s-%s' % (snap.name, count) |
15 | count += 1 |
16 | - naked_snap = removeSecurityProxy(snap) |
17 | naked_snap.owner = to_person |
18 | naked_snap.name = new_name |
19 | if not snaps.is_empty(): |
20 | IStore(snaps[0]).flush() |
21 | |
22 | |
23 | +def _mergeSnapSubscription(cur, from_id, to_id): |
24 | + # Update only the SnapSubscription that will not conflict. |
25 | + cur.execute(''' |
26 | + UPDATE SnapSubscription |
27 | + SET person=%(to_id)d |
28 | + WHERE person=%(from_id)d AND snap NOT IN |
29 | + ( |
30 | + SELECT snap |
31 | + FROM SnapSubscription |
32 | + WHERE person = %(to_id)d |
33 | + ) |
34 | + ''' % vars()) |
35 | + # and delete those left over. |
36 | + cur.execute(''' |
37 | + DELETE FROM SnapSubscription WHERE person=%(from_id)d |
38 | + ''' % vars()) |
39 | + |
40 | + |
41 | def _mergeOCIRecipe(cur, from_person, to_person): |
42 | # This shouldn't use removeSecurityProxy |
43 | oci_recipes = getUtility(IOCIRecipeSet).findByOwner(from_person) |
44 | @@ -917,8 +935,7 @@ def merge_people(from_person, to_person, reviewer, delete=False): |
45 | _mergeSnap(cur, from_person, to_person) |
46 | skip.append(('snap', 'owner')) |
47 | |
48 | - # XXX pappacena 2021-02-18: add tests for this once we have |
49 | - # SnapSubscription model in place. |
50 | + _mergeSnapSubscription(cur, from_id, to_id) |
51 | skip.append(('snapsubscription', 'person')) |
52 | |
53 | _mergeOCIRecipe(cur, from_person, to_person) |
54 | diff --git a/lib/lp/registry/tests/test_personmerge.py b/lib/lp/registry/tests/test_personmerge.py |
55 | index 02fefcc..e69872f 100644 |
56 | --- a/lib/lp/registry/tests/test_personmerge.py |
57 | +++ b/lib/lp/registry/tests/test_personmerge.py |
58 | @@ -1,4 +1,4 @@ |
59 | -# Copyright 2009-2020 Canonical Ltd. This software is licensed under the |
60 | +# Copyright 2009-2021 Canonical Ltd. This software is licensed under the |
61 | # GNU Affero General Public License version 3 (see the file LICENSE). |
62 | |
63 | """Tests for merge_people.""" |
64 | @@ -8,6 +8,7 @@ from operator import attrgetter |
65 | |
66 | import pytz |
67 | from testtools.matchers import ( |
68 | + Equals, |
69 | MatchesSetwise, |
70 | MatchesStructure, |
71 | ) |
72 | @@ -48,13 +49,17 @@ from lp.services.identity.interfaces.emailaddress import ( |
73 | EmailAddressStatus, |
74 | IEmailAddressSet, |
75 | ) |
76 | -from lp.snappy.interfaces.snap import ISnapSet |
77 | +from lp.snappy.interfaces.snap import ( |
78 | + ISnapSet, |
79 | + SNAP_TESTING_FLAGS, |
80 | + ) |
81 | from lp.soyuz.enums import ArchiveStatus |
82 | from lp.soyuz.interfaces.livefs import ( |
83 | ILiveFSSet, |
84 | LIVEFS_FEATURE_FLAG, |
85 | ) |
86 | from lp.testing import ( |
87 | + admin_logged_in, |
88 | celebrity_logged_in, |
89 | login_person, |
90 | person_logged_in, |
91 | @@ -664,6 +669,36 @@ class TestMergePeople(TestCaseWithFactory, KarmaTestMixin): |
92 | self.assertIsNone(snaps[1].git_path) |
93 | self.assertEqual(u'foo-1', snaps[1].name) |
94 | |
95 | + def test_merge_snapsubscription(self): |
96 | + # Checks that merging users moves subscriptions. |
97 | + self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS)) |
98 | + duplicate = self.factory.makePerson() |
99 | + mergee = self.factory.makePerson() |
100 | + snap = removeSecurityProxy(self.factory.makeSnap( |
101 | + owner=duplicate, registrant=duplicate, |
102 | + name=u'foo', private=True)) |
103 | + with admin_logged_in(): |
104 | + # Owner should have being subscribed automatically on creation. |
105 | + self.assertTrue(snap.visibleByUser(duplicate)) |
106 | + self.assertThat(snap._getSubscription(duplicate), MatchesStructure( |
107 | + snap=Equals(snap), |
108 | + person=Equals(duplicate) |
109 | + )) |
110 | + self.assertFalse(snap.visibleByUser(mergee)) |
111 | + self.assertIsNone(snap._getSubscription(mergee)) |
112 | + |
113 | + self._do_premerge(duplicate, mergee) |
114 | + login_person(mergee) |
115 | + duplicate, mergee = self._do_merge(duplicate, mergee) |
116 | + |
117 | + self.assertTrue(snap.visibleByUser(mergee)) |
118 | + self.assertThat(snap._getSubscription(mergee), MatchesStructure( |
119 | + snap=Equals(snap), |
120 | + person=Equals(mergee) |
121 | + )) |
122 | + self.assertFalse(snap.visibleByUser(duplicate)) |
123 | + self.assertIsNone(snap._getSubscription(duplicate)) |
124 | + |
125 | def test_merge_moves_oci_recipes(self): |
126 | # When person/teams are merged, oci recipes owned by the from |
127 | # person are moved. |
128 | diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py |
129 | index 50390e9..025574c 100644 |
130 | --- a/lib/lp/snappy/browser/tests/test_snap.py |
131 | +++ b/lib/lp/snappy/browser/tests/test_snap.py |
132 | @@ -59,6 +59,7 @@ from lp.code.tests.helpers import ( |
133 | from lp.registry.enums import ( |
134 | BranchSharingPolicy, |
135 | PersonVisibility, |
136 | + TeamMembershipPolicy, |
137 | ) |
138 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
139 | from lp.registry.interfaces.series import SeriesStatus |
140 | @@ -388,6 +389,7 @@ class TestSnapAddView(BaseTestSnapView): |
141 | login_person(self.person) |
142 | self.factory.makeTeam( |
143 | name='super-private', owner=self.person, |
144 | + membership_policy=TeamMembershipPolicy.MODERATED, |
145 | visibility=PersonVisibility.PRIVATE) |
146 | branch = self.factory.makeAnyBranch() |
147 | |
148 | @@ -411,6 +413,7 @@ class TestSnapAddView(BaseTestSnapView): |
149 | login_person(self.person) |
150 | private_team = self.factory.makeTeam( |
151 | name='super-private', owner=self.person, |
152 | + membership_policy=TeamMembershipPolicy.MODERATED, |
153 | visibility=PersonVisibility.PRIVATE) |
154 | branch = self.factory.makeAnyBranch( |
155 | owner=self.person, registrant=self.person, |
156 | @@ -440,6 +443,7 @@ class TestSnapAddView(BaseTestSnapView): |
157 | login_person(self.person) |
158 | private_team = self.factory.makeTeam( |
159 | name='super-private', owner=self.person, |
160 | + membership_policy=TeamMembershipPolicy.MODERATED, |
161 | visibility=PersonVisibility.PRIVATE) |
162 | [git_ref] = self.factory.makeGitRefs( |
163 | owner=self.person, registrant=self.person, |
164 | @@ -762,6 +766,7 @@ class TestSnapAdminView(BaseTestSnapView): |
165 | # Cannot make snap public if it still contains private information. |
166 | login_person(self.person) |
167 | team = self.factory.makeTeam( |
168 | + membership_policy=TeamMembershipPolicy.MODERATED, |
169 | owner=self.person, visibility=PersonVisibility.PRIVATE) |
170 | project = self.factory.makeProduct( |
171 | information_type=InformationType.PUBLIC, |
172 | diff --git a/lib/lp/snappy/interfaces/snapsubscription.py b/lib/lp/snappy/interfaces/snapsubscription.py |
173 | new file mode 100644 |
174 | index 0000000..58206cc |
175 | --- /dev/null |
176 | +++ b/lib/lp/snappy/interfaces/snapsubscription.py |
177 | @@ -0,0 +1,42 @@ |
178 | +# Copyright 2020-2021 Canonical Ltd. This software is licensed under the |
179 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
180 | + |
181 | +"""Snap subscription model.""" |
182 | + |
183 | +from __future__ import absolute_import, print_function, unicode_literals |
184 | + |
185 | +__metaclass__ = type |
186 | +__all__ = [ |
187 | + 'ISnapSubscription' |
188 | +] |
189 | + |
190 | +from lazr.restful.fields import Reference |
191 | +from zope.interface import Interface |
192 | +from zope.schema import ( |
193 | + Datetime, |
194 | + Int, |
195 | + ) |
196 | + |
197 | +from lp import _ |
198 | +from lp.services.fields import PersonChoice |
199 | +from lp.snappy.interfaces.snap import ISnap |
200 | + |
201 | + |
202 | +class ISnapSubscription(Interface): |
203 | + """A person subscription to a specific Snap recipe.""" |
204 | + |
205 | + id = Int(title=_('ID'), readonly=True, required=True) |
206 | + person = PersonChoice( |
207 | + title=_('Person'), required=True, vocabulary='ValidPersonOrTeam', |
208 | + readonly=True, |
209 | + description=_("The person subscribed to the related snap recipe.")) |
210 | + snap = Reference(ISnap, title=_("Snap"), required=True, readonly=True) |
211 | + subscribed_by = PersonChoice( |
212 | + title=_('Subscribed by'), required=True, |
213 | + vocabulary='ValidPersonOrTeam', readonly=True, |
214 | + description=_("The person who created this subscription.")) |
215 | + date_created = Datetime( |
216 | + title=_('Date subscribed'), required=True, readonly=True) |
217 | + |
218 | + def canBeUnsubscribedByUser(user): |
219 | + """Can the user unsubscribe the subscriber from the snap recipe?""" |
220 | diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py |
221 | index 6b9af0a..dabc6fe 100644 |
222 | --- a/lib/lp/snappy/model/snap.py |
223 | +++ b/lib/lp/snappy/model/snap.py |
224 | @@ -66,7 +66,11 @@ from lp.app.enums import ( |
225 | PRIVATE_INFORMATION_TYPES, |
226 | PUBLIC_INFORMATION_TYPES, |
227 | ) |
228 | -from lp.app.errors import IncompatibleArguments |
229 | +from lp.app.errors import ( |
230 | + IncompatibleArguments, |
231 | + SubscriptionPrivacyViolation, |
232 | + UserCannotUnsubscribePerson, |
233 | + ) |
234 | from lp.app.interfaces.security import IAuthorization |
235 | from lp.app.interfaces.services import IService |
236 | from lp.buildmaster.enums import BuildStatus |
237 | @@ -108,7 +112,10 @@ from lp.code.model.branchnamespace import ( |
238 | from lp.code.model.gitcollection import GenericGitCollection |
239 | from lp.code.model.gitrepository import GitRepository |
240 | from lp.registry.errors import PrivatePersonLinkageError |
241 | -from lp.registry.interfaces.accesspolicy import IAccessArtifactSource |
242 | +from lp.registry.interfaces.accesspolicy import ( |
243 | + IAccessArtifactGrantSource, |
244 | + IAccessArtifactSource, |
245 | + ) |
246 | from lp.registry.interfaces.person import ( |
247 | IPerson, |
248 | IPersonSet, |
249 | @@ -203,6 +210,7 @@ from lp.snappy.interfaces.snappyseries import ISnappyDistroSeriesSet |
250 | from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient |
251 | from lp.snappy.model.snapbuild import SnapBuild |
252 | from lp.snappy.model.snapjob import SnapJob |
253 | +from lp.snappy.model.snapsubscription import SnapSubscription |
254 | from lp.soyuz.interfaces.archive import ArchiveDisabled |
255 | from lp.soyuz.model.archive import ( |
256 | Archive, |
257 | @@ -1129,25 +1137,66 @@ class Snap(Storm, WebhookTargetMixin): |
258 | |
259 | def visibleByUser(self, user): |
260 | """See `ISnap`.""" |
261 | + if self.information_type in PUBLIC_INFORMATION_TYPES: |
262 | + return True |
263 | + if user is None: |
264 | + return False |
265 | store = IStore(self) |
266 | return not store.find( |
267 | Snap, |
268 | Snap.id == self.id, |
269 | get_snap_privacy_filter(user)).is_empty() |
270 | |
271 | + def _getSubscription(self, person): |
272 | + """Returns person's subscription to this snap recipe, or None if no |
273 | + subscription is available. |
274 | + """ |
275 | + if person is None: |
276 | + return None |
277 | + return Store.of(self).find( |
278 | + SnapSubscription, |
279 | + SnapSubscription.person == person, |
280 | + SnapSubscription.snap == self).one() |
281 | + |
282 | + def _userCanBeSubscribed(self, person): |
283 | + """Checks if the given person can subscribe to this snap recipe.""" |
284 | + return not ( |
285 | + self.private and |
286 | + person.is_team and |
287 | + person.anyone_can_join()) |
288 | + |
289 | def subscribe(self, person, subscribed_by, ignore_permissions=False): |
290 | """See `ISnap`.""" |
291 | - # XXX pappacena 2021-02-05: We may need a "SnapSubscription" here. |
292 | + if not self._userCanBeSubscribed(person): |
293 | + raise SubscriptionPrivacyViolation( |
294 | + "Open and delegated teams cannot be subscribed to private " |
295 | + "snap recipes.") |
296 | + subscription = self._getSubscription(person) |
297 | + if subscription is None: |
298 | + subscription = SnapSubscription( |
299 | + person=person, snap=self, subscribed_by=subscribed_by) |
300 | + Store.of(subscription).flush() |
301 | service = getUtility(IService, "sharing") |
302 | service.ensureAccessGrants( |
303 | [person], subscribed_by, snaps=[self], |
304 | ignore_permissions=ignore_permissions) |
305 | |
306 | - def unsubscribe(self, person, unsubscribed_by): |
307 | + def unsubscribe(self, person, unsubscribed_by, ignore_permissions=False): |
308 | """See `ISnap`.""" |
309 | - service = getUtility(IService, "sharing") |
310 | - service.revokeAccessGrants( |
311 | - self.pillar, person, unsubscribed_by, snaps=[self]) |
312 | + subscription = self._getSubscription(person) |
313 | + if subscription is None: |
314 | + return |
315 | + if (not ignore_permissions |
316 | + and not subscription.canBeUnsubscribedByUser(unsubscribed_by)): |
317 | + raise UserCannotUnsubscribePerson( |
318 | + '%s does not have permission to unsubscribe %s.' % ( |
319 | + unsubscribed_by.displayname, |
320 | + person.displayname)) |
321 | + artifact = getUtility(IAccessArtifactSource).find([self]) |
322 | + getUtility(IAccessArtifactGrantSource).revokeByArtifact( |
323 | + artifact, [person]) |
324 | + store = Store.of(subscription) |
325 | + store.remove(subscription) |
326 | IStore(self).flush() |
327 | |
328 | def _reconcileAccess(self): |
329 | @@ -1169,6 +1218,11 @@ class Snap(Storm, WebhookTargetMixin): |
330 | """Delete access grants for this snap recipe prior to deleting it.""" |
331 | getUtility(IAccessArtifactSource).delete([self]) |
332 | |
333 | + def _deleteSnapSubscriptions(self): |
334 | + subscriptions = Store.of(self).find( |
335 | + SnapSubscription, SnapSubscription.snap == self) |
336 | + subscriptions.remove() |
337 | + |
338 | def destroySelf(self): |
339 | """See `ISnap`.""" |
340 | store = IStore(Snap) |
341 | @@ -1206,6 +1260,7 @@ class Snap(Storm, WebhookTargetMixin): |
342 | store.find(Job, Job.id.is_in(affected_jobs)).remove() |
343 | getUtility(IWebhookSet).delete(self.webhooks) |
344 | self._deleteAccessGrants() |
345 | + self._deleteSnapSubscriptions() |
346 | store.remove(self) |
347 | store.find( |
348 | BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)).remove() |
349 | diff --git a/lib/lp/snappy/model/snapsubscription.py b/lib/lp/snappy/model/snapsubscription.py |
350 | new file mode 100644 |
351 | index 0000000..89860d7 |
352 | --- /dev/null |
353 | +++ b/lib/lp/snappy/model/snapsubscription.py |
354 | @@ -0,0 +1,62 @@ |
355 | +# Copyright 2020-2021 Canonical Ltd. This software is licensed under the |
356 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
357 | + |
358 | +"""Snap subscription model.""" |
359 | + |
360 | +from __future__ import absolute_import, print_function, unicode_literals |
361 | + |
362 | +__metaclass__ = type |
363 | +__all__ = [ |
364 | + 'SnapSubscription' |
365 | +] |
366 | + |
367 | +import pytz |
368 | +from storm.properties import ( |
369 | + DateTime, |
370 | + Int, |
371 | + ) |
372 | +from storm.references import Reference |
373 | +from zope.interface import implementer |
374 | + |
375 | +from lp.registry.interfaces.person import validate_person |
376 | +from lp.registry.interfaces.role import IPersonRoles |
377 | +from lp.services.database.constants import UTC_NOW |
378 | +from lp.services.database.stormbase import StormBase |
379 | +from lp.snappy.interfaces.snapsubscription import ISnapSubscription |
380 | + |
381 | + |
382 | +@implementer(ISnapSubscription) |
383 | +class SnapSubscription(StormBase): |
384 | + """A relationship between a person and a snap recipe.""" |
385 | + |
386 | + __storm_table__ = 'SnapSubscription' |
387 | + |
388 | + id = Int(primary=True) |
389 | + |
390 | + person_id = Int( |
391 | + "person", allow_none=False, validator=validate_person) |
392 | + person = Reference(person_id, "Person.id") |
393 | + |
394 | + snap_id = Int("snap", allow_none=False) |
395 | + snap = Reference(snap_id, "Snap.id") |
396 | + |
397 | + date_created = DateTime(allow_none=False, default=UTC_NOW, tzinfo=pytz.UTC) |
398 | + |
399 | + subscribed_by_id = Int( |
400 | + "subscribed_by", allow_none=False, validator=validate_person) |
401 | + subscribed_by = Reference(subscribed_by_id, "Person.id") |
402 | + |
403 | + def __init__(self, snap, person, subscribed_by): |
404 | + super(SnapSubscription, self).__init__() |
405 | + self.snap = snap |
406 | + self.person = person |
407 | + self.subscribed_by = subscribed_by |
408 | + |
409 | + def canBeUnsubscribedByUser(self, user): |
410 | + """See `ISnapSubscription`.""" |
411 | + if user is None: |
412 | + return False |
413 | + return (user.inTeam(self.snap.owner) or |
414 | + user.inTeam(self.person) or |
415 | + user.inTeam(self.subscribed_by) or |
416 | + IPersonRoles(user).in_admin) |
417 | diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py |
418 | index e8ea61e..f66949b 100644 |
419 | --- a/lib/lp/snappy/tests/test_snap.py |
420 | +++ b/lib/lp/snappy/tests/test_snap.py |
421 | @@ -34,6 +34,7 @@ from testtools.matchers import ( |
422 | Equals, |
423 | GreaterThan, |
424 | Is, |
425 | + IsInstance, |
426 | LessThan, |
427 | MatchesAll, |
428 | MatchesDict, |
429 | @@ -46,6 +47,7 @@ from zope.security.interfaces import Unauthorized |
430 | from zope.security.proxy import removeSecurityProxy |
431 | |
432 | from lp.app.enums import InformationType |
433 | +from lp.app.errors import SubscriptionPrivacyViolation |
434 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
435 | from lp.buildmaster.enums import ( |
436 | BuildQueueStatus, |
437 | @@ -68,6 +70,7 @@ from lp.code.tests.helpers import ( |
438 | from lp.registry.enums import ( |
439 | BranchSharingPolicy, |
440 | PersonVisibility, |
441 | + TeamMembershipPolicy, |
442 | ) |
443 | from lp.registry.interfaces.accesspolicy import IAccessPolicySource |
444 | from lp.registry.interfaces.distribution import IDistributionSet |
445 | @@ -1353,6 +1356,9 @@ class TestSnapVisibility(TestCaseWithFactory): |
446 | AccessArtifactGrant.abstract_artifact_id == AccessArtifact.id, |
447 | *conditions) |
448 | |
449 | + def getSnapSubscription(self, snap, person): |
450 | + return removeSecurityProxy(snap)._getSubscription(person) |
451 | + |
452 | def test_only_owner_can_grant_access(self): |
453 | owner = self.factory.makePerson() |
454 | snap = self.factory.makeSnap( |
455 | @@ -1373,7 +1379,8 @@ class TestSnapVisibility(TestCaseWithFactory): |
456 | |
457 | def test_private_is_visible_by_team_member(self): |
458 | person = self.factory.makePerson() |
459 | - team = self.factory.makeTeam(members=[person]) |
460 | + team = self.factory.makeTeam( |
461 | + members=[person], membership_policy=TeamMembershipPolicy.MODERATED) |
462 | snap = self.factory.makeSnap(private=True, owner=team, |
463 | registrant=person) |
464 | with person_logged_in(team): |
465 | @@ -1388,11 +1395,33 @@ class TestSnapVisibility(TestCaseWithFactory): |
466 | with person_logged_in(owner): |
467 | self.assertFalse(snap.visibleByUser(person)) |
468 | snap.subscribe(person, snap.owner) |
469 | + self.assertThat( |
470 | + self.getSnapSubscription(snap, person), |
471 | + MatchesStructure( |
472 | + person=Equals(person), |
473 | + snap=Equals(snap), |
474 | + subscribed_by=Equals(snap.owner), |
475 | + date_created=IsInstance(datetime))) |
476 | # Calling again should be a no-op. |
477 | snap.subscribe(person, snap.owner) |
478 | self.assertTrue(snap.visibleByUser(person)) |
479 | + |
480 | snap.unsubscribe(person, snap.owner) |
481 | self.assertFalse(snap.visibleByUser(person)) |
482 | + self.assertIsNone(self.getSnapSubscription(snap, person)) |
483 | + |
484 | + def test_snap_owner_can_unsubscribe_anyone(self): |
485 | + person = self.factory.makePerson() |
486 | + owner = self.factory.makePerson() |
487 | + admin = self.factory.makeAdministrator() |
488 | + snap = self.factory.makeSnap( |
489 | + registrant=owner, owner=owner, private=True) |
490 | + with person_logged_in(admin): |
491 | + snap.subscribe(person, admin) |
492 | + self.assertTrue(snap.visibleByUser(person)) |
493 | + with person_logged_in(owner): |
494 | + snap.unsubscribe(person, owner) |
495 | + self.assertFalse(snap.visibleByUser(person)) |
496 | |
497 | def test_reconcile_set_public(self): |
498 | owner = self.factory.makePerson() |
499 | @@ -1401,11 +1430,24 @@ class TestSnapVisibility(TestCaseWithFactory): |
500 | another_user = self.factory.makePerson() |
501 | with admin_logged_in(): |
502 | snap.subscribe(another_user, snap.owner) |
503 | + self.assertEqual(1, self.getSnapGrants(snap, another_user).count()) |
504 | + self.assertThat( |
505 | + self.getSnapSubscription(snap, another_user), |
506 | + MatchesStructure( |
507 | + person=Equals(another_user), |
508 | + snap=Equals(snap), |
509 | + subscribed_by=Equals(snap.owner), |
510 | + date_created=IsInstance(datetime))) |
511 | |
512 | - self.assertEqual(1, self.getSnapGrants(snap, another_user).count()) |
513 | - with admin_logged_in(): |
514 | snap.information_type = InformationType.PUBLIC |
515 | - self.assertEqual(0, self.getSnapGrants(snap, another_user).count()) |
516 | + self.assertEqual(0, self.getSnapGrants(snap, another_user).count()) |
517 | + self.assertThat( |
518 | + self.getSnapSubscription(snap, another_user), |
519 | + MatchesStructure( |
520 | + person=Equals(another_user), |
521 | + snap=Equals(snap), |
522 | + subscribed_by=Equals(snap.owner), |
523 | + date_created=IsInstance(datetime))) |
524 | |
525 | def test_reconcile_permissions_setting_project(self): |
526 | owner = self.factory.makePerson() |
527 | @@ -1429,10 +1471,24 @@ class TestSnapVisibility(TestCaseWithFactory): |
528 | snap.subscribe(another_person, owner) |
529 | self.assertTrue(snap.visibleByUser(another_person)) |
530 | self.assertEqual(2, self.getSnapGrants(snap).count()) |
531 | + self.assertThat( |
532 | + self.getSnapSubscription(snap, another_person), |
533 | + MatchesStructure( |
534 | + person=Equals(another_person), |
535 | + snap=Equals(snap), |
536 | + subscribed_by=Equals(snap.owner), |
537 | + date_created=IsInstance(datetime))) |
538 | |
539 | snap.setProject(new_project) |
540 | self.assertTrue(snap.visibleByUser(another_person)) |
541 | self.assertEqual(2, self.getSnapGrants(snap).count()) |
542 | + self.assertThat( |
543 | + self.getSnapSubscription(snap, another_person), |
544 | + MatchesStructure( |
545 | + person=Equals(another_person), |
546 | + snap=Equals(snap), |
547 | + subscribed_by=Equals(snap.owner), |
548 | + date_created=IsInstance(datetime))) |
549 | |
550 | |
551 | class TestSnapSet(TestCaseWithFactory): |
552 | @@ -1527,12 +1583,23 @@ class TestSnapSet(TestCaseWithFactory): |
553 | self.assertEqual(ref.path, snap.git_path) |
554 | self.assertEqual(ref, snap.git_ref) |
555 | |
556 | + def test_create_private_snap_with_open_team_as_owner_fails(self): |
557 | + components = self.makeSnapComponents() |
558 | + with admin_logged_in(): |
559 | + components['owner'].membership_policy = TeamMembershipPolicy.OPEN |
560 | + components['information_type'] = InformationType.PROPRIETARY |
561 | + self.assertRaises( |
562 | + SubscriptionPrivacyViolation, |
563 | + getUtility(ISnapSet).new, **components) |
564 | + |
565 | def test_private_snap_information_type_compatibility(self): |
566 | login_admin() |
567 | private = InformationType.PROPRIETARY |
568 | public = InformationType.PUBLIC |
569 | + components = self.makeSnapComponents() |
570 | + components['owner'].membership_policy = TeamMembershipPolicy.MODERATED |
571 | private_snap = getUtility(ISnapSet).new( |
572 | - information_type=private, **self.makeSnapComponents()) |
573 | + information_type=private, **components) |
574 | self.assertEqual( |
575 | InformationType.PROPRIETARY, private_snap.information_type) |
576 | |
577 | @@ -1545,7 +1612,10 @@ class TestSnapSet(TestCaseWithFactory): |
578 | # Creating private snaps for public sources is allowed. |
579 | [ref] = self.factory.makeGitRefs() |
580 | components = self.makeSnapComponents(git_ref=ref) |
581 | - components['information_type'] = InformationType.PROPRIETARY |
582 | + with admin_logged_in(): |
583 | + components['information_type'] = InformationType.PROPRIETARY |
584 | + components['owner'].membership_policy = ( |
585 | + TeamMembershipPolicy.MODERATED) |
586 | components['project'] = self.factory.makeProduct( |
587 | information_type=InformationType.PROPRIETARY, |
588 | branch_sharing_policy=BranchSharingPolicy.PROPRIETARY) |
589 | @@ -2725,7 +2795,9 @@ class TestSnapWebservice(TestCaseWithFactory): |
590 | |
591 | def test_new_private(self): |
592 | # Ensure private Snap creation works. |
593 | - team = self.factory.makeTeam(owner=self.person) |
594 | + team = self.factory.makeTeam( |
595 | + membership_policy=TeamMembershipPolicy.MODERATED, |
596 | + owner=self.person) |
597 | distroseries = self.factory.makeDistroSeries(registrant=team) |
598 | [ref] = self.factory.makeGitRefs() |
599 | private_webservice = webservice_for_person( |
600 | diff --git a/lib/lp/snappy/tests/test_snapbuild.py b/lib/lp/snappy/tests/test_snapbuild.py |
601 | index e5dda05..7acff54 100644 |
602 | --- a/lib/lp/snappy/tests/test_snapbuild.py |
603 | +++ b/lib/lp/snappy/tests/test_snapbuild.py |
604 | @@ -1,4 +1,4 @@ |
605 | -# Copyright 2015-2020 Canonical Ltd. This software is licensed under the |
606 | +# Copyright 2015-2021 Canonical Ltd. This software is licensed under the |
607 | # GNU Affero General Public License version 3 (see the file LICENSE). |
608 | |
609 | """Test snap package build features.""" |
610 | @@ -36,7 +36,10 @@ from lp.buildmaster.enums import BuildStatus |
611 | from lp.buildmaster.interfaces.buildqueue import IBuildQueue |
612 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
613 | from lp.buildmaster.interfaces.processor import IProcessorSet |
614 | -from lp.registry.enums import PersonVisibility |
615 | +from lp.registry.enums import ( |
616 | + PersonVisibility, |
617 | + TeamMembershipPolicy, |
618 | + ) |
619 | from lp.registry.interfaces.series import SeriesStatus |
620 | from lp.services.authserver.xmlrpc import AuthServerAPIView |
621 | from lp.services.config import config |
622 | @@ -172,6 +175,7 @@ class TestSnapBuild(TestCaseWithFactory): |
623 | # A SnapBuild is private iff its Snap and archive are. |
624 | self.assertFalse(self.build.is_private) |
625 | private_team = self.factory.makeTeam( |
626 | + membership_policy=TeamMembershipPolicy.MODERATED, |
627 | visibility=PersonVisibility.PRIVATE) |
628 | with person_logged_in(private_team.teamowner): |
629 | build = self.factory.makeSnapBuild( |
630 | @@ -775,6 +779,7 @@ class TestSnapBuildWebservice(TestCaseWithFactory): |
631 | def test_private_snap(self): |
632 | # A SnapBuild with a private Snap is private. |
633 | db_team = self.factory.makeTeam( |
634 | + membership_policy=TeamMembershipPolicy.MODERATED, |
635 | owner=self.person, visibility=PersonVisibility.PRIVATE) |
636 | with person_logged_in(self.person): |
637 | db_build = self.factory.makeSnapBuild( |
638 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
639 | index 6948419..034095d 100644 |
640 | --- a/lib/lp/testing/factory.py |
641 | +++ b/lib/lp/testing/factory.py |
642 | @@ -4764,7 +4764,14 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
643 | if registrant is None: |
644 | registrant = self.makePerson() |
645 | if owner is None: |
646 | - owner = self.makeTeam(registrant) |
647 | + is_private_snap = ( |
648 | + private or information_type not in PUBLIC_INFORMATION_TYPES) |
649 | + # Private snaps cannot be owned by non-moderated teams. |
650 | + membership_policy = ( |
651 | + TeamMembershipPolicy.OPEN if not is_private_snap |
652 | + else TeamMembershipPolicy.MODERATED) |
653 | + owner = self.makeTeam( |
654 | + registrant, membership_policy=membership_policy) |
655 | if distroseries is _DEFAULT: |
656 | distroseries = self.makeDistroSeries() |
657 | if name is None: |
658 | @@ -4778,7 +4785,7 @@ class BareLaunchpadObjectFactory(ObjectFactory): |
659 | if auto_build_pocket is None: |
660 | auto_build_pocket = PackagePublishingPocket.UPDATES |
661 | if private and project is _DEFAULT: |
662 | - # If we are creating a private snap and didn't explictly set a |
663 | + # If we are creating a private snap and didn't explicitly set a |
664 | # pillar for it, we must create a pillar. |
665 | branch_sharing = ( |
666 | BranchSharingPolicy.PUBLIC_OR_PROPRIETARY if not private |
Pushed the requested changes.