← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:ocirecipe-private-accesspolicy into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:ocirecipe-private-accesspolicy into launchpad:master.

Commit message:
Allowing OCI recipes to be access artifacts

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/399394
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:ocirecipe-private-accesspolicy into launchpad:master.
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index a23fa03..26e48e0 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -61,6 +61,7 @@ from zope.schema import (
 from zope.security.interfaces import Unauthorized
 
 from lp import _
+from lp.app.enums import InformationType
 from lp.app.errors import NameLookupFailed
 from lp.app.validators.name import name_validator
 from lp.app.validators.path import path_does_not_escape
@@ -380,6 +381,12 @@ class IOCIRecipeEditableAttributes(IHasOwner):
         description=_("The owner of this OCI recipe."),
         readonly=False))
 
+    information_type = exported(Choice(
+        title=_("Information type"), vocabulary=InformationType,
+        required=True, readonly=False, default=InformationType.PUBLIC,
+        description=_(
+            "The type of information contained in this OCI recipe.")))
+
     oci_project = exported(Reference(
         IOCIProject,
         title=_("OCI project"),
@@ -492,7 +499,8 @@ class IOCIRecipeSet(Interface):
     def new(name, registrant, owner, oci_project, git_ref, build_file,
             description=None, official=False, require_virtualized=True,
             build_daily=False, processors=None, date_created=DEFAULT,
-            allow_internet=True, build_args=None):
+            allow_internet=True, build_args=None,
+            information_type=InformationType.PUBLIC):
         """Create an IOCIRecipe."""
 
     def exists(owner, oci_project, name):
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index e4edcae..67fe0f8 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -44,6 +44,7 @@ from zope.security.proxy import (
     removeSecurityProxy,
     )
 
+from lp.app.enums import InformationType
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.interfaces.security import IAuthorization
 from lp.app.validators.validation import validate_oci_branch_name
@@ -85,6 +86,7 @@ from lp.oci.model.ocirecipejob import OCIRecipeJob
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.role import IPersonRoles
+from lp.registry.model.accesspolicy import reconcile_access_for_artifact
 from lp.registry.model.distribution import Distribution
 from lp.registry.model.distroseries import DistroSeries
 from lp.registry.model.person import Person
@@ -95,6 +97,7 @@ from lp.services.database.constants import (
     UTC_NOW,
     )
 from lp.services.database.decoratedresultset import DecoratedResultSet
+from lp.services.database.enumcol import DBEnum
 from lp.services.database.interfaces import (
     IMasterStore,
     IStore,
@@ -141,6 +144,10 @@ class OCIRecipe(Storm, WebhookTargetMixin):
     owner_id = Int(name='owner', allow_none=False)
     owner = Reference(owner_id, 'Person.id')
 
+    _information_type = DBEnum(
+        enum=InformationType, default=InformationType.PUBLIC,
+        name="information_type")
+
     oci_project_id = Int(name='oci_project', allow_none=False)
     oci_project = Reference(oci_project_id, "OCIProject.id")
 
@@ -171,13 +178,14 @@ class OCIRecipe(Storm, WebhookTargetMixin):
                  description=None, official=False, require_virtualized=True,
                  build_file=None, build_daily=False, date_created=DEFAULT,
                  allow_internet=True, build_args=None, build_path=None,
-                 image_name=None):
+                 image_name=None, information_type=InformationType.PUBLIC):
         if not getFeatureFlag(OCI_RECIPE_ALLOW_CREATE):
             raise OCIRecipeFeatureDisabled()
         super(OCIRecipe, self).__init__()
         self.name = name
         self.registrant = registrant
         self.owner = owner
+        self.information_type = information_type
         self.oci_project = oci_project
         self.description = description
         self.build_file = build_file
@@ -198,6 +206,20 @@ class OCIRecipe(Storm, WebhookTargetMixin):
             self.oci_project.name, self.name)
 
     @property
+    def information_type(self):
+        if self._information_type is None:
+            return InformationType.PUBLIC
+        return self._information_type
+
+    @information_type.setter
+    def information_type(self, information_type):
+        self._information_type = information_type
+
+    @property
+    def pillar(self):
+        return self.oci_project.pillar
+
+    @property
     def valid_webhook_event_types(self):
         return ["oci-recipe:build:0.1"]
 
@@ -220,6 +242,15 @@ class OCIRecipe(Storm, WebhookTargetMixin):
         self._build_args = {k: six.text_type(v)
                             for k, v in (value or {}).items()}
 
+    def _reconcileAccess(self):
+        """Reconcile the snap's sharing information.
+
+        Takes the privacy and pillar and makes the related AccessArtifact
+        and AccessPolicyArtifacts match.
+        """
+        reconcile_access_for_artifact(self, self.information_type,
+                                      [self.pillar])
+
     def destroySelf(self):
         """See `IOCIRecipe`."""
         # XXX twom 2019-11-26 This needs to expand as more build artifacts
@@ -609,7 +640,7 @@ class OCIRecipeSet:
             description=None, official=False, require_virtualized=True,
             build_daily=False, processors=None, date_created=DEFAULT,
             allow_internet=True, build_args=None, build_path=None,
-            image_name=None):
+            image_name=None, information_type=InformationType.PUBLIC):
         """See `IOCIRecipeSet`."""
         if not registrant.inTeam(owner):
             if owner.is_team:
@@ -634,8 +665,10 @@ class OCIRecipeSet:
         oci_recipe = OCIRecipe(
             name, registrant, owner, oci_project, git_ref, description,
             official, require_virtualized, build_file, build_daily,
-            date_created, allow_internet, build_args, build_path, image_name)
+            date_created, allow_internet, build_args, build_path, image_name,
+            information_type)
         store.add(oci_recipe)
+        oci_recipe._reconcileAccess()
 
         if processors is None:
             processors = [
diff --git a/lib/lp/registry/browser/pillar.py b/lib/lp/registry/browser/pillar.py
index 52b1554..c5bfd3f 100644
--- a/lib/lp/registry/browser/pillar.py
+++ b/lib/lp/registry/browser/pillar.py
@@ -439,15 +439,22 @@ class PillarPersonSharingView(LaunchpadView):
     def _loadSharedArtifacts(self):
         # As a concrete can by linked via more than one policy, we use sets to
         # filter out dupes.
-        (self.bugtasks, self.branches, self.gitrepositories, self.snaps,
-         self.specifications) = (
-            self.sharing_service.getSharedArtifacts(
-                self.pillar, self.person, self.user))
+        artifacts = self.sharing_service.getSharedArtifacts(
+                self.pillar, self.person, self.user)
+        self.bugtasks = artifacts["bugtasks"]
+        self.branches = artifacts["branches"]
+        self.gitrepositories = artifacts["gitrepositories"]
+        self.snaps = artifacts["snaps"]
+        self.specifications = artifacts["specifications"]
+        self.ocirecipes = artifacts["ocirecipes"]
+
         bug_ids = set([bugtask.bug.id for bugtask in self.bugtasks])
         self.shared_bugs_count = len(bug_ids)
         self.shared_branches_count = len(self.branches)
         self.shared_gitrepositories_count = len(self.gitrepositories)
+        self.shared_snaps_count = len(self.snaps)
         self.shared_specifications_count = len(self.specifications)
+        self.shared_ocirecipe_count = len(self.ocirecipes)
 
     def _build_specification_template_data(self, specs, request):
         spec_data = []
diff --git a/lib/lp/registry/interfaces/accesspolicy.py b/lib/lp/registry/interfaces/accesspolicy.py
index 0e2c8c8..8705584 100644
--- a/lib/lp/registry/interfaces/accesspolicy.py
+++ b/lib/lp/registry/interfaces/accesspolicy.py
@@ -1,4 +1,4 @@
-# Copyright 2011-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2011-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Interfaces for pillar and artifact access policies."""
@@ -38,6 +38,7 @@ class IAccessArtifact(Interface):
     gitrepository_id = Attribute("gitrepository_id")
     snap_id = Attribute("snap_id")
     specification_id = Attribute("specification_id")
+    ocirecipe_id = Attribute("ocirecipe_id")
 
 
 class IAccessArtifactGrant(Interface):
diff --git a/lib/lp/registry/interfaces/sharingservice.py b/lib/lp/registry/interfaces/sharingservice.py
index f6c39fb..b000090 100644
--- a/lib/lp/registry/interfaces/sharingservice.py
+++ b/lib/lp/registry/interfaces/sharingservice.py
@@ -35,6 +35,7 @@ from lp.blueprints.interfaces.specification import ISpecification
 from lp.bugs.interfaces.bug import IBug
 from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.gitrepository import IGitRepository
+from lp.oci.interfaces.ocirecipe import IOCIRecipe
 from lp.registry.enums import (
     BranchSharingPolicy,
     BugSharingPolicy,
@@ -161,6 +162,14 @@ class ISharingService(IService):
         :return: a collection of Git repositories.
         """
 
+    def getSharedSnaps(pillar, person, user):
+        """Return the Snap recipes shared between the pillar and person.
+
+        :param user: the user making the request. Only Snap recipes visible
+            to the user will be included in the result.
+        :return: a collection of OCI recipes.
+        """
+
     @export_read_operation()
     @call_with(user=REQUEST_USER)
     @operation_parameters(
@@ -176,9 +185,17 @@ class ISharingService(IService):
         :return: a collection of specifications.
         """
 
+    def getSharedOCIRecipes(pillar, person, user):
+        """Return the OCI recipes shared between the pillar and person.
+
+        :param user: the user making the request. Only OCI recipes visible 
+            to the user will be included in the result.
+        :return: a collection of OCI recipes.
+        """
+
     def getVisibleArtifacts(person, bugs=None, branches=None,
                             gitrepositories=None, snaps=None,
-                            specifications=None):
+                            specifications=None, ocirecipes=None):
         """Return the artifacts shared with person.
 
         Given lists of artifacts, return those a person has access to either
@@ -192,6 +209,8 @@ class ISharingService(IService):
         :param snaps: the snap recipes to check for which a person has access.
         :param specifications: the specifications to check for which a
             person has access.
+        :param ocirecipes: the OCI recipes to check for which a person
+            has access.
         :return: a collection of artifacts the person can see.
         """
 
@@ -335,11 +354,14 @@ class ISharingService(IService):
             title=_('Snap recipes'), required=False),
         specifications=List(
             Reference(schema=ISpecification), title=_('Specifications'),
-            required=False))
+            required=False),
+        ocirecipes=List(
+            Reference(schema=IOCIRecipe),
+            title=_('Snap recipes'), required=False))
     @operation_for_version('devel')
     def revokeAccessGrants(pillar, grantee, user, bugs=None, branches=None,
                            gitrepositories=None, snaps=None,
-                           specifications=None):
+                           specifications=None, ocirecipes=None):
         """Remove a grantee's access to the specified artifacts.
 
         :param pillar: the pillar from which to remove access
@@ -350,6 +372,7 @@ class ISharingService(IService):
         :param gitrepositories: the Git repositories for which to revoke access
         :param snaps: The snap recipes for which to revoke access
         :param specifications: the specifications for which to revoke access
+        :param snaps: The OCI recipes for which to revoke access
         """
 
     @export_write_operation()
@@ -367,12 +390,15 @@ class ISharingService(IService):
             title=_('Git repositories'), required=False),
         snaps=List(
             Reference(schema=ISnap),
-            title=_('Snap recipes'), required=False)
+            title=_('Snap recipes'), required=False),
+        ocirecipes=List(
+            Reference(schema=ISnap),
+            title=_('OCI recipes'), required=False)
     )
     @operation_for_version('devel')
     def ensureAccessGrants(grantees, user, bugs=None, branches=None,
                            gitrepositories=None, snaps=None,
-                           specifications=None):
+                           specifications=None, ocirecipes=None):
         """Ensure a grantee has an access grant to the specified artifacts.
 
         :param grantees: the people or teams for whom to grant access
@@ -382,6 +408,7 @@ class ISharingService(IService):
         :param gitrepositories: the Git repositories for which to grant access
         :param snaps: the snap recipes for which to grant access
         :param specifications: the specifications for which to grant access
+        :param ocirecipes: the OCI recipes for which to grant access
         """
 
     @export_write_operation()
diff --git a/lib/lp/registry/model/accesspolicy.py b/lib/lp/registry/model/accesspolicy.py
index 7bb5e9f..64b69c8 100644
--- a/lib/lp/registry/model/accesspolicy.py
+++ b/lib/lp/registry/model/accesspolicy.py
@@ -102,6 +102,8 @@ class AccessArtifact(StormBase):
     snap = Reference(snap_id, 'Snap.id')
     specification_id = Int(name='specification')
     specification = Reference(specification_id, 'Specification.id')
+    ocirecipe_id = Int(name="ocirecipe")
+    ocirecipe = Reference(ocirecipe_id, 'OCIRecipe.id')
 
     @property
     def concrete_artifact(self):
@@ -117,6 +119,7 @@ class AccessArtifact(StormBase):
         from lp.code.interfaces.branch import IBranch
         from lp.code.interfaces.gitrepository import IGitRepository
         from lp.snappy.interfaces.snap import ISnap
+        from lp.oci.interfaces.ocirecipe import IOCIRecipe
         if IBug.providedBy(concrete_artifact):
             col = cls.bug
         elif IBranch.providedBy(concrete_artifact):
@@ -127,6 +130,8 @@ class AccessArtifact(StormBase):
             col = cls.snap
         elif ISpecification.providedBy(concrete_artifact):
             col = cls.specification
+        elif IOCIRecipe.providedBy(concrete_artifact):
+            col = cls.ocirecipe
         else:
             raise ValueError(
                 "%r is not a valid artifact" % concrete_artifact)
@@ -149,6 +154,7 @@ class AccessArtifact(StormBase):
         from lp.code.interfaces.branch import IBranch
         from lp.code.interfaces.gitrepository import IGitRepository
         from lp.snappy.interfaces.snap import ISnap
+        from lp.oci.interfaces.ocirecipe import IOCIRecipe
 
         existing = list(cls.find(concrete_artifacts))
         if len(existing) == len(concrete_artifacts):
@@ -162,19 +168,21 @@ class AccessArtifact(StormBase):
         insert_values = []
         for concrete in needed:
             if IBug.providedBy(concrete):
-                insert_values.append((concrete, None, None, None, None))
+                insert_values.append((concrete, None, None, None, None, None))
             elif IBranch.providedBy(concrete):
-                insert_values.append((None, concrete, None, None, None))
+                insert_values.append((None, concrete, None, None, None, None))
             elif IGitRepository.providedBy(concrete):
-                insert_values.append((None, None, concrete, None, None))
+                insert_values.append((None, None, concrete, None, None, None))
             elif ISnap.providedBy(concrete):
-                insert_values.append((None, None, None, concrete, None))
+                insert_values.append((None, None, None, concrete, None, None))
             elif ISpecification.providedBy(concrete):
-                insert_values.append((None, None, None, None, concrete))
+                insert_values.append((None, None, None, None, concrete, None))
+            elif IOCIRecipe.providedBy(concrete):
+                insert_values.append((None, None, None, None, None, concrete))
             else:
                 raise ValueError("%r is not a supported artifact" % concrete)
         columns = (cls.bug, cls.branch, cls.gitrepository, cls.snap,
-                   cls.specification)
+                   cls.specification, cls.ocirecipe)
         new = create(columns, insert_values, get_objects=True)
         return list(existing) + new
 
diff --git a/lib/lp/registry/personmerge.py b/lib/lp/registry/personmerge.py
index 949e8d9..5fa7293 100644
--- a/lib/lp/registry/personmerge.py
+++ b/lib/lp/registry/personmerge.py
@@ -924,6 +924,10 @@ def merge_people(from_person, to_person, reviewer, delete=False):
     _mergeOCIRecipe(cur, from_person, to_person)
     skip.append(('ocirecipe', 'owner'))
 
+    # XXX pappacena 2021-03-05: We need to implement the proper handling for
+    # this once we have OCIRecipeSubscription implemented.
+    skip.append(('ocirecipesubscription', 'person'))
+
     # Sanity check. If we have a reference that participates in a
     # UNIQUE index, it must have already been handled by this point.
     # We can tell this by looking at the skip list.
diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
index 01e90da..542066d 100644
--- a/lib/lp/registry/services/sharingservice.py
+++ b/lib/lp/registry/services/sharingservice.py
@@ -39,6 +39,8 @@ from lp.bugs.interfaces.bugtask import IBugTaskSet
 from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
 from lp.code.interfaces.branchcollection import IAllBranches
 from lp.code.interfaces.gitcollection import IAllGitRepositories
+from lp.oci.interfaces.ocirecipe import IOCIRecipe
+from lp.oci.model.ocirecipe import OCIRecipe
 from lp.registry.enums import (
     BranchSharingPolicy,
     BugSharingPolicy,
@@ -201,13 +203,15 @@ class SharingService:
     @available_with_permission('launchpad.Driver', 'pillar')
     def getSharedArtifacts(self, pillar, person, user, include_bugs=True,
                            include_branches=True, include_gitrepositories=True,
-                           include_snaps=True, include_specifications=True):
+                           include_snaps=True, include_specifications=True,
+                           include_ocirecipes=True):
         """See `ISharingService`."""
         bug_ids = set()
         branch_ids = set()
         gitrepository_ids = set()
         snap_ids = set()
         specification_ids = set()
+        ocirecipe_ids = set()
         for artifact in self.getArtifactGrantsForPersonOnPillar(
             pillar, person):
             if artifact.bug_id and include_bugs:
@@ -220,6 +224,8 @@ class SharingService:
                 snap_ids.add(artifact.snap_id)
             elif artifact.specification_id and include_specifications:
                 specification_ids.add(artifact.specification_id)
+            elif artifact.ocirecipe_id and include_ocirecipes:
+                ocirecipe_ids.add(artifact.ocirecipe_id)
 
         # Load the bugs.
         bugtasks = []
@@ -248,48 +254,68 @@ class SharingService:
         specifications = []
         if specification_ids:
             specifications = load(Specification, specification_ids)
+        ocirecipes = []
+        if ocirecipe_ids:
+            ocirecipes = load(OCIRecipe, ocirecipe_ids)
 
-        return bugtasks, branches, gitrepositories, snaps, specifications
+        return {"bugtasks": bugtasks, "branches": branches,
+                "gitrepositories": gitrepositories, "snaps": snaps,
+                "specifications": specifications, "ocirecipes": ocirecipes}
 
     @available_with_permission('launchpad.Driver', 'pillar')
     def getSharedBugs(self, pillar, person, user):
         """See `ISharingService`."""
-        bugtasks, _, _, _, _ = self.getSharedArtifacts(
+        artifacts = self.getSharedArtifacts(
             pillar, person, user, include_branches=False,
-            include_gitrepositories=False, include_specifications=False)
-        return bugtasks
+            include_gitrepositories=False,
+            include_specifications=False, include_snaps=False,
+            include_ocirecipes=False)
+        return artifacts["bugtasks"]
 
     @available_with_permission('launchpad.Driver', 'pillar')
     def getSharedBranches(self, pillar, person, user):
         """See `ISharingService`."""
-        _, branches, _, _, _ = self.getSharedArtifacts(
+        artifacts = self.getSharedArtifacts(
             pillar, person, user, include_bugs=False,
-            include_gitrepositories=False, include_specifications=False)
-        return branches
+            include_gitrepositories=False, include_specifications=False,
+            include_snaps=False, include_ocirecipes=False)
+        return artifacts["branches"]
 
     @available_with_permission('launchpad.Driver', 'pillar')
     def getSharedGitRepositories(self, pillar, person, user):
         """See `ISharingService`."""
-        _, _, gitrepositories, _, _ = self.getSharedArtifacts(
+        artifacts = self.getSharedArtifacts(
             pillar, person, user, include_bugs=False, include_branches=False,
-            include_specifications=False)
-        return gitrepositories
+            include_specifications=False, include_snaps=False,
+            include_ocirecipes=False)
+        return artifacts["gitrepositories"]
 
     @available_with_permission('launchpad.Driver', 'pillar')
     def getSharedSnaps(self, pillar, person, user):
         """See `ISharingService`."""
-        _, _, _, snaps, _ = self.getSharedArtifacts(
+        artifacts = self.getSharedArtifacts(
             pillar, person, user, include_bugs=False, include_branches=False,
-            include_gitrepositories=False)
-        return snaps
+            include_gitrepositories=False, include_specifications=False,
+            include_ocirecipes=False)
+        return artifacts["snaps"]
 
     @available_with_permission('launchpad.Driver', 'pillar')
     def getSharedSpecifications(self, pillar, person, user):
         """See `ISharingService`."""
-        _, _, _, _, specifications = self.getSharedArtifacts(
+        artifacts = self.getSharedArtifacts(
             pillar, person, user, include_bugs=False, include_branches=False,
-            include_gitrepositories=False)
-        return specifications
+            include_gitrepositories=False, include_snaps=False,
+            include_ocirecipes=False)
+        return artifacts["specifications"]
+
+    @available_with_permission('launchpad.Driver', 'pillar')
+    def getSharedOCIRecipes(self, pillar, person, user):
+        """See `ISharingService`."""
+        artifacts = self.getSharedArtifacts(
+            pillar, person, user, include_bugs=False, include_branches=False,
+            include_gitrepositories=False, include_snaps=False,
+            include_specifications=False)
+        return artifacts["ocirecipes"]
 
     def _getVisiblePrivateSpecificationIDs(self, person, specifications):
         store = Store.of(specifications[0])
@@ -327,7 +353,8 @@ class SharingService:
 
     def getVisibleArtifacts(self, person, bugs=None, branches=None,
                             gitrepositories=None, snaps=None,
-                            specifications=None, ignore_permissions=False):
+                            specifications=None, ignore_permissions=False,
+                            ocirecipes=None):
         """See `ISharingService`."""
         bug_ids = []
         branch_ids = []
@@ -772,11 +799,11 @@ class SharingService:
     @available_with_permission('launchpad.Edit', 'pillar')
     def revokeAccessGrants(self, pillar, grantee, user, bugs=None,
                            branches=None, gitrepositories=None, snaps=None,
-                           specifications=None):
+                           specifications=None, ocirecipes=None):
         """See `ISharingService`."""
 
         if (not bugs and not branches and not gitrepositories and not snaps and
-            not specifications):
+            not specifications and not ocirecipes):
             raise ValueError(
                 "Either bugs, branches, gitrepositories, or specifications "
                 "must be specified")
@@ -792,6 +819,8 @@ class SharingService:
             artifacts.extend(snaps)
         if specifications:
             artifacts.extend(specifications)
+        if ocirecipes:
+            artifacts.extend(ocirecipes)
         # Find the access artifacts associated with the bugs, branches, Git
         # repositories, and specifications.
         accessartifact_source = getUtility(IAccessArtifactSource)
@@ -806,6 +835,12 @@ class SharingService:
         if not artifacts:
             return
 
+        # XXX: Pappacena 2021-03-09: OCI recipes should not trigger this job,
+        # since we do not have a "OCIRecipeSubscription" yet.
+        artifacts = [i for i in artifacts if not IOCIRecipe.providedBy(i)]
+        if not artifacts:
+            return
+
         # Create a job to remove subscriptions for artifacts the grantee can no
         # longer see.
         return getUtility(IRemoveArtifactSubscriptionsJobSource).create(
@@ -813,7 +848,8 @@ class SharingService:
 
     def ensureAccessGrants(self, grantees, user, bugs=None, branches=None,
                            gitrepositories=None, snaps=None,
-                           specifications=None, ignore_permissions=False):
+                           specifications=None, ocirecipes=None,
+                           ignore_permissions=False):
         """See `ISharingService`."""
 
         artifacts = []
@@ -827,6 +863,8 @@ class SharingService:
             artifacts.extend(snaps)
         if specifications:
             artifacts.extend(specifications)
+        if ocirecipes:
+            artifacts.extend(ocirecipes)
         if not ignore_permissions:
             # The user needs to have launchpad.Edit permission on all supplied
             # bugs and branches or else we raise an Unauthorized exception.
diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py
index 9d0e07c..ccb4765 100644
--- a/lib/lp/registry/services/tests/test_sharingservice.py
+++ b/lib/lp/registry/services/tests/test_sharingservice.py
@@ -1,4 +1,4 @@
-# Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
+# Copyright 2012-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -25,6 +25,7 @@ from lp.code.enums import (
     )
 from lp.code.interfaces.branch import IBranch
 from lp.code.interfaces.gitrepository import IGitRepository
+from lp.oci.tests.helpers import OCIConfigHelperMixin
 from lp.registry.enums import (
     BranchSharingPolicy,
     BugSharingPolicy,
@@ -41,11 +42,11 @@ from lp.registry.interfaces.accesspolicy import (
 from lp.registry.interfaces.person import TeamMembershipPolicy
 from lp.registry.interfaces.role import IPersonRoles
 from lp.registry.services.sharingservice import SharingService
-from lp.services.features.testing import FeatureFixture
 from lp.services.job.tests import block_on_job
 from lp.services.webapp.interaction import ANONYMOUS
 from lp.services.webapp.interfaces import ILaunchpadRoot
 from lp.services.webapp.publisher import canonical_url
+from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
 from lp.testing import (
     admin_logged_in,
     login,
@@ -64,7 +65,7 @@ from lp.testing.matchers import HasQueryCount
 from lp.testing.pages import LaunchpadWebServiceCaller
 
 
-class TestSharingService(TestCaseWithFactory):
+class TestSharingService(TestCaseWithFactory, OCIConfigHelperMixin):
     """Tests for the SharingService."""
 
     layer = CeleryJobLayer
@@ -72,9 +73,12 @@ class TestSharingService(TestCaseWithFactory):
     def setUp(self):
         super(TestSharingService, self).setUp()
         self.service = getUtility(IService, 'sharing')
-        self.useFixture(FeatureFixture({
+        # Set test flags and configurations for Snaps and OCI.
+        flags = SNAP_TESTING_FLAGS.copy()
+        flags.update({
             'jobs.celery.enabled_classes': 'RemoveArtifactSubscriptionsJob',
-        }))
+        })
+        self.setConfig(feature_flags=flags)
 
     def _makeGranteeData(self, grantee, policy_permissions,
                         shared_artifact_types):
@@ -1414,12 +1418,27 @@ class TestSharingService(TestCaseWithFactory):
                 target=product, owner=product.owner,
                 information_type=InformationType.USERDATA)
             gitrepositories.append(gitrepository)
+        snaps = []
+        for x in range(0, 10):
+            snap = self.factory.makeSnap(
+                project=product, owner=product.owner, registrant=product.owner,
+                information_type=InformationType.USERDATA)
+            snaps.append(snap)
         specs = []
         for x in range(0, 10):
             spec = self.factory.makeSpecification(
                 product=product, owner=product.owner,
                 information_type=InformationType.PROPRIETARY)
             specs.append(spec)
+        ocirecipes = []
+        for x in range(0, 10):
+            ociproject = self.factory.makeOCIProject(
+                pillar=product, registrant=product.owner)
+            ocirecipe = self.factory.makeOCIRecipe(
+                oci_project=ociproject, owner=product.owner,
+                registrant=product.owner,
+                information_type=InformationType.USERDATA)
+            ocirecipes.append(ocirecipe)
 
         # Grant access to grantee as well as the person who will be doing the
         # query. The person who will be doing the query is not granted access
@@ -1442,8 +1461,12 @@ class TestSharingService(TestCaseWithFactory):
         for i, gitrepository in enumerate(gitrepositories):
             grant_access(gitrepository, i == 9)
         getUtility(IService, 'sharing').ensureAccessGrants(
+            [grantee], product.owner, snaps=snaps[:9])
+        getUtility(IService, 'sharing').ensureAccessGrants(
             [grantee], product.owner, specifications=specs[:9])
-        return bug_tasks, branches, gitrepositories, specs
+        getUtility(IService, 'sharing').ensureAccessGrants(
+            [grantee], product.owner, ocirecipes=ocirecipes[:9])
+        return bug_tasks, branches, gitrepositories, snaps, specs, ocirecipes
 
     def test_getSharedArtifacts(self):
         # Test the getSharedArtifacts method.
@@ -1454,17 +1477,24 @@ class TestSharingService(TestCaseWithFactory):
         login_person(owner)
         grantee = self.factory.makePerson()
         user = self.factory.makePerson()
-        bug_tasks, branches, gitrepositories, specs = (
+        bug_tasks, branches, gitrepositories, snaps, specs, ocirecipes = (
             self.create_shared_artifacts(product, grantee, user))
 
         # Check the results.
-        (shared_bugtasks, shared_branches, shared_gitrepositories,
-         shared_snaps, shared_specs) = (
-            self.service.getSharedArtifacts(product, grantee, user))
+        artifacts = self.service.getSharedArtifacts(product, grantee, user)
+        shared_bugtasks = artifacts["bugtasks"]
+        shared_branches = artifacts["branches"]
+        shared_gitrepositories = artifacts["gitrepositories"]
+        shared_snaps = artifacts["snaps"]
+        shared_specs = artifacts["specifications"]
+        shared_ocirecipes = artifacts["ocirecipes"]
+
         self.assertContentEqual(bug_tasks[:9], shared_bugtasks)
         self.assertContentEqual(branches[:9], shared_branches)
         self.assertContentEqual(gitrepositories[:9], shared_gitrepositories)
+        self.assertContentEqual(snaps[:9], shared_snaps)
         self.assertContentEqual(specs[:9], shared_specs)
+        self.assertContentEqual(ocirecipes[:9], shared_ocirecipes)
 
     def _assert_getSharedProjects(self, product, who=None):
         # Test that 'who' can query the shared products for a grantee.
@@ -1603,7 +1633,7 @@ class TestSharingService(TestCaseWithFactory):
         login_person(owner)
         grantee = self.factory.makePerson()
         user = self.factory.makePerson()
-        bug_tasks, _, _, _ = self.create_shared_artifacts(
+        bug_tasks, _, _, _, _, _ = self.create_shared_artifacts(
             product, grantee, user)
 
         # Check the results.
@@ -1619,7 +1649,7 @@ class TestSharingService(TestCaseWithFactory):
         login_person(owner)
         grantee = self.factory.makePerson()
         user = self.factory.makePerson()
-        _, branches, _, _ = self.create_shared_artifacts(
+        _, branches, _, _, _, _ = self.create_shared_artifacts(
             product, grantee, user)
 
         # Check the results.
@@ -1636,7 +1666,7 @@ class TestSharingService(TestCaseWithFactory):
         login_person(owner)
         grantee = self.factory.makePerson()
         user = self.factory.makePerson()
-        _, _, gitrepositories, _ = self.create_shared_artifacts(
+        _, _, gitrepositories, _, _, _ = self.create_shared_artifacts(
             product, grantee, user)
 
         # Check the results.
@@ -1644,6 +1674,23 @@ class TestSharingService(TestCaseWithFactory):
             product, grantee, user)
         self.assertContentEqual(gitrepositories[:9], shared_gitrepositories)
 
+    def test_getSharedSnaps(self):
+        # Test the getSharedSnaps method.
+        owner = self.factory.makePerson()
+        product = self.factory.makeProduct(
+            owner=owner, specification_sharing_policy=(
+            SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
+        login_person(owner)
+        grantee = self.factory.makePerson()
+        user = self.factory.makePerson()
+        _, _, _, snaps, _, _ = self.create_shared_artifacts(
+            product, grantee, user)
+
+        # Check the results.
+        shared_snaps = self.service.getSharedSnaps(
+            product, grantee, user)
+        self.assertContentEqual(snaps[:9], shared_snaps)
+
     def test_getSharedSpecifications(self):
         # Test the getSharedSpecifications method.
         owner = self.factory.makePerson()
@@ -1653,7 +1700,7 @@ class TestSharingService(TestCaseWithFactory):
         login_person(owner)
         grantee = self.factory.makePerson()
         user = self.factory.makePerson()
-        _, _, _, specifications = self.create_shared_artifacts(
+        _, _, _, _, specifications, _ = self.create_shared_artifacts(
             product, grantee, user)
 
         # Check the results.
@@ -1661,6 +1708,23 @@ class TestSharingService(TestCaseWithFactory):
             product, grantee, user)
         self.assertContentEqual(specifications[:9], shared_specifications)
 
+    def test_getSharedOCIRecipes(self):
+        # Test the getSharedSnaps method.
+        owner = self.factory.makePerson()
+        product = self.factory.makeProduct(
+            owner=owner, specification_sharing_policy=(
+            SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY))
+        login_person(owner)
+        grantee = self.factory.makePerson()
+        user = self.factory.makePerson()
+        _, _, _, _, _, ocirecipes = self.create_shared_artifacts(
+            product, grantee, user)
+
+        # Check the results.
+        shared_ocirecipes = self.service.getSharedOCIRecipes(
+            product, grantee, user)
+        self.assertContentEqual(ocirecipes[:9], shared_ocirecipes)
+
     def test_getPeopleWithAccessBugs(self):
         # Test the getPeopleWithoutAccess method with bugs.
         owner = self.factory.makePerson()
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index a96eace..b0d9f92 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -1368,7 +1368,7 @@ class SnapSet:
 
     def findByIds(self, snap_ids):
         """See `ISnapSet`."""
-        return IStore(ISnap).find(Snap, Snap.id.is_in(snap_ids))
+        return IStore(Snap).find(Snap, Snap.id.is_in(snap_ids))
 
     def findByOwner(self, owner):
         """See `ISnapSet`."""
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 6948419..3e580fe 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -4961,7 +4961,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
                       oci_project=None, git_ref=None, description=None,
                       official=False, require_virtualized=True,
                       build_file=None, date_created=DEFAULT,
-                      allow_internet=True, build_args=None, build_path=None):
+                      allow_internet=True, build_args=None, build_path=None,
+                      information_type=InformationType.PUBLIC):
         """Make a new OCIRecipe."""
         if name is None:
             name = self.getUniqueString(u"oci-recipe-name")
@@ -4994,7 +4995,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
             require_virtualized=require_virtualized,
             date_created=date_created,
             allow_internet=allow_internet,
-            build_args=build_args)
+            build_args=build_args,
+            information_type=information_type)
 
     def makeOCIRecipeArch(self, recipe=None, processor=None):
         """Make a new OCIRecipeArch."""

Follow ups