Merge ~pappacena/launchpad:ocirecipe-private-reconcile-pillar into launchpad:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: 55440d6a24bece7ed5797241355a3a553cae744c
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/launchpad:ocirecipe-private-reconcile-pillar
Merge into: launchpad:master
Prerequisite: ~pappacena/launchpad:ocirecipe-private-accesspolicy
Diff against target: 498 lines (+156/-49)
11 files modified
lib/lp/blueprints/model/specification.py (+2/-2)
lib/lp/bugs/model/bug.py (+4/-4)
lib/lp/code/model/branch.py (+3/-3)
lib/lp/code/model/gitrepository.py (+3/-3)
lib/lp/oci/model/ocirecipe.py (+3/-3)
lib/lp/oci/tests/test_ocirecipe.py (+55/-0)
lib/lp/registry/model/accesspolicy.py (+9/-8)
lib/lp/registry/model/ociproject.py (+24/-1)
lib/lp/registry/tests/test_accesspolicy.py (+48/-20)
lib/lp/registry/tests/test_sharingjob.py (+3/-3)
lib/lp/snappy/model/snap.py (+2/-2)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+399469@code.launchpad.net

Commit message

Running reconcile for OCI recipes when an OCI project changes pillar

Description of the change

When we change an OCI project's pillar, we should reconcile access artifacts for every OCI recipe associated with that.

This MP got kind of big mostly because of a refactoring on reconcile_access_for_artifact signature, to allow bulk operations.

To post a comment you must log in.
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/blueprints/model/specification.py b/lib/lp/blueprints/model/specification.py
2index 55ce9d5..9f3b00a 100644
3--- a/lib/lp/blueprints/model/specification.py
4+++ b/lib/lp/blueprints/model/specification.py
5@@ -922,14 +922,14 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
6 """See ISpecification."""
7 # avoid circular imports.
8 from lp.registry.model.accesspolicy import (
9- reconcile_access_for_artifact,
10+ reconcile_access_for_artifacts,
11 )
12 if self.information_type == information_type:
13 return False
14 if information_type not in self.getAllowedInformationTypes(who):
15 raise CannotChangeInformationType("Forbidden by project policy.")
16 self.information_type = information_type
17- reconcile_access_for_artifact(self, information_type, [self.target])
18+ reconcile_access_for_artifacts([self], information_type, [self.target])
19 if (information_type in PRIVATE_INFORMATION_TYPES and
20 not self.subscribers.is_empty()):
21 # Grant the subscribers access if they do not have a
22diff --git a/lib/lp/bugs/model/bug.py b/lib/lp/bugs/model/bug.py
23index f7ebc86..f2148e0 100644
24--- a/lib/lp/bugs/model/bug.py
25+++ b/lib/lp/bugs/model/bug.py
26@@ -189,7 +189,7 @@ from lp.registry.interfaces.sharingjob import (
27 IRemoveArtifactSubscriptionsJobSource,
28 )
29 from lp.registry.interfaces.sourcepackage import ISourcePackage
30-from lp.registry.model.accesspolicy import reconcile_access_for_artifact
31+from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
32 from lp.registry.model.person import (
33 Person,
34 person_sort_key,
35@@ -2122,7 +2122,7 @@ class Bug(SQLBase, InformationTypeMixin):
36 BugSubscription.person == person).is_empty()
37
38 def _reconcileAccess(self):
39- # reconcile_access_for_artifact will only use the pillar list if
40+ # reconcile_access_for_artifacts will only use the pillar list if
41 # the information type is private. But affected_pillars iterates
42 # over the tasks immediately, which is needless expense for
43 # public bugs.
44@@ -2130,8 +2130,8 @@ class Bug(SQLBase, InformationTypeMixin):
45 pillars = self.affected_pillars
46 else:
47 pillars = []
48- reconcile_access_for_artifact(
49- self, self.information_type, pillars)
50+ reconcile_access_for_artifacts(
51+ [self], self.information_type, pillars)
52
53 def _attachments_query(self):
54 """Helper for the attachments* properties."""
55diff --git a/lib/lp/code/model/branch.py b/lib/lp/code/model/branch.py
56index 278db40..27746c7 100644
57--- a/lib/lp/code/model/branch.py
58+++ b/lib/lp/code/model/branch.py
59@@ -160,7 +160,7 @@ from lp.registry.interfaces.sharingjob import (
60 )
61 from lp.registry.model.accesspolicy import (
62 AccessPolicyGrant,
63- reconcile_access_for_artifact,
64+ reconcile_access_for_artifacts,
65 )
66 from lp.registry.model.teammembership import TeamParticipation
67 from lp.services.config import config
68@@ -259,8 +259,8 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
69 # works, so only work for products for now.
70 if self.product is not None:
71 pillars = [self.product]
72- reconcile_access_for_artifact(
73- self, self.information_type, pillars, wanted_links)
74+ reconcile_access_for_artifacts(
75+ [self], self.information_type, pillars, wanted_links)
76
77 def setPrivate(self, private, user):
78 """See `IBranch`."""
79diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
80index 21a6f82..75c6905 100644
81--- a/lib/lp/code/model/gitrepository.py
82+++ b/lib/lp/code/model/gitrepository.py
83@@ -173,7 +173,7 @@ from lp.registry.interfaces.sharingjob import (
84 )
85 from lp.registry.model.accesspolicy import (
86 AccessPolicyGrant,
87- reconcile_access_for_artifact,
88+ reconcile_access_for_artifacts,
89 )
90 from lp.registry.model.person import Person
91 from lp.registry.model.teammembership import TeamParticipation
92@@ -618,8 +618,8 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
93 # works, so only work for projects for now.
94 if self.project is not None:
95 pillars = [self.project]
96- reconcile_access_for_artifact(
97- self, self.information_type, pillars, wanted_links)
98+ reconcile_access_for_artifacts(
99+ [self], self.information_type, pillars, wanted_links)
100
101 @property
102 def refs(self):
103diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
104index 1a7dda3..2aa988c 100644
105--- a/lib/lp/oci/model/ocirecipe.py
106+++ b/lib/lp/oci/model/ocirecipe.py
107@@ -94,7 +94,7 @@ from lp.registry.interfaces.person import (
108 validate_public_person,
109 )
110 from lp.registry.interfaces.role import IPersonRoles
111-from lp.registry.model.accesspolicy import reconcile_access_for_artifact
112+from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
113 from lp.registry.model.distribution import Distribution
114 from lp.registry.model.distroseries import DistroSeries
115 from lp.registry.model.person import Person
116@@ -293,8 +293,8 @@ class OCIRecipe(Storm, WebhookTargetMixin):
117 Takes the privacy and pillar and makes the related AccessArtifact
118 and AccessPolicyArtifacts match.
119 """
120- reconcile_access_for_artifact(self, self.information_type,
121- [self.pillar])
122+ reconcile_access_for_artifacts([self], self.information_type,
123+ [self.pillar])
124
125 def destroySelf(self):
126 """See `IOCIRecipe`."""
127diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
128index 0cd3d54..246b069 100644
129--- a/lib/lp/oci/tests/test_ocirecipe.py
130+++ b/lib/lp/oci/tests/test_ocirecipe.py
131@@ -67,6 +67,11 @@ from lp.registry.enums import (
132 PersonVisibility,
133 TeamMembershipPolicy,
134 )
135+from lp.registry.interfaces.accesspolicy import (
136+ IAccessArtifactSource,
137+ IAccessPolicyArtifactSource,
138+ IAccessPolicySource,
139+ )
140 from lp.registry.interfaces.series import SeriesStatus
141 from lp.services.config import config
142 from lp.services.database.constants import (
143@@ -830,6 +835,56 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
144 public_recipe, 'owner', private_team)
145
146
147+class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
148+ layer = DatabaseFunctionalLayer
149+
150+ def setUp(self):
151+ super(TestOCIRecipeAccessControl, self).setUp()
152+ self.setConfig()
153+
154+ def test_change_oci_project_pillar_reconciles_access(self):
155+ person = self.factory.makePerson()
156+ initial_project = self.factory.makeProduct(
157+ name='initial-project',
158+ owner=person, registrant=person)
159+ final_project = self.factory.makeProduct(
160+ name='final-project',
161+ owner=person, registrant=person)
162+ oci_project = self.factory.makeOCIProject(
163+ ociprojectname='the-oci-project', pillar=initial_project,
164+ registrant=person)
165+ recipes = []
166+ for i in range(10):
167+ recipes.append(self.factory.makeOCIRecipe(
168+ registrant=person,
169+ oci_project=oci_project,
170+ information_type=InformationType.USERDATA))
171+
172+ access_artifacts = getUtility(IAccessArtifactSource).find(recipes)
173+ initial_access_policy = getUtility(IAccessPolicySource).find(
174+ [(initial_project, InformationType.USERDATA)]).one()
175+ apasource = getUtility(IAccessPolicyArtifactSource)
176+ policy_artifacts = apasource.find(
177+ [(recipe_artifact, initial_access_policy)
178+ for recipe_artifact in access_artifacts])
179+ self.assertEqual(
180+ {i.policy.pillar for i in policy_artifacts}, {initial_project})
181+
182+ # Changing OCI project's pillar should move the policy artifacts of
183+ # all OCI recipes associated to the new pillar.
184+ flush_database_caches()
185+ with admin_logged_in():
186+ oci_project.pillar = final_project
187+
188+ final_access_policy = getUtility(IAccessPolicySource).find(
189+ [(final_project, InformationType.USERDATA)]).one()
190+ policy_artifacts = apasource.find(
191+ [(recipe_artifact, final_access_policy)
192+ for recipe_artifact in access_artifacts])
193+ self.assertEqual(
194+ {i.policy.pillar for i in policy_artifacts}, {final_project})
195+
196+
197 class TestOCIRecipeProcessors(TestCaseWithFactory):
198
199 layer = DatabaseFunctionalLayer
200diff --git a/lib/lp/registry/model/accesspolicy.py b/lib/lp/registry/model/accesspolicy.py
201index 64b69c8..010e49e 100644
202--- a/lib/lp/registry/model/accesspolicy.py
203+++ b/lib/lp/registry/model/accesspolicy.py
204@@ -11,10 +11,11 @@ __all__ = [
205 'AccessPolicyArtifact',
206 'AccessPolicyGrant',
207 'AccessPolicyGrantFlat',
208- 'reconcile_access_for_artifact',
209+ 'reconcile_access_for_artifacts',
210 ]
211
212 from collections import defaultdict
213+from itertools import product
214
215 import pytz
216 from storm.expr import (
217@@ -57,14 +58,14 @@ from lp.services.database.interfaces import IStore
218 from lp.services.database.stormbase import StormBase
219
220
221-def reconcile_access_for_artifact(artifact, information_type, pillars,
222- wanted_links=None):
223+def reconcile_access_for_artifacts(artifacts, information_type, pillars,
224+ wanted_links=None):
225 if information_type in PUBLIC_INFORMATION_TYPES:
226 # If it's public we can delete all the access information.
227 # IAccessArtifactSource handles the cascade.
228- getUtility(IAccessArtifactSource).delete([artifact])
229+ getUtility(IAccessArtifactSource).delete(artifacts)
230 return
231- [abstract_artifact] = getUtility(IAccessArtifactSource).ensure([artifact])
232+ abstract_artifacts = getUtility(IAccessArtifactSource).ensure(artifacts)
233 aps = getUtility(IAccessPolicySource).find(
234 (pillar, information_type) for pillar in pillars)
235 missing_pillars = set(pillars) - set([ap.pillar for ap in aps])
236@@ -77,11 +78,11 @@ def reconcile_access_for_artifact(artifact, information_type, pillars,
237 # Now determine the existing and desired links, and make them
238 # match. The caller may have provided the wanted_links.
239 apasource = getUtility(IAccessPolicyArtifactSource)
240- wanted_links = (wanted_links
241- or set((abstract_artifact, policy) for policy in aps))
242+ wanted_links = (
243+ wanted_links or set(product(abstract_artifacts, aps)))
244 existing_links = set([
245 (apa.abstract_artifact, apa.policy)
246- for apa in apasource.findByArtifact([abstract_artifact])])
247+ for apa in apasource.findByArtifact(abstract_artifacts)])
248 apasource.create(wanted_links - existing_links)
249 apasource.delete(existing_links - wanted_links)
250
251diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
252index eabde9b..b034602 100644
253--- a/lib/lp/registry/model/ociproject.py
254+++ b/lib/lp/registry/model/ociproject.py
255@@ -1,4 +1,4 @@
256-# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
257+# Copyright 2019-2021 Canonical Ltd. This software is licensed under the
258 # GNU Affero General Public License version 3 (see the file LICENSE).
259
260 """OCI Project implementation."""
261@@ -11,6 +11,8 @@ __all__ = [
262 'OCIProjectSet',
263 ]
264
265+from collections import defaultdict
266+
267 import pytz
268 import six
269 from six import text_type
270@@ -42,6 +44,7 @@ from lp.registry.interfaces.ociprojectname import IOCIProjectNameSet
271 from lp.registry.interfaces.person import IPersonSet
272 from lp.registry.interfaces.product import IProduct
273 from lp.registry.interfaces.series import SeriesStatus
274+from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
275 from lp.registry.model.ociprojectname import OCIProjectName
276 from lp.registry.model.ociprojectseries import OCIProjectSeries
277 from lp.registry.model.person import Person
278@@ -115,6 +118,11 @@ class OCIProject(BugTargetBase, StormBase):
279
280 @pillar.setter
281 def pillar(self, pillar):
282+ """See `IBugTarget`."""
283+ # We need to reconcile access for all OCI recipes from this OCI
284+ # project if we are moving from one pillar to another.
285+ needs_reconcile_access = (
286+ self.pillar is not None and self.pillar != pillar)
287 if IDistribution.providedBy(pillar):
288 self.distribution = pillar
289 self.project = None
290@@ -125,6 +133,8 @@ class OCIProject(BugTargetBase, StormBase):
291 raise ValueError(
292 'The target of an OCIProject must be either an IDistribution '
293 'or IProduct instance.')
294+ if needs_reconcile_access:
295+ self._reconcileAccess()
296
297 @property
298 def display_name(self):
299@@ -135,6 +145,19 @@ class OCIProject(BugTargetBase, StormBase):
300 bugtargetname = display_name
301 bugtargetdisplayname = display_name
302
303+ def _reconcileAccess(self):
304+ """Reconcile access for all OCI recipes of this project."""
305+ from lp.oci.model.ocirecipe import OCIRecipe
306+ rs = IStore(OCIRecipe).find(
307+ OCIRecipe,
308+ OCIRecipe.oci_project == self)
309+ recipes_per_info_type = defaultdict(set)
310+ for recipe in rs:
311+ recipes_per_info_type[recipe.information_type].add(recipe)
312+ for information_type, recipes in recipes_per_info_type.items():
313+ reconcile_access_for_artifacts(
314+ recipes, information_type, [self.pillar])
315+
316 def newRecipe(self, name, registrant, owner, git_ref,
317 build_file, description=None, build_daily=False,
318 require_virtualized=True, build_args=None):
319diff --git a/lib/lp/registry/tests/test_accesspolicy.py b/lib/lp/registry/tests/test_accesspolicy.py
320index 6ab5418..ce70d43 100644
321--- a/lib/lp/registry/tests/test_accesspolicy.py
322+++ b/lib/lp/registry/tests/test_accesspolicy.py
323@@ -1,4 +1,4 @@
324-# Copyright 2011-2015 Canonical Ltd. This software is licensed under the
325+# Copyright 2011-2021 Canonical Ltd. This software is licensed under the
326 # GNU Affero General Public License version 3 (see the file LICENSE).
327
328 __metaclass__ = type
329@@ -22,12 +22,18 @@ from lp.registry.interfaces.accesspolicy import (
330 IAccessPolicyGrantSource,
331 IAccessPolicySource,
332 )
333-from lp.registry.model.accesspolicy import reconcile_access_for_artifact
334+from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
335 from lp.registry.model.person import Person
336 from lp.services.database.interfaces import IStore
337-from lp.testing import TestCaseWithFactory
338+from lp.testing import (
339+ record_two_runs,
340+ TestCaseWithFactory,
341+ )
342 from lp.testing.layers import DatabaseFunctionalLayer
343-from lp.testing.matchers import Provides
344+from lp.testing.matchers import (
345+ HasQueryCount,
346+ Provides,
347+ )
348
349
350 def get_policies_for_artifact(concrete_artifact):
351@@ -729,57 +735,79 @@ class TestReconcileAccessPolicyArtifacts(TestCaseWithFactory):
352 get_policies_for_artifact(bug))
353
354 def test_creates_missing_accessartifact(self):
355- # reconcile_access_for_artifact creates an AccessArtifact for a
356+ # reconcile_access_for_artifacts creates an AccessArtifact for a
357 # private artifact if there isn't one already.
358 bug = self.factory.makeBug()
359
360 self.assertTrue(
361 getUtility(IAccessArtifactSource).find([bug]).is_empty())
362- reconcile_access_for_artifact(bug, InformationType.USERDATA, [])
363+ reconcile_access_for_artifacts([bug], InformationType.USERDATA, [])
364 self.assertFalse(
365 getUtility(IAccessArtifactSource).find([bug]).is_empty())
366
367+ def test_bulk_creates_missing_accessartifact_query_count(self):
368+ # reconcile_access_for_artifacts creates one for each AccessArtifact
369+ # private artifact if there isn't one already.
370+ bugs = [self.factory.makeBug()]
371+
372+ def create_bugs():
373+ while len(bugs):
374+ bugs.pop()
375+ for i in range(10):
376+ bugs.append(self.factory.makeBug())
377+
378+ def reconcile():
379+ reconcile_access_for_artifacts(bugs, InformationType.USERDATA, [])
380+
381+ # Runs with original `bugs` list with 1 item, then cleanup that list
382+ # and create another set of new bugs.
383+ recorder1, recorder2 = record_two_runs(reconcile, create_bugs, 0, 1)
384+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
385+
386+ self.assertEqual(
387+ 10, getUtility(IAccessArtifactSource).find(bugs).count())
388+
389 def test_removes_extra_accessartifact(self):
390- # reconcile_access_for_artifact removes an AccessArtifact for a
391+ # reconcile_access_for_artifacts removes an AccessArtifact for a
392 # public artifact if there's one left over.
393 bug = self.factory.makeBug()
394- reconcile_access_for_artifact(bug, InformationType.USERDATA, [])
395+ reconcile_access_for_artifacts([bug], InformationType.USERDATA, [])
396
397 self.assertFalse(
398 getUtility(IAccessArtifactSource).find([bug]).is_empty())
399- reconcile_access_for_artifact(bug, InformationType.PUBLIC, [])
400+ reconcile_access_for_artifacts([bug], InformationType.PUBLIC, [])
401 self.assertTrue(
402 getUtility(IAccessArtifactSource).find([bug]).is_empty())
403
404 def test_adds_missing_accesspolicyartifacts(self):
405- # reconcile_access_for_artifact adds missing links.
406+ # reconcile_access_for_artifacts adds missing links.
407 product = self.factory.makeProduct()
408 bug = self.factory.makeBug(target=product)
409- reconcile_access_for_artifact(bug, InformationType.USERDATA, [])
410+ reconcile_access_for_artifacts([bug], InformationType.USERDATA, [])
411
412 self.assertPoliciesForBug([], bug)
413- reconcile_access_for_artifact(
414- bug, InformationType.USERDATA, [product])
415+ reconcile_access_for_artifacts(
416+ [bug], InformationType.USERDATA, [product])
417 self.assertPoliciesForBug([(product, InformationType.USERDATA)], bug)
418
419 def test_removes_extra_accesspolicyartifacts(self):
420- # reconcile_access_for_artifact removes excess links.
421+ # reconcile_access_for_artifacts removes excess links.
422 bug = self.factory.makeBug()
423 product = self.factory.makeProduct()
424 other_product = self.factory.makeProduct()
425- reconcile_access_for_artifact(
426- bug, InformationType.USERDATA, [product, other_product])
427+ reconcile_access_for_artifacts(
428+ [bug], InformationType.USERDATA, [product, other_product])
429
430 self.assertPoliciesForBug(
431 [(product, InformationType.USERDATA),
432 (other_product, InformationType.USERDATA)],
433 bug)
434- reconcile_access_for_artifact(
435- bug, InformationType.USERDATA, [product])
436+ reconcile_access_for_artifacts(
437+ [bug], InformationType.USERDATA, [product])
438 self.assertPoliciesForBug([(product, InformationType.USERDATA)], bug)
439
440 def test_raises_exception_on_missing_policies(self):
441- # reconcile_access_for_artifact raises an exception if a pillar is
442+ # reconcile_access_for_artifacts raises an exception if a pillar is
443 # missing an AccessPolicy.
444 product = self.factory.makeProduct()
445 # Creating a product will have created two APs, delete them.
446@@ -792,5 +820,5 @@ class TestReconcileAccessPolicyArtifacts(TestCaseWithFactory):
447 "Pillar(s) %s require an access policy for information type "
448 "Private.") % product.name
449 self.assertRaisesWithContent(
450- AssertionError, expected, reconcile_access_for_artifact, bug,
451+ AssertionError, expected, reconcile_access_for_artifacts, [bug],
452 InformationType.USERDATA, [product])
453diff --git a/lib/lp/registry/tests/test_sharingjob.py b/lib/lp/registry/tests/test_sharingjob.py
454index 32aec0f..73ad71f 100644
455--- a/lib/lp/registry/tests/test_sharingjob.py
456+++ b/lib/lp/registry/tests/test_sharingjob.py
457@@ -27,7 +27,7 @@ from lp.registry.interfaces.sharingjob import (
458 ISharingJob,
459 ISharingJobSource,
460 )
461-from lp.registry.model.accesspolicy import reconcile_access_for_artifact
462+from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
463 from lp.registry.model.sharingjob import (
464 RemoveArtifactSubscriptionsJob,
465 SharingJob,
466@@ -378,8 +378,8 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
467 # Change artifact attributes so that it can become inaccessible for
468 # some users.
469 change_callback(concrete_artifact)
470- reconcile_access_for_artifact(
471- concrete_artifact, concrete_artifact.information_type,
472+ reconcile_access_for_artifacts(
473+ [concrete_artifact], concrete_artifact.information_type,
474 get_pillars(concrete_artifact))
475
476 getUtility(IRemoveArtifactSubscriptionsJobSource).create(
477diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
478index 5b1be23..afa7e7d 100644
479--- a/lib/lp/snappy/model/snap.py
480+++ b/lib/lp/snappy/model/snap.py
481@@ -132,7 +132,7 @@ from lp.registry.interfaces.role import (
482 )
483 from lp.registry.model.accesspolicy import (
484 AccessPolicyGrant,
485- reconcile_access_for_artifact,
486+ reconcile_access_for_artifacts,
487 )
488 from lp.registry.model.distroseries import DistroSeries
489 from lp.registry.model.person import Person
490@@ -1238,7 +1238,7 @@ class Snap(Storm, WebhookTargetMixin):
491 if self.project is None:
492 return
493 pillars = [self.project]
494- reconcile_access_for_artifact(self, self.information_type, pillars)
495+ reconcile_access_for_artifacts([self], self.information_type, pillars)
496
497 def setProject(self, project):
498 self.project = project