Merge lp:~abentley/launchpad/transitive-confidential into lp:launchpad

Proposed by Aaron Bentley on 2012-11-23
Status: Merged
Approved by: Aaron Bentley on 2012-11-23
Approved revision: no longer in the source branch.
Merged at revision: 16306
Proposed branch: lp:~abentley/launchpad/transitive-confidential
Merge into: lp:launchpad
Diff against target: 394 lines (+99/-190)
6 files modified
lib/lp/registry/browser/product.py (+0/-41)
lib/lp/registry/browser/tests/test_product.py (+0/-113)
lib/lp/registry/model/product.py (+40/-13)
lib/lp/registry/tests/test_milestone.py (+0/-21)
lib/lp/registry/tests/test_product.py (+57/-0)
lib/lp/translations/doc/translationsoverview.txt (+2/-2)
To merge this branch: bzr merge lp:~abentley/launchpad/transitive-confidential
Reviewer Review Type Date Requested Status
Abel Deuring (community) code 2012-11-23 Approve on 2012-11-23
Aaron Bentley Pending
Review via email: mp+135926@code.launchpad.net

This proposal supersedes a proposal from 2012-11-19.

Commit Message

Forbid non-proprietary artifacts when transitioning product to Proprietary

Description of the Change

= Summary =
Fix bug #1079785: public artifacts are permitted on confidential projects

== Proposed fix ==
Move checks from view to model, raise exceptions, include private non-proprietary types

== Pre-implementation notes ==
None

== LOC Rationale ==
Reduces LOC

== Implementation details ==
Revert 16258 and migrate code from getPublicWarning. Use early exit to separate on-change code from on-creation code.

Tweak to check for USERDATA and PRIVATESECURITY bugs.

Remove getPublicWarning and related code and tests.

== Tests ==
bin/test -t test_change_info_type_proprietary_check_artifacts -t test_change_info_type_proprietary_check_translations

== Demo and Q/A ==
Create a public product with other/proprietary license. Create a public branch, bug, blueprint. Enable translations. You should not be able to change its information type to proprietary until you've made all artifacts private and disabled translations.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/browser/product.py
  lib/lp/registry/browser/tests/test_product.py
  lib/lp/registry/model/product.py
  lib/lp/registry/tests/test_product.py

To post a comment you must log in.
Richard Harding (rharding) : Posted in a previous version of this proposal
review: Approve
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal

Further discussion with Curtis reveals that only Proprietary and Embargoed artefacts should be permitted when transitioning product to proprietary/embargoed. See bug.

review: Needs Fixing
Abel Deuring (adeuring) wrote :

Looks good to land.

I just have concern: If we assume that somebody creates a public project, adds public bugs, branches, specifications, links packages and enables translations.

If this person decides to make product proprietary, the user experience is terrible:

The first error is "sorry, no public specs allowed", so the user changes the specifications.

The next error then is "sorry, no public bugs allowed" and so on. It would be better to collect all "show stoppers" and tell the user immediately that they have to fix issues with bugs, branches, specs, translations and packages.

review: Approve (code)
Aaron Bentley (abentley) wrote :

I have a plan to show all validation error at form validation time, which I'll pursue in a follow-up branch.

William Grant (wgrant) wrote :

Translations must not exist at all, not just be disabled. Answers must also not exist.

There's also some arbitrary capitalisation here:

274 + raise CommercialSubscribersOnly(
275 + 'A valid commercial subscription is required for private'
276 + ' Projects.')

Aaron Bentley (abentley) wrote :

> Translations must not exist at all, not just be disabled. Answers must also
> not exist.

I know this, and I've already started work on addressing that. See bug #1082422 and branch lp:~abentley/launchpad/more-transition-checks. Please add any checks that I've missed to that bug.

> There's also some arbitrary capitalisation here:
>
> 274 + raise CommercialSubscribersOnly(
> 275 + 'A valid commercial subscription is required for private'
> 276 + ' Projects.')

True, but I only moved that code around.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/product.py'
2--- lib/lp/registry/browser/product.py 2012-11-15 16:03:56 +0000
3+++ lib/lp/registry/browser/product.py 2012-11-23 22:13:26 +0000
4@@ -105,7 +105,6 @@
5 InformationType,
6 PROPRIETARY_INFORMATION_TYPES,
7 PUBLIC_PROPRIETARY_INFORMATION_TYPES,
8- PUBLIC_INFORMATION_TYPES,
9 ServiceUsage,
10 )
11 from lp.app.errors import NotFoundError
12@@ -129,9 +128,6 @@
13 from lp.blueprints.browser.specificationtarget import (
14 HasSpecificationsMenuMixin,
15 )
16-from lp.blueprints.enums import (
17- SpecificationFilter,
18- )
19 from lp.bugs.browser.bugtask import (
20 BugTargetTraversalMixin,
21 get_buglisting_search_filter_url,
22@@ -1450,47 +1446,10 @@
23
24 @action("Change", name='change')
25 def change_action(self, action, data):
26- old_info_type = self.context.information_type
27 try:
28 self.updateContextFromData(data)
29 except CannotChangeInformationType as e:
30 self.setFieldError('information_type', str(e))
31- warning = self.getPublicWarning(old_info_type)
32- if warning is not None:
33- self.request.response.addWarningNotification(warning)
34-
35- def getPublicWarning(self, old_info_type):
36- """Get warning if public artifacts might expose the product.
37-
38- :param old_info_type: The former information type of the product.
39- """
40- if (old_info_type != InformationType.PUBLIC or
41- self.context.information_type == InformationType.PUBLIC):
42- return None
43- public_bugs = self.context.searchTasks(None,
44- information_type=PUBLIC_INFORMATION_TYPES,
45- omit_duplicates=False, omit_targeted=False)
46- public_artifacts = []
47- if not public_bugs.is_empty():
48- public_artifacts.append('bugs')
49- # Default returns all public branches.
50- public_branches = self.context.getBranches()
51- if not public_branches.is_empty():
52- public_artifacts.append('branches')
53- # All specs located by an ALL search are public.
54- public_specs = self.context.specifications(
55- None, filter=[SpecificationFilter.ALL])
56- if not public_specs.is_empty():
57- public_artifacts.append('blueprints')
58- if len(public_artifacts) == 0:
59- return None
60- elif len(public_artifacts) < 3:
61- list_phrase = ' and '.join(public_artifacts)
62- else:
63- list_phrase = 'bugs, branches, and blueprints'
64- return ('Some %s are public. This project will not be fully '
65- '%s until they have been changed.' %
66- (list_phrase, self.context.information_type.title))
67
68
69 class ProductValidationMixin:
70
71=== modified file 'lib/lp/registry/browser/tests/test_product.py'
72--- lib/lp/registry/browser/tests/test_product.py 2012-11-15 16:03:56 +0000
73+++ lib/lp/registry/browser/tests/test_product.py 2012-11-23 22:13:26 +0000
74@@ -25,7 +25,6 @@
75 from lp.app.enums import (
76 InformationType,
77 PROPRIETARY_INFORMATION_TYPES,
78- PUBLIC_PROPRIETARY_INFORMATION_TYPES,
79 ServiceUsage,
80 )
81 from lp.registry.browser.product import (
82@@ -553,118 +552,6 @@
83 self.assertEqual(
84 InformationType.PUBLIC, updated_product.information_type)
85
86- def test_warning_on_public_artifacts(self):
87- # When changing to proprietary, any public artifacts will trigger a
88- # warning.
89- self.useFixture(FeatureFixture(
90- {u'disclosure.private_projects.enabled': u'on'}))
91- owner = self.factory.makePerson()
92- product = self.factory.makeProduct(owner=owner)
93- bug = self.factory.makeBug(target=product)
94-
95- def make_proprietary():
96- with person_logged_in(None):
97- browser = self.getViewBrowser(product, '+edit',
98- user=product.owner)
99- info_type = browser.getControl(name='field.information_type')
100- info_type.value = ['PROPRIETARY']
101- browser.getControl('Change').click()
102- with person_logged_in(owner):
103- product.information_type = InformationType.PUBLIC
104- return browser
105- browser = make_proprietary()
106-
107- def assertWarning(browser, text):
108- warning_tag = Tag('warning', 'div', text=text,
109- attrs={'class': 'warning message'})
110- self.assertThat(browser.contents, HTMLContains(warning_tag))
111- assertWarning(
112- browser, 'Some bugs are public. This project will not be fully '
113- 'Proprietary until they have been changed.')
114- browser = make_proprietary()
115- with person_logged_in(bug.owner):
116- bug.transitionToInformationType(InformationType.PROPRIETARY,
117- bug.owner)
118- browser = make_proprietary()
119- self.assertThat(browser.contents, Not(HTMLContains(
120- Tag('warning', 'div', attrs={'class': 'warning message'}))))
121-
122- def test_getPublicWarning_artifacts(self):
123- # The warning indicates what kinds of artifacts are public, in
124- # grammatical English.
125- owner = self.factory.makePerson()
126- product = self.factory.makeProduct(owner=owner)
127- bug = self.factory.makeBug(target=product)
128- view = create_initialized_view(product, '+edit')
129- with person_logged_in(owner):
130- product.information_type = InformationType.PROPRIETARY
131- warning = view.getPublicWarning(InformationType.PUBLIC)
132- self.assertEqual(
133- 'Some bugs are public. This project will not be fully '
134- 'Proprietary until they have been changed.', warning)
135- spec = self.factory.makeSpecification(product=product)
136- warning = view.getPublicWarning(InformationType.PUBLIC)
137- self.assertEqual(
138- 'Some bugs and blueprints are public. This project will not be '
139- 'fully Proprietary until they have been changed.', warning)
140- branch = self.factory.makeBranch(product=product, owner=owner,
141- information_type=InformationType.PUBLIC)
142- warning = view.getPublicWarning(InformationType.PUBLIC)
143- self.assertEqual(
144- 'Some bugs, branches, and blueprints are public. This project '
145- 'will not be fully Proprietary until they have been changed.',
146- warning)
147- with person_logged_in(owner):
148- bug.transitionToInformationType(
149- InformationType.PROPRIETARY, owner)
150- warning = view.getPublicWarning(InformationType.PUBLIC)
151- self.assertEqual(
152- 'Some branches and blueprints are public. This project '
153- 'will not be fully Proprietary until they have been changed.',
154- warning)
155- with person_logged_in(owner):
156- branch.transitionToInformationType(
157- InformationType.PROPRIETARY, owner)
158- warning = view.getPublicWarning(InformationType.PUBLIC)
159- self.assertEqual(
160- 'Some blueprints are public. This project will not be fully '
161- 'Proprietary until they have been changed.', warning)
162- with person_logged_in(owner):
163- spec.transitionToInformationType(
164- InformationType.PROPRIETARY, owner)
165- self.assertIs(None, view.getPublicWarning(InformationType.PUBLIC))
166-
167- def test_getPublicWarning_embargoed(self):
168- # The message reflects an Embargoed product type.
169- owner = self.factory.makePerson()
170- product = self.factory.makeProduct(owner=owner)
171- self.factory.makeBug(target=product)
172- with person_logged_in(owner):
173- product.information_type = InformationType.EMBARGOED
174- view = create_initialized_view(product, '+edit')
175- warning = view.getPublicWarning(InformationType.PUBLIC)
176- self.assertEqual(
177- 'Some bugs are public. This project will not be fully '
178- 'Embargoed until they have been changed.', warning)
179-
180- def test_getPublicWarning_transition(self):
181- # The warning is emitted only when transitioning from PUBLIC to a
182- # proprietary type.
183- owner = self.factory.makePerson()
184- product = self.factory.makeProduct(owner=owner)
185- self.factory.makeBug(target=product)
186- view = create_initialized_view(product, '+edit')
187- for from_type in PUBLIC_PROPRIETARY_INFORMATION_TYPES:
188- for to_type in PUBLIC_PROPRIETARY_INFORMATION_TYPES:
189- with person_logged_in(owner):
190- product.information_type = to_type
191- warning = view.getPublicWarning(from_type)
192- if (to_type in PROPRIETARY_INFORMATION_TYPES and
193- from_type == InformationType.PUBLIC):
194- self.assertIsNot(None, warning)
195- else:
196- self.assertIs(None, warning)
197-
198
199 class ProductSetReviewLicensesViewTestCase(TestCaseWithFactory):
200 """Tests the ProductSetReviewLicensesView."""
201
202=== modified file 'lib/lp/registry/model/product.py'
203--- lib/lp/registry/model/product.py 2012-11-19 18:23:06 +0000
204+++ lib/lp/registry/model/product.py 2012-11-23 22:13:26 +0000
205@@ -111,6 +111,7 @@
206 BUG_POLICY_DEFAULT_TYPES,
207 )
208 from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
209+from lp.bugs.model.bug import Bug
210 from lp.bugs.model.bugtarget import (
211 BugTargetBase,
212 OfficialBugTagTargetMixin,
213@@ -460,27 +461,53 @@
214 if value in PROPRIETARY_INFORMATION_TYPES:
215 if self.answers_usage == ServiceUsage.LAUNCHPAD:
216 raise CannotChangeInformationType('Answers is enabled.')
217+ if self._SO_creating or value not in PROPRIETARY_INFORMATION_TYPES:
218+ return value
219+ # Additional checks when transitioning an existing product to a
220+ # proprietary type
221+ # All specs located by an ALL search are public.
222+ public_specs = self.specifications(
223+ None, filter=[SpecificationFilter.ALL])
224+ if not public_specs.is_empty():
225+ # Unlike bugs and branches, specifications cannot be USERDATA or a
226+ # security type.
227+ raise CannotChangeInformationType(
228+ 'Some blueprints are public.')
229+ non_proprietary_bugs = Store.of(self).find(Bug,
230+ Not(Bug.information_type.is_in(PROPRIETARY_INFORMATION_TYPES)),
231+ BugTask.bug == Bug.id, BugTask.product == self.id)
232+ if not non_proprietary_bugs.is_empty():
233+ raise CannotChangeInformationType(
234+ 'Some bugs are neither proprietary nor embargoed.')
235+ # Default returns all public branches.
236+ non_proprietary_branches = Store.of(self).find(
237+ Branch, Branch.product == self.id,
238+ Not(Branch.information_type.is_in(PROPRIETARY_INFORMATION_TYPES))
239+ )
240+ if not non_proprietary_branches.is_empty():
241+ raise CannotChangeInformationType(
242+ 'Some branches are neither proprietary nor embargoed.')
243+ if not self.packagings.is_empty():
244+ raise CannotChangeInformationType('Some series are packaged.')
245+ if self.translations_usage == ServiceUsage.LAUNCHPAD:
246+ raise CannotChangeInformationType('Translations are enabled.')
247 # Proprietary check works only after creation, because during
248 # creation, has_commercial_subscription cannot give the right value
249 # and triggers an inappropriate DB flush.
250
251 # If you're changing the license, and setting a PROPRIETARY
252- # information type, yet you don't have a subscription you get one when
253- # the license is set.
254+ # information type, yet you don't have a subscription, you get one
255+ # when the license is set.
256+
257+ # Create the complimentary commercial subscription for the product.
258+ self._ensure_complimentary_subscription()
259
260 # If you have a commercial subscription, but it's not current, you
261 # cannot set the information type to a PROPRIETARY type.
262- if not self._SO_creating and value in PROPRIETARY_INFORMATION_TYPES:
263- if not self.packagings.is_empty():
264- raise CannotChangeInformationType('Some series are packaged.')
265- # Create the complimentary commercial subscription for the product.
266- self._ensure_complimentary_subscription()
267-
268- if not self.has_current_commercial_subscription:
269- raise CommercialSubscribersOnly(
270- 'A valid commercial subscription is required for private'
271- ' Projects.')
272-
273+ if not self.has_current_commercial_subscription:
274+ raise CommercialSubscribersOnly(
275+ 'A valid commercial subscription is required for private'
276+ ' Projects.')
277 return value
278
279 _information_type = EnumCol(
280
281=== modified file 'lib/lp/registry/tests/test_milestone.py'
282--- lib/lp/registry/tests/test_milestone.py 2012-11-14 16:24:59 +0000
283+++ lib/lp/registry/tests/test_milestone.py 2012-11-23 22:13:26 +0000
284@@ -520,27 +520,6 @@
285 self.assertNotIn(
286 specification, list(milestone.getSpecifications(None)))
287
288- def test_bugtasks_milestone_privacy(self):
289- # Ensure bugtasks respects milestone privacy.
290- # This looks wrong, because the bugtask is actually public, and we
291- # don't normally hide bugtasks based on the visibility of their
292- # products. But we're not trying to hide the bugtask. We're hiding
293- # the fact that this bugtask is associated with a proprietary Product
294- # milestone. We create a proprietary product because that's the only
295- # way to get a proprietary milestone.
296- milestone, target_milestone, owner = self.makeMixedMilestone()
297- with person_logged_in(owner):
298- product = target_milestone.product
299- product.information_type = InformationType.PUBLIC
300- product.setBugSharingPolicy(BugSharingPolicy.PUBLIC)
301- bugtask = self.factory.makeBugTask(
302- target=target_milestone.product)
303- product.information_type = InformationType.PROPRIETARY
304- with person_logged_in(bugtask.owner):
305- bugtask.transitionToMilestone(target_milestone, owner)
306- self.assertContentEqual([], milestone.bugtasks(None))
307- self.assertContentEqual([bugtask], milestone.bugtasks(owner))
308-
309 def test_milestones_with_deleted_workitems(self):
310 # Deleted work items do not cause the specification to show up
311 # in the milestone page.
312
313=== modified file 'lib/lp/registry/tests/test_product.py'
314--- lib/lp/registry/tests/test_product.py 2012-11-19 06:26:32 +0000
315+++ lib/lp/registry/tests/test_product.py 2012-11-23 22:13:26 +0000
316@@ -391,6 +391,63 @@
317 expected = [InformationType.USERDATA, InformationType.PRIVATESECURITY]
318 self.assertContentEqual(expected, [policy.type for policy in aps])
319
320+ def test_change_info_type_proprietary_check_artifacts(self):
321+ # Cannot change product information_type if any artifacts are public.
322+ spec_policy = SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY
323+ product = self.factory.makeProduct(
324+ licenses=[License.OTHER_PROPRIETARY],
325+ specification_sharing_policy=spec_policy,
326+ bug_sharing_policy=BugSharingPolicy.PUBLIC_OR_PROPRIETARY,
327+ branch_sharing_policy=BranchSharingPolicy.PUBLIC_OR_PROPRIETARY,
328+ )
329+ self.useContext(person_logged_in(product.owner))
330+ spec = self.factory.makeSpecification(product=product)
331+ for info_type in PROPRIETARY_INFORMATION_TYPES:
332+ with ExpectedException(
333+ CannotChangeInformationType,
334+ 'Some blueprints are public.'):
335+ product.information_type = info_type
336+ spec.transitionToInformationType(InformationType.PROPRIETARY,
337+ product.owner)
338+ bug = self.factory.makeBug(target=product)
339+ for bug_info_type in FREE_INFORMATION_TYPES:
340+ bug.transitionToInformationType(bug_info_type, product.owner)
341+ for info_type in PROPRIETARY_INFORMATION_TYPES:
342+ with ExpectedException(
343+ CannotChangeInformationType,
344+ 'Some bugs are neither proprietary nor embargoed.'):
345+ product.information_type = info_type
346+ bug.transitionToInformationType(InformationType.PROPRIETARY,
347+ product.owner)
348+ branch = self.factory.makeBranch(product=product)
349+ for branch_info_type in FREE_INFORMATION_TYPES:
350+ branch.transitionToInformationType(branch_info_type,
351+ product.owner)
352+ for info_type in PROPRIETARY_INFORMATION_TYPES:
353+ with ExpectedException(
354+ CannotChangeInformationType,
355+ 'Some branches are neither proprietary nor embargoed.'):
356+ product.information_type = info_type
357+ branch.transitionToInformationType(InformationType.PROPRIETARY,
358+ product.owner)
359+ for info_type in PROPRIETARY_INFORMATION_TYPES:
360+ product.information_type = info_type
361+
362+ def test_change_info_type_proprietary_check_translations(self):
363+ product = self.factory.makeProduct(
364+ licenses=[License.OTHER_PROPRIETARY])
365+ with person_logged_in(product.owner):
366+ for usage in ServiceUsage:
367+ product.translations_usage = usage.value
368+ for info_type in PROPRIETARY_INFORMATION_TYPES:
369+ if product.translations_usage == ServiceUsage.LAUNCHPAD:
370+ with ExpectedException(
371+ CannotChangeInformationType,
372+ 'Translations are enabled.'):
373+ product.information_type = info_type
374+ else:
375+ product.information_type = info_type
376+
377 def test_change_info_type_proprietary_sets_policies(self):
378 # Changing information type from public to proprietary sets the
379 # appropriate policies
380
381=== modified file 'lib/lp/translations/doc/translationsoverview.txt'
382--- lib/lp/translations/doc/translationsoverview.txt 2012-10-24 13:52:59 +0000
383+++ lib/lp/translations/doc/translationsoverview.txt 2012-11-23 22:13:26 +0000
384@@ -180,8 +180,8 @@
385 Private projects are never included.
386
387 >>> from lp.app.enums import InformationType
388- >>> removeSecurityProxy(upstart).information_type = (
389- ... InformationType.PROPRIETARY)
390+ >>> upstart.translations_usage = ServiceUsage.NOT_APPLICABLE
391+ >>> upstart.information_type = InformationType.PROPRIETARY
392 >>> display_pillars(overview.getMostTranslatedPillars())
393 alsa-utils: 22
394 Evolution: 20