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

Proposed by Celso Providelo
Status: Merged
Merged at revision: 17911
Proposed branch: lp:~cprov/launchpad/snap-privacy-take1
Merge into: lp:launchpad
Diff against target: 674 lines (+288/-20)
14 files modified
lib/lp/registry/vocabularies.py (+14/-0)
lib/lp/registry/vocabularies.zcml (+14/-0)
lib/lp/security.py (+9/-3)
lib/lp/snappy/browser/configure.zcml (+6/-0)
lib/lp/snappy/browser/snap.py (+19/-3)
lib/lp/snappy/browser/tests/test_snap.py (+64/-1)
lib/lp/snappy/interfaces/snap.py (+23/-3)
lib/lp/snappy/model/snap.py (+31/-3)
lib/lp/snappy/model/snapbuild.py (+5/-1)
lib/lp/snappy/templates/snap-index.pt (+1/-0)
lib/lp/snappy/templates/snap-portlet-privacy.pt (+16/-0)
lib/lp/snappy/tests/test_snap.py (+81/-2)
lib/lp/snappy/tests/test_snapbuild.py (+2/-2)
lib/lp/testing/factory.py (+3/-2)
To merge this branch: bzr merge lp:~cprov/launchpad/snap-privacy-take1
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+284109@code.launchpad.net

Commit message

Initial Snap.private implications.

Description of the change

Initial Snap.private implications.

Snaps implement privacy orthogonally to their contents or ownership, although snaps for private branches or owned by private teams must remain private.

Members of private teams or owners of private branches already create private snaps, however privatising existing snaps for public code requires Commercial/Admin permission.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

Looks like a pretty good start, with a few bits missing. You need to make Snap.requestBuild only permit private archives if the snap itself is private, although I think we ought to keep the snap/archive owner mismatch check as well, at least for now. I don't think SnapBuild privacy needs any other work beyond that, as it already correctly delegates to the snap and the archive.

Last I checked, there were no snaps on production with private owners or snap builds with private archives, but we should double-check that after the upgrade and ensure that they're all explicitly marked private if need be. If you follow my suggestion for SnapBuild.is_private then I think this ordering is safe.

review: Needs Fixing
Revision history for this message
Celso Providelo (cprov) wrote :

Thanks Colin,

I understand and agree will all your comments, they will be addressed shortly.

Revision history for this message
Celso Providelo (cprov) wrote :

Colin,

There are still few issues that requires your help:

1) After checking the code behaviour, it seems to me that SnapBuilds for private archives will remain protected even if their parent Snap become public. So I don't see why ISnapSet.isValidPrivacy() would change. I am probably missing something.

2) Requiring lp.Admin for Snap.private uncovered an issue that I am not sure how to solve. Commercial_admins and ppa_admins cannot admin snaps owned by private teams, it endup allowing only LP admins to administrate private snaps. See `test_admin_snap_privacy_mismatch`.

3) Lastly, I am not happy with the code that inspect the privacy portlet (first_tag_by_class()). Is there anything better ?

Revision history for this message
Colin Watson (cjwatson) wrote :

On Thu, Feb 04, 2016 at 11:58:36AM -0000, Celso Providelo wrote:
> 1) After checking the code behaviour, it seems to me that SnapBuilds for private archives will remain protected even if their parent Snap become public. So I don't see why ISnapSet.isValidPrivacy() would change. I am probably missing something.

You're right; please disregard that comment on your isValidPrivacy
implementation. I was forgetting that the archive was an attribute of
the build, not the snap, so it isn't correct for us to constrain snap
privacy by archive privacy.

> 2) Requiring lp.Admin for Snap.private uncovered an issue that I am not sure how to solve. Commercial_admins and ppa_admins cannot admin snaps owned by private teams, it endup allowing only LP admins to administrate private snaps. See `test_admin_snap_privacy_mismatch`.

Commercial admins should be able to, because they can view any private
team (see ViewPublicOrPrivateTeamMembers.checkAuthenticated); but PPA
admins indeed won't be able to. I think that's fine and correct.

Are you sure you tried using commercial_admin in that test, not just
ppa_admin?

> 3) Lastly, I am not happy with the code that inspect the privacy portlet (first_tag_by_class()). Is there anything better ?

You have id="privacy" set on the div element at the top level of the
portlet, so I'd use find_tag_by_id(content, "privacy"). There are some
other tests that do this, albeit doctests: see for example
lib/lp/bugs/stories/bug-privacy/xx-presenting-private-bugs.txt.

Revision history for this message
Celso Providelo (cprov) wrote :

Colin,

Thanks for the suggestions, highly appreciated. Finally, this seems to have reached a good level of maturity.

Security adapters and tests make sense now, if you could take another look, perhaps playing with it locally, to make sure it matches our plans.

Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/vocabularies.py'
2--- lib/lp/registry/vocabularies.py 2016-01-26 15:47:37 +0000
3+++ lib/lp/registry/vocabularies.py 2016-02-04 13:01:27 +0000
4@@ -1105,6 +1105,20 @@
5 return SimpleTerm(obj, obj.name, obj.displayname)
6
7
8+class AllUserTeamsParticipationPlusSelfSimpleDisplayVocabulary(
9+ AllUserTeamsParticipationPlusSelfVocabulary):
10+ """Like `AllUserTeamsParticipationPlusSelfVocabulary` but the term title is
11+ the person.displayname rather than unique_displayname.
12+
13+ See `UserTeamsParticipationPlusSelfSimpleDisplayVocabulary` for more
14+ information on usage.
15+ """
16+
17+ def toTerm(self, obj):
18+ """See `IVocabulary`."""
19+ return SimpleTerm(obj, obj.name, obj.displayname)
20+
21+
22 @implementer(IHugeVocabulary)
23 class ProductReleaseVocabulary(SQLObjectVocabularyBase):
24 """All `IProductRelease` objects vocabulary."""
25
26=== modified file 'lib/lp/registry/vocabularies.zcml'
27--- lib/lp/registry/vocabularies.zcml 2014-01-03 07:39:34 +0000
28+++ lib/lp/registry/vocabularies.zcml 2016-02-04 13:01:27 +0000
29@@ -328,6 +328,20 @@
30 <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/>
31 </class>
32
33+
34+ <securedutility
35+ name="AllUserTeamsParticipationPlusSelfSimpleDisplay"
36+ component="lp.registry.vocabularies.AllUserTeamsParticipationPlusSelfSimpleDisplayVocabulary"
37+ provides="zope.schema.interfaces.IVocabularyFactory"
38+ >
39+ <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
40+ </securedutility>
41+
42+ <class class="lp.registry.vocabularies.AllUserTeamsParticipationPlusSelfSimpleDisplayVocabulary">
43+ <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary"/>
44+ </class>
45+
46+
47 <securedutility
48 name="ActiveMailingList"
49 component="lp.registry.vocabularies.ActiveMailingListVocabulary"
50
51=== modified file 'lib/lp/security.py'
52--- lib/lp/security.py 2016-01-26 15:47:37 +0000
53+++ lib/lp/security.py 2016-02-04 13:01:27 +0000
54@@ -3112,12 +3112,18 @@
55 obj, obj.webhook, 'launchpad.View')
56
57
58-class ViewSnap(DelegatedAuthorization):
59+class ViewSnap(AuthorizationBase):
60+ """Private snaps are only visible to their owners and admins."""
61 permission = 'launchpad.View'
62 usedfor = ISnap
63
64- def __init__(self, obj):
65- super(ViewSnap, self).__init__(obj, obj.owner, 'launchpad.View')
66+ def checkUnauthenticated(self):
67+ return not self.obj.private
68+
69+ def checkAuthenticated(self, user):
70+ return (
71+ not self.obj.private or
72+ self.forwardCheckAuthenticated(user, self.obj.owner))
73
74
75 class EditSnap(AuthorizationBase):
76
77=== modified file 'lib/lp/snappy/browser/configure.zcml'
78--- lib/lp/snappy/browser/configure.zcml 2015-09-18 13:32:09 +0000
79+++ lib/lp/snappy/browser/configure.zcml 2016-02-04 13:01:27 +0000
80@@ -31,6 +31,12 @@
81 module="lp.snappy.browser.snap"
82 classes="SnapNavigation" />
83 <browser:page
84+ for="lp.snappy.interfaces.snap.ISnap"
85+ class="lp.snappy.browser.snap.SnapView"
86+ permission="launchpad.View"
87+ name="+portlet-privacy"
88+ template="../templates/snap-portlet-privacy.pt"/>
89+ <browser:page
90 for="lp.code.interfaces.branch.IBranch"
91 class="lp.snappy.browser.snap.SnapAddView"
92 permission="launchpad.AnyPerson"
93
94=== modified file 'lib/lp/snappy/browser/snap.py'
95--- lib/lp/snappy/browser/snap.py 2015-10-08 13:58:57 +0000
96+++ lib/lp/snappy/browser/snap.py 2016-02-04 13:01:27 +0000
97@@ -146,7 +146,7 @@
98 def person_picker(self):
99 field = copy_field(
100 ISnap['owner'],
101- vocabularyName='UserTeamsParticipationPlusSelfSimpleDisplay')
102+ vocabularyName='AllUserTeamsParticipationPlusSelfSimpleDisplay')
103 return InlinePersonEditPickerWidget(
104 self.context, field, format_link(self.context.owner),
105 header='Change owner', step_title='Select a new owner')
106@@ -273,6 +273,7 @@
107 use_template(ISnap, include=[
108 'owner',
109 'name',
110+ 'private',
111 'require_virtualized',
112 ])
113 distro_series = Choice(
114@@ -321,9 +322,11 @@
115 kwargs = {'git_ref': self.context}
116 else:
117 kwargs = {'branch': self.context}
118+ private = not getUtility(
119+ ISnapSet).isValidPrivacy(False, data['owner'], **kwargs)
120 snap = getUtility(ISnapSet).new(
121 self.user, data['owner'], data['distro_series'], data['name'],
122- **kwargs)
123+ private=private, **kwargs)
124 self.next_url = canonical_url(snap)
125
126 def validate(self, data):
127@@ -404,7 +407,20 @@
128
129 page_title = 'Administer'
130
131- field_names = ['require_virtualized']
132+ field_names = ['private', 'require_virtualized']
133+
134+ def validate(self, data):
135+ super(SnapAdminView, self).validate(data)
136+ private = data.get('private', None)
137+ if private is not None:
138+ if not getUtility(ISnapSet).isValidPrivacy(
139+ private, self.context.owner, self.context.branch,
140+ self.context.git_ref):
141+ self.setFieldError(
142+ 'private',
143+ u'This snap contains private information and cannot '
144+ u'be public.'
145+ )
146
147
148 class SnapEditView(BaseSnapEditView, EnableProcessorsMixin):
149
150=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
151--- lib/lp/snappy/browser/tests/test_snap.py 2015-11-26 15:46:38 +0000
152+++ lib/lp/snappy/browser/tests/test_snap.py 2016-02-04 13:01:27 +0000
153@@ -27,6 +27,7 @@
154 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
155 from lp.buildmaster.enums import BuildStatus
156 from lp.buildmaster.interfaces.processor import IProcessorSet
157+from lp.registry.enums import PersonVisibility
158 from lp.registry.interfaces.pocket import PackagePublishingPocket
159 from lp.registry.interfaces.series import SeriesStatus
160 from lp.services.database.constants import UTC_NOW
161@@ -63,6 +64,7 @@
162 extract_text,
163 find_main_content,
164 find_tags_by_class,
165+ find_tag_by_id,
166 )
167 from lp.testing.publication import test_traverse
168 from lp.testing.views import (
169@@ -197,6 +199,43 @@
170 ["Test Person (test-person)", "Test Team (test-team)"],
171 sorted(str(option) for option in options))
172
173+ def test_create_new_snap_public(self):
174+ # Public owner implies in public snap.
175+ branch = self.factory.makeAnyBranch()
176+
177+ browser = self.getViewBrowser(
178+ branch, view_name="+new-snap", user=self.person)
179+ browser.getControl("Name").value = "public-snap"
180+ browser.getControl("Create snap package").click()
181+
182+ content = find_main_content(browser.contents)
183+ self.assertEqual("public-snap", extract_text(content.h1))
184+ self.assertEqual(
185+ 'This snap contains Public information',
186+ extract_text(find_tag_by_id(browser.contents, "privacy"))
187+ )
188+
189+ def test_create_new_snap_private(self):
190+ # Private teams will automatically create private snaps.
191+ login_person(self.person)
192+ self.factory.makeTeam(
193+ name='super-private', owner=self.person,
194+ visibility=PersonVisibility.PRIVATE)
195+ branch = self.factory.makeAnyBranch()
196+
197+ browser = self.getViewBrowser(
198+ branch, view_name="+new-snap", user=self.person)
199+ browser.getControl("Name").value = "private-snap"
200+ browser.getControl("Owner").value = ['super-private']
201+ browser.getControl("Create snap package").click()
202+
203+ content = find_main_content(browser.contents)
204+ self.assertEqual("private-snap", extract_text(content.h1))
205+ self.assertEqual(
206+ 'This snap contains Private information',
207+ extract_text(find_tag_by_id(browser.contents, "privacy"))
208+ )
209+
210
211 class TestSnapAdminView(BrowserTestCase):
212
213@@ -222,19 +261,43 @@
214 user=self.person)
215
216 def test_admin_snap(self):
217- # Admins can change require_virtualized.
218+ # Admins can change require_virtualized and privacy.
219 login("admin@canonical.com")
220 ppa_admin = self.factory.makePerson(
221 member_of=[getUtility(ILaunchpadCelebrities).ppa_admin])
222 login_person(self.person)
223 snap = self.factory.makeSnap(registrant=self.person)
224 self.assertTrue(snap.require_virtualized)
225+ self.assertFalse(snap.private)
226+
227 browser = self.getViewBrowser(snap, user=ppa_admin)
228 browser.getLink("Administer snap package").click()
229 browser.getControl("Require virtualized builders").selected = False
230+ browser.getControl("Private").selected = True
231 browser.getControl("Update snap package").click()
232+
233 login_person(self.person)
234 self.assertFalse(snap.require_virtualized)
235+ self.assertTrue(snap.private)
236+
237+ def test_admin_snap_privacy_mismatch(self):
238+ # Cannot make snap public if it still contains private information.
239+ login_person(self.person)
240+ team = self.factory.makeTeam(
241+ owner=self.person, visibility=PersonVisibility.PRIVATE)
242+ snap = self.factory.makeSnap(
243+ registrant=self.person, owner=team, private=True)
244+ # Note that only LP admins or, in this case, commercial_admins
245+ # can reach this snap because it's owned by a private team.
246+ commercial_admin = self.factory.makePerson(
247+ member_of=[getUtility(ILaunchpadCelebrities).commercial_admin])
248+ browser = self.getViewBrowser(snap, user=commercial_admin)
249+ browser.getLink("Administer snap package").click()
250+ browser.getControl("Private").selected = False
251+ browser.getControl("Update snap package").click()
252+ self.assertEqual(
253+ 'This snap contains private information and cannot be public.',
254+ extract_text(find_tags_by_class(browser.contents, "message")[1]))
255
256 def test_admin_snap_sets_date_last_modified(self):
257 # Administering a snap package sets the date_last_modified property.
258
259=== modified file 'lib/lp/snappy/interfaces/snap.py'
260--- lib/lp/snappy/interfaces/snap.py 2015-09-25 17:26:03 +0000
261+++ lib/lp/snappy/interfaces/snap.py 2016-02-04 13:01:27 +0000
262@@ -21,6 +21,7 @@
263 'SnapBuildDisallowedArchitecture',
264 'SnapFeatureDisabled',
265 'SnapNotOwner',
266+ 'SnapPrivacyMismatch',
267 ]
268
269 import httplib
270@@ -66,6 +67,7 @@
271 )
272
273 from lp import _
274+from lp.app.interfaces.launchpad import IPrivacy
275 from lp.app.errors import NameLookupFailed
276 from lp.app.validators.name import name_validator
277 from lp.buildmaster.interfaces.processor import IProcessor
278@@ -164,6 +166,15 @@
279
280
281 @error_status(httplib.BAD_REQUEST)
282+class SnapPrivacyMismatch(Exception):
283+ """Snap package privacy does not match its content."""
284+
285+ def __init__(self):
286+ super(SnapPrivacyMismatch, self).__init__(
287+ "Snap contains private information and cannot be public.")
288+
289+
290+@error_status(httplib.BAD_REQUEST)
291 class CannotDeleteSnap(Exception):
292 """This snap package cannot be deleted."""
293
294@@ -332,6 +343,11 @@
295
296 These attributes need launchpad.View to see, and launchpad.Admin to change.
297 """
298+
299+ private = exported(Bool(
300+ title=_("Private"), required=False, readonly=False,
301+ description=_("Whether or not this snap is private.")))
302+
303 require_virtualized = exported(Bool(
304 title=_("Require virtualized builders"), required=True, readonly=False,
305 description=_("Only build this snap package on virtual builders.")))
306@@ -345,7 +361,8 @@
307
308
309 class ISnap(
310- ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes):
311+ ISnapView, ISnapEdit, ISnapEditableAttributes, ISnapAdminAttributes,
312+ IPrivacy):
313 """A buildable snap package."""
314
315 # XXX cjwatson 2015-07-17 bug=760849: "beta" is a lie to get WADL
316@@ -363,16 +380,19 @@
317 @export_factory_operation(
318 ISnap, [
319 "owner", "distro_series", "name", "description", "branch",
320- "git_ref"])
321+ "git_ref", "private"])
322 @operation_for_version("devel")
323 def new(registrant, owner, distro_series, name, description=None,
324 branch=None, git_ref=None, require_virtualized=True,
325- processors=None, date_created=None):
326+ processors=None, date_created=None, private=False):
327 """Create an `ISnap`."""
328
329 def exists(owner, name):
330 """Check to see if a matching snap exists."""
331
332+ def isValidPrivacy(private, owner, branch=None, git_ref=None):
333+ """Whether or not the privacy context is valid."""
334+
335 @operation_parameters(
336 owner=Reference(IPerson, title=_("Owner"), required=True),
337 name=TextLine(title=_("Snap name"), required=True))
338
339=== modified file 'lib/lp/snappy/model/snap.py'
340--- lib/lp/snappy/model/snap.py 2015-10-08 14:18:29 +0000
341+++ lib/lp/snappy/model/snap.py 2016-02-04 13:01:27 +0000
342@@ -26,6 +26,7 @@
343 from zope.security.interfaces import Unauthorized
344 from zope.security.proxy import removeSecurityProxy
345
346+from lp.app.enums import PRIVATE_INFORMATION_TYPES
347 from lp.app.interfaces.security import IAuthorization
348 from lp.buildmaster.enums import BuildStatus
349 from lp.buildmaster.interfaces.processor import IProcessorSet
350@@ -45,6 +46,7 @@
351 from lp.code.model.branchcollection import GenericBranchCollection
352 from lp.code.model.gitcollection import GenericGitCollection
353 from lp.code.model.gitrepository import GitRepository
354+from lp.registry.enums import PersonVisibility
355 from lp.registry.interfaces.person import (
356 IPerson,
357 IPersonSet,
358@@ -84,6 +86,7 @@
359 SnapBuildDisallowedArchitecture,
360 SnapFeatureDisabled,
361 SnapNotOwner,
362+ SnapPrivacyMismatch,
363 )
364 from lp.snappy.interfaces.snapbuild import ISnapBuildSet
365 from lp.snappy.model.snapbuild import SnapBuild
366@@ -140,9 +143,12 @@
367
368 require_virtualized = Bool(name='require_virtualized')
369
370+ private = Bool(name='private')
371+
372 def __init__(self, registrant, owner, distro_series, name,
373 description=None, branch=None, git_ref=None,
374- require_virtualized=True, date_created=DEFAULT):
375+ require_virtualized=True, date_created=DEFAULT,
376+ private=False):
377 """Construct a `Snap`."""
378 if not getFeatureFlag(SNAP_FEATURE_FLAG):
379 raise SnapFeatureDisabled
380@@ -158,6 +164,7 @@
381 self.require_virtualized = require_virtualized
382 self.date_created = date_created
383 self.date_last_modified = date_created
384+ self.private = private
385
386 @property
387 def git_ref(self):
388@@ -366,7 +373,7 @@
389
390 def new(self, registrant, owner, distro_series, name, description=None,
391 branch=None, git_ref=None, require_virtualized=True,
392- processors=None, date_created=DEFAULT):
393+ processors=None, date_created=DEFAULT, private=False):
394 """See `ISnapSet`."""
395 if not registrant.inTeam(owner):
396 if owner.is_team:
397@@ -383,11 +390,15 @@
398 if self.exists(owner, name):
399 raise DuplicateSnapName
400
401+ if not self.isValidPrivacy(private, owner, branch, git_ref):
402+ raise SnapPrivacyMismatch
403+
404 store = IMasterStore(Snap)
405 snap = Snap(
406 registrant, owner, distro_series, name, description=description,
407 branch=branch, git_ref=git_ref,
408- require_virtualized=require_virtualized, date_created=date_created)
409+ require_virtualized=require_virtualized, date_created=date_created,
410+ private=private)
411 store.add(snap)
412
413 if processors is None:
414@@ -398,6 +409,23 @@
415
416 return snap
417
418+ def isValidPrivacy(self, private, owner, branch=None, git_ref=None):
419+ """See `ISnapSet`."""
420+ # Private snaps may contain anything.
421+ if private:
422+ return True
423+
424+ # Public snaps with private sources are not allowed.
425+ source_ref = branch or git_ref
426+ if source_ref.information_type in PRIVATE_INFORMATION_TYPES:
427+ return False
428+
429+ # Public snaps owned by private teams are not allowed.
430+ if owner.is_team and owner.visibility == PersonVisibility.PRIVATE:
431+ return False
432+
433+ return True
434+
435 def _getByName(self, owner, name):
436 return IStore(Snap).find(
437 Snap, Snap.owner == owner, Snap.name == name).one()
438
439=== modified file 'lib/lp/snappy/model/snapbuild.py'
440--- lib/lp/snappy/model/snapbuild.py 2015-09-16 13:30:33 +0000
441+++ lib/lp/snappy/model/snapbuild.py 2016-02-04 13:01:27 +0000
442@@ -163,7 +163,11 @@
443 @property
444 def is_private(self):
445 """See `IBuildFarmJob`."""
446- return self.snap.owner.private or self.archive.private
447+ return (
448+ self.snap.private or
449+ self.snap.owner.private or
450+ self.archive.private
451+ )
452
453 @property
454 def title(self):
455
456=== modified file 'lib/lp/snappy/templates/snap-index.pt'
457--- lib/lp/snappy/templates/snap-index.pt 2015-09-18 13:32:09 +0000
458+++ lib/lp/snappy/templates/snap-index.pt 2016-02-04 13:01:27 +0000
459@@ -18,6 +18,7 @@
460 </metal:registering>
461
462 <metal:side fill-slot="side">
463+ <div tal:replace="structure context/@@+portlet-privacy" />
464 <div tal:replace="structure context/@@+global-actions"/>
465 </metal:side>
466
467
468=== added file 'lib/lp/snappy/templates/snap-portlet-privacy.pt'
469--- lib/lp/snappy/templates/snap-portlet-privacy.pt 1970-01-01 00:00:00 +0000
470+++ lib/lp/snappy/templates/snap-portlet-privacy.pt 2016-02-04 13:01:27 +0000
471@@ -0,0 +1,16 @@
472+<div
473+ xmlns:tal="http://xml.zope.org/namespaces/tal"
474+ xmlns:metal="http://xml.zope.org/namespaces/metal"
475+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
476+ id="privacy"
477+ tal:attributes="
478+ class python: path('context/private') and 'portlet private' or 'portlet public'
479+ "
480+>
481+ <span tal:attributes="class python: path('context/private') and 'sprite private' or 'sprite public'"
482+ >This snap contains <strong
483+ tal:content="python: path('context/private') and 'Private' or 'Public'"
484+ ></strong> information</span>
485+</div>
486+
487+
488
489=== modified file 'lib/lp/snappy/tests/test_snap.py'
490--- lib/lp/snappy/tests/test_snap.py 2015-11-26 15:46:38 +0000
491+++ lib/lp/snappy/tests/test_snap.py 2016-02-04 13:01:27 +0000
492@@ -15,6 +15,7 @@
493 from zope.event import notify
494 from zope.security.proxy import removeSecurityProxy
495
496+from lp.app.enums import InformationType
497 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
498 from lp.buildmaster.enums import (
499 BuildQueueStatus,
500@@ -23,6 +24,7 @@
501 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
502 from lp.buildmaster.interfaces.processor import IProcessorSet
503 from lp.buildmaster.model.buildqueue import BuildQueue
504+from lp.registry.enums import PersonVisibility
505 from lp.registry.interfaces.distribution import IDistributionSet
506 from lp.registry.interfaces.pocket import PackagePublishingPocket
507 from lp.services.database.constants import (
508@@ -43,6 +45,7 @@
509 SnapBuildAlreadyPending,
510 SnapBuildDisallowedArchitecture,
511 SnapFeatureDisabled,
512+ SnapPrivacyMismatch,
513 )
514 from lp.snappy.interfaces.snapbuild import ISnapBuild
515 from lp.testing import (
516@@ -404,6 +407,7 @@
517 self.assertIsNone(snap.git_path)
518 self.assertIsNone(snap.git_ref)
519 self.assertTrue(snap.require_virtualized)
520+ self.assertFalse(snap.private)
521
522 def test_creation_git(self):
523 # The metadata entries supplied when a Snap is created for a Git
524@@ -420,6 +424,65 @@
525 self.assertEqual(ref.path, snap.git_path)
526 self.assertEqual(ref, snap.git_ref)
527 self.assertTrue(snap.require_virtualized)
528+ self.assertFalse(snap.private)
529+
530+ def test_private_snap_for_public_sources(self):
531+ # Creating private snaps for public sources is allowed.
532+ [ref] = self.factory.makeGitRefs()
533+ components = self.makeSnapComponents(git_ref=ref)
534+ components['private'] = True
535+ snap = getUtility(ISnapSet).new(**components)
536+ with person_logged_in(components['owner']):
537+ self.assertTrue(snap.private)
538+
539+ def test_private_git_requires_private_snap(self):
540+ # Snaps for a private Git branch cannot be public.
541+ owner = self.factory.makePerson()
542+ with person_logged_in(owner):
543+ [git_ref] = self.factory.makeGitRefs(
544+ owner=owner, information_type=InformationType.PRIVATESECURITY)
545+ components = dict(
546+ registrant=owner,
547+ owner=owner,
548+ git_ref=git_ref,
549+ distro_series=self.factory.makeDistroSeries(),
550+ name=self.factory.getUniqueString(u"snap-name"),
551+ )
552+ self.assertRaises(
553+ SnapPrivacyMismatch, getUtility(ISnapSet).new, **components)
554+
555+ def test_private_bzr_requires_private_snap(self):
556+ # Snaps for a private Bzr branch cannot be public.
557+ owner = self.factory.makePerson()
558+ with person_logged_in(owner):
559+ branch = self.factory.makeAnyBranch(
560+ owner=owner, information_type=InformationType.PRIVATESECURITY)
561+ components = dict(
562+ registrant=owner,
563+ owner=owner,
564+ branch=branch,
565+ distro_series=self.factory.makeDistroSeries(),
566+ name=self.factory.getUniqueString(u"snap-name"),
567+ )
568+ self.assertRaises(
569+ SnapPrivacyMismatch, getUtility(ISnapSet).new, **components)
570+
571+ def test_private_team_requires_private_snap(self):
572+ # Snaps owned by private teams cannot be public.
573+ registrant = self.factory.makePerson()
574+ with person_logged_in(registrant):
575+ private_team = self.factory.makeTeam(
576+ owner=registrant, visibility=PersonVisibility.PRIVATE)
577+ [git_ref] = self.factory.makeGitRefs()
578+ components = dict(
579+ registrant=registrant,
580+ owner=private_team,
581+ git_ref=git_ref,
582+ distro_series=self.factory.makeDistroSeries(),
583+ name=self.factory.getUniqueString(u"snap-name"),
584+ )
585+ self.assertRaises(
586+ SnapPrivacyMismatch, getUtility(ISnapSet).new, **components)
587
588 def test_creation_no_source(self):
589 # Attempting to create a Snap with neither a Bazaar branch nor a Git
590@@ -703,7 +766,8 @@
591 return self.webservice.getAbsoluteUrl(api_url(obj))
592
593 def makeSnap(self, owner=None, distroseries=None, branch=None,
594- git_ref=None, processors=None, webservice=None):
595+ git_ref=None, processors=None, webservice=None,
596+ private=False):
597 if owner is None:
598 owner = self.person
599 if distroseries is None:
600@@ -726,7 +790,7 @@
601 logout()
602 response = webservice.named_post(
603 "/+snaps", "new", owner=owner_url, distro_series=distroseries_url,
604- name="mir", **kwargs)
605+ name="mir", private=private, **kwargs)
606 self.assertEqual(201, response.status)
607 return webservice.get(response.getHeader("Location")).jsonBody()
608
609@@ -775,6 +839,21 @@
610 self.assertEqual(self.getURL(ref), snap["git_ref_link"])
611 self.assertTrue(snap["require_virtualized"])
612
613+ def test_new_private(self):
614+ # Ensure private Snap creation works.
615+ team = self.factory.makeTeam(owner=self.person)
616+ distroseries = self.factory.makeDistroSeries(registrant=team)
617+ [ref] = self.factory.makeGitRefs()
618+ private_webservice = webservice_for_person(
619+ self.person, permission=OAuthPermission.WRITE_PRIVATE)
620+ private_webservice.default_api_version = "devel"
621+ login(ANONYMOUS)
622+ snap = self.makeSnap(
623+ owner=team, distroseries=distroseries, git_ref=ref,
624+ webservice=private_webservice, private=True)
625+ with person_logged_in(self.person):
626+ self.assertTrue(snap["private"])
627+
628 def test_duplicate(self):
629 # An attempt to create a duplicate Snap fails.
630 team = self.factory.makeTeam(owner=self.person)
631
632=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
633--- lib/lp/snappy/tests/test_snapbuild.py 2015-09-11 12:20:23 +0000
634+++ lib/lp/snappy/tests/test_snapbuild.py 2016-02-04 13:01:27 +0000
635@@ -127,7 +127,7 @@
636 visibility=PersonVisibility.PRIVATE)
637 with person_logged_in(private_team.teamowner):
638 build = self.factory.makeSnapBuild(
639- requester=private_team.teamowner, owner=private_team)
640+ requester=private_team.teamowner, owner=private_team, private=True)
641 self.assertTrue(build.is_private)
642 private_archive = self.factory.makeArchive(private=True)
643 with person_logged_in(private_archive.owner):
644@@ -366,7 +366,7 @@
645 owner=self.person, visibility=PersonVisibility.PRIVATE)
646 with person_logged_in(self.person):
647 db_build = self.factory.makeSnapBuild(
648- requester=self.person, owner=db_team)
649+ requester=self.person, owner=db_team, private=True)
650 build_url = api_url(db_build)
651 unpriv_webservice = webservice_for_person(
652 self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
653
654=== modified file 'lib/lp/testing/factory.py'
655--- lib/lp/testing/factory.py 2016-01-26 15:14:01 +0000
656+++ lib/lp/testing/factory.py 2016-02-04 13:01:27 +0000
657@@ -4554,7 +4554,7 @@
658 def makeSnap(self, registrant=None, owner=None, distroseries=None,
659 name=None, branch=None, git_ref=None,
660 require_virtualized=True, processors=None,
661- date_created=DEFAULT):
662+ date_created=DEFAULT, private=False):
663 """Make a new Snap."""
664 if registrant is None:
665 registrant = self.makePerson()
666@@ -4569,7 +4569,8 @@
667 snap = getUtility(ISnapSet).new(
668 registrant, owner, distroseries, name,
669 require_virtualized=require_virtualized, processors=processors,
670- date_created=date_created, branch=branch, git_ref=git_ref)
671+ date_created=date_created, branch=branch, git_ref=git_ref,
672+ private=private)
673 IStore(snap).flush()
674 return snap
675