← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cprov/launchpad/snap-privacy-take1 into lp:launchpad

 

Celso Providelo has proposed merging lp:~cprov/launchpad/snap-privacy-take1 into lp:launchpad.

Commit message:
Initial Snap.private implications.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cprov/launchpad/snap-privacy-take1/+merge/284109

Initial Snap.private implications.

Snap privacy is now completely orthogonal to privacy of their owner, code source (repos) or related archives. If private, their visibility is simply tied to membership.

Obviously this is not sufficient and subsequent subsequent branches will hint/validate 'privacy' according to the snap context.

PS: still not propagating snap privacy to their builds, TBD.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cprov/launchpad/snap-privacy-take1 into lp:launchpad.
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py	2016-01-26 15:47:37 +0000
+++ lib/lp/security.py	2016-02-02 18:49:34 +0000
@@ -3112,12 +3112,20 @@
             obj, obj.webhook, 'launchpad.View')
 
 
-class ViewSnap(DelegatedAuthorization):
+class ViewSnap(AuthorizationBase):
+    """Private snaps are only visible to its owners and admins."""
     permission = 'launchpad.View'
     usedfor = ISnap
 
-    def __init__(self, obj):
-        super(ViewSnap, self).__init__(obj, obj.owner, 'launchpad.View')
+    def checkUnauthenticated(self):
+        return not self.obj.private
+
+    def checkAuthenticated(self, user):
+        if not self.obj.private:
+            return True
+        return (
+            user.isOwner(self.obj) or
+            user.in_commercial_admin or user.in_admin)
 
 
 class EditSnap(AuthorizationBase):

=== modified file 'lib/lp/snappy/browser/configure.zcml'
--- lib/lp/snappy/browser/configure.zcml	2015-09-18 13:32:09 +0000
+++ lib/lp/snappy/browser/configure.zcml	2016-02-02 18:49:34 +0000
@@ -31,6 +31,12 @@
             module="lp.snappy.browser.snap"
             classes="SnapNavigation" />
         <browser:page
+            for="lp.snappy.interfaces.snap.ISnap"
+            class="lp.snappy.browser.snap.SnapView"
+            permission="launchpad.View"
+            name="+portlet-privacy"
+            template="../templates/snap-portlet-privacy.pt"/>
+        <browser:page
             for="lp.code.interfaces.branch.IBranch"
             class="lp.snappy.browser.snap.SnapAddView"
             permission="launchpad.AnyPerson"

=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py	2015-10-08 13:58:57 +0000
+++ lib/lp/snappy/browser/snap.py	2016-02-02 18:49:34 +0000
@@ -146,7 +146,7 @@
     def person_picker(self):
         field = copy_field(
             ISnap['owner'],
-            vocabularyName='UserTeamsParticipationPlusSelfSimpleDisplay')
+            vocabularyName='AllUserTeamsParticipationPlusSelf')
         return InlinePersonEditPickerWidget(
             self.context, field, format_link(self.context.owner),
             header='Change owner', step_title='Select a new owner')
@@ -273,6 +273,7 @@
     use_template(ISnap, include=[
         'owner',
         'name',
+        'private',
         'require_virtualized',
         ])
     distro_series = Choice(
@@ -291,7 +292,7 @@
     page_title = label = 'Create a new snap package'
 
     schema = ISnapEditSchema
-    field_names = ['owner', 'name', 'distro_series']
+    field_names = ['owner', 'name', 'private', 'distro_series']
     custom_widget('distro_series', LaunchpadRadioWidget)
 
     def initialize(self):
@@ -321,6 +322,7 @@
             kwargs = {'git_ref': self.context}
         else:
             kwargs = {'branch': self.context}
+        kwargs['private'] = data['private']
         snap = getUtility(ISnapSet).new(
             self.user, data['owner'], data['distro_series'], data['name'],
             **kwargs)
@@ -336,6 +338,19 @@
                     'name',
                     'There is already a snap package owned by %s with this '
                     'name.' % owner.displayname)
+            private = data.get('private', None)
+            if private is not None:
+                if IGitRef.providedBy(self.context):
+                    kwargs = {'git_ref': self.context}
+                else:
+                    kwargs = {'branch': self.context}
+                if not getUtility(ISnapSet).isValidPrivacy(
+                        private, owner, **kwargs):
+                    self.setFieldError(
+                        'private',
+                        u'This snap contains private information and cannot '
+                        u'be public.'
+                    )
 
 
 class BaseSnapEditView(LaunchpadEditFormView):
@@ -417,7 +432,9 @@
     page_title = 'Edit'
 
     field_names = [
-        'owner', 'name', 'distro_series', 'vcs', 'branch', 'git_ref']
+        'owner', 'name', 'private', 'distro_series', 'vcs', 'branch',
+        'git_ref',
+    ]
     custom_widget('distro_series', LaunchpadRadioWidget)
     custom_widget('vcs', LaunchpadRadioWidget)
     custom_widget('git_ref', GitRefWidget)
@@ -453,6 +470,16 @@
                         'this name.' % owner.displayname)
             except NoSuchSnap:
                 pass
+            private = data.get('private', None)
+            if private is not None:
+                if not getUtility(ISnapSet).isValidPrivacy(
+                        private, owner, self.context.branch,
+                        self.context.git_ref):
+                    self.setFieldError(
+                        'private',
+                        u'This snap contains private information and cannot '
+                        u'be public.'
+                    )
         if 'processors' in data:
             available_processors = set(self.context.available_processors)
             widget = self.widgets['processors']

=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py	2015-11-26 15:46:38 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py	2016-02-02 18:49:34 +0000
@@ -27,6 +27,7 @@
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.buildmaster.enums import BuildStatus
 from lp.buildmaster.interfaces.processor import IProcessorSet
+from lp.registry.enums import PersonVisibility
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.database.constants import UTC_NOW
@@ -63,6 +64,7 @@
     extract_text,
     find_main_content,
     find_tags_by_class,
+    first_tag_by_class,
     )
 from lp.testing.publication import test_traverse
 from lp.testing.views import (
@@ -197,6 +199,47 @@
             ["Test Person (test-person)", "Test Team (test-team)"],
             sorted(str(option) for option in options))
 
+    def test_create_new_private_snap(self):
+        # Anyone can create private snaps.
+        branch = self.factory.makeAnyBranch()
+        owner_name = self.person.name
+
+        browser = self.getViewBrowser(
+            branch, view_name="+new-snap", user=self.person)
+        browser.getControl("Name").value = "brand-new-snap"
+        browser.getControl("Owner").value = [owner_name]
+        browser.getControl("Private").selected = True
+        browser.getControl("Create snap package").click()
+
+        content = find_main_content(browser.contents)
+        self.assertEqual("brand-new-snap", extract_text(content.h1))
+        # XXX cprov 20160202: something more clever ...
+        private_portlet_content = extract_text(
+            first_tag_by_class(browser.contents, "portlet")
+        ).replace('\n', ' ')
+        self.assertEqual(
+            'This snap contains Private information',
+            private_portlet_content)
+
+    def test_create_new_snap_privacy_mismatch(self):
+        # Private teams can only create private snaps.
+        login_person(self.person)
+        team = self.factory.makeTeam(
+            owner=self.person, visibility=PersonVisibility.PRIVATE)
+        branch = self.factory.makeAnyBranch()
+        team_name = team.name
+
+        browser = self.getViewBrowser(
+            branch, view_name="+new-snap", user=self.person)
+        # XXX cprov 20160202: what other controls named 'Name' ?
+        browser.getControl("Name", index=0).value = "snap-name"
+        browser.getControl("Owner").value = [team_name]
+        browser.getControl("Create snap package").click()
+
+        self.assertEqual(
+            'This snap contains private information and cannot be public.',
+            extract_text(find_tags_by_class(browser.contents, "message")[1]))
+
 
 class TestSnapAdminView(BrowserTestCase):
 
@@ -329,6 +372,21 @@
             "name.",
             extract_text(find_tags_by_class(browser.contents, "message")[1]))
 
+    def test_edit_snap_privacy(self):
+        # Cannot make snap public if there still contain private information.
+        login_person(self.person)
+        team = self.factory.makeTeam(
+            owner=self.person, visibility=PersonVisibility.PRIVATE)
+        snap = self.factory.makeSnap(
+            registrant=self.person, owner=team, private=True)
+        browser = self.getViewBrowser(snap, user=self.person)
+        browser.getLink("Edit snap package").click()
+        browser.getControl("Private").selected = False
+        browser.getControl("Update snap package").click()
+        self.assertEqual(
+            'This snap contains private information and cannot be public.',
+            extract_text(find_tags_by_class(browser.contents, "message")[1]))
+
     def setUpDistroSeries(self):
         """Set up a distroseries with some available processors."""
         distroseries = self.factory.makeUbuntuDistroSeries()

=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py	2015-09-25 17:26:03 +0000
+++ lib/lp/snappy/interfaces/snap.py	2016-02-02 18:49:34 +0000
@@ -21,6 +21,7 @@
     'SnapBuildDisallowedArchitecture',
     'SnapFeatureDisabled',
     'SnapNotOwner',
+    'SnapPrivacyMismatch',
     ]
 
 import httplib
@@ -66,6 +67,7 @@
     )
 
 from lp import _
+from lp.app.interfaces.launchpad import IPrivacy
 from lp.app.errors import NameLookupFailed
 from lp.app.validators.name import name_validator
 from lp.buildmaster.interfaces.processor import IProcessor
@@ -164,6 +166,15 @@
 
 
 @error_status(httplib.BAD_REQUEST)
+class SnapPrivacyMismatch(Exception):
+    """Snap package privacy does not match its content."""
+
+    def __init__(self):
+        super(SnapPrivacyMismatch, self).__init__(
+            "Snap contains private information and cannot be public.")
+
+
+@error_status(httplib.BAD_REQUEST)
 class CannotDeleteSnap(Exception):
     """This snap package cannot be deleted."""
 
@@ -326,6 +337,10 @@
             "The Git branch containing a snapcraft.yaml recipe at the top "
             "level.")))
 
+    private = exported(Bool(
+        title=_("Private"), required=False, readonly=False,
+        description=_("Whether or not this snap is private.")))
+
 
 class ISnapAdminAttributes(Interface):
     """`ISnap` attributes that can be edited by admins.
@@ -345,7 +360,8 @@
 
 
 class ISnap(
-    ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes):
+        ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes,
+        IPrivacy):
     """A buildable snap package."""
 
     # XXX cjwatson 2015-07-17 bug=760849: "beta" is a lie to get WADL
@@ -363,16 +379,19 @@
     @export_factory_operation(
         ISnap, [
             "owner", "distro_series", "name", "description", "branch",
-            "git_ref"])
+            "git_ref", "private"])
     @operation_for_version("devel")
     def new(registrant, owner, distro_series, name, description=None,
             branch=None, git_ref=None, require_virtualized=True,
-            processors=None, date_created=None):
+            processors=None, date_created=None, private=False):
         """Create an `ISnap`."""
 
     def exists(owner, name):
         """Check to see if a matching snap exists."""
 
+    def isValidPrivacy(private, owner, branch=None, git_ref=None):
+        """Whether or not the privacy context is valid."""
+
     @operation_parameters(
         owner=Reference(IPerson, title=_("Owner"), required=True),
         name=TextLine(title=_("Snap name"), required=True))

=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py	2015-10-08 14:18:29 +0000
+++ lib/lp/snappy/model/snap.py	2016-02-02 18:49:34 +0000
@@ -26,6 +26,7 @@
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
+from lp.app.enums import PRIVATE_INFORMATION_TYPES
 from lp.app.interfaces.security import IAuthorization
 from lp.buildmaster.enums import BuildStatus
 from lp.buildmaster.interfaces.processor import IProcessorSet
@@ -45,6 +46,7 @@
 from lp.code.model.branchcollection import GenericBranchCollection
 from lp.code.model.gitcollection import GenericGitCollection
 from lp.code.model.gitrepository import GitRepository
+from lp.registry.enums import PersonVisibility
 from lp.registry.interfaces.person import (
     IPerson,
     IPersonSet,
@@ -84,6 +86,7 @@
     SnapBuildDisallowedArchitecture,
     SnapFeatureDisabled,
     SnapNotOwner,
+    SnapPrivacyMismatch,
     )
 from lp.snappy.interfaces.snapbuild import ISnapBuildSet
 from lp.snappy.model.snapbuild import SnapBuild
@@ -140,9 +143,12 @@
 
     require_virtualized = Bool(name='require_virtualized')
 
+    private = Bool(name='private')
+
     def __init__(self, registrant, owner, distro_series, name,
                  description=None, branch=None, git_ref=None,
-                 require_virtualized=True, date_created=DEFAULT):
+                 require_virtualized=True, date_created=DEFAULT,
+                 private=False):
         """Construct a `Snap`."""
         if not getFeatureFlag(SNAP_FEATURE_FLAG):
             raise SnapFeatureDisabled
@@ -158,6 +164,7 @@
         self.require_virtualized = require_virtualized
         self.date_created = date_created
         self.date_last_modified = date_created
+        self.private = private
 
     @property
     def git_ref(self):
@@ -283,6 +290,9 @@
 
     def _getBuilds(self, filter_term, order_by):
         """The actual query to get the builds."""
+
+        # XXX cprov 20160114: missing privacy checks.
+
         query_args = [
             SnapBuild.snap == self,
             SnapBuild.archive_id == Archive.id,
@@ -366,7 +376,7 @@
 
     def new(self, registrant, owner, distro_series, name, description=None,
             branch=None, git_ref=None, require_virtualized=True,
-            processors=None, date_created=DEFAULT):
+            processors=None, date_created=DEFAULT, private=False):
         """See `ISnapSet`."""
         if not registrant.inTeam(owner):
             if owner.is_team:
@@ -383,11 +393,15 @@
         if self.exists(owner, name):
             raise DuplicateSnapName
 
+        if not self.isValidPrivacy(private, owner, branch, git_ref):
+            raise SnapPrivacyMismatch
+
         store = IMasterStore(Snap)
         snap = Snap(
             registrant, owner, distro_series, name, description=description,
             branch=branch, git_ref=git_ref,
-            require_virtualized=require_virtualized, date_created=date_created)
+            require_virtualized=require_virtualized, date_created=date_created,
+            private=private)
         store.add(snap)
 
         if processors is None:
@@ -398,6 +412,23 @@
 
         return snap
 
+    def isValidPrivacy(self, private, owner, branch=None, git_ref=None):
+        """See `ISnapSet`."""
+        # Private snaps may contain anything.
+        if private:
+            return True
+
+        # Public snaps with private sources are not allowed.
+        source_ref = branch or git_ref
+        if source_ref.information_type in PRIVATE_INFORMATION_TYPES:
+            return False
+
+        # Public snaps owned by private teams are not allowed.
+        if owner.is_team and owner.visibility == PersonVisibility.PRIVATE:
+            return False
+
+        return True
+
     def _getByName(self, owner, name):
         return IStore(Snap).find(
             Snap, Snap.owner == owner, Snap.name == name).one()

=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py	2015-09-16 13:30:33 +0000
+++ lib/lp/snappy/model/snapbuild.py	2016-02-02 18:49:34 +0000
@@ -163,6 +163,9 @@
     @property
     def is_private(self):
         """See `IBuildFarmJob`."""
+
+        # XXX cprov 20160114: consider snap privacy.
+
         return self.snap.owner.private or self.archive.private
 
     @property

=== modified file 'lib/lp/snappy/templates/snap-edit.pt'
--- lib/lp/snappy/templates/snap-edit.pt	2015-09-25 17:26:03 +0000
+++ lib/lp/snappy/templates/snap-edit.pt	2016-02-02 18:49:34 +0000
@@ -25,6 +25,9 @@
         <tal:widget define="widget nocall:view/widgets/name">
           <metal:block use-macro="context/@@launchpad_form/widget_row" />
         </tal:widget>
+        <tal:widget define="widget nocall:view/widgets/private">
+          <metal:block use-macro="context/@@launchpad_form/widget_row" />
+        </tal:widget>
         <tal:widget define="widget nocall:view/widgets/distro_series">
           <metal:block use-macro="context/@@launchpad_form/widget_row" />
         </tal:widget>

=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt	2015-09-18 13:32:09 +0000
+++ lib/lp/snappy/templates/snap-index.pt	2016-02-02 18:49:34 +0000
@@ -18,6 +18,7 @@
   </metal:registering>
 
   <metal:side fill-slot="side">
+    <div tal:replace="structure context/@@+portlet-privacy" />
     <div tal:replace="structure context/@@+global-actions"/>
   </metal:side>
 

=== modified file 'lib/lp/snappy/templates/snap-new.pt'
--- lib/lp/snappy/templates/snap-new.pt	2015-09-18 10:43:49 +0000
+++ lib/lp/snappy/templates/snap-new.pt	2016-02-02 18:49:34 +0000
@@ -28,6 +28,9 @@
         <tal:widget define="widget nocall:view/widgets/owner">
           <metal:block use-macro="context/@@launchpad_form/widget_row" />
         </tal:widget>
+        <tal:widget define="widget nocall:view/widgets/private">
+          <metal:block use-macro="context/@@launchpad_form/widget_row" />
+        </tal:widget>
         <tal:widget define="widget nocall:view/widgets/distro_series">
           <metal:block use-macro="context/@@launchpad_form/widget_row" />
         </tal:widget>

=== added file 'lib/lp/snappy/templates/snap-portlet-privacy.pt'
--- lib/lp/snappy/templates/snap-portlet-privacy.pt	1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/templates/snap-portlet-privacy.pt	2016-02-02 18:49:34 +0000
@@ -0,0 +1,16 @@
+<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";
+  id="privacy"
+  tal:attributes="
+    class python: path('context/private') and 'portlet private' or 'portlet public'
+  "
+>
+    <span tal:attributes="class python: path('context/private') and 'sprite private' or 'sprite public'"
+      >This snap contains
+      <strong tal:content="python: path('context/private') and 'Private' or 'Public'"></strong>
+      information</span>
+</div>
+
+

=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py	2015-11-26 15:46:38 +0000
+++ lib/lp/snappy/tests/test_snap.py	2016-02-02 18:49:34 +0000
@@ -15,6 +15,7 @@
 from zope.event import notify
 from zope.security.proxy import removeSecurityProxy
 
+from lp.app.enums import InformationType
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.buildmaster.enums import (
     BuildQueueStatus,
@@ -23,6 +24,7 @@
 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
 from lp.buildmaster.interfaces.processor import IProcessorSet
 from lp.buildmaster.model.buildqueue import BuildQueue
+from lp.registry.enums import PersonVisibility
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.services.database.constants import (
@@ -43,6 +45,7 @@
     SnapBuildAlreadyPending,
     SnapBuildDisallowedArchitecture,
     SnapFeatureDisabled,
+    SnapPrivacyMismatch,
     )
 from lp.snappy.interfaces.snapbuild import ISnapBuild
 from lp.testing import (
@@ -404,6 +407,7 @@
         self.assertIsNone(snap.git_path)
         self.assertIsNone(snap.git_ref)
         self.assertTrue(snap.require_virtualized)
+        self.assertFalse(snap.private)
 
     def test_creation_git(self):
         # The metadata entries supplied when a Snap is created for a Git
@@ -420,6 +424,65 @@
         self.assertEqual(ref.path, snap.git_path)
         self.assertEqual(ref, snap.git_ref)
         self.assertTrue(snap.require_virtualized)
+        self.assertFalse(snap.private)
+
+    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['private'] = True
+        snap = getUtility(ISnapSet).new(**components)
+        with person_logged_in(components['owner']):
+            self.assertTrue(snap.private)
+
+    def test_private_git_requires_private_snap(self):
+        # Snaps for a private Git branch cannot be public.
+        owner = self.factory.makePerson()
+        with person_logged_in(owner):
+            [git_ref] = self.factory.makeGitRefs(
+                owner=owner, information_type=InformationType.PRIVATESECURITY)
+            components = dict(
+                registrant=owner,
+                owner=owner,
+                git_ref=git_ref,
+                distro_series=self.factory.makeDistroSeries(),
+                name=self.factory.getUniqueString(u"snap-name"),
+            )
+            self.assertRaises(
+                SnapPrivacyMismatch, getUtility(ISnapSet).new, **components)
+
+    def test_private_bzr_requires_private_snap(self):
+        # Snaps for a private Bzr branch cannot be public.
+        owner = self.factory.makePerson()
+        with person_logged_in(owner):
+            branch = self.factory.makeAnyBranch(
+                owner=owner, information_type=InformationType.PRIVATESECURITY)
+            components = dict(
+                registrant=owner,
+                owner=owner,
+                branch=branch,
+                distro_series=self.factory.makeDistroSeries(),
+                name=self.factory.getUniqueString(u"snap-name"),
+            )
+            self.assertRaises(
+                SnapPrivacyMismatch, getUtility(ISnapSet).new, **components)
+
+    def test_private_team_requires_private_snap(self):
+        # Snaps owned by private teams cannot be public.
+        registrant = self.factory.makePerson()
+        with person_logged_in(registrant):
+            private_team = self.factory.makeTeam(
+                owner=registrant, visibility=PersonVisibility.PRIVATE)
+            [git_ref] = self.factory.makeGitRefs()
+            components = dict(
+                registrant=registrant,
+                owner=private_team,
+                git_ref=git_ref,
+                distro_series=self.factory.makeDistroSeries(),
+                name=self.factory.getUniqueString(u"snap-name"),
+            )
+            self.assertRaises(
+                SnapPrivacyMismatch, getUtility(ISnapSet).new, **components)
 
     def test_creation_no_source(self):
         # Attempting to create a Snap with neither a Bazaar branch nor a Git
@@ -703,14 +766,17 @@
         return self.webservice.getAbsoluteUrl(api_url(obj))
 
     def makeSnap(self, owner=None, distroseries=None, branch=None,
-                 git_ref=None, processors=None, webservice=None):
+                 git_ref=None, processors=None, webservice=None,
+                 private=False):
         if owner is None:
             owner = self.person
         if distroseries is None:
             distroseries = self.factory.makeDistroSeries(registrant=owner)
         if branch is None and git_ref is None:
             branch = self.factory.makeAnyBranch()
-        kwargs = {}
+        kwargs = {
+            'private': private,
+        }
         if webservice is None:
             webservice = self.webservice
         transaction.commit()
@@ -775,6 +841,21 @@
             self.assertEqual(self.getURL(ref), snap["git_ref_link"])
             self.assertTrue(snap["require_virtualized"])
 
+    def test_new_private(self):
+        # Ensure private Snap creation works.
+        team = self.factory.makeTeam(owner=self.person)
+        distroseries = self.factory.makeDistroSeries(registrant=team)
+        [ref] = self.factory.makeGitRefs()
+        private_webservice = webservice_for_person(
+            self.person, permission=OAuthPermission.WRITE_PRIVATE)
+        private_webservice.default_api_version = "devel"
+        login(ANONYMOUS)
+        snap = self.makeSnap(
+            owner=team, distroseries=distroseries, git_ref=ref,
+            webservice=private_webservice, private=True)
+        with person_logged_in(self.person):
+            self.assertTrue(snap["private"])
+
     def test_duplicate(self):
         # An attempt to create a duplicate Snap fails.
         team = self.factory.makeTeam(owner=self.person)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2016-01-26 15:14:01 +0000
+++ lib/lp/testing/factory.py	2016-02-02 18:49:34 +0000
@@ -4554,7 +4554,7 @@
     def makeSnap(self, registrant=None, owner=None, distroseries=None,
                  name=None, branch=None, git_ref=None,
                  require_virtualized=True, processors=None,
-                 date_created=DEFAULT):
+                 date_created=DEFAULT, private=False):
         """Make a new Snap."""
         if registrant is None:
             registrant = self.makePerson()
@@ -4569,7 +4569,8 @@
         snap = getUtility(ISnapSet).new(
             registrant, owner, distroseries, name,
             require_virtualized=require_virtualized, processors=processors,
-            date_created=date_created, branch=branch, git_ref=git_ref)
+            date_created=date_created, branch=branch, git_ref=git_ref,
+            private=private)
         IStore(snap).flush()
         return snap
 


Follow ups