launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #26232
[Merge] ~pappacena/launchpad:snap-pillar-accesspolicy into launchpad:master
Thiago F. Pappacena has proposed merging ~pappacena/launchpad:snap-pillar-accesspolicy into launchpad:master with ~pappacena/launchpad:snap-pillar as a prerequisite.
Commit message:
Adding Snap as an artifact for sharing services
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/397604
Depends on https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/397459
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:snap-pillar-accesspolicy into launchpad:master.
diff --git a/lib/lp/registry/browser/pillar.py b/lib/lp/registry/browser/pillar.py
index dd83648..52b1554 100644
--- a/lib/lp/registry/browser/pillar.py
+++ b/lib/lp/registry/browser/pillar.py
@@ -439,7 +439,7 @@ 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.bugtasks, self.branches, self.gitrepositories, self.snaps,
self.specifications) = (
self.sharing_service.getSharedArtifacts(
self.pillar, self.person, self.user))
diff --git a/lib/lp/registry/interfaces/accesspolicy.py b/lib/lp/registry/interfaces/accesspolicy.py
index 2c454a7..0e2c8c8 100644
--- a/lib/lp/registry/interfaces/accesspolicy.py
+++ b/lib/lp/registry/interfaces/accesspolicy.py
@@ -36,6 +36,7 @@ class IAccessArtifact(Interface):
bug_id = Attribute("bug_id")
branch_id = Attribute("branch_id")
gitrepository_id = Attribute("gitrepository_id")
+ snap_id = Attribute("snap_id")
specification_id = Attribute("specification_id")
diff --git a/lib/lp/registry/interfaces/sharingservice.py b/lib/lp/registry/interfaces/sharingservice.py
index 386fef4..f6c39fb 100644
--- a/lib/lp/registry/interfaces/sharingservice.py
+++ b/lib/lp/registry/interfaces/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).
"""Interfaces for sharing service."""
@@ -45,7 +45,7 @@ from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.person import IPerson
from lp.registry.interfaces.pillar import IPillar
from lp.registry.interfaces.product import IProduct
-
+from lp.snappy.interfaces.snap import ISnap
# XXX 2012-02-24 wallyworld bug 939910
# Need to export for version 'beta' even though we only want to use it in
@@ -177,7 +177,8 @@ class ISharingService(IService):
"""
def getVisibleArtifacts(person, bugs=None, branches=None,
- gitrepositories=None, specifications=None):
+ gitrepositories=None, snaps=None,
+ specifications=None):
"""Return the artifacts shared with person.
Given lists of artifacts, return those a person has access to either
@@ -188,6 +189,7 @@ class ISharingService(IService):
:param branches: the branches to check for which a person has access.
:param gitrepositories: the Git repositories to check for which a
person has access.
+ :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.
:return: a collection of artifacts the person can see.
@@ -328,12 +330,16 @@ class ISharingService(IService):
gitrepositories=List(
Reference(schema=IGitRepository),
title=_('Git repositories'), required=False),
+ snaps=List(
+ Reference(schema=ISnap),
+ title=_('Snap recipes'), required=False),
specifications=List(
Reference(schema=ISpecification), title=_('Specifications'),
required=False))
@operation_for_version('devel')
def revokeAccessGrants(pillar, grantee, user, bugs=None, branches=None,
- gitrepositories=None, specifications=None):
+ gitrepositories=None, snaps=None,
+ specifications=None):
"""Remove a grantee's access to the specified artifacts.
:param pillar: the pillar from which to remove access
@@ -342,6 +348,7 @@ class ISharingService(IService):
:param bugs: the bugs for which to revoke access
:param branches: the branches for which to revoke access
: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
"""
@@ -357,10 +364,15 @@ class ISharingService(IService):
Reference(schema=IBranch), title=_('Branches'), required=False),
gitrepositories=List(
Reference(schema=IGitRepository),
- title=_('Git repositories'), required=False))
+ title=_('Git repositories'), required=False),
+ snaps=List(
+ Reference(schema=ISnap),
+ title=_('Snap recipes'), required=False)
+ )
@operation_for_version('devel')
def ensureAccessGrants(grantees, user, bugs=None, branches=None,
- gitrepositories=None, specifications=None):
+ gitrepositories=None, snaps=None,
+ specifications=None):
"""Ensure a grantee has an access grant to the specified artifacts.
:param grantees: the people or teams for whom to grant access
@@ -368,6 +380,7 @@ class ISharingService(IService):
:param bugs: the bugs for which to grant access
:param branches: the branches for which to grant access
: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
"""
diff --git a/lib/lp/registry/model/accesspolicy.py b/lib/lp/registry/model/accesspolicy.py
index 2e8fddf..472509f 100644
--- a/lib/lp/registry/model/accesspolicy.py
+++ b/lib/lp/registry/model/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).
"""Model classes for pillar and artifact access policies."""
@@ -98,6 +98,8 @@ class AccessArtifact(StormBase):
branch = Reference(branch_id, 'Branch.id')
gitrepository_id = Int(name='gitrepository')
gitrepository = Reference(gitrepository_id, 'GitRepository.id')
+ snap_id = Int(name="snap")
+ snap = Reference(snap_id, 'Snap.id')
specification_id = Int(name='specification')
specification = Reference(specification_id, 'Specification.id')
@@ -114,12 +116,15 @@ class AccessArtifact(StormBase):
from lp.bugs.interfaces.bug import IBug
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
+ from lp.snappy.interfaces.snap import ISnap
if IBug.providedBy(concrete_artifact):
col = cls.bug
elif IBranch.providedBy(concrete_artifact):
col = cls.branch
elif IGitRepository.providedBy(concrete_artifact):
col = cls.gitrepository
+ elif ISnap.providedBy(concrete_artifact):
+ col = cls.snap
elif ISpecification.providedBy(concrete_artifact):
col = cls.specification
else:
@@ -143,6 +148,7 @@ class AccessArtifact(StormBase):
from lp.bugs.interfaces.bug import IBug
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
+ from lp.snappy.interfaces.snap import ISnap
existing = list(cls.find(concrete_artifacts))
if len(existing) == len(concrete_artifacts):
@@ -156,18 +162,20 @@ class AccessArtifact(StormBase):
insert_values = []
for concrete in needed:
if IBug.providedBy(concrete):
- insert_values.append((concrete, None, None, None))
+ insert_values.append((concrete, None, None, None, None))
elif IBranch.providedBy(concrete):
- insert_values.append((None, concrete, None, None))
+ insert_values.append((None, concrete, None, None, None))
elif IGitRepository.providedBy(concrete):
- insert_values.append((None, None, concrete, None))
+ insert_values.append((None, None, concrete, None, None))
elif ISpecification.providedBy(concrete):
- insert_values.append((None, None, None, concrete))
+ insert_values.append((None, None, None, concrete, None))
+ elif ISnap.providedBy(concrete):
+ insert_values.append((None, None, None, None, concrete))
else:
raise ValueError("%r is not a supported artifact" % concrete)
- new = create(
- (cls.bug, cls.branch, cls.gitrepository, cls.specification),
- insert_values, get_objects=True)
+ columns = (cls.bug, cls.branch, cls.gitrepository, cls.specification,
+ cls.snap)
+ new = create(columns, insert_values, get_objects=True)
return list(existing) + new
@classmethod
diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
index 09744ee..01e90da 100644
--- a/lib/lp/registry/services/sharingservice.py
+++ b/lib/lp/registry/services/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).
"""Classes for pillar and artifact sharing service."""
@@ -81,6 +81,10 @@ from lp.services.webapp.authorization import (
available_with_permission,
check_permission,
)
+from lp.snappy.interfaces.snap import (
+ ISnap,
+ ISnapSet,
+ )
@implementer(ISharingService)
@@ -197,11 +201,12 @@ class SharingService:
@available_with_permission('launchpad.Driver', 'pillar')
def getSharedArtifacts(self, pillar, person, user, include_bugs=True,
include_branches=True, include_gitrepositories=True,
- include_specifications=True):
+ include_snaps=True, include_specifications=True):
"""See `ISharingService`."""
bug_ids = set()
branch_ids = set()
gitrepository_ids = set()
+ snap_ids = set()
specification_ids = set()
for artifact in self.getArtifactGrantsForPersonOnPillar(
pillar, person):
@@ -211,6 +216,8 @@ class SharingService:
branch_ids.add(artifact.branch_id)
elif artifact.gitrepository_id and include_gitrepositories:
gitrepository_ids.add(artifact.gitrepository_id)
+ elif artifact.snap_id and include_snaps:
+ snap_ids.add(artifact.snap_id)
elif artifact.specification_id and include_specifications:
specification_ids.add(artifact.specification_id)
@@ -234,16 +241,20 @@ class SharingService:
wanted_gitrepositories = all_gitrepositories.visibleByUser(
user).withIds(*gitrepository_ids)
gitrepositories = list(wanted_gitrepositories.getRepositories())
+ snaps = []
+ if snap_ids:
+ all_snaps = getUtility(ISnapSet)
+ snaps = all_snaps.findByIds(snap_ids)
specifications = []
if specification_ids:
specifications = load(Specification, specification_ids)
- return bugtasks, branches, gitrepositories, specifications
+ return bugtasks, branches, gitrepositories, snaps, specifications
@available_with_permission('launchpad.Driver', 'pillar')
def getSharedBugs(self, pillar, person, user):
"""See `ISharingService`."""
- bugtasks, _, _, _ = self.getSharedArtifacts(
+ bugtasks, _, _, _, _ = self.getSharedArtifacts(
pillar, person, user, include_branches=False,
include_gitrepositories=False, include_specifications=False)
return bugtasks
@@ -251,7 +262,7 @@ class SharingService:
@available_with_permission('launchpad.Driver', 'pillar')
def getSharedBranches(self, pillar, person, user):
"""See `ISharingService`."""
- _, branches, _, _ = self.getSharedArtifacts(
+ _, branches, _, _, _ = self.getSharedArtifacts(
pillar, person, user, include_bugs=False,
include_gitrepositories=False, include_specifications=False)
return branches
@@ -259,15 +270,23 @@ class SharingService:
@available_with_permission('launchpad.Driver', 'pillar')
def getSharedGitRepositories(self, pillar, person, user):
"""See `ISharingService`."""
- _, _, gitrepositories, _ = self.getSharedArtifacts(
+ _, _, gitrepositories, _, _ = self.getSharedArtifacts(
pillar, person, user, include_bugs=False, include_branches=False,
include_specifications=False)
return gitrepositories
@available_with_permission('launchpad.Driver', 'pillar')
+ def getSharedSnaps(self, pillar, person, user):
+ """See `ISharingService`."""
+ _, _, _, snaps, _ = self.getSharedArtifacts(
+ pillar, person, user, include_bugs=False, include_branches=False,
+ include_gitrepositories=False)
+ return snaps
+
+ @available_with_permission('launchpad.Driver', 'pillar')
def getSharedSpecifications(self, pillar, person, user):
"""See `ISharingService`."""
- _, _, _, specifications = self.getSharedArtifacts(
+ _, _, _, _, specifications = self.getSharedArtifacts(
pillar, person, user, include_bugs=False, include_branches=False,
include_gitrepositories=False)
return specifications
@@ -307,8 +326,8 @@ class SharingService:
In(Specification.id, spec_ids)))
def getVisibleArtifacts(self, person, bugs=None, branches=None,
- gitrepositories=None, specifications=None,
- ignore_permissions=False):
+ gitrepositories=None, snaps=None,
+ specifications=None, ignore_permissions=False):
"""See `ISharingService`."""
bug_ids = []
branch_ids = []
@@ -752,11 +771,11 @@ class SharingService:
@available_with_permission('launchpad.Edit', 'pillar')
def revokeAccessGrants(self, pillar, grantee, user, bugs=None,
- branches=None, gitrepositories=None,
+ branches=None, gitrepositories=None, snaps=None,
specifications=None):
"""See `ISharingService`."""
- if (not bugs and not branches and not gitrepositories and
+ if (not bugs and not branches and not gitrepositories and not snaps and
not specifications):
raise ValueError(
"Either bugs, branches, gitrepositories, or specifications "
@@ -769,6 +788,8 @@ class SharingService:
artifacts.extend(branches)
if gitrepositories:
artifacts.extend(gitrepositories)
+ if snaps:
+ artifacts.extend(snaps)
if specifications:
artifacts.extend(specifications)
# Find the access artifacts associated with the bugs, branches, Git
@@ -779,14 +800,20 @@ class SharingService:
getUtility(IAccessArtifactGrantSource).revokeByArtifact(
artifacts_to_delete, [grantee])
+ # XXX: Pappacena 2021-02-05: snaps should not trigger this job,
+ # since we do not have a "SnapSubscription" yet.
+ artifacts = [i for i in artifacts if not ISnap.providedBy(i)]
+ if not artifacts:
+ return
+
# Create a job to remove subscriptions for artifacts the grantee can no
# longer see.
return getUtility(IRemoveArtifactSubscriptionsJobSource).create(
user, artifacts, grantee=grantee, pillar=pillar)
def ensureAccessGrants(self, grantees, user, bugs=None, branches=None,
- gitrepositories=None, specifications=None,
- ignore_permissions=False):
+ gitrepositories=None, snaps=None,
+ specifications=None, ignore_permissions=False):
"""See `ISharingService`."""
artifacts = []
@@ -796,6 +823,8 @@ class SharingService:
artifacts.extend(branches)
if gitrepositories:
artifacts.extend(gitrepositories)
+ if snaps:
+ artifacts.extend(snaps)
if specifications:
artifacts.extend(specifications)
if not ignore_permissions:
diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py
index 50ce8c6..9d0e07c 100644
--- a/lib/lp/registry/services/tests/test_sharingservice.py
+++ b/lib/lp/registry/services/tests/test_sharingservice.py
@@ -1459,7 +1459,7 @@ class TestSharingService(TestCaseWithFactory):
# Check the results.
(shared_bugtasks, shared_branches, shared_gitrepositories,
- shared_specs) = (
+ shared_snaps, shared_specs) = (
self.service.getSharedArtifacts(product, grantee, user))
self.assertContentEqual(bug_tasks[:9], shared_bugtasks)
self.assertContentEqual(branches[:9], shared_branches)
diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
index e17e224..2404b85 100644
--- a/lib/lp/snappy/interfaces/snap.py
+++ b/lib/lp/snappy/interfaces/snap.py
@@ -859,6 +859,12 @@ class ISnapAdminAttributes(Interface):
"Allow access to external network resources via a proxy. "
"Resources hosted on Launchpad itself are always allowed.")))
+ def subscribe(person, subscribed_by):
+ """Subscribe a person to this snap recipe."""
+
+ def unsubscribe(person, unsubscribed_by):
+ """Unsubscribe a person to this snap recipe."""
+
# XXX cjwatson 2015-07-17 bug=760849: "beta" is a lie to get WADL
# generation working. Individual attributes must set their version to
@@ -902,6 +908,9 @@ class ISnapSet(Interface):
def isValidPrivacy(private, owner, branch=None, git_ref=None):
"""Whether or not the privacy context is valid."""
+ def findByIds(snap_ids):
+ """Return all snap packages with the given ids."""
+
@operation_parameters(
owner=Reference(IPerson, title=_("Owner"), required=True),
name=TextLine(title=_("Snap name"), required=True))
@@ -923,7 +932,8 @@ class ISnapSet(Interface):
"""Return all snap packages relevant to `person`.
This returns snap packages for Bazaar or Git branches owned by
- `person`, or where `person` is the owner of the snap package.
+ `person`, or where `person` is the owner of the snap
+ package.
:param person: An `IPerson`.
:param visible_by_user: If not None, only return packages visible by
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index fea5f29..1d4f277 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -24,11 +24,14 @@ import six
from six.moves.urllib.parse import urlsplit
from storm.expr import (
And,
+ Coalesce,
Desc,
+ Join,
LeftJoin,
Not,
Or,
Select,
+ SQL,
)
from storm.locals import (
Bool,
@@ -59,6 +62,7 @@ from lp.app.browser.tales import (
)
from lp.app.errors import IncompatibleArguments
from lp.app.interfaces.security import IAuthorization
+from lp.app.interfaces.services import IService
from lp.buildmaster.enums import BuildStatus
from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
from lp.buildmaster.model.builder import Builder
@@ -94,6 +98,10 @@ from lp.code.model.branchcollection import GenericBranchCollection
from lp.code.model.gitcollection import GenericGitCollection
from lp.code.model.gitrepository import GitRepository
from lp.registry.errors import PrivatePersonLinkageError
+from lp.registry.interfaces.accesspolicy import (
+ IAccessArtifactGrantSource,
+ IAccessArtifactSource,
+ )
from lp.registry.interfaces.person import (
IPerson,
IPersonSet,
@@ -105,6 +113,7 @@ from lp.registry.interfaces.role import (
IHasOwner,
IPersonRoles,
)
+from lp.registry.model.accesspolicy import AccessPolicyGrant
from lp.registry.model.distroseries import DistroSeries
from lp.registry.model.series import ACTIVE_STATUSES
from lp.registry.model.teammembership import TeamParticipation
@@ -123,6 +132,9 @@ from lp.services.database.interfaces import (
IStore,
)
from lp.services.database.stormexpr import (
+ Array,
+ ArrayAgg,
+ ArrayIntersects,
Greatest,
IsDistinctFrom,
NullsLast,
@@ -1052,6 +1064,19 @@ class Snap(Storm, WebhookTargetMixin):
order_by = Desc(SnapBuild.id)
return self._getBuilds(filter_term, order_by)
+ def subscribe(self, person, subscribed_by):
+ """See `ISnap`."""
+ # XXX pappacena 2021-02-05: We may need a "SnapSubscription" here.
+ service = getUtility(IService, "sharing")
+ service.ensureAccessGrants([person], subscribed_by, snaps=[self])
+
+ def unsubscribe(self, person, unsubscribed_by):
+ """See `ISnap`."""
+ service = getUtility(IService, "sharing")
+ service.revokeAccessGrants(
+ self.pillar, person, unsubscribed_by, snaps=[self])
+ IStore(self).flush()
+
def destroySelf(self):
"""See `ISnap`."""
store = IStore(Snap)
@@ -1227,6 +1252,10 @@ class SnapSet:
expressions.append(Snap.owner == owner)
return IStore(Snap).find(Snap, *expressions)
+ def findByIds(self, snap_ids):
+ """See `ISnapSet`."""
+ return IStore(ISnap).find(Snap, Snap.id.is_in(snap_ids))
+
def findByOwner(self, owner):
"""See `ISnapSet`."""
return IStore(Snap).find(Snap, Snap.owner == owner)
@@ -1311,7 +1340,8 @@ class SnapSet:
Snap.private == False,
Snap.owner_id.is_in(Select(
TeamParticipation.teamID,
- TeamParticipation.person == visible_by_user)))
+ TeamParticipation.person == visible_by_user)),
+ *get_private_snap_subscriber_filter(visible_by_user))
def findByURL(self, url, owner=None, visible_by_user=None):
"""See `ISnapSet`."""
@@ -1508,3 +1538,31 @@ class SnapStoreSecretsEncryptedContainer(NaClEncryptedContainerBase):
config.snappy.store_secrets_private_key.encode("UTF-8"))
else:
return None
+
+
+def get_private_snap_subscriber_filter(user):
+ """Returns the filter for all private Snaps that the given user is
+ subscribed to (that is, has access without being directly an owner).
+ """
+ artifact_grant_query = Coalesce(
+ ArrayIntersects(
+ SQL("%s.access_grants" % Snap.__storm_table__),
+ Select(
+ ArrayAgg(TeamParticipation.teamID),
+ tables=TeamParticipation,
+ where=(TeamParticipation.person == user)
+ )), False)
+
+ policy_grant_query = Coalesce(
+ ArrayIntersects(
+ Array(SQL("%s.access_policy" % Snap.__storm_table__)),
+ Select(
+ ArrayAgg(AccessPolicyGrant.policy_id),
+ tables=(AccessPolicyGrant,
+ Join(TeamParticipation,
+ TeamParticipation.teamID ==
+ AccessPolicyGrant.grantee_id)),
+ where=(TeamParticipation.person == user)
+ )), False)
+
+ return [Or(artifact_grant_query, policy_grant_query)]
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index bbc8be1..e21efa1 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -1519,6 +1519,42 @@ class TestSnapSet(TestCaseWithFactory):
self.assertContentEqual(snaps[:3], snap_set.findByPerson(owners[0]))
self.assertContentEqual(snaps[3:], snap_set.findByPerson(owners[1]))
+ def test_findSnapVisibilityClause_includes_grants(self):
+ grantee, creator = [self.factory.makePerson() for i in range(2)]
+ # All snaps are owned by "creator", and "grantee" will later have
+ # access granted using sharing service.
+ snap_data = dict(registrant=creator, owner=creator, private=True)
+ private_snaps = [self.factory.makeSnap(**snap_data) for _ in range(2)]
+ shared_snaps = [self.factory.makeSnap(**snap_data) for _ in range(2)]
+ snap_data["private"] = False
+ public_snaps = [self.factory.makeSnap(**snap_data) for _ in range(3)]
+
+ with admin_logged_in():
+ for snap in shared_snaps:
+ snap.subscribe(grantee, creator)
+
+ snap_set = getUtility(ISnapSet)
+
+ def all_snaps_visible_by(person):
+ snaps = removeSecurityProxy(snap_set)
+ return IStore(Snap).find(
+ Snap, snaps._findSnapVisibilityClause(person))
+
+ # Creator should get all snaps.
+ self.assertContentEqual(
+ public_snaps + private_snaps + shared_snaps,
+ all_snaps_visible_by(creator))
+
+ # Grantee should get public and shared snaps.
+ self.assertContentEqual(
+ public_snaps + shared_snaps, all_snaps_visible_by(grantee))
+
+ with admin_logged_in():
+ # After revoking, Grantee should have no access to the shared ones.
+ for snap in shared_snaps:
+ snap.unsubscribe(grantee, creator)
+ self.assertContentEqual(public_snaps, all_snaps_visible_by(grantee))
+
def test_findByProject(self):
# ISnapSet.findByProject returns all Snaps based on branches or
# repositories for the given project.
Follow ups