Merge lp:~cprov/launchpad/snap-privacy-take1 into lp:launchpad
- snap-privacy-take1
- Merge into devel
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 |
Related bugs: |
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.
Celso Providelo (cprov) wrote : | # |
Thanks Colin,
I understand and agree will all your comments, they will be addressed shortly.
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.
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_
3) Lastly, I am not happy with the code that inspect the privacy portlet (first_
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.
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_
Commercial admins should be able to, because they can view any private
team (see ViewPublicOrPri
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_
You have id="privacy" set on the div element at the top level of the
portlet, so I'd use find_tag_
other tests that do this, albeit doctests: see for example
lib/lp/
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.
Colin Watson (cjwatson) : | # |
Preview Diff
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 |
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.