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
diff --git a/lib/lp/blueprints/model/specification.py b/lib/lp/blueprints/model/specification.py
index 55ce9d5..9f3b00a 100644
--- a/lib/lp/blueprints/model/specification.py
+++ b/lib/lp/blueprints/model/specification.py
@@ -922,14 +922,14 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
922 """See ISpecification."""922 """See ISpecification."""
923 # avoid circular imports.923 # avoid circular imports.
924 from lp.registry.model.accesspolicy import (924 from lp.registry.model.accesspolicy import (
925 reconcile_access_for_artifact,925 reconcile_access_for_artifacts,
926 )926 )
927 if self.information_type == information_type:927 if self.information_type == information_type:
928 return False928 return False
929 if information_type not in self.getAllowedInformationTypes(who):929 if information_type not in self.getAllowedInformationTypes(who):
930 raise CannotChangeInformationType("Forbidden by project policy.")930 raise CannotChangeInformationType("Forbidden by project policy.")
931 self.information_type = information_type931 self.information_type = information_type
932 reconcile_access_for_artifact(self, information_type, [self.target])932 reconcile_access_for_artifacts([self], information_type, [self.target])
933 if (information_type in PRIVATE_INFORMATION_TYPES and933 if (information_type in PRIVATE_INFORMATION_TYPES and
934 not self.subscribers.is_empty()):934 not self.subscribers.is_empty()):
935 # Grant the subscribers access if they do not have a935 # Grant the subscribers access if they do not have a
diff --git a/lib/lp/bugs/model/bug.py b/lib/lp/bugs/model/bug.py
index f7ebc86..f2148e0 100644
--- a/lib/lp/bugs/model/bug.py
+++ b/lib/lp/bugs/model/bug.py
@@ -189,7 +189,7 @@ from lp.registry.interfaces.sharingjob import (
189 IRemoveArtifactSubscriptionsJobSource,189 IRemoveArtifactSubscriptionsJobSource,
190 )190 )
191from lp.registry.interfaces.sourcepackage import ISourcePackage191from lp.registry.interfaces.sourcepackage import ISourcePackage
192from lp.registry.model.accesspolicy import reconcile_access_for_artifact192from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
193from lp.registry.model.person import (193from lp.registry.model.person import (
194 Person,194 Person,
195 person_sort_key,195 person_sort_key,
@@ -2122,7 +2122,7 @@ class Bug(SQLBase, InformationTypeMixin):
2122 BugSubscription.person == person).is_empty()2122 BugSubscription.person == person).is_empty()
21232123
2124 def _reconcileAccess(self):2124 def _reconcileAccess(self):
2125 # reconcile_access_for_artifact will only use the pillar list if2125 # reconcile_access_for_artifacts will only use the pillar list if
2126 # the information type is private. But affected_pillars iterates2126 # the information type is private. But affected_pillars iterates
2127 # over the tasks immediately, which is needless expense for2127 # over the tasks immediately, which is needless expense for
2128 # public bugs.2128 # public bugs.
@@ -2130,8 +2130,8 @@ class Bug(SQLBase, InformationTypeMixin):
2130 pillars = self.affected_pillars2130 pillars = self.affected_pillars
2131 else:2131 else:
2132 pillars = []2132 pillars = []
2133 reconcile_access_for_artifact(2133 reconcile_access_for_artifacts(
2134 self, self.information_type, pillars)2134 [self], self.information_type, pillars)
21352135
2136 def _attachments_query(self):2136 def _attachments_query(self):
2137 """Helper for the attachments* properties."""2137 """Helper for the attachments* properties."""
diff --git a/lib/lp/code/model/branch.py b/lib/lp/code/model/branch.py
index 278db40..27746c7 100644
--- a/lib/lp/code/model/branch.py
+++ b/lib/lp/code/model/branch.py
@@ -160,7 +160,7 @@ from lp.registry.interfaces.sharingjob import (
160 )160 )
161from lp.registry.model.accesspolicy import (161from lp.registry.model.accesspolicy import (
162 AccessPolicyGrant,162 AccessPolicyGrant,
163 reconcile_access_for_artifact,163 reconcile_access_for_artifacts,
164 )164 )
165from lp.registry.model.teammembership import TeamParticipation165from lp.registry.model.teammembership import TeamParticipation
166from lp.services.config import config166from lp.services.config import config
@@ -259,8 +259,8 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
259 # works, so only work for products for now.259 # works, so only work for products for now.
260 if self.product is not None:260 if self.product is not None:
261 pillars = [self.product]261 pillars = [self.product]
262 reconcile_access_for_artifact(262 reconcile_access_for_artifacts(
263 self, self.information_type, pillars, wanted_links)263 [self], self.information_type, pillars, wanted_links)
264264
265 def setPrivate(self, private, user):265 def setPrivate(self, private, user):
266 """See `IBranch`."""266 """See `IBranch`."""
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 21a6f82..75c6905 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -173,7 +173,7 @@ from lp.registry.interfaces.sharingjob import (
173 )173 )
174from lp.registry.model.accesspolicy import (174from lp.registry.model.accesspolicy import (
175 AccessPolicyGrant,175 AccessPolicyGrant,
176 reconcile_access_for_artifact,176 reconcile_access_for_artifacts,
177 )177 )
178from lp.registry.model.person import Person178from lp.registry.model.person import Person
179from lp.registry.model.teammembership import TeamParticipation179from lp.registry.model.teammembership import TeamParticipation
@@ -618,8 +618,8 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
618 # works, so only work for projects for now.618 # works, so only work for projects for now.
619 if self.project is not None:619 if self.project is not None:
620 pillars = [self.project]620 pillars = [self.project]
621 reconcile_access_for_artifact(621 reconcile_access_for_artifacts(
622 self, self.information_type, pillars, wanted_links)622 [self], self.information_type, pillars, wanted_links)
623623
624 @property624 @property
625 def refs(self):625 def refs(self):
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 1a7dda3..2aa988c 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -94,7 +94,7 @@ from lp.registry.interfaces.person import (
94 validate_public_person,94 validate_public_person,
95 )95 )
96from lp.registry.interfaces.role import IPersonRoles96from lp.registry.interfaces.role import IPersonRoles
97from lp.registry.model.accesspolicy import reconcile_access_for_artifact97from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
98from lp.registry.model.distribution import Distribution98from lp.registry.model.distribution import Distribution
99from lp.registry.model.distroseries import DistroSeries99from lp.registry.model.distroseries import DistroSeries
100from lp.registry.model.person import Person100from lp.registry.model.person import Person
@@ -293,8 +293,8 @@ class OCIRecipe(Storm, WebhookTargetMixin):
293 Takes the privacy and pillar and makes the related AccessArtifact293 Takes the privacy and pillar and makes the related AccessArtifact
294 and AccessPolicyArtifacts match.294 and AccessPolicyArtifacts match.
295 """295 """
296 reconcile_access_for_artifact(self, self.information_type,296 reconcile_access_for_artifacts([self], self.information_type,
297 [self.pillar])297 [self.pillar])
298298
299 def destroySelf(self):299 def destroySelf(self):
300 """See `IOCIRecipe`."""300 """See `IOCIRecipe`."""
diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
index 0cd3d54..246b069 100644
--- a/lib/lp/oci/tests/test_ocirecipe.py
+++ b/lib/lp/oci/tests/test_ocirecipe.py
@@ -67,6 +67,11 @@ from lp.registry.enums import (
67 PersonVisibility,67 PersonVisibility,
68 TeamMembershipPolicy,68 TeamMembershipPolicy,
69 )69 )
70from lp.registry.interfaces.accesspolicy import (
71 IAccessArtifactSource,
72 IAccessPolicyArtifactSource,
73 IAccessPolicySource,
74 )
70from lp.registry.interfaces.series import SeriesStatus75from lp.registry.interfaces.series import SeriesStatus
71from lp.services.config import config76from lp.services.config import config
72from lp.services.database.constants import (77from lp.services.database.constants import (
@@ -830,6 +835,56 @@ class TestOCIRecipe(OCIConfigHelperMixin, TestCaseWithFactory):
830 public_recipe, 'owner', private_team)835 public_recipe, 'owner', private_team)
831836
832837
838class TestOCIRecipeAccessControl(TestCaseWithFactory, OCIConfigHelperMixin):
839 layer = DatabaseFunctionalLayer
840
841 def setUp(self):
842 super(TestOCIRecipeAccessControl, self).setUp()
843 self.setConfig()
844
845 def test_change_oci_project_pillar_reconciles_access(self):
846 person = self.factory.makePerson()
847 initial_project = self.factory.makeProduct(
848 name='initial-project',
849 owner=person, registrant=person)
850 final_project = self.factory.makeProduct(
851 name='final-project',
852 owner=person, registrant=person)
853 oci_project = self.factory.makeOCIProject(
854 ociprojectname='the-oci-project', pillar=initial_project,
855 registrant=person)
856 recipes = []
857 for i in range(10):
858 recipes.append(self.factory.makeOCIRecipe(
859 registrant=person,
860 oci_project=oci_project,
861 information_type=InformationType.USERDATA))
862
863 access_artifacts = getUtility(IAccessArtifactSource).find(recipes)
864 initial_access_policy = getUtility(IAccessPolicySource).find(
865 [(initial_project, InformationType.USERDATA)]).one()
866 apasource = getUtility(IAccessPolicyArtifactSource)
867 policy_artifacts = apasource.find(
868 [(recipe_artifact, initial_access_policy)
869 for recipe_artifact in access_artifacts])
870 self.assertEqual(
871 {i.policy.pillar for i in policy_artifacts}, {initial_project})
872
873 # Changing OCI project's pillar should move the policy artifacts of
874 # all OCI recipes associated to the new pillar.
875 flush_database_caches()
876 with admin_logged_in():
877 oci_project.pillar = final_project
878
879 final_access_policy = getUtility(IAccessPolicySource).find(
880 [(final_project, InformationType.USERDATA)]).one()
881 policy_artifacts = apasource.find(
882 [(recipe_artifact, final_access_policy)
883 for recipe_artifact in access_artifacts])
884 self.assertEqual(
885 {i.policy.pillar for i in policy_artifacts}, {final_project})
886
887
833class TestOCIRecipeProcessors(TestCaseWithFactory):888class TestOCIRecipeProcessors(TestCaseWithFactory):
834889
835 layer = DatabaseFunctionalLayer890 layer = DatabaseFunctionalLayer
diff --git a/lib/lp/registry/model/accesspolicy.py b/lib/lp/registry/model/accesspolicy.py
index 64b69c8..010e49e 100644
--- a/lib/lp/registry/model/accesspolicy.py
+++ b/lib/lp/registry/model/accesspolicy.py
@@ -11,10 +11,11 @@ __all__ = [
11 'AccessPolicyArtifact',11 'AccessPolicyArtifact',
12 'AccessPolicyGrant',12 'AccessPolicyGrant',
13 'AccessPolicyGrantFlat',13 'AccessPolicyGrantFlat',
14 'reconcile_access_for_artifact',14 'reconcile_access_for_artifacts',
15 ]15 ]
1616
17from collections import defaultdict17from collections import defaultdict
18from itertools import product
1819
19import pytz20import pytz
20from storm.expr import (21from storm.expr import (
@@ -57,14 +58,14 @@ from lp.services.database.interfaces import IStore
57from lp.services.database.stormbase import StormBase58from lp.services.database.stormbase import StormBase
5859
5960
60def reconcile_access_for_artifact(artifact, information_type, pillars,61def reconcile_access_for_artifacts(artifacts, information_type, pillars,
61 wanted_links=None):62 wanted_links=None):
62 if information_type in PUBLIC_INFORMATION_TYPES:63 if information_type in PUBLIC_INFORMATION_TYPES:
63 # If it's public we can delete all the access information.64 # If it's public we can delete all the access information.
64 # IAccessArtifactSource handles the cascade.65 # IAccessArtifactSource handles the cascade.
65 getUtility(IAccessArtifactSource).delete([artifact])66 getUtility(IAccessArtifactSource).delete(artifacts)
66 return67 return
67 [abstract_artifact] = getUtility(IAccessArtifactSource).ensure([artifact])68 abstract_artifacts = getUtility(IAccessArtifactSource).ensure(artifacts)
68 aps = getUtility(IAccessPolicySource).find(69 aps = getUtility(IAccessPolicySource).find(
69 (pillar, information_type) for pillar in pillars)70 (pillar, information_type) for pillar in pillars)
70 missing_pillars = set(pillars) - set([ap.pillar for ap in aps])71 missing_pillars = set(pillars) - set([ap.pillar for ap in aps])
@@ -77,11 +78,11 @@ def reconcile_access_for_artifact(artifact, information_type, pillars,
77 # Now determine the existing and desired links, and make them78 # Now determine the existing and desired links, and make them
78 # match. The caller may have provided the wanted_links.79 # match. The caller may have provided the wanted_links.
79 apasource = getUtility(IAccessPolicyArtifactSource)80 apasource = getUtility(IAccessPolicyArtifactSource)
80 wanted_links = (wanted_links81 wanted_links = (
81 or set((abstract_artifact, policy) for policy in aps))82 wanted_links or set(product(abstract_artifacts, aps)))
82 existing_links = set([83 existing_links = set([
83 (apa.abstract_artifact, apa.policy)84 (apa.abstract_artifact, apa.policy)
84 for apa in apasource.findByArtifact([abstract_artifact])])85 for apa in apasource.findByArtifact(abstract_artifacts)])
85 apasource.create(wanted_links - existing_links)86 apasource.create(wanted_links - existing_links)
86 apasource.delete(existing_links - wanted_links)87 apasource.delete(existing_links - wanted_links)
8788
diff --git a/lib/lp/registry/model/ociproject.py b/lib/lp/registry/model/ociproject.py
index eabde9b..b034602 100644
--- a/lib/lp/registry/model/ociproject.py
+++ b/lib/lp/registry/model/ociproject.py
@@ -1,4 +1,4 @@
1# Copyright 2019-2020 Canonical Ltd. This software is licensed under the1# Copyright 2019-2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""OCI Project implementation."""4"""OCI Project implementation."""
@@ -11,6 +11,8 @@ __all__ = [
11 'OCIProjectSet',11 'OCIProjectSet',
12 ]12 ]
1313
14from collections import defaultdict
15
14import pytz16import pytz
15import six17import six
16from six import text_type18from six import text_type
@@ -42,6 +44,7 @@ from lp.registry.interfaces.ociprojectname import IOCIProjectNameSet
42from lp.registry.interfaces.person import IPersonSet44from lp.registry.interfaces.person import IPersonSet
43from lp.registry.interfaces.product import IProduct45from lp.registry.interfaces.product import IProduct
44from lp.registry.interfaces.series import SeriesStatus46from lp.registry.interfaces.series import SeriesStatus
47from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
45from lp.registry.model.ociprojectname import OCIProjectName48from lp.registry.model.ociprojectname import OCIProjectName
46from lp.registry.model.ociprojectseries import OCIProjectSeries49from lp.registry.model.ociprojectseries import OCIProjectSeries
47from lp.registry.model.person import Person50from lp.registry.model.person import Person
@@ -115,6 +118,11 @@ class OCIProject(BugTargetBase, StormBase):
115118
116 @pillar.setter119 @pillar.setter
117 def pillar(self, pillar):120 def pillar(self, pillar):
121 """See `IBugTarget`."""
122 # We need to reconcile access for all OCI recipes from this OCI
123 # project if we are moving from one pillar to another.
124 needs_reconcile_access = (
125 self.pillar is not None and self.pillar != pillar)
118 if IDistribution.providedBy(pillar):126 if IDistribution.providedBy(pillar):
119 self.distribution = pillar127 self.distribution = pillar
120 self.project = None128 self.project = None
@@ -125,6 +133,8 @@ class OCIProject(BugTargetBase, StormBase):
125 raise ValueError(133 raise ValueError(
126 'The target of an OCIProject must be either an IDistribution '134 'The target of an OCIProject must be either an IDistribution '
127 'or IProduct instance.')135 'or IProduct instance.')
136 if needs_reconcile_access:
137 self._reconcileAccess()
128138
129 @property139 @property
130 def display_name(self):140 def display_name(self):
@@ -135,6 +145,19 @@ class OCIProject(BugTargetBase, StormBase):
135 bugtargetname = display_name145 bugtargetname = display_name
136 bugtargetdisplayname = display_name146 bugtargetdisplayname = display_name
137147
148 def _reconcileAccess(self):
149 """Reconcile access for all OCI recipes of this project."""
150 from lp.oci.model.ocirecipe import OCIRecipe
151 rs = IStore(OCIRecipe).find(
152 OCIRecipe,
153 OCIRecipe.oci_project == self)
154 recipes_per_info_type = defaultdict(set)
155 for recipe in rs:
156 recipes_per_info_type[recipe.information_type].add(recipe)
157 for information_type, recipes in recipes_per_info_type.items():
158 reconcile_access_for_artifacts(
159 recipes, information_type, [self.pillar])
160
138 def newRecipe(self, name, registrant, owner, git_ref,161 def newRecipe(self, name, registrant, owner, git_ref,
139 build_file, description=None, build_daily=False,162 build_file, description=None, build_daily=False,
140 require_virtualized=True, build_args=None):163 require_virtualized=True, build_args=None):
diff --git a/lib/lp/registry/tests/test_accesspolicy.py b/lib/lp/registry/tests/test_accesspolicy.py
index 6ab5418..ce70d43 100644
--- a/lib/lp/registry/tests/test_accesspolicy.py
+++ b/lib/lp/registry/tests/test_accesspolicy.py
@@ -1,4 +1,4 @@
1# Copyright 2011-2015 Canonical Ltd. This software is licensed under the1# Copyright 2011-2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -22,12 +22,18 @@ from lp.registry.interfaces.accesspolicy import (
22 IAccessPolicyGrantSource,22 IAccessPolicyGrantSource,
23 IAccessPolicySource,23 IAccessPolicySource,
24 )24 )
25from lp.registry.model.accesspolicy import reconcile_access_for_artifact25from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
26from lp.registry.model.person import Person26from lp.registry.model.person import Person
27from lp.services.database.interfaces import IStore27from lp.services.database.interfaces import IStore
28from lp.testing import TestCaseWithFactory28from lp.testing import (
29 record_two_runs,
30 TestCaseWithFactory,
31 )
29from lp.testing.layers import DatabaseFunctionalLayer32from lp.testing.layers import DatabaseFunctionalLayer
30from lp.testing.matchers import Provides33from lp.testing.matchers import (
34 HasQueryCount,
35 Provides,
36 )
3137
3238
33def get_policies_for_artifact(concrete_artifact):39def get_policies_for_artifact(concrete_artifact):
@@ -729,57 +735,79 @@ class TestReconcileAccessPolicyArtifacts(TestCaseWithFactory):
729 get_policies_for_artifact(bug))735 get_policies_for_artifact(bug))
730736
731 def test_creates_missing_accessartifact(self):737 def test_creates_missing_accessartifact(self):
732 # reconcile_access_for_artifact creates an AccessArtifact for a738 # reconcile_access_for_artifacts creates an AccessArtifact for a
733 # private artifact if there isn't one already.739 # private artifact if there isn't one already.
734 bug = self.factory.makeBug()740 bug = self.factory.makeBug()
735741
736 self.assertTrue(742 self.assertTrue(
737 getUtility(IAccessArtifactSource).find([bug]).is_empty())743 getUtility(IAccessArtifactSource).find([bug]).is_empty())
738 reconcile_access_for_artifact(bug, InformationType.USERDATA, [])744 reconcile_access_for_artifacts([bug], InformationType.USERDATA, [])
739 self.assertFalse(745 self.assertFalse(
740 getUtility(IAccessArtifactSource).find([bug]).is_empty())746 getUtility(IAccessArtifactSource).find([bug]).is_empty())
741747
748 def test_bulk_creates_missing_accessartifact_query_count(self):
749 # reconcile_access_for_artifacts creates one for each AccessArtifact
750 # private artifact if there isn't one already.
751 bugs = [self.factory.makeBug()]
752
753 def create_bugs():
754 while len(bugs):
755 bugs.pop()
756 for i in range(10):
757 bugs.append(self.factory.makeBug())
758
759 def reconcile():
760 reconcile_access_for_artifacts(bugs, InformationType.USERDATA, [])
761
762 # Runs with original `bugs` list with 1 item, then cleanup that list
763 # and create another set of new bugs.
764 recorder1, recorder2 = record_two_runs(reconcile, create_bugs, 0, 1)
765 self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
766
767 self.assertEqual(
768 10, getUtility(IAccessArtifactSource).find(bugs).count())
769
742 def test_removes_extra_accessartifact(self):770 def test_removes_extra_accessartifact(self):
743 # reconcile_access_for_artifact removes an AccessArtifact for a771 # reconcile_access_for_artifacts removes an AccessArtifact for a
744 # public artifact if there's one left over.772 # public artifact if there's one left over.
745 bug = self.factory.makeBug()773 bug = self.factory.makeBug()
746 reconcile_access_for_artifact(bug, InformationType.USERDATA, [])774 reconcile_access_for_artifacts([bug], InformationType.USERDATA, [])
747775
748 self.assertFalse(776 self.assertFalse(
749 getUtility(IAccessArtifactSource).find([bug]).is_empty())777 getUtility(IAccessArtifactSource).find([bug]).is_empty())
750 reconcile_access_for_artifact(bug, InformationType.PUBLIC, [])778 reconcile_access_for_artifacts([bug], InformationType.PUBLIC, [])
751 self.assertTrue(779 self.assertTrue(
752 getUtility(IAccessArtifactSource).find([bug]).is_empty())780 getUtility(IAccessArtifactSource).find([bug]).is_empty())
753781
754 def test_adds_missing_accesspolicyartifacts(self):782 def test_adds_missing_accesspolicyartifacts(self):
755 # reconcile_access_for_artifact adds missing links.783 # reconcile_access_for_artifacts adds missing links.
756 product = self.factory.makeProduct()784 product = self.factory.makeProduct()
757 bug = self.factory.makeBug(target=product)785 bug = self.factory.makeBug(target=product)
758 reconcile_access_for_artifact(bug, InformationType.USERDATA, [])786 reconcile_access_for_artifacts([bug], InformationType.USERDATA, [])
759787
760 self.assertPoliciesForBug([], bug)788 self.assertPoliciesForBug([], bug)
761 reconcile_access_for_artifact(789 reconcile_access_for_artifacts(
762 bug, InformationType.USERDATA, [product])790 [bug], InformationType.USERDATA, [product])
763 self.assertPoliciesForBug([(product, InformationType.USERDATA)], bug)791 self.assertPoliciesForBug([(product, InformationType.USERDATA)], bug)
764792
765 def test_removes_extra_accesspolicyartifacts(self):793 def test_removes_extra_accesspolicyartifacts(self):
766 # reconcile_access_for_artifact removes excess links.794 # reconcile_access_for_artifacts removes excess links.
767 bug = self.factory.makeBug()795 bug = self.factory.makeBug()
768 product = self.factory.makeProduct()796 product = self.factory.makeProduct()
769 other_product = self.factory.makeProduct()797 other_product = self.factory.makeProduct()
770 reconcile_access_for_artifact(798 reconcile_access_for_artifacts(
771 bug, InformationType.USERDATA, [product, other_product])799 [bug], InformationType.USERDATA, [product, other_product])
772800
773 self.assertPoliciesForBug(801 self.assertPoliciesForBug(
774 [(product, InformationType.USERDATA),802 [(product, InformationType.USERDATA),
775 (other_product, InformationType.USERDATA)],803 (other_product, InformationType.USERDATA)],
776 bug)804 bug)
777 reconcile_access_for_artifact(805 reconcile_access_for_artifacts(
778 bug, InformationType.USERDATA, [product])806 [bug], InformationType.USERDATA, [product])
779 self.assertPoliciesForBug([(product, InformationType.USERDATA)], bug)807 self.assertPoliciesForBug([(product, InformationType.USERDATA)], bug)
780808
781 def test_raises_exception_on_missing_policies(self):809 def test_raises_exception_on_missing_policies(self):
782 # reconcile_access_for_artifact raises an exception if a pillar is810 # reconcile_access_for_artifacts raises an exception if a pillar is
783 # missing an AccessPolicy.811 # missing an AccessPolicy.
784 product = self.factory.makeProduct()812 product = self.factory.makeProduct()
785 # Creating a product will have created two APs, delete them.813 # Creating a product will have created two APs, delete them.
@@ -792,5 +820,5 @@ class TestReconcileAccessPolicyArtifacts(TestCaseWithFactory):
792 "Pillar(s) %s require an access policy for information type "820 "Pillar(s) %s require an access policy for information type "
793 "Private.") % product.name821 "Private.") % product.name
794 self.assertRaisesWithContent(822 self.assertRaisesWithContent(
795 AssertionError, expected, reconcile_access_for_artifact, bug,823 AssertionError, expected, reconcile_access_for_artifacts, [bug],
796 InformationType.USERDATA, [product])824 InformationType.USERDATA, [product])
diff --git a/lib/lp/registry/tests/test_sharingjob.py b/lib/lp/registry/tests/test_sharingjob.py
index 32aec0f..73ad71f 100644
--- a/lib/lp/registry/tests/test_sharingjob.py
+++ b/lib/lp/registry/tests/test_sharingjob.py
@@ -27,7 +27,7 @@ from lp.registry.interfaces.sharingjob import (
27 ISharingJob,27 ISharingJob,
28 ISharingJobSource,28 ISharingJobSource,
29 )29 )
30from lp.registry.model.accesspolicy import reconcile_access_for_artifact30from lp.registry.model.accesspolicy import reconcile_access_for_artifacts
31from lp.registry.model.sharingjob import (31from lp.registry.model.sharingjob import (
32 RemoveArtifactSubscriptionsJob,32 RemoveArtifactSubscriptionsJob,
33 SharingJob,33 SharingJob,
@@ -378,8 +378,8 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
378 # Change artifact attributes so that it can become inaccessible for378 # Change artifact attributes so that it can become inaccessible for
379 # some users.379 # some users.
380 change_callback(concrete_artifact)380 change_callback(concrete_artifact)
381 reconcile_access_for_artifact(381 reconcile_access_for_artifacts(
382 concrete_artifact, concrete_artifact.information_type,382 [concrete_artifact], concrete_artifact.information_type,
383 get_pillars(concrete_artifact))383 get_pillars(concrete_artifact))
384384
385 getUtility(IRemoveArtifactSubscriptionsJobSource).create(385 getUtility(IRemoveArtifactSubscriptionsJobSource).create(
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index 5b1be23..afa7e7d 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -132,7 +132,7 @@ from lp.registry.interfaces.role import (
132 )132 )
133from lp.registry.model.accesspolicy import (133from lp.registry.model.accesspolicy import (
134 AccessPolicyGrant,134 AccessPolicyGrant,
135 reconcile_access_for_artifact,135 reconcile_access_for_artifacts,
136 )136 )
137from lp.registry.model.distroseries import DistroSeries137from lp.registry.model.distroseries import DistroSeries
138from lp.registry.model.person import Person138from lp.registry.model.person import Person
@@ -1238,7 +1238,7 @@ class Snap(Storm, WebhookTargetMixin):
1238 if self.project is None:1238 if self.project is None:
1239 return1239 return
1240 pillars = [self.project]1240 pillars = [self.project]
1241 reconcile_access_for_artifact(self, self.information_type, pillars)1241 reconcile_access_for_artifacts([self], self.information_type, pillars)
12421242
1243 def setProject(self, project):1243 def setProject(self, project):
1244 self.project = project1244 self.project = project