← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~pappacena/launchpad:snap-pillar-subscribe-ui into launchpad:master

 

Thiago F. Pappacena has proposed merging ~pappacena/launchpad:snap-pillar-subscribe-ui into launchpad:master with ~pappacena/launchpad:snap-pillar-subscribe-removal-job as a prerequisite.

Commit message:
UI workflow for users to subscribe to snaps

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~pappacena/launchpad/+git/launchpad/+merge/398319
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~pappacena/launchpad:snap-pillar-subscribe-ui into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index c20e7fd..e343a5f 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -2106,8 +2106,6 @@ public.job                              = SELECT, INSERT, UPDATE
 public.person                           = SELECT
 public.product                          = SELECT
 public.sharingjob                       = SELECT, INSERT, UPDATE
-public.snap                             = SELECT
-public.snapsubscription                 = SELECT, INSERT, UPDATE, DELETE
 public.specification                    = SELECT
 public.specificationsubscription        = SELECT, DELETE
 public.teamparticipation                = SELECT
diff --git a/lib/lp/blueprints/model/specification.py b/lib/lp/blueprints/model/specification.py
index 55ce9d5..e0d4001 100644
--- a/lib/lp/blueprints/model/specification.py
+++ b/lib/lp/blueprints/model/specification.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -755,7 +755,7 @@ class Specification(SQLBase, BugLinkTargetMixin, InformationTypeMixin):
             # Grant the subscriber access if they can't see the
             # specification.
             service = getUtility(IService, 'sharing')
-            _, _, _, _, shared_specs = service.getVisibleArtifacts(
+            _, _, _, shared_specs = service.getVisibleArtifacts(
                 person, specifications=[self], ignore_permissions=True)
             if not shared_specs:
                 service.ensureAccessGrants(
diff --git a/lib/lp/blueprints/tests/test_specification.py b/lib/lp/blueprints/tests/test_specification.py
index 86c4e4a..8de08c9 100644
--- a/lib/lp/blueprints/tests/test_specification.py
+++ b/lib/lp/blueprints/tests/test_specification.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Unit tests for Specification."""
@@ -492,7 +492,7 @@ class SpecificationTests(TestCaseWithFactory):
                 product=product, information_type=InformationType.PROPRIETARY)
             spec.subscribe(user, subscribed_by=owner)
             service = getUtility(IService, 'sharing')
-            _, _, _, _, shared_specs = service.getVisibleArtifacts(
+            _, _, _, shared_specs = service.getVisibleArtifacts(
                 user, specifications=[spec])
             self.assertEqual([spec], shared_specs)
             # The spec is also returned by getSharedSpecifications(),
@@ -509,7 +509,7 @@ class SpecificationTests(TestCaseWithFactory):
             service.sharePillarInformation(
                 product, user_2, owner, permissions)
             spec.subscribe(user_2, subscribed_by=owner)
-            _, _, _, _, shared_specs = service.getVisibleArtifacts(
+            _, _, _, shared_specs = service.getVisibleArtifacts(
                 user_2, specifications=[spec])
             self.assertEqual([spec], shared_specs)
             self.assertEqual(
@@ -529,7 +529,7 @@ class SpecificationTests(TestCaseWithFactory):
             spec.subscribe(user, subscribed_by=owner)
             spec.unsubscribe(user, unsubscribed_by=owner)
             service = getUtility(IService, 'sharing')
-            _, _, _, _, shared_specs = service.getVisibleArtifacts(
+            _, _, _, shared_specs = service.getVisibleArtifacts(
                 user, specifications=[spec])
             self.assertEqual([], shared_specs)
 
diff --git a/lib/lp/bugs/model/bug.py b/lib/lp/bugs/model/bug.py
index f7ebc86..a9b177c 100644
--- a/lib/lp/bugs/model/bug.py
+++ b/lib/lp/bugs/model/bug.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Launchpad bug-related database table classes."""
@@ -875,7 +875,7 @@ class Bug(SQLBase, InformationTypeMixin):
         # there is at least one bugtask for which access can be checked.
         if self.default_bugtask:
             service = getUtility(IService, 'sharing')
-            bugs, _, _, _, _ = service.getVisibleArtifacts(
+            bugs, _, _, _ = service.getVisibleArtifacts(
                 person, bugs=[self], ignore_permissions=True)
             if not bugs:
                 service.ensureAccessGrants(
@@ -1819,7 +1819,7 @@ class Bug(SQLBase, InformationTypeMixin):
         if information_type in PRIVATE_INFORMATION_TYPES:
             service = getUtility(IService, 'sharing')
             for person in (who, self.owner):
-                bugs, _, _, _, _ = service.getVisibleArtifacts(
+                bugs, _, _, _ = service.getVisibleArtifacts(
                     person, bugs=[self], ignore_permissions=True)
                 if not bugs:
                     # subscribe() isn't sufficient if a subscription
diff --git a/lib/lp/code/browser/branchsubscription.py b/lib/lp/code/browser/branchsubscription.py
index cee4504..fbd98bc 100644
--- a/lib/lp/code/browser/branchsubscription.py
+++ b/lib/lp/code/browser/branchsubscription.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -276,7 +276,7 @@ class BranchSubscriptionEditView(LaunchpadEditFormView):
         url = canonical_url(self.branch)
         # If the subscriber can no longer see the branch, redirect them away.
         service = getUtility(IService, 'sharing')
-        _, branches, _, _, _ = service.getVisibleArtifacts(
+        _, branches, _, _ = service.getVisibleArtifacts(
             self.person, branches=[self.branch], ignore_permissions=True)
         if not branches:
             url = canonical_url(self.branch.target)
diff --git a/lib/lp/code/browser/gitsubscription.py b/lib/lp/code/browser/gitsubscription.py
index a18d593..3eda78c 100644
--- a/lib/lp/code/browser/gitsubscription.py
+++ b/lib/lp/code/browser/gitsubscription.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2018 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -280,7 +280,7 @@ class GitSubscriptionEditView(LaunchpadEditFormView):
         # If the subscriber can no longer see the repository, redirect them
         # away.
         service = getUtility(IService, "sharing")
-        _, _, repositories, _, _ = service.getVisibleArtifacts(
+        _, _, repositories, _ = service.getVisibleArtifacts(
             self.person, gitrepositories=[self.repository],
             ignore_permissions=True)
         if not repositories:
diff --git a/lib/lp/code/model/branch.py b/lib/lp/code/model/branch.py
index 278db40..947aaf1 100644
--- a/lib/lp/code/model/branch.py
+++ b/lib/lp/code/model/branch.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -1041,7 +1041,7 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
             subscription.review_level = code_review_level
         # Grant the subscriber access if they can't see the branch.
         service = getUtility(IService, 'sharing')
-        _, branches, _, _, _ = service.getVisibleArtifacts(
+        _, branches, _, _ = service.getVisibleArtifacts(
             person, branches=[self], ignore_permissions=True)
         if not branches:
             service.ensureAccessGrants(
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 589dcbf..74d94ec 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -1002,7 +1002,7 @@ class GitRepository(StormBase, WebhookTargetMixin, GitIdentityMixin):
             subscription.review_level = code_review_level
         # Grant the subscriber access if they can't see the repository.
         service = getUtility(IService, "sharing")
-        _, _, repositories, _, _ = service.getVisibleArtifacts(
+        _, _, repositories, _ = service.getVisibleArtifacts(
             person, gitrepositories=[self], ignore_permissions=True)
         if not repositories:
             service.ensureAccessGrants(
diff --git a/lib/lp/code/model/tests/test_branchsubscription.py b/lib/lp/code/model/tests/test_branchsubscription.py
index b5b234e..6a15cd0 100644
--- a/lib/lp/code/model/tests/test_branchsubscription.py
+++ b/lib/lp/code/model/tests/test_branchsubscription.py
@@ -1,4 +1,4 @@
-# Copyright 2010-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2010-2017 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for the BranchSubscription model object."""
@@ -134,7 +134,7 @@ class TestBranchSubscriptions(TestCaseWithFactory):
                 None, CodeReviewNotificationLevel.NOEMAIL, owner)
             # The stacked on branch should be visible.
             service = getUtility(IService, 'sharing')
-            _, visible_branches, _, _, _ = service.getVisibleArtifacts(
+            _, visible_branches, _, _ = service.getVisibleArtifacts(
                 grantee, branches=[private_stacked_on_branch])
             self.assertContentEqual(
                 [private_stacked_on_branch], visible_branches)
@@ -162,7 +162,7 @@ class TestBranchSubscriptions(TestCaseWithFactory):
                 grantee, BranchSubscriptionNotificationLevel.NOEMAIL,
                 None, CodeReviewNotificationLevel.NOEMAIL, owner)
             # The stacked on branch should not be visible.
-            _, visible_branches, _, _, _ = service.getVisibleArtifacts(
+            _, visible_branches, _, _ = service.getVisibleArtifacts(
                 grantee, branches=[private_stacked_on_branch])
             self.assertContentEqual([], visible_branches)
             self.assertIn(
diff --git a/lib/lp/registry/model/sharingjob.py b/lib/lp/registry/model/sharingjob.py
index fe3838e..7d10fa9 100644
--- a/lib/lp/registry/model/sharingjob.py
+++ b/lib/lp/registry/model/sharingjob.py
@@ -1,4 +1,4 @@
-# Copyright 2012-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2012-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Job classes related to the sharing feature are in here."""
@@ -91,12 +91,6 @@ from lp.services.job.model.job import (
     )
 from lp.services.job.runner import BaseRunnableJob
 from lp.services.mail.sendmail import format_address_for_person
-from lp.snappy.interfaces.snap import ISnap
-from lp.snappy.model.snap import (
-    get_private_snap_subscriber_filter,
-    Snap,
-    )
-from lp.snappy.model.snapsubscription import SnapSubscription
 
 
 class SharingJobType(DBEnumeratedType):
@@ -269,7 +263,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
         bug_ids = []
         branch_ids = []
         gitrepository_ids = []
-        snap_ids = []
         specification_ids = []
         if artifacts:
             for artifact in artifacts:
@@ -279,8 +272,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
                     branch_ids.append(artifact.id)
                 elif IGitRepository.providedBy(artifact):
                     gitrepository_ids.append(artifact.id)
-                elif ISnap.providedBy(artifact):
-                    snap_ids.append(artifact.id)
                 elif ISpecification.providedBy(artifact):
                     specification_ids.append(artifact.id)
                 else:
@@ -293,7 +284,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
             'bug_ids': bug_ids,
             'branch_ids': branch_ids,
             'gitrepository_ids': gitrepository_ids,
-            'snap_ids': snap_ids,
             'specification_ids': specification_ids,
             'information_types': information_types,
             'requestor.id': requestor.id
@@ -330,10 +320,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
         return self.metadata.get('gitrepository_ids', [])
 
     @property
-    def snap_ids(self):
-        return self.metadata.get('snap_ids', [])
-
-    @property
     def specification_ids(self):
         return self.metadata.get('specification_ids', [])
 
@@ -363,7 +349,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
             'bug_ids': self.bug_ids,
             'branch_ids': self.branch_ids,
             'gitrepository_ids': self.gitrepository_ids,
-            'snap_ids': self.snap_ids,
             'specification_ids': self.specification_ids,
             'pillar': getattr(self.pillar, 'name', None),
             'grantee': getattr(self.grantee, 'name', None)
@@ -380,7 +365,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
         bug_filters = []
         branch_filters = []
         gitrepository_filters = []
-        snap_filters = []
         specification_filters = []
 
         if self.branch_ids:
@@ -388,8 +372,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
         if self.gitrepository_ids:
             gitrepository_filters.append(GitRepository.id.is_in(
                 self.gitrepository_ids))
-        if self.snap_ids:
-            snap_filters.append(Snap.id.is_in(self.snap_ids))
         if self.specification_ids:
             specification_filters.append(Specification.id.is_in(
                 self.specification_ids))
@@ -405,8 +387,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
                 gitrepository_filters.append(
                     GitRepository.information_type.is_in(
                         self.information_types))
-                snap_filters.append(Snap._information_type.is_in(
-                    self.information_types))
                 specification_filters.append(
                     Specification.information_type.is_in(
                         self.information_types))
@@ -443,11 +423,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
                     Select(
                         TeamParticipation.personID,
                         where=TeamParticipation.team == self.grantee)))
-            snap_filters.append(
-                In(SnapSubscription.person_id,
-                   Select(
-                       TeamParticipation.personID,
-                       where=TeamParticipation.team == self.grantee)))
             specification_filters.append(
                 In(SpecificationSubscription.person_id,
                     Select(
@@ -491,17 +466,6 @@ class RemoveArtifactSubscriptionsJob(SharingJobDerived):
             for sub in gitrepository_subscriptions:
                 sub.repository.unsubscribe(
                     sub.person, self.requestor, ignore_permissions=True)
-        if snap_filters:
-            snap_filters.append(Not(
-                Or(*get_private_snap_subscriber_filter(
-                    SnapSubscription.person_id))))
-            snap_subscriptions = IStore(SnapSubscription).using(
-                SnapSubscription,
-                Join(Snap, Snap.id == SnapSubscription.snap_id)
-            ).find(SnapSubscription, *snap_filters).config(distinct=True)
-            for sub in snap_subscriptions:
-                sub.snap.unsubscribe(
-                    sub.person, self.requestor, ignore_permissions=True)
         if specification_filters:
             specification_filters.append(Not(*get_specification_privacy_filter(
                 SpecificationSubscription.person_id)))
diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
index 4909604..01e90da 100644
--- a/lib/lp/registry/services/sharingservice.py
+++ b/lib/lp/registry/services/sharingservice.py
@@ -81,7 +81,10 @@ from lp.services.webapp.authorization import (
     available_with_permission,
     check_permission,
     )
-from lp.snappy.interfaces.snap import ISnapSet
+from lp.snappy.interfaces.snap import (
+    ISnap,
+    ISnapSet,
+    )
 
 
 @implementer(ISharingService)
@@ -329,7 +332,6 @@ class SharingService:
         bug_ids = []
         branch_ids = []
         gitrepository_ids = []
-        snap_ids = []
         for bug in bugs or []:
             if (not ignore_permissions
                 and not check_permission('launchpad.View', bug)):
@@ -342,14 +344,9 @@ class SharingService:
             branch_ids.append(branch.id)
         for gitrepository in gitrepositories or []:
             if (not ignore_permissions
-                    and not check_permission('launchpad.View', gitrepository)):
+                and not check_permission('launchpad.View', gitrepository)):
                 raise Unauthorized
             gitrepository_ids.append(gitrepository.id)
-        for snap in snaps or []:
-            if (not ignore_permissions
-                and not check_permission('launchpad.View', snap)):
-                raise Unauthorized
-            snap_ids.append(snap.id)
         for spec in specifications or []:
             if (not ignore_permissions
                 and not check_permission('launchpad.View', spec)):
@@ -379,12 +376,6 @@ class SharingService:
             visible_gitrepositories = list(
                 wanted_gitrepositories.getRepositories())
 
-        # Load the Snaps.
-        visible_snaps = []
-        if snap_ids:
-            visible_snaps = list(getUtility(ISnapSet).findByIds(
-                snap_ids, visible_by_user=person))
-
         # Load the specifications.
         visible_specs = []
         if specifications:
@@ -396,7 +387,7 @@ class SharingService:
 
         return (
             visible_bugs, visible_branches, visible_gitrepositories,
-            visible_snaps, visible_specs)
+            visible_specs)
 
     def getInvisibleArtifacts(self, person, bugs=None, branches=None,
                               gitrepositories=None):
@@ -809,6 +800,12 @@ 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(
diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py
index 8d94723..9d0e07c 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-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
@@ -1101,7 +1101,7 @@ class TestSharingService(TestCaseWithFactory):
         # Check that grantees have expected access grants and subscriptions.
         for person in [team_grantee, person_grantee]:
             (visible_bugs, visible_branches, visible_gitrepositories,
-             visible_snaps, visible_specs) = (
+             visible_specs) = (
                 self.service.getVisibleArtifacts(
                     person, bugs=bugs, branches=branches,
                     gitrepositories=gitrepositories,
@@ -1133,7 +1133,7 @@ class TestSharingService(TestCaseWithFactory):
             for bug in bugs or []:
                 self.assertNotIn(person, bug.getDirectSubscribers())
             (visible_bugs, visible_branches, visible_gitrepositories,
-             visible_snaps, visible_specs) = (
+             visible_specs) = (
                 self.service.getVisibleArtifacts(
                     person, bugs=bugs, branches=branches,
                     gitrepositories=gitrepositories))
@@ -1783,8 +1783,7 @@ class TestSharingService(TestCaseWithFactory):
         grantee, ignore, bugs, branches, gitrepositories, specs = (
             self._make_Artifacts())
         # Check the results.
-        (shared_bugs, shared_branches, shared_gitrepositories,
-         shared_snaps, shared_specs) = (
+        shared_bugs, shared_branches, shared_gitrepositories, shared_specs = (
             self.service.getVisibleArtifacts(
                 grantee, bugs=bugs, branches=branches,
                 gitrepositories=gitrepositories, specifications=specs))
@@ -1798,8 +1797,7 @@ class TestSharingService(TestCaseWithFactory):
         # user has a policy grant for the pillar of the specification.
         _, owner, bugs, branches, gitrepositories, specs = (
             self._make_Artifacts())
-        (shared_bugs, shared_branches, shared_gitrepositories,
-         shared_snaps, shared_specs) = (
+        shared_bugs, shared_branches, shared_gitrepositories, shared_specs = (
             self.service.getVisibleArtifacts(
                 owner, bugs=bugs, branches=branches,
                 gitrepositories=gitrepositories, specifications=specs))
@@ -1842,8 +1840,7 @@ class TestSharingService(TestCaseWithFactory):
                 information_type=InformationType.USERDATA)
             bugs.append(bug)
 
-        (shared_bugs, shared_branches, shared_gitrepositories,
-         visible_snaps, shared_specs) = (
+        shared_bugs, shared_branches, shared_gitrepositories, shared_specs = (
             self.service.getVisibleArtifacts(grantee, bugs=bugs))
         self.assertContentEqual(bugs, shared_bugs)
 
@@ -1851,8 +1848,7 @@ class TestSharingService(TestCaseWithFactory):
         for x in range(0, 5):
             change_callback(bugs[x], owner)
         # Check the results.
-        (shared_bugs, shared_branches, shared_gitrepositories,
-         visible_snaps, shared_specs) = (
+        shared_bugs, shared_branches, shared_gitrepositories, shared_specs = (
             self.service.getVisibleArtifacts(grantee, bugs=bugs))
         self.assertContentEqual(bugs[5:], shared_bugs)
 
diff --git a/lib/lp/registry/tests/test_personmerge.py b/lib/lp/registry/tests/test_personmerge.py
index d080746..f1a86b6 100644
--- a/lib/lp/registry/tests/test_personmerge.py
+++ b/lib/lp/registry/tests/test_personmerge.py
@@ -680,22 +680,22 @@ class TestMergePeople(TestCaseWithFactory, KarmaTestMixin):
         login_admin()
         snap.subscribe(duplicate, snap.owner)
         self.assertTrue(snap.visibleByUser(duplicate))
-        self.assertThat(snap._getSubscription(duplicate), MatchesStructure(
+        self.assertThat(snap.getSubscription(duplicate), MatchesStructure(
             snap=Equals(snap),
             person=Equals(duplicate)
         ))
         self.assertFalse(snap.visibleByUser(mergee))
-        self.assertIsNone(snap._getSubscription(mergee))
+        self.assertIsNone(snap.getSubscription(mergee))
 
         duplicate, mergee = self._do_merge(duplicate, mergee)
 
         self.assertTrue(snap.visibleByUser(mergee))
-        self.assertThat(snap._getSubscription(mergee), MatchesStructure(
+        self.assertThat(snap.getSubscription(mergee), MatchesStructure(
             snap=Equals(snap),
             person=Equals(mergee)
         ))
         self.assertFalse(snap.visibleByUser(duplicate))
-        self.assertIsNone(snap._getSubscription(duplicate))
+        self.assertIsNone(snap.getSubscription(duplicate))
 
     def test_merge_moves_oci_recipes(self):
         # When person/teams are merged, oci recipes owned by the from
diff --git a/lib/lp/registry/tests/test_sharingjob.py b/lib/lp/registry/tests/test_sharingjob.py
index 32aec0f..58fa5c1 100644
--- a/lib/lp/registry/tests/test_sharingjob.py
+++ b/lib/lp/registry/tests/test_sharingjob.py
@@ -1,4 +1,4 @@
-# Copyright 2012-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2012-2015 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for SharingJobs."""
@@ -39,7 +39,6 @@ from lp.services.features.testing import FeatureFixture
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.tests import block_on_job
 from lp.services.mail.sendmail import format_address_for_person
-from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
 from lp.testing import (
     login_person,
     person_logged_in,
@@ -128,16 +127,6 @@ class SharingJobDerivedTestCase(TestCaseWithFactory):
             'for gitrepository_ids=[%d], requestor=%s>'
             % (gitrepository.id, requestor.name), repr(job))
 
-    def test_repr_snaps(self):
-        requestor = self.factory.makePerson()
-        snap = self.factory.makeSnap()
-        job = getUtility(IRemoveArtifactSubscriptionsJobSource).create(
-            requestor, artifacts=[snap])
-        self.assertEqual(
-            '<REMOVE_ARTIFACT_SUBSCRIPTIONS job reconciling subscriptions '
-            'for requestor=%s, snap_ids=[%d]>'
-            % (requestor.name, snap.id), repr(job))
-
     def test_repr_specifications(self):
         requestor = self.factory.makePerson()
         specification = self.factory.makeSpecification()
@@ -252,11 +241,9 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
     layer = CeleryJobLayer
 
     def setUp(self):
-        features = {
+        self.useFixture(FeatureFixture({
             'jobs.celery.enabled_classes': 'RemoveArtifactSubscriptionsJob',
-        }
-        features.update(SNAP_TESTING_FLAGS)
-        self.useFixture(FeatureFixture(features))
+        }))
         super(RemoveArtifactSubscriptionsJobTestCase, self).setUp()
 
     def test_create(self):
@@ -328,9 +315,6 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
         gitrepository = self.factory.makeGitRepository(
             owner=owner, target=product,
             information_type=InformationType.USERDATA)
-        snap = self.factory.makeSnap(
-            owner=owner, registrant=owner, project=product,
-            information_type=InformationType.USERDATA)
         specification = self.factory.makeSpecification(
             owner=owner, product=product,
             information_type=InformationType.PROPRIETARY)
@@ -348,7 +332,6 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
         gitrepository.subscribe(artifact_indirect_grantee,
             BranchSubscriptionNotificationLevel.NOEMAIL, None,
             CodeReviewNotificationLevel.NOEMAIL, owner)
-        snap.subscribe(artifact_indirect_grantee, owner)
         # Subscribing somebody to a specification does not automatically
         # create an artifact grant.
         spec_artifact = self.factory.makeAccessArtifact(specification)
@@ -358,11 +341,10 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
             specification.subscribe(artifact_indirect_grantee, owner)
 
         # pick one of the concrete artifacts (bug, branch, Git repository,
-        # snap, or spec) and subscribe the teams and persons.
+        # or spec) and subscribe the teams and persons.
         concrete_artifact, get_pillars, get_subscribers = configure_test(
-            bug, branch, gitrepository, snap, specification,
-            policy_team_grantee, policy_indirect_grantee,
-            artifact_team_grantee, owner)
+            bug, branch, gitrepository, specification, policy_team_grantee,
+            policy_indirect_grantee, artifact_team_grantee, owner)
 
         # Subscribing policy_team_grantee has created an artifact grant so we
         # need to revoke that to test the job.
@@ -395,7 +377,6 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
         self.assertIn(artifact_indirect_grantee, bug.getDirectSubscribers())
         self.assertIn(artifact_indirect_grantee, branch.subscribers)
         self.assertIn(artifact_indirect_grantee, gitrepository.subscribers)
-        self.assertIn(artifact_indirect_grantee, snap.subscribers)
         self.assertIn(artifact_indirect_grantee,
                       removeSecurityProxy(specification).subscribers)
 
@@ -408,7 +389,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
             return removeSecurityProxy(
                 concrete_artifact).getDirectSubscribers()
 
-        def configure_test(bug, branch, gitrepository, snap, specification,
+        def configure_test(bug, branch, gitrepository, specification,
                            policy_team_grantee, policy_indirect_grantee,
                            artifact_team_grantee, owner):
             concrete_artifact = bug
@@ -428,7 +409,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
         def get_subscribers(concrete_artifact):
             return concrete_artifact.subscribers
 
-        def configure_test(bug, branch, gitrepository, snap, specification,
+        def configure_test(bug, branch, gitrepository, specification,
                            policy_team_grantee, policy_indirect_grantee,
                            artifact_team_grantee, owner):
             concrete_artifact = branch
@@ -457,7 +438,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
         def get_subscribers(concrete_artifact):
             return concrete_artifact.subscribers
 
-        def configure_test(bug, branch, gitrepository, snap, specification,
+        def configure_test(bug, branch, gitrepository, specification,
                            policy_team_grantee, policy_indirect_grantee,
                            artifact_team_grantee, owner):
             concrete_artifact = gitrepository
@@ -478,26 +459,6 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
         self._assert_artifact_change_unsubscribes(
             change_callback, configure_test)
 
-    def _assert_snap_change_unsubscribes(self, change_callback):
-
-        def get_pillars(concrete_artifact):
-            return [concrete_artifact.project]
-
-        def get_subscribers(concrete_artifact):
-            return concrete_artifact.subscribers
-
-        def configure_test(bug, branch, gitrepository, snap, specification,
-                           policy_team_grantee, policy_indirect_grantee,
-                           artifact_team_grantee, owner):
-            concrete_artifact = snap
-            snap.subscribe(policy_team_grantee, owner)
-            snap.subscribe(policy_indirect_grantee, owner)
-            snap.subscribe(artifact_team_grantee, owner)
-            return concrete_artifact, get_pillars, get_subscribers
-
-        self._assert_artifact_change_unsubscribes(
-            change_callback, configure_test)
-
     def _assert_specification_change_unsubscribes(self, change_callback):
 
         def get_pillars(concrete_artifact):
@@ -506,7 +467,7 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
         def get_subscribers(concrete_artifact):
             return concrete_artifact.subscribers
 
-        def configure_test(bug, branch, gitrepository, snap, specification,
+        def configure_test(bug, branch, gitrepository, specification,
                            policy_team_grantee, policy_indirect_grantee,
                            artifact_team_grantee, owner):
             naked_spec = removeSecurityProxy(specification)
@@ -535,13 +496,6 @@ class RemoveArtifactSubscriptionsJobTestCase(TestCaseWithFactory):
 
         self._assert_gitrepository_change_unsubscribes(change_information_type)
 
-    def test_change_information_type_snap(self):
-        def change_information_type(snap):
-            removeSecurityProxy(snap).information_type = (
-                InformationType.PRIVATESECURITY)
-
-        self._assert_snap_change_unsubscribes(change_information_type)
-
     def test_change_information_type_specification(self):
         def change_information_type(specification):
             removeSecurityProxy(specification).information_type = (
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 35c8b8c..2e4739f 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -1,4 +1,4 @@
-# Copyright 2009-2020 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2021 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Security policies for using content objects."""
@@ -214,6 +214,7 @@ from lp.snappy.interfaces.snappyseries import (
     ISnappySeries,
     ISnappySeriesSet,
     )
+from lp.snappy.interfaces.snapsubscription import ISnapSubscription
 from lp.soyuz.interfaces.archive import IArchive
 from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthToken
 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
@@ -3298,17 +3299,13 @@ class ViewSnap(AuthorizationBase):
     permission = 'launchpad.View'
     usedfor = ISnap
 
-    def checkUnauthenticated(self):
-        return not self.obj.private
-
     def checkAuthenticated(self, user):
-        if not self.obj.private:
+        if user.isOwner(self.obj) or user.in_commercial_admin or user.in_admin:
             return True
+        return self.obj.visibleByUser(user.person)
 
-        return (
-            user.isOwner(self.obj) or
-            user.in_commercial_admin or
-            user.in_admin)
+    def checkUnauthenticated(self):
+        return self.obj.visibleByUser(None)
 
 
 class EditSnap(AuthorizationBase):
@@ -3339,6 +3336,30 @@ class AdminSnap(AuthorizationBase):
             and EditSnap(self.obj).checkAuthenticated(user))
 
 
+class SnapSubscriptionEdit(AuthorizationBase):
+    permission = 'launchpad.Edit'
+    usedfor = ISnapSubscription
+
+    def checkAuthenticated(self, user):
+        """Is the user able to edit a Snap recipe subscription?
+
+        Any team member can edit a Snap recipe subscription for their
+        team.
+        Launchpad Admins can also edit any Snap recipe subscription.
+        The owner of the subscribed Snap can edit the subscription. If
+        the Snap owner is a team, then members of the team can edit
+        the subscription.
+        """
+        return (user.inTeam(self.obj.snap.owner) or
+                user.inTeam(self.obj.person) or
+                user.inTeam(self.obj.subscribed_by) or
+                user.in_admin)
+
+
+class SnapSubscriptionView(SnapSubscriptionEdit):
+    permission = 'launchpad.View'
+
+
 class ViewSnapBuildRequest(DelegatedAuthorization):
     permission = 'launchpad.View'
     usedfor = ISnapBuildRequest
diff --git a/lib/lp/snappy/browser/configure.zcml b/lib/lp/snappy/browser/configure.zcml
index 9da248a..b11f797 100644
--- a/lib/lp/snappy/browser/configure.zcml
+++ b/lib/lp/snappy/browser/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2015-2020 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2015-2021 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -37,6 +37,45 @@
             name="+portlet-privacy"
             template="../templates/snap-portlet-privacy.pt"/>
         <browser:page
+            for="lp.snappy.interfaces.snap.ISnap"
+            permission="launchpad.View"
+            name="+portlet-subscribers"
+            template="../templates/snap-portlet-subscribers.pt"/>
+        <browser:page
+            for="lp.snappy.interfaces.snap.ISnap"
+            class="lp.snappy.browser.snapsubscription.SnapPortletSubscribersContent"
+            permission="launchpad.View"
+            name="+snap-portlet-subscriber-content"
+            template="../templates/snap-portlet-subscribers-content.pt"/>
+
+        <browser:defaultView
+            for="lp.snappy.interfaces.snapsubscription.ISnapSubscription"
+            name="+index"/>
+        <browser:page
+            for="lp.snappy.interfaces.snapsubscription.ISnapSubscription"
+            class="lp.snappy.browser.snapsubscription.SnapSubscriptionEditView"
+            permission="launchpad.Edit"
+            name="+index"
+            template="../templates/snapsubscription-edit.pt"/>
+        <browser:page
+            for="lp.snappy.interfaces.snap.ISnap"
+            class="lp.snappy.browser.snapsubscription.SnapSubscriptionAddView"
+            permission="launchpad.AnyPerson"
+            name="+subscribe"
+            template="../../app/templates/generic-edit.pt"/>
+        <browser:page
+            for="lp.snappy.interfaces.snap.ISnap"
+            class="lp.snappy.browser.snapsubscription.SnapSubscriptionAddOtherView"
+            permission="launchpad.AnyPerson"
+            name="+addsubscriber"
+            template="../../app/templates/generic-edit.pt"/>
+        <browser:url
+            for="lp.snappy.interfaces.snapsubscription.ISnapSubscription"
+            path_expression="string:+subscription/${person/name}"
+            attribute_to_parent="snap"
+            rootsite="code"/>
+
+        <browser:page
             for="lp.code.interfaces.branch.IBranch"
             class="lp.snappy.browser.snap.SnapAddView"
             permission="launchpad.AnyPerson"
diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
index 7056519..d8bc3d4 100644
--- a/lib/lp/snappy/browser/snap.py
+++ b/lib/lp/snappy/browser/snap.py
@@ -45,25 +45,19 @@ from lp.app.browser.launchpadform import (
     )
 from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
 from lp.app.browser.tales import format_link
-from lp.app.enums import (
-    FREE_INFORMATION_TYPES,
-    InformationType,
-    PRIVATE_INFORMATION_TYPES,
-    PROPRIETARY_INFORMATION_TYPES,
-    )
+from lp.app.enums import PRIVATE_INFORMATION_TYPES
 from lp.app.interfaces.informationtype import IInformationType
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
-from lp.app.vocabularies import InformationTypeVocabulary
 from lp.app.widgets.itemswidgets import (
     LabeledMultiCheckBoxWidget,
     LaunchpadDropdownWidget,
     LaunchpadRadioWidget,
-    LaunchpadRadioWidgetWithDescription,
     )
 from lp.buildmaster.interfaces.processor import IProcessorSet
 from lp.code.browser.widgets.gitref import GitRefWidget
 from lp.code.interfaces.gitref import IGitRef
 from lp.registry.enums import VCSType
+from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.services.features import getFeatureFlag
 from lp.services.propertycache import cachedproperty
@@ -133,6 +127,13 @@ class SnapNavigation(WebhookTargetNavigationMixin, Navigation):
             return None
         return build
 
+    @stepthrough("+subscription")
+    def traverse_subscription(self, name):
+        """Traverses to an `ISnapSubscription`."""
+        person = getUtility(IPersonSet).getByName(name)
+        if person is not None:
+            return self.context.getSubscription(person)
+
 
 class SnapBreadcrumb(NameBreadcrumb):
 
@@ -187,12 +188,31 @@ class SnapContextMenu(ContextMenu):
 
     facet = 'overview'
 
-    links = ('request_builds',)
+    links = ('request_builds', 'add_subscriber', 'subscription')
 
     @enabled_with_permission('launchpad.Edit')
     def request_builds(self):
         return Link('+request-builds', 'Request builds', icon='add')
 
+    @enabled_with_permission("launchpad.AnyPerson")
+    def subscription(self):
+        if self.context.hasSubscription(self.user):
+            url = "+subscription/%s" % self.user.name
+            text = "Edit your subscription"
+            icon = "edit"
+        elif self.context.userCanBeSubscribed(self.user):
+            url = "+subscribe"
+            text = "Subscribe yourself"
+            icon = "add"
+        else:
+            return None
+        return Link(url, text, icon=icon)
+
+    @enabled_with_permission("launchpad.Edit")
+    def add_subscriber(self):
+        text = "Subscribe someone else"
+        return Link("+addsubscriber", text, icon="add")
+
 
 class SnapView(LaunchpadView):
     """Default view of a Snap."""
@@ -350,7 +370,7 @@ class ISnapEditSchema(Interface):
     use_template(ISnap, include=[
         'owner',
         'name',
-        'information_type',
+        'private',
         'project',
         'require_virtualized',
         'allow_internet',
@@ -359,7 +379,6 @@ class ISnapEditSchema(Interface):
         'auto_build_channels',
         'store_upload',
         ])
-
     store_distro_series = Choice(
         vocabulary='SnappyDistroSeries', required=True,
         title='Series')
@@ -529,10 +548,8 @@ class SnapAddView(
             kwargs = {'git_ref': self.context}
         else:
             kwargs = {'branch': self.context}
-        private = not getUtility(ISnapSet).isValidPrivacy(
-            False, data['owner'], **kwargs)
-        information_type = (InformationType.PROPRIETARY if private else
-                            InformationType.PUBLIC)
+        private = not getUtility(
+            ISnapSet).isValidPrivacy(False, data['owner'], **kwargs)
         if not data.get('auto_build', False):
             data['auto_build_archive'] = None
             data['auto_build_pocket'] = None
@@ -543,8 +560,7 @@ class SnapAddView(
             auto_build_archive=data['auto_build_archive'],
             auto_build_pocket=data['auto_build_pocket'],
             auto_build_channels=data['auto_build_channels'],
-            information_type=information_type,
-            processors=data['processors'],
+            processors=data['processors'], private=private,
             build_source_tarball=data['build_source_tarball'],
             store_upload=data['store_upload'],
             store_series=data['store_distro_series'].snappy_series,
@@ -624,33 +640,31 @@ class BaseSnapEditView(LaunchpadEditFormView, SnapAuthorizeMixin):
 
     def validate(self, data):
         super(BaseSnapEditView, self).validate(data)
-        info_type = data.get('information_type', self.context.information_type)
-        editing_info_type = 'information_type' in data
-        private = info_type in PRIVATE_INFORMATION_TYPES
-        if private is False:
+        if data.get('private', self.context.private) is False:
             # These are the requirements for public snaps.
-            if 'information_type' in data or 'owner' in data:
+            if 'private' in data or 'owner' in data:
                 owner = data.get('owner', self.context.owner)
                 if owner is not None and owner.private:
                     self.setFieldError(
-                        'information_type' if editing_info_type else 'owner',
+                        'private' if 'private' in data else 'owner',
                         'A public snap cannot have a private owner.')
-            if 'information_type' in data or 'branch' in data:
+            if 'private' in data or 'branch' in data:
                 branch = data.get('branch', self.context.branch)
                 if branch is not None and branch.private:
                     self.setFieldError(
-                        'information_type' if editing_info_type else 'branch',
+                        'private' if 'private' in data else 'branch',
                         'A public snap cannot have a private branch.')
-            if 'information_type' in data or 'git_ref' in data:
+            if 'private' in data or 'git_ref' in data:
                 ref = data.get('git_ref', self.context.git_ref)
                 if ref is not None and ref.private:
                     self.setFieldError(
-                        'information_type' if editing_info_type else 'git_ref',
+                        'private' if 'private' in data else 'git_ref',
                         'A public snap cannot have a private repository.')
         else:
-            # Requirements for private snaps.
+            # These are the requirements for private snaps.
             project = data.get('project', self.context.project)
-            if project is None:
+            private = data.get('private', self.context.private)
+            if private and project is None:
                 msg = ('Private Snap recipes should be associated '
                        'with a project.')
                 self.setFieldError('project', msg)
@@ -720,36 +734,25 @@ class SnapAdminView(BaseSnapEditView):
     page_title = 'Administer'
 
     field_names = [
-        'project', 'information_type', 'require_virtualized', 'allow_internet']
-
-    custom_widget_information_type = CustomWidgetFactory(
-        LaunchpadRadioWidgetWithDescription,
-        vocabulary=InformationTypeVocabulary(
-            types=FREE_INFORMATION_TYPES + PROPRIETARY_INFORMATION_TYPES))
-
-    @property
-    def initial_values(self):
-        """Set initial values for the form."""
-        # XXX pappacena 2021-02-12: Until we back fill information_type
-        # database column, it will be NULL, but snap.information_type
-        # property has a fallback to check "private" property. This should
-        # be removed once we back fill snap.information_type.
-        return {'information_type': self.context.information_type}
+        'project', 'private', 'require_virtualized', 'allow_internet']
 
     def validate(self, data):
         super(SnapAdminView, self).validate(data)
         # BaseSnapEditView.validate checks the rules for 'private' in
         # combination with other attributes.
-        if data.get('information_type', None) in PRIVATE_INFORMATION_TYPES:
+        if data.get('private', None) is True:
             if not getFeatureFlag(SNAP_PRIVATE_FEATURE_FLAG):
                 self.setFieldError(
-                    'information_type',
+                    'private',
                     'You do not have permission to create private snaps.')
 
     def updateContextFromData(self, data, context=None, notify_modified=True):
         if 'project' in data:
             project = data.pop('project')
             self.context.setProject(project)
+        if 'private' in data:
+            private = data.pop('private')
+            self.context.setPrivate(private)
         super(SnapAdminView, self).updateContextFromData(
             data, context, notify_modified)
 
diff --git a/lib/lp/snappy/browser/snapsubscription.py b/lib/lp/snappy/browser/snapsubscription.py
new file mode 100644
index 0000000..84c1597
--- /dev/null
+++ b/lib/lp/snappy/browser/snapsubscription.py
@@ -0,0 +1,173 @@
+# Copyright 2020-2021 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Snap subscription views."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+    'SnapPortletSubscribersContent'
+]
+
+from zope.component._api import getUtility
+from zope.formlib.form import action
+
+from lp.app.browser.launchpadform import (
+    LaunchpadEditFormView,
+    LaunchpadFormView,
+    )
+from lp.registry.interfaces.person import IPersonSet
+from lp.services.webapp import (
+    canonical_url,
+    LaunchpadView,
+    )
+from lp.services.webapp.authorization import (
+    check_permission,
+    precache_permission_for_objects,
+    )
+from lp.snappy.interfaces.snapsubscription import ISnapSubscription
+
+
+class SnapPortletSubscribersContent(LaunchpadView):
+    """View for the contents for the subscribers portlet."""
+
+    def subscriptions(self):
+        """Return a decorated list of Snap recipe subscriptions."""
+
+        # Cache permissions so private subscribers can be rendered.
+        # The security adaptor will do the job also but we don't want or
+        # need the expense of running several complex SQL queries.
+        subscriptions = list(self.context.subscriptions)
+        person_ids = [sub.person.id for sub in subscriptions]
+        list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+            person_ids, need_validity=True))
+        if self.user is not None:
+            subscribers = [
+                subscription.person for subscription in subscriptions]
+            precache_permission_for_objects(
+                self.request, "launchpad.LimitedView", subscribers)
+
+        visible_subscriptions = [
+            subscription for subscription in subscriptions
+            if check_permission("launchpad.LimitedView", subscription.person)]
+        return sorted(
+            visible_subscriptions,
+            key=lambda subscription: subscription.person.displayname)
+
+
+class RedirectToSnapMixin:
+    @property
+    def next_url(self):
+        url = canonical_url(self.snap)
+        # If the subscriber can no longer see the Snap recipe, redirect them
+        # away.
+        if not self.snap.visibleByUser(self.user):
+            url = canonical_url(self.snap.project)
+        return url
+
+    cancel_url = next_url
+
+
+class SnapSubscriptionEditView(RedirectToSnapMixin, LaunchpadEditFormView):
+    """The view for editing Snap recipe subscriptions."""
+    schema = ISnapSubscription
+    field_names = []
+
+    @property
+    def page_title(self):
+        return (
+            "Edit subscription to Snap recipe %s" %
+            self.snap.displayname)
+
+    @property
+    def label(self):
+        return (
+            "Edit subscription to Snap recipe for %s" %
+            self.person.displayname)
+
+    def initialize(self):
+        self.snap = self.context.snap
+        self.person = self.context.person
+        super(SnapSubscriptionEditView, self).initialize()
+
+    @action("Unsubscribe", name="unsubscribe")
+    def unsubscribe_action(self, action, data):
+        """Unsubscribe the team from the Snap recipe."""
+        self.snap.unsubscribe(self.person, self.user)
+        self.request.response.addNotification(
+            "%s has been unsubscribed from this Snap recipe."
+            % self.person.displayname)
+
+
+class _SnapSubscriptionCreationView(RedirectToSnapMixin, LaunchpadFormView):
+    """Contains the common functionality of the Add and Edit views."""
+
+    schema = ISnapSubscription
+    field_names = []
+
+    def initialize(self):
+        self.snap = self.context
+        super(_SnapSubscriptionCreationView, self).initialize()
+
+
+class SnapSubscriptionAddView(_SnapSubscriptionCreationView):
+
+    page_title = label = "Subscribe to Snap recipe"
+
+    @action("Subscribe")
+    def subscribe(self, action, data):
+        # To catch the stale post problem, check that the user is not
+        # subscribed before continuing.
+        if self.context.hasSubscription(self.user):
+            self.request.response.addNotification(
+                "You are already subscribed to this Snap recipe.")
+        else:
+            self.context.subscribe(self.user, self.user)
+
+            self.request.response.addNotification(
+                "You have subscribed to this Snap recipe.")
+
+
+class SnapSubscriptionAddOtherView(_SnapSubscriptionCreationView):
+    """View used to subscribe someone other than the current user."""
+
+    field_names = ["person"]
+    for_input = True
+
+    # Since we are subscribing other people, the current user
+    # is never considered subscribed.
+    user_is_subscribed = False
+
+    page_title = label = "Subscribe to Snap recipe"
+
+    def validate(self, data):
+        if "person" in data:
+            person = data["person"]
+            subscription = self.context.getSubscription(person)
+            if (subscription is None
+                    and not self.context.userCanBeSubscribed(person)):
+                self.setFieldError(
+                    "person",
+                    "Open and delegated teams cannot be subscribed to "
+                    "private Snap recipes.")
+
+    @action("Subscribe", name="subscribe_action")
+    def subscribe_action(self, action, data):
+        """Subscribe the specified user to the Snap recipe.
+
+        The user must be a member of a team in order to subscribe that team
+        to the Snap recipe. Launchpad Admins are special and they can
+        subscribe any team.
+        """
+        person = data["person"]
+        subscription = self.context.getSubscription(person)
+        if subscription is None:
+            self.context.subscribe(person, self.user)
+            self.request.response.addNotification(
+                "%s has been subscribed to this Snap recipe." %
+                person.displayname)
+        else:
+            self.request.response.addNotification(
+                "%s was already subscribed to this Snap recipe with." %
+                person.displayname)
diff --git a/lib/lp/snappy/browser/tests/test_snap.py b/lib/lp/snappy/browser/tests/test_snap.py
index 77e7f6a..d048778 100644
--- a/lib/lp/snappy/browser/tests/test_snap.py
+++ b/lib/lp/snappy/browser/tests/test_snap.py
@@ -2,7 +2,7 @@
 # NOTE: The first line above must stay first; do not move the copyright
 # notice to the top.  See http://www.python.org/dev/peps/pep-0263/.
 #
-# Copyright 2015-2021 Canonical Ltd.  This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Test snap package views."""
@@ -659,21 +659,18 @@ class TestSnapAdminView(BaseTestSnapView):
             project.information_type = InformationType.PROPRIETARY
         snap = self.factory.makeSnap(registrant=self.person)
         self.assertTrue(snap.require_virtualized)
-        self.assertIsNone(snap.project)
         self.assertFalse(snap.private)
         self.assertTrue(snap.allow_internet)
 
-        private = InformationType.PROPRIETARY.name
         browser = self.getViewBrowser(snap, user=commercial_admin)
         browser.getLink("Administer snap package").click()
         browser.getControl(name='field.project').value = "my-project"
         browser.getControl("Require virtualized builders").selected = False
-        browser.getControl(name="field.information_type").value = private
+        browser.getControl("Private").selected = True
         browser.getControl("Allow external network access").selected = False
         browser.getControl("Update snap package").click()
 
         login_person(self.person)
-        self.assertEqual(project, snap.project)
         self.assertFalse(snap.require_virtualized)
         self.assertTrue(snap.private)
         self.assertFalse(snap.allow_internet)
@@ -684,11 +681,10 @@ class TestSnapAdminView(BaseTestSnapView):
         snap = self.factory.makeSnap(registrant=self.person)
         commercial_admin = self.factory.makePerson(
             member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
-        private = InformationType.PROPRIETARY.name
         browser = self.getViewBrowser(snap, user=commercial_admin)
         browser.getLink("Administer snap package").click()
         browser.getControl(name='field.project').value = ''
-        browser.getControl(name="field.information_type").value = private
+        browser.getControl("Private").selected = True
         browser.getControl("Update snap package").click()
         self.assertEqual(
             'Private Snap recipes should be associated with a project.',
@@ -705,10 +701,9 @@ class TestSnapAdminView(BaseTestSnapView):
         # can reach this snap because it's owned by a private team.
         commercial_admin = self.factory.makePerson(
             member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
-        public = InformationType.PUBLIC.name
         browser = self.getViewBrowser(snap, user=commercial_admin)
         browser.getLink("Administer snap package").click()
-        browser.getControl(name="field.information_type").value = public
+        browser.getControl("Private").selected = False
         browser.getControl("Update snap package").click()
         self.assertEqual(
             'A public snap cannot have a private owner.',
diff --git a/lib/lp/snappy/configure.zcml b/lib/lp/snappy/configure.zcml
index a16c664..05529f8 100644
--- a/lib/lp/snappy/configure.zcml
+++ b/lib/lp/snappy/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2015-2019 Canonical Ltd.  This software is licensed under the
+<!-- Copyright 2015-2021 Canonical Ltd.  This software is licensed under the
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
@@ -42,6 +42,15 @@
         <allow interface="lp.snappy.interfaces.snap.ISnapSet" />
     </securedutility>
 
+    <!-- SnapSubscription -->
+
+    <class class="lp.snappy.model.snapsubscription.SnapSubscription">
+      <allow interface="lp.snappy.interfaces.snapsubscription.ISnapSubscription"/>
+      <require
+          permission="zope.Public"
+          set_schema="lp.snappy.interfaces.snapsubscription.ISnapSubscription"/>
+    </class>
+
     <!-- SnapBuildRequest -->
     <class class="lp.snappy.model.snap.SnapBuildRequest">
         <require
diff --git a/lib/lp/snappy/interfaces/snap.py b/lib/lp/snappy/interfaces/snap.py
index 687a605..9d96db1 100644
--- a/lib/lp/snappy/interfaces/snap.py
+++ b/lib/lp/snappy/interfaces/snap.py
@@ -89,9 +89,7 @@ from zope.security.interfaces import (
     )
 
 from lp import _
-from lp.app.enums import InformationType
 from lp.app.errors import NameLookupFailed
-from lp.app.interfaces.informationtype import IInformationType
 from lp.app.interfaces.launchpad import IPrivacy
 from lp.app.validators.name import name_validator
 from lp.buildmaster.interfaces.processor import IProcessor
@@ -570,10 +568,25 @@ class ISnapView(Interface):
         # Really ISnapBuild, patched in lp.snappy.interfaces.webservice.
         value_type=Reference(schema=Interface), readonly=True)))
 
+    subscriptions = CollectionField(
+        title=_("SnapSubscriptions associated with this repository."),
+        readonly=True,
+        # Really IGitSubscription, patched in _schema_circular_imports.py.
+        value_type=Reference(Interface))
+
     subscribers = CollectionField(
-        title=_("Persons subscribed to this repository."),
+        title=_("Persons subscribed to this snap recipe."),
         readonly=True, value_type=Reference(IPerson))
 
+    def getSubscription(person):
+        """Returns the person's snap subscription for this snap recipe."""
+
+    def hasSubscription(person):
+        """Is this person subscribed to the snap recipe?"""
+
+    def userCanBeSubscribed(person):
+        """Checks if the given person can be subscribed to this snap recipe."""
+
     def visibleByUser(user):
         """Can the specified user see this snap recipe?"""
 
@@ -853,12 +866,6 @@ class ISnapAdminAttributes(Interface):
         title=_("Private"), required=False, readonly=False,
         description=_("Whether or not this snap is private.")))
 
-    information_type = exported(Choice(
-        title=_("Information type"), vocabulary=InformationType,
-        required=True, readonly=False, default=InformationType.PUBLIC,
-        description=_(
-            "The type of information contained in this Snap recipe.")))
-
     require_virtualized = exported(Bool(
         title=_("Require virtualized builders"), required=True, readonly=False,
         description=_("Only build this snap package on virtual builders.")))
@@ -883,6 +890,9 @@ class ISnapAdminAttributes(Interface):
     def unsubscribe(person, unsubscribed_by):
         """Unsubscribe a person to this snap recipe."""
 
+    def setPrivate(private):
+        """Set the current snap recipe as public or private."""
+
 
 # XXX cjwatson 2015-07-17 bug=760849: "beta" is a lie to get WADL
 # generation working.  Individual attributes must set their version to
@@ -890,7 +900,7 @@ class ISnapAdminAttributes(Interface):
 @exported_as_webservice_entry(as_of="beta")
 class ISnap(
     ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes,
-    IPrivacy, IInformationType):
+    IPrivacy):
     """A buildable snap package."""
 
 
@@ -900,13 +910,6 @@ class ISnapSet(Interface):
 
     @call_with(registrant=REQUEST_USER)
     @operation_parameters(
-        # Redefining information_type param to make it optional on the API
-        # (although it is mandatory on the UI).
-        information_type=Choice(
-            title=_("Information type"), vocabulary=InformationType,
-            required=False, default=InformationType.PUBLIC,
-            description=_(
-                "The type of information contained in this Snap recipe.")),
         processors=List(
             value_type=Reference(schema=IProcessor), required=False))
     @export_factory_operation(
@@ -914,16 +917,15 @@ class ISnapSet(Interface):
             "owner", "distro_series", "name", "description", "branch",
             "git_repository", "git_repository_url", "git_path", "git_ref",
             "auto_build", "auto_build_archive", "auto_build_pocket",
-            "store_upload", "store_series", "store_name", "store_channels",
-            "project"])
+            "private", "store_upload", "store_series", "store_name",
+            "store_channels", "project"])
     @operation_for_version("devel")
     def new(registrant, owner, distro_series, name, description=None,
             branch=None, git_repository=None, git_repository_url=None,
             git_path=None, git_ref=None, auto_build=False,
             auto_build_archive=None, auto_build_pocket=None,
             require_virtualized=True, processors=None, date_created=None,
-            information_type=InformationType.PUBLIC, store_upload=False,
-            store_series=None,
+            private=False, store_upload=False, store_series=None,
             store_name=None, store_secrets=None, store_channels=None,
             project=None):
         """Create an `ISnap`."""
@@ -937,10 +939,6 @@ class ISnapSet(Interface):
     def findByIds(snap_ids):
         """Return all snap packages with the given ids."""
 
-    def isValidInformationType(
-            information_type, owner, branch=None, git_ref=None):
-        """Whether or not the information type context is valid."""
-
     @operation_parameters(
         owner=Reference(IPerson, title=_("Owner"), required=True),
         name=TextLine(title=_("Snap name"), required=True))
diff --git a/lib/lp/snappy/model/snap.py b/lib/lp/snappy/model/snap.py
index a1000ea..886b752 100644
--- a/lib/lp/snappy/model/snap.py
+++ b/lib/lp/snappy/model/snap.py
@@ -5,7 +5,6 @@ from __future__ import absolute_import, print_function, unicode_literals
 
 __metaclass__ = type
 __all__ = [
-    'get_private_snap_subscriber_filter',
     'Snap',
     ]
 
@@ -61,14 +60,10 @@ from lp.app.browser.tales import (
     ArchiveFormatterAPI,
     DateTimeFormatterAPI,
     )
-from lp.app.enums import (
-    InformationType,
-    PUBLIC_INFORMATION_TYPES,
-    )
+from lp.app.enums import InformationType
 from lp.app.errors import (
     IncompatibleArguments,
     SubscriptionPrivacyViolation,
-    UserCannotUnsubscribePerson,
     )
 from lp.app.interfaces.security import IAuthorization
 from lp.app.interfaces.services import IService
@@ -369,18 +364,13 @@ class Snap(Storm, WebhookTargetMixin):
 
     require_virtualized = Bool(name='require_virtualized')
 
-    _private = Bool(name='private')
-
-    def _valid_information_type(self, attr, value):
-        if not getUtility(ISnapSet).isValidInformationType(
+    def _validate_private(self, attr, value):
+        if not getUtility(ISnapSet).isValidPrivacy(
                 value, self.owner, self.branch, self.git_ref):
             raise SnapPrivacyMismatch
         return value
 
-    _information_type = DBEnum(
-        enum=InformationType, default=InformationType.PUBLIC,
-        name="information_type",
-        validator=_valid_information_type)
+    private = Bool(name='private', validator=_validate_private)
 
     allow_internet = Bool(name='allow_internet', allow_none=False)
 
@@ -401,21 +391,18 @@ class Snap(Storm, WebhookTargetMixin):
                  description=None, branch=None, git_ref=None, auto_build=False,
                  auto_build_archive=None, auto_build_pocket=None,
                  auto_build_channels=None, require_virtualized=True,
-                 date_created=DEFAULT, information_type=InformationType.PUBLIC,
-                 allow_internet=True, build_source_tarball=False,
-                 store_upload=False, store_series=None, store_name=None,
-                 store_secrets=None, store_channels=None, project=None):
+                 date_created=DEFAULT, private=False, allow_internet=True,
+                 build_source_tarball=False, store_upload=False,
+                 store_series=None, store_name=None, store_secrets=None,
+                 store_channels=None, project=None):
         """Construct a `Snap`."""
         super(Snap, self).__init__()
 
-        # Set the information type first so that other validators can perform
+        # Set the private flag first so that other validators can perform
         # suitable privacy checks, but pillar should also be set, since it's
         # mandatory for private snaps.
-        # Note that we set self._information_type (not self.information_type)
-        # to avoid the call to self._reconcileAccess() while building the
-        # Snap instance.
         self.project = project
-        self._information_type = information_type
+        self.private = private
 
         self.registrant = registrant
         self.owner = owner
@@ -443,22 +430,6 @@ class Snap(Storm, WebhookTargetMixin):
         return "<Snap ~%s/+snap/%s>" % (self.owner.name, self.name)
 
     @property
-    def information_type(self):
-        if self._information_type is None:
-            return (InformationType.PROPRIETARY if self._private
-                    else InformationType.PUBLIC)
-        return self._information_type
-
-    @information_type.setter
-    def information_type(self, information_type):
-        self._information_type = information_type
-        self._reconcileAccess()
-
-    @property
-    def private(self):
-        return self.information_type not in PUBLIC_INFORMATION_TYPES
-
-    @property
     def valid_webhook_event_types(self):
         return ["snap:build:0.1"]
 
@@ -1098,6 +1069,18 @@ class Snap(Storm, WebhookTargetMixin):
         order_by = Desc(SnapBuild.id)
         return self._getBuilds(filter_term, order_by)
 
+    @property
+    def subscriptions(self):
+        return Store.of(self).find(
+            SnapSubscription, SnapSubscription.snap == self)
+
+    @property
+    def subscribers(self):
+        return Store.of(self).find(
+            Person,
+            SnapSubscription.person_id == Person.id,
+            SnapSubscription.snap == self)
+
     def visibleByUser(self, user):
         """See `IGitRepository`."""
         if not self.private:
@@ -1115,7 +1098,11 @@ class Snap(Storm, WebhookTargetMixin):
             Snap.id == self.id,
             visibility_clause).is_empty()
 
-    def _getSubscription(self, person):
+    def hasSubscription(self, person):
+        """See `ISnap`."""
+        return self.getSubscription(person) is not None
+
+    def getSubscription(self, person):
         """Returns person's subscription to this snap recipe, or None if no
         subscription is available.
         """
@@ -1126,49 +1113,33 @@ class Snap(Storm, WebhookTargetMixin):
             SnapSubscription.person == person,
             SnapSubscription.snap == self).one()
 
-    def _userCanBeSubscribed(self, person):
+    def userCanBeSubscribed(self, person):
         """Checks if the given person can subscribe to this snap recipe."""
         return not (
             self.private and
             person.is_team and
             person.anyone_can_join())
 
-    @property
-    def subscribers(self):
-        return Store.of(self).find(
-            Person,
-            SnapSubscription.person_id == Person.id,
-            SnapSubscription.snap == self)
-
     def subscribe(self, person, subscribed_by):
         """See `ISnap`."""
-        if not self._userCanBeSubscribed(person):
+        if not self.userCanBeSubscribed(person):
             raise SubscriptionPrivacyViolation(
                 "Open and delegated teams cannot be subscribed to private "
                 "snap recipes.")
-        subscription = self._getSubscription(person)
+        subscription = self.getSubscription(person)
         if subscription is None:
             subscription = SnapSubscription(
                 person=person, snap=self, subscribed_by=subscribed_by)
             Store.of(subscription).flush()
         service = getUtility(IService, "sharing")
-        _, _, _, snaps, _ = service.getVisibleArtifacts(
-            person, snaps=[self], ignore_permissions=True)
-        if not snaps:
-            service.ensureAccessGrants([person], subscribed_by, snaps=[self])
+        service.ensureAccessGrants([person], subscribed_by, snaps=[self])
 
-    def unsubscribe(self, person, unsubscribed_by, ignore_permissions=False):
+    def unsubscribe(self, person, unsubscribed_by):
         """See `ISnap`."""
         service = getUtility(IService, "sharing")
         service.revokeAccessGrants(
             self.pillar, person, unsubscribed_by, snaps=[self])
-        subscription = self._getSubscription(person)
-        if (not ignore_permissions
-                and not subscription.canBeUnsubscribedByUser(unsubscribed_by)):
-            raise UserCannotUnsubscribePerson(
-                '%s does not have permission to unsubscribe %s.' % (
-                    unsubscribed_by.displayname,
-                    person.displayname))
+        subscription = self.getSubscription(person)
         # It should never be None, since we always create a SnapSubscription
         # on Snap.subscribe. But just in case...
         if subscription is not None:
@@ -1184,8 +1155,14 @@ class Snap(Storm, WebhookTargetMixin):
         """
         if self.project is None:
             return
+        info_type = (InformationType.PUBLIC if not self.private
+                     else InformationType.PROPRIETARY)
         pillars = [self.project]
-        reconcile_access_for_artifact(self, self.information_type, pillars)
+        reconcile_access_for_artifact(self, info_type, pillars)
+
+    def setPrivate(self, private):
+        self.private = private
+        self._reconcileAccess()
 
     def setProject(self, project):
         self.project = project
@@ -1254,11 +1231,10 @@ class SnapSet:
             git_path=None, git_ref=None, auto_build=False,
             auto_build_archive=None, auto_build_pocket=None,
             auto_build_channels=None, require_virtualized=True,
-            processors=None, date_created=DEFAULT,
-            information_type=InformationType.PUBLIC, allow_internet=True,
-            build_source_tarball=False, store_upload=False,
-            store_series=None, store_name=None, store_secrets=None,
-            store_channels=None, project=None):
+            processors=None, date_created=DEFAULT, private=False,
+            allow_internet=True, build_source_tarball=False,
+            store_upload=False, store_series=None, store_name=None,
+            store_secrets=None, store_channels=None, project=None):
         """See `ISnapSet`."""
         if not registrant.inTeam(owner):
             if owner.is_team:
@@ -1295,8 +1271,7 @@ class SnapSet:
         # IntegrityError due to exceptions being raised during object
         # creation and to ensure that everything relevant is in the Storm
         # cache.
-        if not self.isValidInformationType(
-                information_type, owner, branch, git_ref):
+        if not self.isValidPrivacy(private, owner, branch, git_ref):
             raise SnapPrivacyMismatch
 
         store = IMasterStore(Snap)
@@ -1307,7 +1282,7 @@ class SnapSet:
             auto_build_pocket=auto_build_pocket,
             auto_build_channels=auto_build_channels,
             require_virtualized=require_virtualized, date_created=date_created,
-            information_type=information_type, allow_internet=allow_internet,
+            private=private, allow_internet=allow_internet,
             build_source_tarball=build_source_tarball,
             store_upload=store_upload, store_series=store_series,
             store_name=store_name, store_secrets=store_secrets,
@@ -1341,11 +1316,6 @@ class SnapSet:
 
         return True
 
-    def isValidInformationType(self, information_type, owner, branch=None,
-                               git_ref=None):
-        private = information_type not in PUBLIC_INFORMATION_TYPES
-        return self.isValidPrivacy(private, owner, branch, git_ref)
-
     def _getByName(self, owner, name):
         return IStore(Snap).find(
             Snap, Snap.owner == owner, Snap.name == name).one()
@@ -1373,12 +1343,9 @@ class SnapSet:
             expressions.append(Snap.owner == owner)
         return IStore(Snap).find(Snap, *expressions)
 
-    def findByIds(self, snap_ids, visible_by_user=None):
+    def findByIds(self, snap_ids):
         """See `ISnapSet`."""
-        clauses = [Snap.id.is_in(snap_ids)]
-        if visible_by_user is not None:
-            clauses.append(self._findSnapVisibilityClause(visible_by_user))
-        return IStore(Snap).find(Snap, *clauses)
+        return IStore(Snap).find(Snap, Snap.id.is_in(snap_ids))
 
     def findByOwner(self, owner):
         """See `ISnapSet`."""
@@ -1453,22 +1420,15 @@ class SnapSet:
         # XXX cjwatson 2016-11-25: This is in principle a poor query, but we
         # don't yet have the access grant infrastructure to do better, and
         # in any case the numbers involved should be very small.
-        # XXX pappacena 2021-02-12: Once we do the migration to back fill
-        # information_type, we should be able to change this.
-        private_snap = SQL(
-            "CASE information_type"
-            "    WHEN NULL THEN private"
-            "    ELSE information_type NOT IN ?"
-            "END", params=[tuple(i.value for i in PUBLIC_INFORMATION_TYPES)])
         if visible_by_user is None:
-            return private_snap == False
+            return Snap.private == False
         else:
             roles = IPersonRoles(visible_by_user)
             if roles.in_admin or roles.in_commercial_admin:
                 return True
             else:
                 return Or(
-                    private_snap == False,
+                    Snap.private == False,
                     Snap.owner_id.is_in(Select(
                         TeamParticipation.teamID,
                         TeamParticipation.person == visible_by_user)),
diff --git a/lib/lp/snappy/templates/snap-index.pt b/lib/lp/snappy/templates/snap-index.pt
index 7d3083e..09eea5c 100644
--- a/lib/lp/snappy/templates/snap-index.pt
+++ b/lib/lp/snappy/templates/snap-index.pt
@@ -32,6 +32,7 @@
   <metal:side fill-slot="side">
     <div tal:replace="structure context/@@+portlet-privacy" />
     <div tal:replace="structure context/@@+global-actions"/>
+    <tal:subscribers replace="structure context/@@+portlet-subscribers" />
   </metal:side>
 
   <metal:heading fill-slot="heading">
diff --git a/lib/lp/snappy/templates/snap-portlet-subscribers-content.pt b/lib/lp/snappy/templates/snap-portlet-subscribers-content.pt
new file mode 100644
index 0000000..05495ff
--- /dev/null
+++ b/lib/lp/snappy/templates/snap-portlet-subscribers-content.pt
@@ -0,0 +1,31 @@
+<div
+  tal:omit-tag=""
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";>
+  <div class="section snap-subscribers">
+    <div
+      tal:condition="view/subscriptions"
+      tal:repeat="subscription view/subscriptions"
+      tal:attributes="id string:subscriber-${subscription/person/name}">
+        <a tal:condition="subscription/person/name|nothing"
+           tal:attributes="href subscription/person/fmt:url">
+
+          <tal:block replace="structure subscription/person/fmt:icon" />
+          <tal:block replace="subscription/person/fmt:displayname/fmt:shorten/20" />
+        </a>
+
+        <a tal:condition="subscription/required:launchpad.Edit"
+           tal:attributes="
+             href subscription/fmt:url;
+             title string:Edit subscription ${subscription/person/fmt:displayname};
+             id string:editsubscription-${subscription/person/name}">
+          <img class="editsub-icon" src="/@@/edit"
+            tal:attributes="id string:editsubscription-icon-${subscription/person/name}" />
+        </a>
+    </div>
+    <div id="none-subscribers" tal:condition="not:view/subscriptions">
+      No subscribers.
+    </div>
+  </div>
+</div>
diff --git a/lib/lp/snappy/templates/snap-portlet-subscribers.pt b/lib/lp/snappy/templates/snap-portlet-subscribers.pt
new file mode 100644
index 0000000..5f0dd60
--- /dev/null
+++ b/lib/lp/snappy/templates/snap-portlet-subscribers.pt
@@ -0,0 +1,29 @@
+<div
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  class="portlet" id="portlet-subscribers">
+  <div tal:define="context_menu view/context/menu:context">
+    <div>
+      <div class="section">
+        <div
+          tal:define="link context_menu/subscription"
+          tal:condition="link/enabled"
+          id="selfsubscriptioncontainer">
+          <a class="sprite add subscribe-self"
+             tal:attributes="href link/url"
+             tal:content="link/text" />
+        </div>
+        <div
+          tal:define="link context_menu/add_subscriber"
+          tal:condition="link/enabled"
+          tal:content="structure link/render" />
+      </div>
+    </div>
+
+    <h2>Subscribers</h2>
+    <div id="snap-subscribers-outer">
+      <div tal:replace="structure context/@@+snap-portlet-subscriber-content" />
+    </div>
+  </div>
+</div>
diff --git a/lib/lp/snappy/templates/snapsubscription-edit.pt b/lib/lp/snappy/templates/snapsubscription-edit.pt
new file mode 100644
index 0000000..f2d9d8c
--- /dev/null
+++ b/lib/lp/snappy/templates/snapsubscription-edit.pt
@@ -0,0 +1,25 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  metal:use-macro="view/macro:page/main_only"
+  i18n:domain="launchpad"
+>
+  <body>
+
+<div metal:fill-slot="main">
+
+  <div metal:use-macro="context/@@launchpad_form/form" >
+    <metal:extra fill-slot="extra_info">
+      <p class="documentDescription">
+        If you unsubscribe from a snap recipe it will no longer show up on
+        your personal pages.
+      </p>
+    </metal:extra>
+  </div>
+
+</div>
+
+</body>
+</html>
diff --git a/lib/lp/snappy/tests/test_snap.py b/lib/lp/snappy/tests/test_snap.py
index d089a46..052e196 100644
--- a/lib/lp/snappy/tests/test_snap.py
+++ b/lib/lp/snappy/tests/test_snap.py
@@ -138,7 +138,6 @@ from lp.testing import (
     ANONYMOUS,
     api_url,
     login,
-    login_admin,
     logout,
     person_logged_in,
     record_two_runs,
@@ -173,8 +172,7 @@ class TestSnapFeatureFlag(TestCaseWithFactory):
         self.assertRaises(
             SnapPrivateFeatureDisabled, getUtility(ISnapSet).new,
             person, person, None, None,
-            branch=self.factory.makeAnyBranch(),
-            information_type=InformationType.PROPRIETARY)
+            branch=self.factory.makeAnyBranch(), private=True)
 
 
 class TestSnap(TestCaseWithFactory):
@@ -1350,9 +1348,6 @@ class TestSnapVisibility(TestCaseWithFactory):
             AccessArtifactGrant.abstract_artifact_id == AccessArtifact.id,
             *conditions)
 
-    def getSnapSubscription(self, snap, person):
-        return removeSecurityProxy(snap)._getSubscription(person)
-
     def test_only_owner_can_grant_access(self):
         owner = self.factory.makePerson()
         pillar = self.factory.makeProduct(owner=owner)
@@ -1389,20 +1384,18 @@ class TestSnapVisibility(TestCaseWithFactory):
         with person_logged_in(owner):
             self.assertFalse(snap.visibleByUser(person))
             snap.subscribe(person, snap.owner)
-            self.assertThat(
-                self.getSnapSubscription(snap, person),
-                MatchesStructure(
-                    person=Equals(person),
-                    snap=Equals(snap),
-                    subscribed_by=Equals(snap.owner),
-                    date_created=IsInstance(datetime)))
+            self.assertThat(snap.getSubscription(person), MatchesStructure(
+                person=Equals(person),
+                snap=Equals(snap),
+                subscribed_by=Equals(snap.owner),
+                date_created=IsInstance(datetime)))
             # Calling again should be a no-op.
             snap.subscribe(person, snap.owner)
             self.assertTrue(snap.visibleByUser(person))
 
             snap.unsubscribe(person, snap.owner)
             self.assertFalse(snap.visibleByUser(person))
-            self.assertIsNone(self.getSnapSubscription(snap, person))
+            self.assertIsNone(snap.getSubscription(person))
 
     def test_reconcile_set_public(self):
         owner = self.factory.makePerson()
@@ -1413,17 +1406,17 @@ class TestSnapVisibility(TestCaseWithFactory):
             snap.subscribe(another_user, snap.owner)
             self.assertEqual(1, self.getSnapGrants(snap, another_user).count())
             self.assertThat(
-                self.getSnapSubscription(snap, another_user),
+                snap.getSubscription(another_user),
                 MatchesStructure(
                     person=Equals(another_user),
                     snap=Equals(snap),
                     subscribed_by=Equals(snap.owner),
                     date_created=IsInstance(datetime)))
 
-            snap.information_type = InformationType.PUBLIC
+            snap.setPrivate(False)
             self.assertEqual(0, self.getSnapGrants(snap, another_user).count())
             self.assertThat(
-                self.getSnapSubscription(snap, another_user),
+                snap.getSubscription(another_user),
                 MatchesStructure(
                     person=Equals(another_user),
                     snap=Equals(snap),
@@ -1448,7 +1441,7 @@ class TestSnapVisibility(TestCaseWithFactory):
             self.assertTrue(snap.visibleByUser(another_person))
             self.assertEqual(1, self.getSnapGrants(snap).count())
             self.assertThat(
-                self.getSnapSubscription(snap, another_person),
+                snap.getSubscription(another_person),
                 MatchesStructure(
                     person=Equals(another_person),
                     snap=Equals(snap),
@@ -1459,7 +1452,7 @@ class TestSnapVisibility(TestCaseWithFactory):
             self.assertTrue(snap.visibleByUser(another_person))
             self.assertEqual(1, self.getSnapGrants(snap).count())
             self.assertThat(
-                self.getSnapSubscription(snap, another_person),
+                snap.getSubscription(another_person),
                 MatchesStructure(
                     person=Equals(another_person),
                     snap=Equals(snap),
@@ -1559,25 +1552,11 @@ class TestSnapSet(TestCaseWithFactory):
         self.assertEqual(ref.path, snap.git_path)
         self.assertEqual(ref, snap.git_ref)
 
-    def test_private_snap_information_type_compatibility(self):
-        login_admin()
-        private = InformationType.PROPRIETARY
-        public = InformationType.PUBLIC
-        private_snap = getUtility(ISnapSet).new(
-            information_type=private, **self.makeSnapComponents())
-        self.assertEqual(
-            InformationType.PROPRIETARY, private_snap.information_type)
-
-        public_snap = getUtility(ISnapSet).new(
-            information_type=public, **self.makeSnapComponents())
-        self.assertEqual(
-            InformationType.PUBLIC, public_snap.information_type)
-
     def test_private_snap_for_public_sources(self):
         # Creating private snaps for public sources is allowed.
         [ref] = self.factory.makeGitRefs()
         components = self.makeSnapComponents(git_ref=ref)
-        components['information_type'] = InformationType.PROPRIETARY
+        components['private'] = True
         components['project'] = self.factory.makeProduct()
         snap = getUtility(ISnapSet).new(**components)
         with person_logged_in(components['owner']):
@@ -2690,11 +2669,9 @@ class TestSnapWebservice(TestCaseWithFactory):
         if auto_build_pocket is not None:
             kwargs["auto_build_pocket"] = auto_build_pocket.title
         logout()
-        information_type = (InformationType.PROPRIETARY if private else
-                            InformationType.PUBLIC)
         response = webservice.named_post(
             "/+snaps", "new", owner=owner_url, distro_series=distroseries_url,
-            name="mir", information_type=information_type.title, **kwargs)
+            name="mir", private=private, **kwargs)
         self.assertEqual(201, response.status)
         return webservice.get(response.getHeader("Location")).jsonBody()
 
@@ -2839,8 +2816,7 @@ class TestSnapWebservice(TestCaseWithFactory):
             admin, permission=OAuthPermission.WRITE_PRIVATE)
         admin_webservice.default_api_version = "devel"
         response = admin_webservice.patch(
-            snap_url, "application/json",
-            json.dumps({"information_type": 'Public'}))
+            snap_url, "application/json", json.dumps({"private": False}))
         self.assertEqual(400, response.status)
         self.assertEqual(
             b"Snap recipe contains private information and cannot be public.",
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 580a5f3..950323e 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -4748,10 +4748,10 @@ class BareLaunchpadObjectFactory(ObjectFactory):
                  auto_build_archive=None, auto_build_pocket=None,
                  auto_build_channels=None, is_stale=None,
                  require_virtualized=True, processors=None,
-                 date_created=DEFAULT, private=None, information_type=None,
-                 allow_internet=True, build_source_tarball=False,
-                 store_upload=False, store_series=None, store_name=None,
-                 store_secrets=None, store_channels=None, project=_DEFAULT):
+                 date_created=DEFAULT, private=False, allow_internet=True,
+                 build_source_tarball=False, store_upload=False,
+                 store_series=None, store_name=None, store_secrets=None,
+                 store_channels=None, project=_DEFAULT):
         """Make a new Snap."""
         if registrant is None:
             registrant = self.makePerson()
@@ -4775,18 +4775,13 @@ class BareLaunchpadObjectFactory(ObjectFactory):
             project = self.makeProduct()
         if project is _DEFAULT:
             project = None
-        assert information_type is None or private is None
-        if information_type is None:
-            information_type = (InformationType.PUBLIC if not private
-                                else InformationType.PROPRIETARY)
         snap = getUtility(ISnapSet).new(
             registrant, owner, distroseries, name,
             require_virtualized=require_virtualized, processors=processors,
             date_created=date_created, branch=branch, git_ref=git_ref,
             auto_build=auto_build, auto_build_archive=auto_build_archive,
             auto_build_pocket=auto_build_pocket,
-            auto_build_channels=auto_build_channels,
-            information_type=information_type,
+            auto_build_channels=auto_build_channels, private=private,
             allow_internet=allow_internet,
             build_source_tarball=build_source_tarball,
             store_upload=store_upload, store_series=store_series,

References