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