Merge ~cjwatson/launchpad:distribution-sharing-policies into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 1c71c17badc5afb1d511ce041b5a3a666e763e06
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:distribution-sharing-policies
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:distribution-information-type
Diff against target: 2414 lines (+1329/-322)
20 files modified
lib/lp/code/model/branch.py (+2/-2)
lib/lp/code/model/branchnamespace.py (+23/-3)
lib/lp/code/model/gitnamespace.py (+20/-2)
lib/lp/code/model/gitrepository.py (+2/-2)
lib/lp/code/model/tests/test_branch.py (+40/-16)
lib/lp/code/model/tests/test_branchnamespace.py (+180/-16)
lib/lp/code/model/tests/test_gitnamespace.py (+177/-14)
lib/lp/code/model/tests/test_gitrepository.py (+54/-17)
lib/lp/registry/interfaces/distribution.py (+36/-0)
lib/lp/registry/model/distribution.py (+50/-71)
lib/lp/registry/model/product.py (+2/-110)
lib/lp/registry/model/sharingpolicy.py (+155/-0)
lib/lp/registry/services/sharingservice.py (+5/-24)
lib/lp/registry/services/tests/test_sharingservice.py (+0/-22)
lib/lp/registry/tests/test_distribution.py (+478/-11)
lib/lp/registry/tests/test_product.py (+1/-1)
lib/lp/scripts/garbo.py (+30/-2)
lib/lp/scripts/tests/test_garbo.py (+37/-3)
lib/lp/security.py (+13/-5)
lib/lp/testing/factory.py (+24/-1)
Reviewer Review Type Date Requested Status
William Grant code Approve
Ioana Lasc Approve
Review via email: mp+415623@code.launchpad.net

Commit message

Add distribution sharing policies

Description of the change

This is heavily based on the equivalent code for projects.

To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) :
review: Approve
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/code/model/branch.py b/lib/lp/code/model/branch.py
2index f86f754..fc7da1a 100644
3--- a/lib/lp/code/model/branch.py
4+++ b/lib/lp/code/model/branch.py
5@@ -253,10 +253,10 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
6 (abstract_artifact, policy) for policy in
7 getUtility(IAccessPolicySource).findByTeam([self.owner])}
8 else:
9- # We haven't yet quite worked out how distribution privacy
10- # works, so only work for products for now.
11 if self.product is not None:
12 pillars = [self.product]
13+ elif self.distribution is not None:
14+ pillars = [self.distribution]
15 reconcile_access_for_artifacts(
16 [self], self.information_type, pillars, wanted_links)
17
18diff --git a/lib/lp/code/model/branchnamespace.py b/lib/lp/code/model/branchnamespace.py
19index 326d1d4..b007d99 100644
20--- a/lib/lp/code/model/branchnamespace.py
21+++ b/lib/lp/code/model/branchnamespace.py
22@@ -26,7 +26,6 @@ from lp.app.enums import (
23 FREE_INFORMATION_TYPES,
24 InformationType,
25 NON_EMBARGOED_INFORMATION_TYPES,
26- PUBLIC_INFORMATION_TYPES,
27 )
28 from lp.app.interfaces.services import IService
29 from lp.code.enums import (
30@@ -420,11 +419,32 @@ class PackageBranchNamespace(_BaseBranchNamespace):
31
32 def getAllowedInformationTypes(self, who=None):
33 """See `IBranchNamespace`."""
34- return PUBLIC_INFORMATION_TYPES
35+ # The distribution uses the new simplified branch_sharing_policy
36+ # rules, so check them.
37+
38+ # Some policies require that the branch owner or current user have
39+ # full access to an information type. If it's required and the user
40+ # doesn't hold it, no information types are legal.
41+ distribution = self.sourcepackage.distribution
42+ required_grant = BRANCH_POLICY_REQUIRED_GRANTS[
43+ distribution.branch_sharing_policy]
44+ if (required_grant is not None
45+ and not getUtility(IService, 'sharing').checkPillarAccess(
46+ [distribution], required_grant, self.owner)
47+ and (who is None
48+ or not getUtility(IService, 'sharing').checkPillarAccess(
49+ [distribution], required_grant, who))):
50+ return []
51+
52+ return BRANCH_POLICY_ALLOWED_TYPES[distribution.branch_sharing_policy]
53
54 def getDefaultInformationType(self, who=None):
55 """See `IBranchNamespace`."""
56- return InformationType.PUBLIC
57+ default_type = BRANCH_POLICY_DEFAULT_TYPES[
58+ self.sourcepackage.distribution.branch_sharing_policy]
59+ if default_type not in self.getAllowedInformationTypes(who):
60+ return None
61+ return default_type
62
63
64 class BranchNamespaceSet:
65diff --git a/lib/lp/code/model/gitnamespace.py b/lib/lp/code/model/gitnamespace.py
66index 7ba9a13..e7b2c92 100644
67--- a/lib/lp/code/model/gitnamespace.py
68+++ b/lib/lp/code/model/gitnamespace.py
69@@ -510,11 +510,29 @@ class PackageGitNamespace(_BaseGitNamespace):
70
71 def getAllowedInformationTypes(self, who=None):
72 """See `IGitNamespace`."""
73- return PUBLIC_INFORMATION_TYPES
74+ # Some policies require that the repository owner or current user
75+ # have full access to an information type. If it's required and the
76+ # user doesn't hold it, no information types are legal.
77+ distribution = self.distro_source_package.distribution
78+ required_grant = BRANCH_POLICY_REQUIRED_GRANTS[
79+ distribution.branch_sharing_policy]
80+ if (required_grant is not None
81+ and not getUtility(IService, 'sharing').checkPillarAccess(
82+ [distribution], required_grant, self.owner)
83+ and (who is None
84+ or not getUtility(IService, 'sharing').checkPillarAccess(
85+ [distribution], required_grant, who))):
86+ return []
87+
88+ return BRANCH_POLICY_ALLOWED_TYPES[distribution.branch_sharing_policy]
89
90 def getDefaultInformationType(self, who=None):
91 """See `IGitNamespace`."""
92- return InformationType.PUBLIC
93+ default_type = BRANCH_POLICY_DEFAULT_TYPES[
94+ self.distro_source_package.distribution.branch_sharing_policy]
95+ if default_type not in self.getAllowedInformationTypes(who):
96+ return None
97+ return default_type
98
99 def areRepositoriesMergeable(self, this, other):
100 """See `IGitNamespacePolicy`."""
101diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
102index 1677d9e..0dd205b 100644
103--- a/lib/lp/code/model/gitrepository.py
104+++ b/lib/lp/code/model/gitrepository.py
105@@ -659,10 +659,10 @@ class GitRepository(StormBase, WebhookTargetMixin, AccessTokenTargetMixin,
106 (abstract_artifact, policy) for policy in
107 getUtility(IAccessPolicySource).findByTeam([self.owner])}
108 else:
109- # We haven't yet quite worked out how distribution privacy
110- # works, so only work for projects for now.
111 if self.project is not None:
112 pillars = [self.project]
113+ elif self.distribution is not None:
114+ pillars = [self.distribution]
115 reconcile_access_for_artifacts(
116 [self], self.information_type, pillars, wanted_links)
117
118diff --git a/lib/lp/code/model/tests/test_branch.py b/lib/lp/code/model/tests/test_branch.py
119index 4419b12..9692104 100644
120--- a/lib/lp/code/model/tests/test_branch.py
121+++ b/lib/lp/code/model/tests/test_branch.py
122@@ -17,6 +17,10 @@ from pytz import UTC
123 import six
124 from storm.exceptions import LostObjectError
125 from storm.locals import Store
126+from testscenarios import (
127+ load_tests_apply_scenarios,
128+ WithScenarios,
129+ )
130 from testtools import ExpectedException
131 from testtools.matchers import (
132 Not,
133@@ -2531,13 +2535,17 @@ class TestBranchPrivacy(TestCaseWithFactory):
134 [(branch.product, InformationType.USERDATA)]),
135 get_policies_for_artifact(branch))
136
137- def test__reconcileAccess_for_distro_branch(self):
138- # Branch privacy isn't yet supported for distributions, so no
139- # AccessPolicyArtifact is created for a distro branch.
140+ def test__reconcileAccess_for_package_branch(self):
141+ # _reconcileAccess uses a distribution policy for a package branch.
142 branch = self.factory.makePackageBranch(
143 information_type=InformationType.USERDATA)
144+ [artifact] = getUtility(IAccessArtifactSource).ensure([branch])
145+ getUtility(IAccessPolicyArtifactSource).deleteByArtifact([artifact])
146 removeSecurityProxy(branch)._reconcileAccess()
147- self.assertEqual([], get_policies_for_artifact(branch))
148+ self.assertContentEqual(
149+ getUtility(IAccessPolicySource).find(
150+ [(branch.distribution, InformationType.USERDATA)]),
151+ get_policies_for_artifact(branch))
152
153 def test__reconcileAccess_for_personal_branch(self):
154 # _reconcileAccess uses a person policy for a personal branch.
155@@ -2701,15 +2709,26 @@ class TestBranchSetPrivate(TestCaseWithFactory):
156 InformationType.PRIVATESECURITY, branch.information_type)
157
158
159-class BranchModerateTestCase(TestCaseWithFactory):
160- """Test that product owners and commercial admins can moderate branches."""
161+class BranchModerateTestCase(WithScenarios, TestCaseWithFactory):
162+ """Test that pillar owners and commercial admins can moderate branches."""
163
164 layer = DatabaseFunctionalLayer
165+ scenarios = [
166+ ("project", {"branch_factory_name": "makeProductBranch"}),
167+ ("distribution", {"branch_factory_name": "makePackageBranch"}),
168+ ]
169+
170+ def _makeBranch(self, **kwargs):
171+ return getattr(self.factory, self.branch_factory_name)(**kwargs)
172+
173+ def _getPillar(self, branch):
174+ return branch.product or branch.distribution
175
176 def test_moderate_permission(self):
177 # Test the ModerateBranch security checker.
178- branch = self.factory.makeProductBranch()
179- with person_logged_in(branch.product.owner):
180+ branch = self._makeBranch()
181+ pillar = self._getPillar(branch)
182+ with person_logged_in(pillar.owner):
183 self.assertTrue(
184 check_permission('launchpad.Moderate', branch))
185 with celebrity_logged_in('commercial_admin'):
186@@ -2718,25 +2737,27 @@ class BranchModerateTestCase(TestCaseWithFactory):
187
188 def test_methods_smoketest(self):
189 # Users with launchpad.Moderate can call transitionToInformationType.
190- branch = self.factory.makeProductBranch()
191- with person_logged_in(branch.product.owner):
192- branch.product.setBranchSharingPolicy(BranchSharingPolicy.PUBLIC)
193+ branch = self._makeBranch()
194+ pillar = self._getPillar(branch)
195+ with person_logged_in(pillar.owner):
196+ pillar.setBranchSharingPolicy(BranchSharingPolicy.PUBLIC)
197 branch.transitionToInformationType(
198- InformationType.PRIVATESECURITY, branch.product.owner)
199+ InformationType.PRIVATESECURITY, pillar.owner)
200 self.assertEqual(
201 InformationType.PRIVATESECURITY, branch.information_type)
202
203 def test_attribute_smoketest(self):
204 # Users with launchpad.Moderate can set attrs.
205- branch = self.factory.makeProductBranch()
206- with person_logged_in(branch.product.owner):
207+ branch = self._makeBranch()
208+ pillar = self._getPillar(branch)
209+ with person_logged_in(pillar.owner):
210 branch.name = 'not-secret'
211 branch.description = 'redacted'
212- branch.reviewer = branch.product.owner
213+ branch.reviewer = pillar.owner
214 branch.lifecycle_status = BranchLifecycleStatus.EXPERIMENTAL
215 self.assertEqual('not-secret', branch.name)
216 self.assertEqual('redacted', branch.description)
217- self.assertEqual(branch.product.owner, branch.reviewer)
218+ self.assertEqual(pillar.owner, branch.reviewer)
219 self.assertEqual(
220 BranchLifecycleStatus.EXPERIMENTAL, branch.lifecycle_status)
221
222@@ -3574,3 +3595,6 @@ class TestWebservice(TestCaseWithFactory):
223 with admin_logged_in():
224 self.assertEqual(
225 1, len(list(getUtility(IBranchScanJobSource).iterReady())))
226+
227+
228+load_tests = load_tests_apply_scenarios
229diff --git a/lib/lp/code/model/tests/test_branchnamespace.py b/lib/lp/code/model/tests/test_branchnamespace.py
230index aff13b1..7a4ff61 100644
231--- a/lib/lp/code/model/tests/test_branchnamespace.py
232+++ b/lib/lp/code/model/tests/test_branchnamespace.py
233@@ -10,7 +10,6 @@ from lp.app.enums import (
234 FREE_INFORMATION_TYPES,
235 InformationType,
236 NON_EMBARGOED_INFORMATION_TYPES,
237- PUBLIC_INFORMATION_TYPES,
238 )
239 from lp.app.interfaces.services import IService
240 from lp.app.validators import LaunchpadValidationError
241@@ -594,6 +593,186 @@ class TestPackageBranchNamespace(TestCaseWithFactory, NamespaceMixin):
242 self.assertEqual(IBranchTarget(package), namespace.target)
243
244
245+class TestPackageBranchNamespacePrivacyWithInformationType(
246+ TestCaseWithFactory):
247+ """Tests for the privacy aspects of `PackageBranchNamespace`.
248+
249+ This tests the behaviour for a package in a distribution using the new
250+ branch_sharing_policy rules.
251+ """
252+
253+ layer = DatabaseFunctionalLayer
254+
255+ def makePackageBranchNamespace(self, sharing_policy, person=None):
256+ if person is None:
257+ person = self.factory.makePerson()
258+ package = self.factory.makeSourcePackage()
259+ self.factory.makeCommercialSubscription(pillar=package.distribution)
260+ with person_logged_in(package.distribution.owner):
261+ package.distribution.setBranchSharingPolicy(sharing_policy)
262+ namespace = PackageBranchNamespace(person, package)
263+ return namespace
264+
265+ def test_public_anyone(self):
266+ namespace = self.makePackageBranchNamespace(
267+ BranchSharingPolicy.PUBLIC)
268+ self.assertContentEqual(
269+ FREE_INFORMATION_TYPES, namespace.getAllowedInformationTypes())
270+ self.assertEqual(
271+ InformationType.PUBLIC, namespace.getDefaultInformationType())
272+
273+ def test_forbidden_anyone(self):
274+ namespace = self.makePackageBranchNamespace(
275+ BranchSharingPolicy.FORBIDDEN)
276+ self.assertContentEqual([], namespace.getAllowedInformationTypes())
277+ self.assertEqual(None, namespace.getDefaultInformationType())
278+
279+ def test_public_or_proprietary_anyone(self):
280+ namespace = self.makePackageBranchNamespace(
281+ BranchSharingPolicy.PUBLIC_OR_PROPRIETARY)
282+ self.assertContentEqual(
283+ NON_EMBARGOED_INFORMATION_TYPES,
284+ namespace.getAllowedInformationTypes())
285+ self.assertEqual(
286+ InformationType.PUBLIC, namespace.getDefaultInformationType())
287+
288+ def test_proprietary_or_public_anyone(self):
289+ namespace = self.makePackageBranchNamespace(
290+ BranchSharingPolicy.PROPRIETARY_OR_PUBLIC)
291+ self.assertContentEqual([], namespace.getAllowedInformationTypes())
292+ self.assertIs(None, namespace.getDefaultInformationType())
293+
294+ def test_proprietary_or_public_owner_grantee(self):
295+ namespace = self.makePackageBranchNamespace(
296+ BranchSharingPolicy.PROPRIETARY_OR_PUBLIC)
297+ distribution = namespace.sourcepackage.distribution
298+ with person_logged_in(distribution.owner):
299+ getUtility(IService, 'sharing').sharePillarInformation(
300+ distribution, namespace.owner, distribution.owner,
301+ {InformationType.PROPRIETARY: SharingPermission.ALL})
302+ self.assertContentEqual(
303+ NON_EMBARGOED_INFORMATION_TYPES,
304+ namespace.getAllowedInformationTypes())
305+ self.assertEqual(
306+ InformationType.PROPRIETARY,
307+ namespace.getDefaultInformationType())
308+
309+ def test_proprietary_or_public_caller_grantee(self):
310+ namespace = self.makePackageBranchNamespace(
311+ BranchSharingPolicy.PROPRIETARY_OR_PUBLIC)
312+ distribution = namespace.sourcepackage.distribution
313+ grantee = self.factory.makePerson()
314+ with person_logged_in(distribution.owner):
315+ getUtility(IService, 'sharing').sharePillarInformation(
316+ distribution, grantee, distribution.owner,
317+ {InformationType.PROPRIETARY: SharingPermission.ALL})
318+ self.assertContentEqual(
319+ NON_EMBARGOED_INFORMATION_TYPES,
320+ namespace.getAllowedInformationTypes(grantee))
321+ self.assertEqual(
322+ InformationType.PROPRIETARY,
323+ namespace.getDefaultInformationType(grantee))
324+
325+ def test_proprietary_anyone(self):
326+ namespace = self.makePackageBranchNamespace(
327+ BranchSharingPolicy.PROPRIETARY)
328+ self.assertContentEqual([], namespace.getAllowedInformationTypes())
329+ self.assertIs(None, namespace.getDefaultInformationType())
330+
331+ def test_proprietary_branch_owner_grantee(self):
332+ namespace = self.makePackageBranchNamespace(
333+ BranchSharingPolicy.PROPRIETARY)
334+ distribution = namespace.sourcepackage.distribution
335+ with person_logged_in(distribution.owner):
336+ getUtility(IService, 'sharing').sharePillarInformation(
337+ distribution, namespace.owner, distribution.owner,
338+ {InformationType.PROPRIETARY: SharingPermission.ALL})
339+ self.assertContentEqual(
340+ [InformationType.PROPRIETARY],
341+ namespace.getAllowedInformationTypes())
342+ self.assertEqual(
343+ InformationType.PROPRIETARY,
344+ namespace.getDefaultInformationType())
345+
346+ def test_proprietary_caller_grantee(self):
347+ namespace = self.makePackageBranchNamespace(
348+ BranchSharingPolicy.PROPRIETARY)
349+ distribution = namespace.sourcepackage.distribution
350+ grantee = self.factory.makePerson()
351+ with person_logged_in(distribution.owner):
352+ getUtility(IService, 'sharing').sharePillarInformation(
353+ distribution, grantee, distribution.owner,
354+ {InformationType.PROPRIETARY: SharingPermission.ALL})
355+ self.assertContentEqual(
356+ [InformationType.PROPRIETARY],
357+ namespace.getAllowedInformationTypes(grantee))
358+ self.assertEqual(
359+ InformationType.PROPRIETARY,
360+ namespace.getDefaultInformationType(grantee))
361+
362+ def test_embargoed_or_proprietary_anyone(self):
363+ namespace = self.makePackageBranchNamespace(
364+ BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY)
365+ self.assertContentEqual([], namespace.getAllowedInformationTypes())
366+ self.assertIs(None, namespace.getDefaultInformationType())
367+
368+ def test_embargoed_or_proprietary_owner_grantee(self):
369+ namespace = self.makePackageBranchNamespace(
370+ BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY)
371+ distribution = namespace.sourcepackage.distribution
372+ with person_logged_in(distribution.owner):
373+ getUtility(IService, 'sharing').sharePillarInformation(
374+ distribution, namespace.owner, distribution.owner,
375+ {InformationType.PROPRIETARY: SharingPermission.ALL})
376+ self.assertContentEqual(
377+ [InformationType.PROPRIETARY, InformationType.EMBARGOED],
378+ namespace.getAllowedInformationTypes())
379+ self.assertEqual(
380+ InformationType.EMBARGOED,
381+ namespace.getDefaultInformationType())
382+
383+ def test_embargoed_or_proprietary_caller_grantee(self):
384+ namespace = self.makePackageBranchNamespace(
385+ BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY)
386+ distribution = namespace.sourcepackage.distribution
387+ grantee = self.factory.makePerson()
388+ with person_logged_in(distribution.owner):
389+ getUtility(IService, 'sharing').sharePillarInformation(
390+ distribution, grantee, distribution.owner,
391+ {InformationType.PROPRIETARY: SharingPermission.ALL})
392+ self.assertContentEqual(
393+ [InformationType.PROPRIETARY, InformationType.EMBARGOED],
394+ namespace.getAllowedInformationTypes(grantee))
395+ self.assertEqual(
396+ InformationType.EMBARGOED,
397+ namespace.getDefaultInformationType(grantee))
398+
399+ def test_grantee_has_no_artifact_grant(self):
400+ # The owner of a new branch in a distribution whose default
401+ # information type is non-public does not have an artifact grant
402+ # specifically for the new branch, because their existing policy
403+ # grant is sufficient.
404+ person = self.factory.makePerson()
405+ team = self.factory.makeTeam(members=[person])
406+ namespace = self.makePackageBranchNamespace(
407+ BranchSharingPolicy.PROPRIETARY, person=person)
408+ distribution = namespace.sourcepackage.distribution
409+ with person_logged_in(distribution.owner):
410+ getUtility(IService, 'sharing').sharePillarInformation(
411+ distribution, team, distribution.owner,
412+ {InformationType.PROPRIETARY: SharingPermission.ALL})
413+ branch = namespace.createBranch(
414+ BranchType.HOSTED, self.factory.getUniqueString(), person)
415+ [policy] = getUtility(IAccessPolicySource).find(
416+ [(distribution, InformationType.PROPRIETARY)])
417+ apgfs = getUtility(IAccessPolicyGrantFlatSource)
418+ self.assertContentEqual(
419+ [(distribution.owner, {policy: SharingPermission.ALL}, []),
420+ (team, {policy: SharingPermission.ALL}, [])],
421+ apgfs.findGranteePermissionsByPolicy([policy]))
422+ self.assertTrue(removeSecurityProxy(branch).visibleByUser(person))
423+
424+
425 class TestNamespaceSet(TestCaseWithFactory):
426 """Tests for `get_namespace`."""
427
428@@ -1041,21 +1220,6 @@ class TestPersonalBranchNamespaceAllowedInformationTypes(TestCaseWithFactory):
429 namespace.getAllowedInformationTypes())
430
431
432-class TestPackageBranchNamespaceAllowedInformationTypes(TestCaseWithFactory):
433- """Tests for PackageBranchNamespace.getAllowedInformationTypes."""
434-
435- layer = DatabaseFunctionalLayer
436-
437- def test_anyone(self):
438- # Source package branches are always public.
439- source_package = self.factory.makeSourcePackage()
440- person = self.factory.makePerson()
441- namespace = PackageBranchNamespace(person, source_package)
442- self.assertContentEqual(
443- PUBLIC_INFORMATION_TYPES,
444- namespace.getAllowedInformationTypes())
445-
446-
447 class BaseValidateNewBranchMixin:
448
449 layer = DatabaseFunctionalLayer
450diff --git a/lib/lp/code/model/tests/test_gitnamespace.py b/lib/lp/code/model/tests/test_gitnamespace.py
451index ac469e9..1ca8b5f 100644
452--- a/lib/lp/code/model/tests/test_gitnamespace.py
453+++ b/lib/lp/code/model/tests/test_gitnamespace.py
454@@ -885,6 +885,183 @@ class TestPackageGitNamespace(TestCaseWithFactory, NamespaceMixin):
455 repositories[0].namespace.collection.getRepositories())
456
457
458+class TestPackageGitNamespacePrivacyWithInformationType(TestCaseWithFactory):
459+ """Tests for the privacy aspects of `PackageGitNamespace`.
460+
461+ This tests the behaviour for a package in a distribution using the new
462+ branch_sharing_policy rules.
463+ """
464+
465+ layer = DatabaseFunctionalLayer
466+
467+ def makePackageGitNamespace(self, sharing_policy, person=None):
468+ if person is None:
469+ person = self.factory.makePerson()
470+ dsp = self.factory.makeDistributionSourcePackage()
471+ self.factory.makeCommercialSubscription(pillar=dsp.distribution)
472+ with person_logged_in(dsp.distribution.owner):
473+ dsp.distribution.setBranchSharingPolicy(sharing_policy)
474+ namespace = PackageGitNamespace(person, dsp)
475+ return namespace
476+
477+ def test_public_anyone(self):
478+ namespace = self.makePackageGitNamespace(BranchSharingPolicy.PUBLIC)
479+ self.assertContentEqual(
480+ FREE_INFORMATION_TYPES, namespace.getAllowedInformationTypes())
481+ self.assertEqual(
482+ InformationType.PUBLIC, namespace.getDefaultInformationType())
483+
484+ def test_forbidden_anyone(self):
485+ namespace = self.makePackageGitNamespace(BranchSharingPolicy.FORBIDDEN)
486+ self.assertEqual([], namespace.getAllowedInformationTypes())
487+ self.assertIsNone(namespace.getDefaultInformationType())
488+
489+ def test_public_or_proprietary_anyone(self):
490+ namespace = self.makePackageGitNamespace(
491+ BranchSharingPolicy.PUBLIC_OR_PROPRIETARY)
492+ self.assertContentEqual(
493+ NON_EMBARGOED_INFORMATION_TYPES,
494+ namespace.getAllowedInformationTypes())
495+ self.assertEqual(
496+ InformationType.PUBLIC, namespace.getDefaultInformationType())
497+
498+ def test_proprietary_or_public_anyone(self):
499+ namespace = self.makePackageGitNamespace(
500+ BranchSharingPolicy.PROPRIETARY_OR_PUBLIC)
501+ self.assertEqual([], namespace.getAllowedInformationTypes())
502+ self.assertIsNone(namespace.getDefaultInformationType())
503+
504+ def test_proprietary_or_public_owner_grantee(self):
505+ namespace = self.makePackageGitNamespace(
506+ BranchSharingPolicy.PROPRIETARY_OR_PUBLIC)
507+ distribution = namespace.distro_source_package.distribution
508+ with person_logged_in(distribution.owner):
509+ getUtility(IService, "sharing").sharePillarInformation(
510+ distribution, namespace.owner, distribution.owner,
511+ {InformationType.PROPRIETARY: SharingPermission.ALL})
512+ self.assertContentEqual(
513+ NON_EMBARGOED_INFORMATION_TYPES,
514+ namespace.getAllowedInformationTypes())
515+ self.assertEqual(
516+ InformationType.PROPRIETARY,
517+ namespace.getDefaultInformationType())
518+
519+ def test_proprietary_or_public_caller_grantee(self):
520+ namespace = self.makePackageGitNamespace(
521+ BranchSharingPolicy.PROPRIETARY_OR_PUBLIC)
522+ distribution = namespace.distro_source_package.distribution
523+ grantee = self.factory.makePerson()
524+ with person_logged_in(distribution.owner):
525+ getUtility(IService, "sharing").sharePillarInformation(
526+ distribution, grantee, distribution.owner,
527+ {InformationType.PROPRIETARY: SharingPermission.ALL})
528+ self.assertContentEqual(
529+ NON_EMBARGOED_INFORMATION_TYPES,
530+ namespace.getAllowedInformationTypes(grantee))
531+ self.assertEqual(
532+ InformationType.PROPRIETARY,
533+ namespace.getDefaultInformationType(grantee))
534+
535+ def test_proprietary_anyone(self):
536+ namespace = self.makePackageGitNamespace(
537+ BranchSharingPolicy.PROPRIETARY)
538+ self.assertEqual([], namespace.getAllowedInformationTypes())
539+ self.assertIsNone(namespace.getDefaultInformationType())
540+
541+ def test_proprietary_repository_owner_grantee(self):
542+ namespace = self.makePackageGitNamespace(
543+ BranchSharingPolicy.PROPRIETARY)
544+ distribution = namespace.distro_source_package.distribution
545+ with person_logged_in(distribution.owner):
546+ getUtility(IService, "sharing").sharePillarInformation(
547+ distribution, namespace.owner, distribution.owner,
548+ {InformationType.PROPRIETARY: SharingPermission.ALL})
549+ self.assertContentEqual(
550+ [InformationType.PROPRIETARY],
551+ namespace.getAllowedInformationTypes())
552+ self.assertEqual(
553+ InformationType.PROPRIETARY,
554+ namespace.getDefaultInformationType())
555+
556+ def test_proprietary_caller_grantee(self):
557+ namespace = self.makePackageGitNamespace(
558+ BranchSharingPolicy.PROPRIETARY)
559+ distribution = namespace.distro_source_package.distribution
560+ grantee = self.factory.makePerson()
561+ with person_logged_in(distribution.owner):
562+ getUtility(IService, "sharing").sharePillarInformation(
563+ distribution, grantee, distribution.owner,
564+ {InformationType.PROPRIETARY: SharingPermission.ALL})
565+ self.assertContentEqual(
566+ [InformationType.PROPRIETARY],
567+ namespace.getAllowedInformationTypes(grantee))
568+ self.assertEqual(
569+ InformationType.PROPRIETARY,
570+ namespace.getDefaultInformationType(grantee))
571+
572+ def test_embargoed_or_proprietary_anyone(self):
573+ namespace = self.makePackageGitNamespace(
574+ BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY)
575+ self.assertEqual([], namespace.getAllowedInformationTypes())
576+ self.assertIsNone(namespace.getDefaultInformationType())
577+
578+ def test_embargoed_or_proprietary_owner_grantee(self):
579+ namespace = self.makePackageGitNamespace(
580+ BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY)
581+ distribution = namespace.distro_source_package.distribution
582+ with person_logged_in(distribution.owner):
583+ getUtility(IService, "sharing").sharePillarInformation(
584+ distribution, namespace.owner, distribution.owner,
585+ {InformationType.PROPRIETARY: SharingPermission.ALL})
586+ self.assertContentEqual(
587+ [InformationType.PROPRIETARY, InformationType.EMBARGOED],
588+ namespace.getAllowedInformationTypes())
589+ self.assertEqual(
590+ InformationType.EMBARGOED,
591+ namespace.getDefaultInformationType())
592+
593+ def test_embargoed_or_proprietary_caller_grantee(self):
594+ namespace = self.makePackageGitNamespace(
595+ BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY)
596+ distribution = namespace.distro_source_package.distribution
597+ grantee = self.factory.makePerson()
598+ with person_logged_in(distribution.owner):
599+ getUtility(IService, "sharing").sharePillarInformation(
600+ distribution, grantee, distribution.owner,
601+ {InformationType.PROPRIETARY: SharingPermission.ALL})
602+ self.assertContentEqual(
603+ [InformationType.PROPRIETARY, InformationType.EMBARGOED],
604+ namespace.getAllowedInformationTypes(grantee))
605+ self.assertEqual(
606+ InformationType.EMBARGOED,
607+ namespace.getDefaultInformationType(grantee))
608+
609+ def test_grantee_has_no_artifact_grant(self):
610+ # The owner of a new repository in a distribution whose default
611+ # information type is non-public does not have an artifact grant
612+ # specifically for the new repository, because their existing policy
613+ # grant is sufficient.
614+ person = self.factory.makePerson()
615+ team = self.factory.makeTeam(members=[person])
616+ namespace = self.makePackageGitNamespace(
617+ BranchSharingPolicy.PROPRIETARY, person=person)
618+ distribution = namespace.distro_source_package.distribution
619+ with person_logged_in(distribution.owner):
620+ getUtility(IService, 'sharing').sharePillarInformation(
621+ distribution, team, distribution.owner,
622+ {InformationType.PROPRIETARY: SharingPermission.ALL})
623+ repository = namespace.createRepository(
624+ GitRepositoryType.HOSTED, person, self.factory.getUniqueUnicode())
625+ [policy] = getUtility(IAccessPolicySource).find(
626+ [(distribution, InformationType.PROPRIETARY)])
627+ apgfs = getUtility(IAccessPolicyGrantFlatSource)
628+ self.assertContentEqual(
629+ [(distribution.owner, {policy: SharingPermission.ALL}, []),
630+ (team, {policy: SharingPermission.ALL}, [])],
631+ apgfs.findGranteePermissionsByPolicy([policy]))
632+ self.assertTrue(removeSecurityProxy(repository).visibleByUser(person))
633+
634+
635 class BaseCanCreateRepositoriesMixin:
636 """Common tests for all namespaces."""
637
638@@ -1040,20 +1217,6 @@ class TestPersonalGitNamespaceAllowedInformationTypes(TestCaseWithFactory):
639 namespace.getAllowedInformationTypes())
640
641
642-class TestPackageGitNamespaceAllowedInformationTypes(TestCaseWithFactory):
643- """Tests for PackageGitNamespace.getAllowedInformationTypes."""
644-
645- layer = DatabaseFunctionalLayer
646-
647- def test_anyone(self):
648- # Package repositories are always public.
649- dsp = self.factory.makeDistributionSourcePackage()
650- person = self.factory.makePerson()
651- namespace = PackageGitNamespace(person, dsp)
652- self.assertContentEqual(
653- PUBLIC_INFORMATION_TYPES, namespace.getAllowedInformationTypes())
654-
655-
656 class TestOCIProjectGitNamespaceAllowedInformationTypes(TestCaseWithFactory):
657 """Tests for OCIProjectGitNamespace.getAllowedInformationTypes."""
658
659diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
660index 3272b3b..46781f2 100644
661--- a/lib/lp/code/model/tests/test_gitrepository.py
662+++ b/lib/lp/code/model/tests/test_gitrepository.py
663@@ -20,6 +20,10 @@ import pytz
664 import six
665 from storm.exceptions import LostObjectError
666 from storm.store import Store
667+from testscenarios import (
668+ load_tests_apply_scenarios,
669+ WithScenarios,
670+ )
671 from testtools.matchers import (
672 AnyMatch,
673 ContainsDict,
674@@ -141,6 +145,10 @@ from lp.registry.interfaces.accesspolicy import (
675 IAccessPolicyArtifactSource,
676 IAccessPolicySource,
677 )
678+from lp.registry.interfaces.distributionsourcepackage import (
679+ IDistributionSourcePackage,
680+ )
681+from lp.registry.interfaces.ociproject import IOCIProject
682 from lp.registry.interfaces.person import IPerson
683 from lp.registry.interfaces.persondistributionsourcepackage import (
684 IPersonDistributionSourcePackageFactory,
685@@ -1746,13 +1754,18 @@ class TestGitRepositoryPrivacy(TestCaseWithFactory):
686 get_policies_for_artifact(repository))
687
688 def test__reconcileAccess_for_package_repository(self):
689- # Git repository privacy isn't yet supported for distributions, so
690- # no AccessPolicyArtifact is created for a package repository.
691+ # _reconcileAccess uses a distribution policy for a package
692+ # repository.
693 repository = self.factory.makeGitRepository(
694 target=self.factory.makeDistributionSourcePackage(),
695 information_type=InformationType.USERDATA)
696+ [artifact] = getUtility(IAccessArtifactSource).ensure([repository])
697+ getUtility(IAccessPolicyArtifactSource).deleteByArtifact([artifact])
698 removeSecurityProxy(repository)._reconcileAccess()
699- self.assertEqual([], get_policies_for_artifact(repository))
700+ self.assertContentEqual(
701+ getUtility(IAccessPolicySource).find(
702+ [(repository.target.distribution, InformationType.USERDATA)]),
703+ get_policies_for_artifact(repository))
704
705 def test__reconcileAccess_for_oci_project_repository(self):
706 # Git repository privacy isn't yet supported for OCI projects, so no
707@@ -2356,17 +2369,36 @@ class TestGitRepositoryGetAllowedInformationTypes(TestCaseWithFactory):
708 repository.getAllowedInformationTypes(admin))
709
710
711-class TestGitRepositoryModerate(TestCaseWithFactory):
712+class TestGitRepositoryModerate(WithScenarios, TestCaseWithFactory):
713 """Test that project owners and commercial admins can moderate Git
714 repositories."""
715
716 layer = DatabaseFunctionalLayer
717+ scenarios = [
718+ ("project", {"target_factory_name": "makeProduct"}),
719+ ("distribution",
720+ {"target_factory_name": "makeDistributionSourcePackage"}),
721+ ("OCI project", {"target_factory_name": "makeOCIProject"}),
722+ ]
723+
724+ def _makeGitRepository(self, **kwargs):
725+ target = getattr(self.factory, self.target_factory_name)()
726+ return self.factory.makeGitRepository(target=target, **kwargs)
727+
728+ def _getPillar(self, repository):
729+ target = repository.target
730+ if IDistributionSourcePackage.providedBy(target):
731+ return target.distribution
732+ elif IOCIProject.providedBy(target):
733+ return target.pillar
734+ else:
735+ return target
736
737 def test_moderate_permission(self):
738 # Test the ModerateGitRepository security checker.
739- project = self.factory.makeProduct()
740- repository = self.factory.makeGitRepository(target=project)
741- with person_logged_in(project.owner):
742+ repository = self._makeGitRepository()
743+ pillar = self._getPillar(repository)
744+ with person_logged_in(pillar.owner):
745 self.assertTrue(check_permission("launchpad.Moderate", repository))
746 with celebrity_logged_in("commercial_admin"):
747 self.assertTrue(check_permission("launchpad.Moderate", repository))
748@@ -2376,24 +2408,26 @@ class TestGitRepositoryModerate(TestCaseWithFactory):
749
750 def test_methods_smoketest(self):
751 # Users with launchpad.Moderate can call transitionToInformationType.
752- project = self.factory.makeProduct()
753- repository = self.factory.makeGitRepository(target=project)
754- with person_logged_in(project.owner):
755- project.setBranchSharingPolicy(BranchSharingPolicy.PUBLIC)
756+ if self.target_factory_name == "makeOCIProject":
757+ self.skipTest("Not implemented for OCI projects yet.")
758+ repository = self._makeGitRepository()
759+ pillar = self._getPillar(repository)
760+ with person_logged_in(pillar.owner):
761+ pillar.setBranchSharingPolicy(BranchSharingPolicy.PUBLIC)
762 repository.transitionToInformationType(
763- InformationType.PRIVATESECURITY, project.owner)
764+ InformationType.PRIVATESECURITY, pillar.owner)
765 self.assertEqual(
766 InformationType.PRIVATESECURITY, repository.information_type)
767
768 def test_attribute_smoketest(self):
769 # Users with launchpad.Moderate can set attributes.
770- project = self.factory.makeProduct()
771- repository = self.factory.makeGitRepository(target=project)
772- with person_logged_in(project.owner):
773+ repository = self._makeGitRepository()
774+ pillar = self._getPillar(repository)
775+ with person_logged_in(pillar.owner):
776 repository.description = "something"
777- repository.reviewer = project.owner
778+ repository.reviewer = pillar.owner
779 self.assertEqual("something", repository.description)
780- self.assertEqual(project.owner, repository.reviewer)
781+ self.assertEqual(pillar.owner, repository.reviewer)
782
783
784 class TestGitRepositoryIsPersonTrustedReviewer(TestCaseWithFactory):
785@@ -5471,3 +5505,6 @@ class TestGitRepositoryMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
786 ["Caveat check for '%s' failed." %
787 find_caveats_by_name(macaroon2, "lp.expires")[0].caveat_id],
788 issuer, macaroon2, repository, user=repository.owner)
789+
790+
791+load_tests = load_tests_apply_scenarios
792diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py
793index 7e943c1..2e3a406 100644
794--- a/lib/lp/registry/interfaces/distribution.py
795+++ b/lib/lp/registry/interfaces/distribution.py
796@@ -29,6 +29,7 @@ from lazr.restful.declarations import (
797 exported,
798 exported_as_webservice_collection,
799 exported_as_webservice_entry,
800+ mutator_for,
801 operation_for_version,
802 operation_parameters,
803 operation_returns_collection_of,
804@@ -780,6 +781,41 @@ class IDistributionView(
805 class IDistributionEditRestricted(IOfficialBugTagTargetRestricted):
806 """IDistribution properties requiring launchpad.Edit permission."""
807
808+ @mutator_for(IDistributionView['bug_sharing_policy'])
809+ @operation_parameters(bug_sharing_policy=copy_field(
810+ IDistributionView['bug_sharing_policy']))
811+ @export_write_operation()
812+ @operation_for_version("devel")
813+ def setBugSharingPolicy(bug_sharing_policy):
814+ """Mutator for bug_sharing_policy.
815+
816+ Checks authorization and entitlement.
817+ """
818+
819+ @mutator_for(IDistributionView['branch_sharing_policy'])
820+ @operation_parameters(
821+ branch_sharing_policy=copy_field(
822+ IDistributionView['branch_sharing_policy']))
823+ @export_write_operation()
824+ @operation_for_version("devel")
825+ def setBranchSharingPolicy(branch_sharing_policy):
826+ """Mutator for branch_sharing_policy.
827+
828+ Checks authorization and entitlement.
829+ """
830+
831+ @mutator_for(IDistributionView['specification_sharing_policy'])
832+ @operation_parameters(
833+ specification_sharing_policy=copy_field(
834+ IDistributionView['specification_sharing_policy']))
835+ @export_write_operation()
836+ @operation_for_version("devel")
837+ def setSpecificationSharingPolicy(specification_sharing_policy):
838+ """Mutator for specification_sharing_policy.
839+
840+ Checks authorization and entitlement.
841+ """
842+
843 def checkInformationType(value):
844 """Check whether the information type change should be permitted.
845
846diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
847index 00bbc18..ff56b03 100644
848--- a/lib/lp/registry/model/distribution.py
849+++ b/lib/lp/registry/model/distribution.py
850@@ -55,7 +55,6 @@ from lp.app.enums import (
851 FREE_INFORMATION_TYPES,
852 InformationType,
853 PILLAR_INFORMATION_TYPES,
854- PRIVATE_INFORMATION_TYPES,
855 PROPRIETARY_INFORMATION_TYPES,
856 PUBLIC_INFORMATION_TYPES,
857 ServiceUsage,
858@@ -83,11 +82,17 @@ from lp.blueprints.enums import SpecificationFilter
859 from lp.blueprints.model.specification import (
860 HasSpecificationsMixin,
861 Specification,
862+ SPECIFICATION_POLICY_ALLOWED_TYPES,
863+ SPECIFICATION_POLICY_DEFAULT_TYPES,
864 )
865 from lp.blueprints.model.specificationsearch import search_specifications
866 from lp.blueprints.model.sprint import HasSprintsMixin
867 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
868 from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
869+from lp.bugs.interfaces.bugtarget import (
870+ BUG_POLICY_ALLOWED_TYPES,
871+ BUG_POLICY_DEFAULT_TYPES,
872+ )
873 from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
874 from lp.bugs.model.bugtarget import (
875 BugTargetBase,
876@@ -118,10 +123,6 @@ from lp.registry.errors import (
877 NoSuchDistroSeries,
878 ProprietaryPillar,
879 )
880-from lp.registry.interfaces.accesspolicy import (
881- IAccessPolicyGrantSource,
882- IAccessPolicySource,
883- )
884 from lp.registry.interfaces.distribution import (
885 IDistribution,
886 IDistributionSet,
887@@ -171,6 +172,7 @@ from lp.registry.model.milestone import (
888 from lp.registry.model.ociprojectname import OCIProjectName
889 from lp.registry.model.oopsreferences import referenced_oops
890 from lp.registry.model.pillar import HasAliasMixin
891+from lp.registry.model.sharingpolicy import SharingPolicyMixin
892 from lp.registry.model.sourcepackagename import SourcePackageName
893 from lp.registry.model.teammembership import TeamParticipation
894 from lp.services.database.bulk import load_referencing
895@@ -240,6 +242,24 @@ from lp.translations.model.potemplate import POTemplate
896 from lp.translations.model.translationpolicy import TranslationPolicyMixin
897
898
899+bug_policy_default = {
900+ InformationType.PUBLIC: BugSharingPolicy.PUBLIC,
901+ InformationType.PROPRIETARY: BugSharingPolicy.PROPRIETARY,
902+ }
903+
904+
905+branch_policy_default = {
906+ InformationType.PUBLIC: BranchSharingPolicy.PUBLIC,
907+ InformationType.PROPRIETARY: BranchSharingPolicy.PROPRIETARY,
908+ }
909+
910+
911+specification_policy_default = {
912+ InformationType.PUBLIC: SpecificationSharingPolicy.PUBLIC,
913+ InformationType.PROPRIETARY: SpecificationSharingPolicy.PROPRIETARY,
914+ }
915+
916+
917 @implementer(
918 IBugSummaryDimension, IDistribution, IHasBugSupervisor,
919 IHasBuildRecords, IHasIcon, IHasLogo, IHasMugshot,
920@@ -250,7 +270,7 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
921 OfficialBugTagTargetMixin, QuestionTargetMixin,
922 StructuralSubscriptionTargetMixin, HasMilestonesMixin,
923 HasDriversMixin, TranslationPolicyMixin,
924- InformationTypeMixin):
925+ InformationTypeMixin, SharingPolicyMixin):
926 """A distribution of an operating system, e.g. Debian GNU/Linux."""
927
928 _table = 'Distribution'
929@@ -462,6 +482,10 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
930 if (old_info_type == InformationType.PUBLIC and
931 value != InformationType.PUBLIC):
932 self._ensure_complimentary_subscription()
933+ self.setBranchSharingPolicy(branch_policy_default[value])
934+ self.setBugSharingPolicy(bug_policy_default[value])
935+ self.setSpecificationSharingPolicy(
936+ specification_policy_default[value])
937 self._ensurePolicies([value])
938
939 @property
940@@ -469,67 +493,21 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
941 """See `IPillar`."""
942 return "Distribution"
943
944- @property
945- def branch_sharing_policy(self):
946- """See `IHasSharingPolicies."""
947- # Sharing policy for distributions is always PUBLIC.
948- return BranchSharingPolicy.PUBLIC
949-
950- @property
951- def bug_sharing_policy(self):
952- """See `IHasSharingPolicies."""
953- # Sharing policy for distributions is always PUBLIC.
954- return BugSharingPolicy.PUBLIC
955-
956- @property
957- def specification_sharing_policy(self):
958- """See `IHasSharingPolicies."""
959- # Sharing policy for distributions is always PUBLIC.
960- return SpecificationSharingPolicy.PUBLIC
961+ bug_sharing_policy = DBEnum(
962+ enum=BugSharingPolicy, allow_none=True,
963+ default=BugSharingPolicy.PUBLIC)
964+ branch_sharing_policy = DBEnum(
965+ enum=BranchSharingPolicy, allow_none=True,
966+ default=BranchSharingPolicy.PUBLIC)
967+ specification_sharing_policy = DBEnum(
968+ enum=SpecificationSharingPolicy, allow_none=True,
969+ default=SpecificationSharingPolicy.PUBLIC)
970
971 # Cache of AccessPolicy.ids that convey launchpad.LimitedView.
972 # Unlike artifacts' cached access_policies, an AccessArtifactGrant
973 # to an artifact in the policy is sufficient for access.
974 access_policies = List(type=Int())
975
976- def _ensurePolicies(self, information_types):
977- # Ensure that the distribution has access policies for the specified
978- # information types.
979- aps = getUtility(IAccessPolicySource)
980- existing_policies = aps.findByPillar([self])
981- existing_types = {
982- access_policy.type for access_policy in existing_policies}
983- # Create the missing policies.
984- required_types = set(information_types).difference(
985- existing_types).intersection(PRIVATE_INFORMATION_TYPES)
986- policies = itertools.product((self,), required_types)
987- policies = getUtility(IAccessPolicySource).create(policies)
988-
989- # Add the maintainer to the policies.
990- grants = []
991- for p in policies:
992- grants.append((p, self.owner, self.owner))
993- getUtility(IAccessPolicyGrantSource).grant(grants)
994-
995- self._cacheAccessPolicies()
996-
997- def _cacheAccessPolicies(self):
998- # Update the cache of AccessPolicy.ids for which an
999- # AccessPolicyGrant or AccessArtifactGrant is sufficient to
1000- # convey launchpad.LimitedView on this Distribution.
1001- #
1002- # We only need a cache for proprietary types, and it only
1003- # includes proprietary policies in case a policy like Private
1004- # Security was somehow left around when a project was
1005- # transitioned to Proprietary.
1006- if self.information_type in PROPRIETARY_INFORMATION_TYPES:
1007- self.access_policies = [
1008- policy.id for policy in
1009- getUtility(IAccessPolicySource).find(
1010- [(self, type) for type in PROPRIETARY_INFORMATION_TYPES])]
1011- else:
1012- self.access_policies = None
1013-
1014 @cachedproperty
1015 def commercial_subscription(self):
1016 return IStore(CommercialSubscription).find(
1017@@ -1157,11 +1135,13 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
1018
1019 def getAllowedSpecificationInformationTypes(self):
1020 """See `ISpecificationTarget`."""
1021- return (InformationType.PUBLIC,)
1022+ return SPECIFICATION_POLICY_ALLOWED_TYPES[
1023+ self.specification_sharing_policy]
1024
1025 def getDefaultSpecificationInformationType(self):
1026 """See `ISpecificationTarget`."""
1027- return InformationType.PUBLIC
1028+ return SPECIFICATION_POLICY_DEFAULT_TYPES[
1029+ self.specification_sharing_policy]
1030
1031 def searchQuestions(self, search_text=None,
1032 status=QUESTION_STATUS_DEFAULT_SEARCH,
1033@@ -1639,11 +1619,11 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
1034
1035 def getAllowedBugInformationTypes(self):
1036 """See `IDistribution.`"""
1037- return FREE_INFORMATION_TYPES
1038+ return BUG_POLICY_ALLOWED_TYPES[self.bug_sharing_policy]
1039
1040 def getDefaultBugInformationType(self):
1041 """See `IDistribution.`"""
1042- return InformationType.PUBLIC
1043+ return BUG_POLICY_DEFAULT_TYPES[self.bug_sharing_policy]
1044
1045 def userCanEdit(self, user):
1046 """See `IDistribution`."""
1047@@ -1924,12 +1904,11 @@ class DistributionSet:
1048 distribution=distro, owner=owner, purpose=ArchivePurpose.PRIMARY)
1049 if information_type != InformationType.PUBLIC:
1050 distro._ensure_complimentary_subscription()
1051- # XXX cjwatson 2022-02-10: Replace this with sharing policies once
1052- # those are defined here.
1053- distro._ensurePolicies(
1054- [information_type]
1055- if information_type == InformationType.PROPRIETARY
1056- else FREE_INFORMATION_TYPES)
1057+ distro.setBugSharingPolicy(bug_policy_default[information_type])
1058+ distro.setBranchSharingPolicy(
1059+ branch_policy_default[information_type])
1060+ distro.setSpecificationSharingPolicy(
1061+ specification_policy_default[information_type])
1062 return distro
1063
1064 def getCurrentSourceReleases(self, distro_source_packagenames):
1065diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py
1066index 8e8c0f2..466a60a 100644
1067--- a/lib/lp/registry/model/product.py
1068+++ b/lib/lp/registry/model/product.py
1069@@ -13,7 +13,6 @@ __all__ = [
1070
1071 import datetime
1072 import http.client
1073-import itertools
1074 import operator
1075
1076 from lazr.lifecycle.event import ObjectModifiedEvent
1077@@ -57,7 +56,6 @@ from lp.app.enums import (
1078 FREE_INFORMATION_TYPES,
1079 InformationType,
1080 PILLAR_INFORMATION_TYPES,
1081- PRIVATE_INFORMATION_TYPES,
1082 PROPRIETARY_INFORMATION_TYPES,
1083 PUBLIC_INFORMATION_TYPES,
1084 service_uses_launchpad,
1085@@ -109,7 +107,6 @@ from lp.code.interfaces.branchcollection import IBranchCollection
1086 from lp.code.interfaces.gitcollection import IGitCollection
1087 from lp.code.interfaces.gitrepository import IGitRepositorySet
1088 from lp.code.model.branch import Branch
1089-from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
1090 from lp.code.model.gitrepository import GitRepository
1091 from lp.code.model.hasbranches import (
1092 HasBranchesMixin,
1093@@ -130,11 +127,6 @@ from lp.registry.errors import (
1094 CommercialSubscribersOnly,
1095 ProprietaryPillar,
1096 )
1097-from lp.registry.interfaces.accesspolicy import (
1098- IAccessPolicyArtifactSource,
1099- IAccessPolicyGrantSource,
1100- IAccessPolicySource,
1101- )
1102 from lp.registry.interfaces.ociproject import IOCIProjectSet
1103 from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
1104 from lp.registry.interfaces.person import (
1105@@ -172,6 +164,7 @@ from lp.registry.model.productlicense import ProductLicense
1106 from lp.registry.model.productrelease import ProductRelease
1107 from lp.registry.model.productseries import ProductSeries
1108 from lp.registry.model.series import ACTIVE_STATUSES
1109+from lp.registry.model.sharingpolicy import SharingPolicyMixin
1110 from lp.registry.model.sourcepackagename import SourcePackageName
1111 from lp.registry.model.teammembership import TeamParticipation
1112 from lp.services.database import bulk
1113@@ -293,7 +286,7 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements,
1114 OfficialBugTagTargetMixin, HasBranchesMixin,
1115 HasCustomLanguageCodesMixin, HasMergeProposalsMixin,
1116 HasCodeImportsMixin, InformationTypeMixin,
1117- TranslationPolicyMixin):
1118+ TranslationPolicyMixin, SharingPolicyMixin):
1119 """A Product."""
1120
1121 _table = 'Product'
1122@@ -675,42 +668,6 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements,
1123 notNull=True, default=False,
1124 storm_validator=_validate_license_approved)
1125
1126- def _prepare_to_set_sharing_policy(self, var, enum, kind, allowed_types):
1127- if (var not in [enum.PUBLIC, enum.FORBIDDEN] and
1128- not self.has_current_commercial_subscription):
1129- raise CommercialSubscribersOnly(
1130- "A current commercial subscription is required to use "
1131- "proprietary %s." % kind)
1132- if self.information_type != InformationType.PUBLIC:
1133- if InformationType.PUBLIC in allowed_types[var]:
1134- raise ProprietaryPillar(
1135- "The project is %s." % self.information_type.title)
1136- self._ensurePolicies(allowed_types[var])
1137-
1138- def setBranchSharingPolicy(self, branch_sharing_policy):
1139- """See `IProductEditRestricted`."""
1140- self._prepare_to_set_sharing_policy(
1141- branch_sharing_policy, BranchSharingPolicy, 'branches',
1142- BRANCH_POLICY_ALLOWED_TYPES)
1143- self.branch_sharing_policy = branch_sharing_policy
1144- self._pruneUnusedPolicies()
1145-
1146- def setBugSharingPolicy(self, bug_sharing_policy):
1147- """See `IProductEditRestricted`."""
1148- self._prepare_to_set_sharing_policy(
1149- bug_sharing_policy, BugSharingPolicy, 'bugs',
1150- BUG_POLICY_ALLOWED_TYPES)
1151- self.bug_sharing_policy = bug_sharing_policy
1152- self._pruneUnusedPolicies()
1153-
1154- def setSpecificationSharingPolicy(self, specification_sharing_policy):
1155- """See `IProductEditRestricted`."""
1156- self._prepare_to_set_sharing_policy(
1157- specification_sharing_policy, SpecificationSharingPolicy,
1158- 'specifications', SPECIFICATION_POLICY_ALLOWED_TYPES)
1159- self.specification_sharing_policy = specification_sharing_policy
1160- self._pruneUnusedPolicies()
1161-
1162 def getAllowedBugInformationTypes(self):
1163 """See `IProduct.`"""
1164 return BUG_POLICY_ALLOWED_TYPES[self.bug_sharing_policy]
1165@@ -729,71 +686,6 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements,
1166 return SPECIFICATION_POLICY_DEFAULT_TYPES[
1167 self.specification_sharing_policy]
1168
1169- def _ensurePolicies(self, information_types):
1170- # Ensure that the product has access policies for the specified
1171- # information types.
1172- aps = getUtility(IAccessPolicySource)
1173- existing_policies = aps.findByPillar([self])
1174- existing_types = {
1175- access_policy.type for access_policy in existing_policies}
1176- # Create the missing policies.
1177- required_types = set(information_types).difference(
1178- existing_types).intersection(PRIVATE_INFORMATION_TYPES)
1179- policies = itertools.product((self,), required_types)
1180- policies = getUtility(IAccessPolicySource).create(policies)
1181-
1182- # Add the maintainer to the policies.
1183- grants = []
1184- for p in policies:
1185- grants.append((p, self.owner, self.owner))
1186- getUtility(IAccessPolicyGrantSource).grant(grants)
1187-
1188- self._cacheAccessPolicies()
1189-
1190- def _cacheAccessPolicies(self):
1191- # Update the cache of AccessPolicy.ids for which an
1192- # AccessPolicyGrant or AccessArtifactGrant is sufficient to
1193- # convey launchpad.LimitedView on this Product.
1194- #
1195- # We only need a cache for proprietary types, and it only
1196- # includes proprietary policies in case a policy like Private
1197- # Security was somehow left around when a project was
1198- # transitioned to Proprietary.
1199- if self.information_type in PROPRIETARY_INFORMATION_TYPES:
1200- self.access_policies = [
1201- policy.id for policy in
1202- getUtility(IAccessPolicySource).find(
1203- [(self, type) for type in PROPRIETARY_INFORMATION_TYPES])]
1204- else:
1205- self.access_policies = None
1206-
1207- def _pruneUnusedPolicies(self):
1208- allowed_bug_types = set(
1209- BUG_POLICY_ALLOWED_TYPES.get(
1210- self.bug_sharing_policy, FREE_INFORMATION_TYPES))
1211- allowed_branch_types = set(
1212- BRANCH_POLICY_ALLOWED_TYPES.get(
1213- self.branch_sharing_policy, FREE_INFORMATION_TYPES))
1214- allowed_spec_types = set(
1215- SPECIFICATION_POLICY_ALLOWED_TYPES.get(
1216- self.specification_sharing_policy, [InformationType.PUBLIC]))
1217- allowed_types = (
1218- allowed_bug_types | allowed_branch_types | allowed_spec_types)
1219- allowed_types.add(self.information_type)
1220- # Fetch all APs, and after filtering out ones that are forbidden
1221- # by the bug, branch, and specification policies, the APs that have no
1222- # APAs are unused and can be deleted.
1223- ap_source = getUtility(IAccessPolicySource)
1224- access_policies = set(ap_source.findByPillar([self]))
1225- apa_source = getUtility(IAccessPolicyArtifactSource)
1226- unused_aps = [
1227- ap for ap in access_policies
1228- if ap.type not in allowed_types
1229- and apa_source.findByPolicy([ap]).is_empty()]
1230- getUtility(IAccessPolicyGrantSource).revokeByPolicy(unused_aps)
1231- ap_source.delete([(ap.pillar, ap.type) for ap in unused_aps])
1232- self._cacheAccessPolicies()
1233-
1234 @cachedproperty
1235 def commercial_subscription(self):
1236 return IStore(CommercialSubscription).find(
1237diff --git a/lib/lp/registry/model/sharingpolicy.py b/lib/lp/registry/model/sharingpolicy.py
1238new file mode 100644
1239index 0000000..acc28d6
1240--- /dev/null
1241+++ b/lib/lp/registry/model/sharingpolicy.py
1242@@ -0,0 +1,155 @@
1243+# Copyright 2012-2022 Canonical Ltd. This software is licensed under the
1244+# GNU Affero General Public License version 3 (see the file LICENSE).
1245+
1246+"""Sharing policies for pillars."""
1247+
1248+__all__ = [
1249+ "SharingPolicyMixin",
1250+ ]
1251+
1252+import itertools
1253+
1254+from zope.component import getUtility
1255+
1256+from lp.app.enums import (
1257+ FREE_INFORMATION_TYPES,
1258+ InformationType,
1259+ PRIVATE_INFORMATION_TYPES,
1260+ PROPRIETARY_INFORMATION_TYPES,
1261+ )
1262+from lp.blueprints.model.specification import (
1263+ SPECIFICATION_POLICY_ALLOWED_TYPES,
1264+ )
1265+from lp.bugs.interfaces.bugtarget import BUG_POLICY_ALLOWED_TYPES
1266+from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
1267+from lp.registry.enums import (
1268+ BranchSharingPolicy,
1269+ BugSharingPolicy,
1270+ SpecificationSharingPolicy,
1271+ )
1272+from lp.registry.errors import (
1273+ CommercialSubscribersOnly,
1274+ ProprietaryPillar,
1275+ )
1276+from lp.registry.interfaces.accesspolicy import (
1277+ IAccessPolicyArtifactSource,
1278+ IAccessPolicyGrantSource,
1279+ IAccessPolicySource,
1280+ )
1281+
1282+
1283+class SharingPolicyMixin:
1284+ """Sharing policy support for pillars.
1285+
1286+ The pillar should define `bug_sharing_policy`, `branch_sharing_policy`,
1287+ `specification_sharing_policy`, and `access_policies` fields.
1288+ """
1289+
1290+ def _prepare_to_set_sharing_policy(self, var, enum, kind, allowed_types):
1291+ if (var not in {enum.PUBLIC, enum.FORBIDDEN} and
1292+ not self.has_current_commercial_subscription):
1293+ raise CommercialSubscribersOnly(
1294+ "A current commercial subscription is required to use "
1295+ "proprietary %s." % kind)
1296+ if self.information_type != InformationType.PUBLIC:
1297+ if InformationType.PUBLIC in allowed_types[var]:
1298+ raise ProprietaryPillar(
1299+ "The pillar is %s." % self.information_type.title)
1300+ self._ensurePolicies(allowed_types[var])
1301+
1302+ def setBranchSharingPolicy(self, branch_sharing_policy):
1303+ """Mutator for branch_sharing_policy.
1304+
1305+ Checks authorization and entitlement.
1306+ """
1307+ self._prepare_to_set_sharing_policy(
1308+ branch_sharing_policy, BranchSharingPolicy, 'branches',
1309+ BRANCH_POLICY_ALLOWED_TYPES)
1310+ self.branch_sharing_policy = branch_sharing_policy
1311+ self._pruneUnusedPolicies()
1312+
1313+ def setBugSharingPolicy(self, bug_sharing_policy):
1314+ """Mutator for bug_sharing_policy.
1315+
1316+ Checks authorization and entitlement.
1317+ """
1318+ self._prepare_to_set_sharing_policy(
1319+ bug_sharing_policy, BugSharingPolicy, 'bugs',
1320+ BUG_POLICY_ALLOWED_TYPES)
1321+ self.bug_sharing_policy = bug_sharing_policy
1322+ self._pruneUnusedPolicies()
1323+
1324+ def setSpecificationSharingPolicy(self, specification_sharing_policy):
1325+ """Mutator for specification_sharing_policy.
1326+
1327+ Checks authorization and entitlement.
1328+ """
1329+ self._prepare_to_set_sharing_policy(
1330+ specification_sharing_policy, SpecificationSharingPolicy,
1331+ 'specifications', SPECIFICATION_POLICY_ALLOWED_TYPES)
1332+ self.specification_sharing_policy = specification_sharing_policy
1333+ self._pruneUnusedPolicies()
1334+
1335+ def _ensurePolicies(self, information_types):
1336+ # Ensure that the pillar has access policies for the specified
1337+ # information types.
1338+ aps = getUtility(IAccessPolicySource)
1339+ existing_policies = aps.findByPillar([self])
1340+ existing_types = {
1341+ access_policy.type for access_policy in existing_policies}
1342+ # Create the missing policies.
1343+ required_types = set(information_types).difference(
1344+ existing_types).intersection(PRIVATE_INFORMATION_TYPES)
1345+ policies = itertools.product((self,), required_types)
1346+ policies = getUtility(IAccessPolicySource).create(policies)
1347+
1348+ # Add the maintainer to the policies.
1349+ grants = []
1350+ for p in policies:
1351+ grants.append((p, self.owner, self.owner))
1352+ getUtility(IAccessPolicyGrantSource).grant(grants)
1353+
1354+ self._cacheAccessPolicies()
1355+
1356+ def _cacheAccessPolicies(self):
1357+ # Update the cache of AccessPolicy.ids for which an
1358+ # AccessPolicyGrant or AccessArtifactGrant is sufficient to convey
1359+ # launchpad.LimitedView on this pillar.
1360+ #
1361+ # We only need a cache for proprietary types, and it only includes
1362+ # proprietary policies in case a policy like Private Security was
1363+ # somehow left around when a pillar was transitioned to Proprietary.
1364+ if self.information_type in PROPRIETARY_INFORMATION_TYPES:
1365+ self.access_policies = [
1366+ policy.id for policy in
1367+ getUtility(IAccessPolicySource).find(
1368+ [(self, type) for type in PROPRIETARY_INFORMATION_TYPES])]
1369+ else:
1370+ self.access_policies = None
1371+
1372+ def _pruneUnusedPolicies(self):
1373+ allowed_bug_types = set(
1374+ BUG_POLICY_ALLOWED_TYPES.get(
1375+ self.bug_sharing_policy, FREE_INFORMATION_TYPES))
1376+ allowed_branch_types = set(
1377+ BRANCH_POLICY_ALLOWED_TYPES.get(
1378+ self.branch_sharing_policy, FREE_INFORMATION_TYPES))
1379+ allowed_spec_types = set(
1380+ SPECIFICATION_POLICY_ALLOWED_TYPES.get(
1381+ self.specification_sharing_policy, [InformationType.PUBLIC]))
1382+ allowed_types = (
1383+ allowed_bug_types | allowed_branch_types | allowed_spec_types)
1384+ allowed_types.add(self.information_type)
1385+ # Fetch all APs, and after filtering out ones that are forbidden
1386+ # by the bug, branch, and specification policies, the APs that have no
1387+ # APAs are unused and can be deleted.
1388+ ap_source = getUtility(IAccessPolicySource)
1389+ access_policies = set(ap_source.findByPillar([self]))
1390+ apa_source = getUtility(IAccessPolicyArtifactSource)
1391+ unused_aps = [
1392+ ap for ap in access_policies
1393+ if ap.type not in allowed_types
1394+ and apa_source.findByPolicy([ap]).is_empty()]
1395+ getUtility(IAccessPolicyGrantSource).revokeByPolicy(unused_aps)
1396+ ap_source.delete([(ap.pillar, ap.type) for ap in unused_aps])
1397+ self._cacheAccessPolicies()
1398diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
1399index 9480512..9713061 100644
1400--- a/lib/lp/registry/services/sharingservice.py
1401+++ b/lib/lp/registry/services/sharingservice.py
1402@@ -549,15 +549,9 @@ class SharingService:
1403
1404 def getBranchSharingPolicies(self, pillar):
1405 """See `ISharingService`."""
1406- # Only Products have branch sharing policies. Distributions just
1407- # default to Public.
1408- # If the branch sharing policy is EMBARGOED_OR_PROPRIETARY, then we
1409- # do not allow any other policies.
1410 allowed_policies = [BranchSharingPolicy.PUBLIC]
1411- # Commercial projects also allow proprietary branches.
1412- if (IProduct.providedBy(pillar)
1413- and pillar.has_current_commercial_subscription):
1414-
1415+ # Commercial pillars also allow proprietary branches.
1416+ if pillar.has_current_commercial_subscription:
1417 if pillar.private:
1418 allowed_policies = [
1419 BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY,
1420@@ -579,13 +573,9 @@ class SharingService:
1421
1422 def getBugSharingPolicies(self, pillar):
1423 """See `ISharingService`."""
1424- # Only Products have bug sharing policies. Distributions just
1425- # default to Public.
1426 allowed_policies = [BugSharingPolicy.PUBLIC]
1427- # Commercial projects also allow proprietary bugs.
1428- if (IProduct.providedBy(pillar)
1429- and pillar.has_current_commercial_subscription):
1430-
1431+ # Commercial pillars also allow proprietary bugs.
1432+ if pillar.has_current_commercial_subscription:
1433 if pillar.private:
1434 allowed_policies = [
1435 BugSharingPolicy.EMBARGOED_OR_PROPRIETARY,
1436@@ -607,13 +597,8 @@ class SharingService:
1437
1438 def getSpecificationSharingPolicies(self, pillar):
1439 """See `ISharingService`."""
1440- # Only Products have specification sharing policies. Distributions just
1441- # default to Public.
1442 allowed_policies = [SpecificationSharingPolicy.PUBLIC]
1443- # Commercial projects also allow proprietary specifications.
1444- if (IProduct.providedBy(pillar)
1445- and pillar.has_current_commercial_subscription):
1446-
1447+ if pillar.has_current_commercial_subscription:
1448 if pillar.private:
1449 allowed_policies = [
1450 SpecificationSharingPolicy.EMBARGOED_OR_PROPRIETARY,
1451@@ -910,10 +895,6 @@ class SharingService:
1452 if (not branch_sharing_policy and not bug_sharing_policy and not
1453 specification_sharing_policy):
1454 return None
1455- # Only Products have sharing policies.
1456- if not IProduct.providedBy(pillar):
1457- raise ValueError(
1458- "Sharing policies are only supported for products.")
1459 if branch_sharing_policy:
1460 pillar.setBranchSharingPolicy(branch_sharing_policy)
1461 if bug_sharing_policy:
1462diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py
1463index 8ce3be1..8c557f8 100644
1464--- a/lib/lp/registry/services/tests/test_sharingservice.py
1465+++ b/lib/lp/registry/services/tests/test_sharingservice.py
1466@@ -91,10 +91,6 @@ class PillarScenariosMixin(WithScenarios):
1467 self.skipTest("Only relevant for Product.")
1468
1469 def _makePillar(self, **kwargs):
1470- if ("bug_sharing_policy" in kwargs or
1471- "branch_sharing_policy" in kwargs or
1472- "specification_sharing_policy" in kwargs):
1473- self._skipUnlessProduct()
1474 return getattr(self.factory, self.pillar_factory_name)(**kwargs)
1475
1476 def _makeBranch(self, pillar, **kwargs):
1477@@ -246,14 +242,12 @@ class TestSharingService(
1478 pillar, [BranchSharingPolicy.PUBLIC])
1479
1480 def test_getBranchSharingPolicies_expired_commercial(self):
1481- self._skipUnlessProduct()
1482 pillar = self._makePillar()
1483 self.factory.makeCommercialSubscription(pillar, expired=True)
1484 self._assert_getBranchSharingPolicies(
1485 pillar, [BranchSharingPolicy.PUBLIC])
1486
1487 def test_getBranchSharingPolicies_commercial(self):
1488- self._skipUnlessProduct()
1489 pillar = self._makePillar()
1490 self.factory.makeCommercialSubscription(pillar)
1491 self._assert_getBranchSharingPolicies(
1492@@ -266,7 +260,6 @@ class TestSharingService(
1493 def test_getBranchSharingPolicies_non_public(self):
1494 # When the pillar is non-public the policy options are limited to
1495 # only proprietary or embargoed/proprietary.
1496- self._skipUnlessProduct()
1497 owner = self.factory.makePerson()
1498 pillar = self._makePillar(
1499 information_type=InformationType.PROPRIETARY,
1500@@ -280,7 +273,6 @@ class TestSharingService(
1501 def test_getBranchSharingPolicies_disallowed_policy(self):
1502 # getBranchSharingPolicies includes a pillar's current policy even if
1503 # it is nominally not allowed.
1504- self._skipUnlessProduct()
1505 pillar = self._makePillar()
1506 self.factory.makeCommercialSubscription(pillar, expired=True)
1507 with person_logged_in(pillar.owner):
1508@@ -313,14 +305,12 @@ class TestSharingService(
1509 pillar, [SpecificationSharingPolicy.PUBLIC])
1510
1511 def test_getSpecificationSharingPolicies_expired_commercial(self):
1512- self._skipUnlessProduct()
1513 pillar = self._makePillar()
1514 self.factory.makeCommercialSubscription(pillar, expired=True)
1515 self._assert_getSpecificationSharingPolicies(
1516 pillar, [SpecificationSharingPolicy.PUBLIC])
1517
1518 def test_getSpecificationSharingPolicies_commercial(self):
1519- self._skipUnlessProduct()
1520 pillar = self._makePillar()
1521 self.factory.makeCommercialSubscription(pillar)
1522 self._assert_getSpecificationSharingPolicies(
1523@@ -333,7 +323,6 @@ class TestSharingService(
1524 def test_getSpecificationSharingPolicies_non_public(self):
1525 # When the pillar is non-public the policy options are limited to
1526 # only proprietary or embargoed/proprietary.
1527- self._skipUnlessProduct()
1528 owner = self.factory.makePerson()
1529 pillar = self._makePillar(
1530 information_type=InformationType.PROPRIETARY,
1531@@ -367,13 +356,11 @@ class TestSharingService(
1532 self._assert_getBugSharingPolicies(pillar, [BugSharingPolicy.PUBLIC])
1533
1534 def test_getBugSharingPolicies_expired_commercial(self):
1535- self._skipUnlessProduct()
1536 pillar = self._makePillar()
1537 self.factory.makeCommercialSubscription(pillar, expired=True)
1538 self._assert_getBugSharingPolicies(pillar, [BugSharingPolicy.PUBLIC])
1539
1540 def test_getBugSharingPolicies_commercial(self):
1541- self._skipUnlessProduct()
1542 pillar = self._makePillar()
1543 self.factory.makeCommercialSubscription(pillar)
1544 self._assert_getBugSharingPolicies(
1545@@ -386,7 +373,6 @@ class TestSharingService(
1546 def test_getBugSharingPolicies_non_public(self):
1547 # When the pillar is non-public the policy options are limited to
1548 # only proprietary or embargoed/proprietary.
1549- self._skipUnlessProduct()
1550 owner = self.factory.makePerson()
1551 pillar = self._makePillar(
1552 information_type=InformationType.PROPRIETARY,
1553@@ -400,7 +386,6 @@ class TestSharingService(
1554 def test_getBugSharingPolicies_disallowed_policy(self):
1555 # getBugSharingPolicies includes a pillar's current policy even if it
1556 # is nominally not allowed.
1557- self._skipUnlessProduct()
1558 pillar = self._makePillar()
1559 self.factory.makeCommercialSubscription(pillar, expired=True)
1560 with person_logged_in(pillar.owner):
1561@@ -1294,7 +1279,6 @@ class TestSharingService(
1562
1563 def test_ensureAccessGrantsBranches(self):
1564 # Access grants can be created for branches.
1565- self._skipUnlessProduct()
1566 owner = self.factory.makePerson()
1567 pillar = self._makePillar(owner=owner)
1568 login_person(owner)
1569@@ -1305,7 +1289,6 @@ class TestSharingService(
1570
1571 def test_ensureAccessGrantsGitRepositories(self):
1572 # Access grants can be created for Git repositories.
1573- self._skipUnlessProduct()
1574 owner = self.factory.makePerson()
1575 pillar = self._makePillar(owner=owner)
1576 login_person(owner)
1577@@ -1375,7 +1358,6 @@ class TestSharingService(
1578
1579 def test_updatePillarBugSharingPolicy(self):
1580 # updatePillarSharingPolicies works for bugs.
1581- self._skipUnlessProduct()
1582 owner = self.factory.makePerson()
1583 pillar = self._makePillar(owner=owner)
1584 self.factory.makeCommercialSubscription(pillar)
1585@@ -1388,7 +1370,6 @@ class TestSharingService(
1586
1587 def test_updatePillarBranchSharingPolicy(self):
1588 # updatePillarSharingPolicies works for branches.
1589- self._skipUnlessProduct()
1590 owner = self.factory.makePerson()
1591 pillar = self._makePillar(owner=owner)
1592 self.factory.makeCommercialSubscription(pillar)
1593@@ -1401,7 +1382,6 @@ class TestSharingService(
1594
1595 def test_updatePillarSpecificationSharingPolicy(self):
1596 # updatePillarSharingPolicies works for specifications.
1597- self._skipUnlessProduct()
1598 owner = self.factory.makePerson()
1599 pillar = self._makePillar(owner=owner)
1600 self.factory.makeCommercialSubscription(pillar)
1601@@ -1730,7 +1710,6 @@ class TestSharingService(
1602
1603 def test_getPeopleWithAccessBranches(self):
1604 # Test the getPeopleWithoutAccess method with branches.
1605- self._skipUnlessProduct()
1606 owner = self.factory.makePerson()
1607 pillar = self._makePillar(owner=owner)
1608 branch = self._makeBranch(
1609@@ -1741,7 +1720,6 @@ class TestSharingService(
1610
1611 def test_getPeopleWithAccessGitRepositories(self):
1612 # Test the getPeopleWithoutAccess method with Git repositories.
1613- self._skipUnlessProduct()
1614 owner = self.factory.makePerson()
1615 pillar = self._makePillar(owner=owner)
1616 gitrepository = self._makeGitRepository(
1617diff --git a/lib/lp/registry/tests/test_distribution.py b/lib/lp/registry/tests/test_distribution.py
1618index 49f55df..e2a5647 100644
1619--- a/lib/lp/registry/tests/test_distribution.py
1620+++ b/lib/lp/registry/tests/test_distribution.py
1621@@ -31,6 +31,12 @@ from lp.app.errors import (
1622 ServiceUsageForbidden,
1623 )
1624 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1625+from lp.app.interfaces.services import IService
1626+from lp.blueprints.model.specification import (
1627+ SPECIFICATION_POLICY_ALLOWED_TYPES,
1628+ )
1629+from lp.bugs.interfaces.bugtarget import BUG_POLICY_ALLOWED_TYPES
1630+from lp.code.model.branchnamespace import BRANCH_POLICY_ALLOWED_TYPES
1631 from lp.oci.tests.helpers import OCIConfigHelperMixin
1632 from lp.registry.enums import (
1633 BranchSharingPolicy,
1634@@ -38,6 +44,7 @@ from lp.registry.enums import (
1635 DistributionDefaultTraversalPolicy,
1636 EXCLUSIVE_TEAM_POLICY,
1637 INCLUSIVE_TEAM_POLICY,
1638+ SpecificationSharingPolicy,
1639 TeamMembershipPolicy,
1640 )
1641 from lp.registry.errors import (
1642@@ -69,6 +76,7 @@ from lp.soyuz.interfaces.distributionsourcepackagerelease import (
1643 IDistributionSourcePackageRelease,
1644 )
1645 from lp.testing import (
1646+ admin_logged_in,
1647 api_url,
1648 celebrity_logged_in,
1649 login_person,
1650@@ -398,13 +406,52 @@ class TestDistribution(TestCaseWithFactory):
1651 grantees = {grant.grantee for grant in grants}
1652 self.assertEqual(expected_grantess, grantees)
1653
1654+ def test_open_creation_sharing_policies(self):
1655+ # Creating a new open (non-proprietary) distribution sets the bug
1656+ # and branch sharing policies to public, and creates policies if
1657+ # required.
1658+ owner = self.factory.makePerson()
1659+ with person_logged_in(owner):
1660+ distribution = self.factory.makeDistribution(owner=owner)
1661+ self.assertEqual(
1662+ BugSharingPolicy.PUBLIC, distribution.bug_sharing_policy)
1663+ self.assertEqual(
1664+ BranchSharingPolicy.PUBLIC, distribution.branch_sharing_policy)
1665+ self.assertEqual(
1666+ SpecificationSharingPolicy.PUBLIC,
1667+ distribution.specification_sharing_policy)
1668+ aps = getUtility(IAccessPolicySource).findByPillar([distribution])
1669+ expected = [
1670+ InformationType.USERDATA, InformationType.PRIVATESECURITY]
1671+ self.assertContentEqual(expected, [policy.type for policy in aps])
1672+
1673+ def test_proprietary_creation_sharing_policies(self):
1674+ # Creating a new proprietary distribution sets the bug, branch, and
1675+ # specification sharing policies to proprietary.
1676+ owner = self.factory.makePerson()
1677+ with person_logged_in(owner):
1678+ distribution = self.factory.makeDistribution(
1679+ owner=owner, information_type=InformationType.PROPRIETARY)
1680+ self.assertEqual(
1681+ BugSharingPolicy.PROPRIETARY, distribution.bug_sharing_policy)
1682+ self.assertEqual(
1683+ BranchSharingPolicy.PROPRIETARY,
1684+ distribution.branch_sharing_policy)
1685+ self.assertEqual(
1686+ SpecificationSharingPolicy.PROPRIETARY,
1687+ distribution.specification_sharing_policy)
1688+ aps = getUtility(IAccessPolicySource).findByPillar([distribution])
1689+ expected = [InformationType.PROPRIETARY]
1690+ self.assertContentEqual(expected, [policy.type for policy in aps])
1691+
1692 def test_change_info_type_proprietary_check_artifacts(self):
1693 # Cannot change distribution information_type if any artifacts are
1694 # public.
1695- # XXX cjwatson 2022-02-11: Make this use
1696- # artifact.transitionToInformationType once sharing policies are in
1697- # place.
1698- distribution = self.factory.makeDistribution()
1699+ distribution = self.factory.makeDistribution(
1700+ specification_sharing_policy=(
1701+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY),
1702+ bug_sharing_policy=BugSharingPolicy.PUBLIC_OR_PROPRIETARY,
1703+ branch_sharing_policy=BranchSharingPolicy.PUBLIC_OR_PROPRIETARY)
1704 self.useContext(person_logged_in(distribution.owner))
1705 spec = self.factory.makeSpecification(distribution=distribution)
1706 for info_type in PRIVATE_DISTRIBUTION_TYPES:
1707@@ -412,32 +459,34 @@ class TestDistribution(TestCaseWithFactory):
1708 CannotChangeInformationType,
1709 "Some blueprints are public."):
1710 distribution.information_type = info_type
1711- removeSecurityProxy(spec).information_type = (
1712- InformationType.PROPRIETARY)
1713+ spec.transitionToInformationType(
1714+ InformationType.PROPRIETARY, distribution.owner)
1715 dsp = self.factory.makeDistributionSourcePackage(
1716 distribution=distribution)
1717 bug = self.factory.makeBug(target=dsp)
1718 for bug_info_type in FREE_INFORMATION_TYPES:
1719- removeSecurityProxy(bug).information_type = bug_info_type
1720+ bug.transitionToInformationType(bug_info_type, distribution.owner)
1721 for info_type in PRIVATE_DISTRIBUTION_TYPES:
1722 with ExpectedException(
1723 CannotChangeInformationType,
1724 "Some bugs are neither proprietary nor embargoed."):
1725 distribution.information_type = info_type
1726- removeSecurityProxy(bug).information_type = InformationType.PROPRIETARY
1727+ bug.transitionToInformationType(
1728+ InformationType.PROPRIETARY, distribution.owner)
1729 distroseries = self.factory.makeDistroSeries(distribution=distribution)
1730 sp = self.factory.makeSourcePackage(distroseries=distroseries)
1731 branch = self.factory.makeBranch(sourcepackage=sp)
1732 for branch_info_type in FREE_INFORMATION_TYPES:
1733- removeSecurityProxy(branch).information_type = branch_info_type
1734+ branch.transitionToInformationType(
1735+ branch_info_type, distribution.owner)
1736 for info_type in PRIVATE_DISTRIBUTION_TYPES:
1737 with ExpectedException(
1738 CannotChangeInformationType,
1739 "Some branches are neither proprietary nor "
1740 "embargoed."):
1741 distribution.information_type = info_type
1742- removeSecurityProxy(branch).information_type = (
1743- InformationType.PROPRIETARY)
1744+ branch.transitionToInformationType(
1745+ InformationType.PROPRIETARY, distribution.owner)
1746 for info_type in PRIVATE_DISTRIBUTION_TYPES:
1747 distribution.information_type = info_type
1748
1749@@ -457,6 +506,40 @@ class TestDistribution(TestCaseWithFactory):
1750 else:
1751 distribution.information_type = info_type
1752
1753+ def test_change_info_type_proprietary_sets_policies(self):
1754+ # Changing information type from public to proprietary sets the
1755+ # appropriate policies.
1756+ distribution = self.factory.makeDistribution()
1757+ with person_logged_in(distribution.owner):
1758+ distribution.information_type = InformationType.PROPRIETARY
1759+ self.assertEqual(
1760+ BranchSharingPolicy.PROPRIETARY,
1761+ distribution.branch_sharing_policy)
1762+ self.assertEqual(
1763+ BugSharingPolicy.PROPRIETARY, distribution.bug_sharing_policy)
1764+ self.assertEqual(
1765+ SpecificationSharingPolicy.PROPRIETARY,
1766+ distribution.specification_sharing_policy)
1767+
1768+ def test_proprietary_to_public_leaves_policies(self):
1769+ # Changing information type from public leaves sharing policies
1770+ # unchanged.
1771+ owner = self.factory.makePerson()
1772+ distribution = self.factory.makeDistribution(
1773+ information_type=InformationType.PROPRIETARY, owner=owner)
1774+ with person_logged_in(owner):
1775+ distribution.information_type = InformationType.PUBLIC
1776+ # Setting information type to the current type should be a no-op.
1777+ distribution.information_type = InformationType.PUBLIC
1778+ self.assertEqual(
1779+ BranchSharingPolicy.PROPRIETARY,
1780+ distribution.branch_sharing_policy)
1781+ self.assertEqual(
1782+ BugSharingPolicy.PROPRIETARY, distribution.bug_sharing_policy)
1783+ self.assertEqual(
1784+ SpecificationSharingPolicy.PROPRIETARY,
1785+ distribution.specification_sharing_policy)
1786+
1787 def test_cacheAccessPolicies(self):
1788 # Distribution.access_policies is a list caching AccessPolicy.ids
1789 # for which an AccessPolicyGrant or AccessArtifactGrant gives a
1790@@ -483,6 +566,18 @@ class TestDistribution(TestCaseWithFactory):
1791 naked_distribution.information_type = InformationType.PUBLIC
1792 self.assertIsNone(naked_distribution.access_policies)
1793
1794+ # Proprietary distributions can have both Proprietary and Embargoed
1795+ # artifacts, and someone who can see either needs LimitedView on the
1796+ # pillar they're on. So both policies are permissible if they
1797+ # exist.
1798+ naked_distribution.information_type = InformationType.PROPRIETARY
1799+ naked_distribution.setBugSharingPolicy(
1800+ BugSharingPolicy.EMBARGOED_OR_PROPRIETARY)
1801+ [emb_policy] = aps.find([(distribution, InformationType.EMBARGOED)])
1802+ self.assertContentEqual(
1803+ [prop_policy.id, emb_policy.id],
1804+ naked_distribution.access_policies)
1805+
1806 def test_checkInformationType_bug_supervisor(self):
1807 # Bug supervisors of proprietary distributions must not have
1808 # inclusive membership policies.
1809@@ -744,6 +839,378 @@ class TestDistribution(TestCaseWithFactory):
1810 distribution.information_type = InformationType.PROPRIETARY
1811
1812
1813+class TestDistributionBugInformationTypes(TestCaseWithFactory):
1814+
1815+ layer = DatabaseFunctionalLayer
1816+
1817+ def makeDistributionWithPolicy(self, bug_sharing_policy):
1818+ distribution = self.factory.makeDistribution()
1819+ self.factory.makeCommercialSubscription(pillar=distribution)
1820+ with person_logged_in(distribution.owner):
1821+ distribution.setBugSharingPolicy(bug_sharing_policy)
1822+ return distribution
1823+
1824+ def test_no_policy(self):
1825+ # New distributions can only use the non-proprietary information
1826+ # types.
1827+ distribution = self.factory.makeDistribution()
1828+ self.assertContentEqual(
1829+ FREE_INFORMATION_TYPES,
1830+ distribution.getAllowedBugInformationTypes())
1831+ self.assertEqual(
1832+ InformationType.PUBLIC,
1833+ distribution.getDefaultBugInformationType())
1834+
1835+ def test_sharing_policy_public_or_proprietary(self):
1836+ # bug_sharing_policy can enable Proprietary.
1837+ distribution = self.makeDistributionWithPolicy(
1838+ BugSharingPolicy.PUBLIC_OR_PROPRIETARY)
1839+ self.assertContentEqual(
1840+ FREE_INFORMATION_TYPES + (InformationType.PROPRIETARY,),
1841+ distribution.getAllowedBugInformationTypes())
1842+ self.assertEqual(
1843+ InformationType.PUBLIC,
1844+ distribution.getDefaultBugInformationType())
1845+
1846+ def test_sharing_policy_proprietary_or_public(self):
1847+ # bug_sharing_policy can enable and default to Proprietary.
1848+ distribution = self.makeDistributionWithPolicy(
1849+ BugSharingPolicy.PROPRIETARY_OR_PUBLIC)
1850+ self.assertContentEqual(
1851+ FREE_INFORMATION_TYPES + (InformationType.PROPRIETARY,),
1852+ distribution.getAllowedBugInformationTypes())
1853+ self.assertEqual(
1854+ InformationType.PROPRIETARY,
1855+ distribution.getDefaultBugInformationType())
1856+
1857+ def test_sharing_policy_proprietary(self):
1858+ # bug_sharing_policy can enable only Proprietary.
1859+ distribution = self.makeDistributionWithPolicy(
1860+ BugSharingPolicy.PROPRIETARY)
1861+ self.assertContentEqual(
1862+ [InformationType.PROPRIETARY],
1863+ distribution.getAllowedBugInformationTypes())
1864+ self.assertEqual(
1865+ InformationType.PROPRIETARY,
1866+ distribution.getDefaultBugInformationType())
1867+
1868+
1869+class TestDistributionSpecificationPolicyAndInformationTypes(
1870+ TestCaseWithFactory):
1871+
1872+ layer = DatabaseFunctionalLayer
1873+
1874+ def makeDistributionWithPolicy(self, specification_sharing_policy):
1875+ distribution = self.factory.makeDistribution()
1876+ self.factory.makeCommercialSubscription(pillar=distribution)
1877+ with person_logged_in(distribution.owner):
1878+ distribution.setSpecificationSharingPolicy(
1879+ specification_sharing_policy)
1880+ return distribution
1881+
1882+ def test_no_policy(self):
1883+ # Distributions that have not specified a policy can use the PUBLIC
1884+ # information type.
1885+ distribution = self.factory.makeDistribution()
1886+ self.assertContentEqual(
1887+ [InformationType.PUBLIC],
1888+ distribution.getAllowedSpecificationInformationTypes())
1889+ self.assertEqual(
1890+ InformationType.PUBLIC,
1891+ distribution.getDefaultSpecificationInformationType())
1892+
1893+ def test_sharing_policy_public(self):
1894+ # Distributions with a purely public policy should use PUBLIC
1895+ # information type.
1896+ distribution = self.makeDistributionWithPolicy(
1897+ SpecificationSharingPolicy.PUBLIC)
1898+ self.assertContentEqual(
1899+ [InformationType.PUBLIC],
1900+ distribution.getAllowedSpecificationInformationTypes())
1901+ self.assertEqual(
1902+ InformationType.PUBLIC,
1903+ distribution.getDefaultSpecificationInformationType())
1904+
1905+ def test_sharing_policy_public_or_proprietary(self):
1906+ # specification_sharing_policy can enable Proprietary.
1907+ distribution = self.makeDistributionWithPolicy(
1908+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY)
1909+ self.assertContentEqual(
1910+ [InformationType.PUBLIC, InformationType.PROPRIETARY],
1911+ distribution.getAllowedSpecificationInformationTypes())
1912+ self.assertEqual(
1913+ InformationType.PUBLIC,
1914+ distribution.getDefaultSpecificationInformationType())
1915+
1916+ def test_sharing_policy_proprietary_or_public(self):
1917+ # specification_sharing_policy can enable and default to Proprietary.
1918+ distribution = self.makeDistributionWithPolicy(
1919+ SpecificationSharingPolicy.PROPRIETARY_OR_PUBLIC)
1920+ self.assertContentEqual(
1921+ [InformationType.PUBLIC, InformationType.PROPRIETARY],
1922+ distribution.getAllowedSpecificationInformationTypes())
1923+ self.assertEqual(
1924+ InformationType.PROPRIETARY,
1925+ distribution.getDefaultSpecificationInformationType())
1926+
1927+ def test_sharing_policy_proprietary(self):
1928+ # specification_sharing_policy can enable only Proprietary.
1929+ distribution = self.makeDistributionWithPolicy(
1930+ SpecificationSharingPolicy.PROPRIETARY)
1931+ self.assertContentEqual(
1932+ [InformationType.PROPRIETARY],
1933+ distribution.getAllowedSpecificationInformationTypes())
1934+ self.assertEqual(
1935+ InformationType.PROPRIETARY,
1936+ distribution.getDefaultSpecificationInformationType())
1937+
1938+ def test_sharing_policy_embargoed_or_proprietary(self):
1939+ # specification_sharing_policy can be embargoed and then proprietary.
1940+ distribution = self.makeDistributionWithPolicy(
1941+ SpecificationSharingPolicy.EMBARGOED_OR_PROPRIETARY)
1942+ self.assertContentEqual(
1943+ [InformationType.PROPRIETARY, InformationType.EMBARGOED],
1944+ distribution.getAllowedSpecificationInformationTypes())
1945+ self.assertEqual(
1946+ InformationType.EMBARGOED,
1947+ distribution.getDefaultSpecificationInformationType())
1948+
1949+
1950+class BaseSharingPolicyTests:
1951+ """Common tests for distribution sharing policies."""
1952+
1953+ layer = DatabaseFunctionalLayer
1954+
1955+ def setSharingPolicy(self, policy, user):
1956+ raise NotImplementedError
1957+
1958+ def getSharingPolicy(self):
1959+ raise NotImplementedError
1960+
1961+ def setUp(self):
1962+ super().setUp()
1963+ self.distribution = self.factory.makeDistribution()
1964+ self.commercial_admin = self.factory.makeCommercialAdmin()
1965+
1966+ def test_owner_can_set_policy(self):
1967+ # Distribution maintainers can set sharing policies.
1968+ self.setSharingPolicy(self.public_policy, self.distribution.owner)
1969+ self.assertEqual(self.public_policy, self.getSharingPolicy())
1970+
1971+ def test_commercial_admin_can_set_policy(self):
1972+ # Commercial admins can set sharing policies for commercial
1973+ # distributions.
1974+ self.factory.makeCommercialSubscription(pillar=self.distribution)
1975+ self.setSharingPolicy(self.public_policy, self.commercial_admin)
1976+ self.assertEqual(self.public_policy, self.getSharingPolicy())
1977+
1978+ def test_random_cannot_set_policy(self):
1979+ # An unrelated user can't set sharing policies.
1980+ person = self.factory.makePerson()
1981+ self.assertRaises(
1982+ Unauthorized, self.setSharingPolicy, self.public_policy, person)
1983+
1984+ def test_anonymous_cannot_set_policy(self):
1985+ # An anonymous user can't set sharing policies.
1986+ self.assertRaises(
1987+ Unauthorized, self.setSharingPolicy, self.public_policy, None)
1988+
1989+ def test_proprietary_forbidden_without_commercial_sub(self):
1990+ # No policy that allows Proprietary can be configured without a
1991+ # commercial subscription.
1992+ self.setSharingPolicy(self.public_policy, self.distribution.owner)
1993+ self.assertEqual(self.public_policy, self.getSharingPolicy())
1994+ for policy in self.commercial_policies:
1995+ self.assertRaises(
1996+ CommercialSubscribersOnly,
1997+ self.setSharingPolicy, policy, self.distribution.owner)
1998+
1999+ def test_proprietary_allowed_with_commercial_sub(self):
2000+ # All policies are valid when there's a current commercial
2001+ # subscription.
2002+ self.factory.makeCommercialSubscription(pillar=self.distribution)
2003+ for policy in self.enum.items:
2004+ self.setSharingPolicy(policy, self.commercial_admin)
2005+ self.assertEqual(policy, self.getSharingPolicy())
2006+
2007+ def test_setting_proprietary_creates_access_policy(self):
2008+ # Setting a policy that allows Proprietary creates a
2009+ # corresponding access policy and shares it with the the
2010+ # maintainer.
2011+ self.factory.makeCommercialSubscription(pillar=self.distribution)
2012+ self.assertEqual(
2013+ [InformationType.PRIVATESECURITY, InformationType.USERDATA],
2014+ [policy.type for policy in
2015+ getUtility(IAccessPolicySource).findByPillar(
2016+ [self.distribution])])
2017+ self.setSharingPolicy(
2018+ self.commercial_policies[0], self.commercial_admin)
2019+ self.assertEqual(
2020+ [InformationType.PRIVATESECURITY, InformationType.USERDATA,
2021+ InformationType.PROPRIETARY],
2022+ [policy.type for policy in
2023+ getUtility(IAccessPolicySource).findByPillar(
2024+ [self.distribution])])
2025+ self.assertTrue(
2026+ getUtility(IService, 'sharing').checkPillarAccess(
2027+ [self.distribution], InformationType.PROPRIETARY,
2028+ self.distribution.owner))
2029+
2030+ def test_unused_policies_are_pruned(self):
2031+ # When a sharing policy is changed, the allowed information types may
2032+ # become more restricted. If this case, any existing access polices
2033+ # for the now defunct information type(s) should be removed so long as
2034+ # there are no corresponding policy artifacts.
2035+
2036+ # We create a distribution with and ensure there's an APA.
2037+ ap_source = getUtility(IAccessPolicySource)
2038+ distribution = self.factory.makeDistribution()
2039+ [ap] = ap_source.find(
2040+ [(distribution, InformationType.PRIVATESECURITY)])
2041+ self.factory.makeAccessPolicyArtifact(policy=ap)
2042+
2043+ def getAccessPolicyTypes(pillar):
2044+ return [
2045+ ap.type
2046+ for ap in ap_source.findByPillar([pillar])]
2047+
2048+ # Now change the sharing policies to PROPRIETARY
2049+ self.factory.makeCommercialSubscription(pillar=distribution)
2050+ with person_logged_in(distribution.owner):
2051+ distribution.setBugSharingPolicy(BugSharingPolicy.PROPRIETARY)
2052+ # Just bug sharing policy has been changed so all previous policy
2053+ # types are still valid.
2054+ self.assertContentEqual(
2055+ [InformationType.PRIVATESECURITY, InformationType.USERDATA,
2056+ InformationType.PROPRIETARY],
2057+ getAccessPolicyTypes(distribution))
2058+
2059+ distribution.setBranchSharingPolicy(
2060+ BranchSharingPolicy.PROPRIETARY)
2061+ # Proprietary is permitted by the sharing policy, and there's a
2062+ # Private Security artifact. But Private isn't in use or allowed
2063+ # by a sharing policy, so it's now gone.
2064+ self.assertContentEqual(
2065+ [InformationType.PRIVATESECURITY, InformationType.PROPRIETARY],
2066+ getAccessPolicyTypes(distribution))
2067+
2068+ def test_proprietary_distributions_forbid_public_policies(self):
2069+ # A proprietary distribution forbids any sharing policy that would
2070+ # permit public artifacts.
2071+ owner = self.distribution.owner
2072+ with admin_logged_in():
2073+ self.distribution.information_type = InformationType.PROPRIETARY
2074+ policies_permitting_public = [self.public_policy]
2075+ policies_permitting_public.extend(
2076+ policy for policy in self.commercial_policies if
2077+ InformationType.PUBLIC in self.allowed_types[policy])
2078+ for policy in policies_permitting_public:
2079+ with ExpectedException(
2080+ ProprietaryPillar, "The pillar is Proprietary."):
2081+ self.setSharingPolicy(policy, owner)
2082+
2083+
2084+class TestDistributionBugSharingPolicy(
2085+ BaseSharingPolicyTests, TestCaseWithFactory):
2086+ """Test Distribution.bug_sharing_policy."""
2087+
2088+ layer = DatabaseFunctionalLayer
2089+
2090+ enum = BugSharingPolicy
2091+ public_policy = BugSharingPolicy.PUBLIC
2092+ commercial_policies = (
2093+ BugSharingPolicy.PUBLIC_OR_PROPRIETARY,
2094+ BugSharingPolicy.PROPRIETARY_OR_PUBLIC,
2095+ BugSharingPolicy.PROPRIETARY,
2096+ )
2097+ allowed_types = BUG_POLICY_ALLOWED_TYPES
2098+
2099+ def setSharingPolicy(self, policy, user):
2100+ with person_logged_in(user):
2101+ result = self.distribution.setBugSharingPolicy(policy)
2102+ return result
2103+
2104+ def getSharingPolicy(self):
2105+ return self.distribution.bug_sharing_policy
2106+
2107+
2108+class TestDistributionBranchSharingPolicy(
2109+ BaseSharingPolicyTests, TestCaseWithFactory):
2110+ """Test Distribution.branch_sharing_policy."""
2111+
2112+ layer = DatabaseFunctionalLayer
2113+
2114+ enum = BranchSharingPolicy
2115+ public_policy = BranchSharingPolicy.PUBLIC
2116+ commercial_policies = (
2117+ BranchSharingPolicy.PUBLIC_OR_PROPRIETARY,
2118+ BranchSharingPolicy.PROPRIETARY_OR_PUBLIC,
2119+ BranchSharingPolicy.PROPRIETARY,
2120+ BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY,
2121+ )
2122+ allowed_types = BRANCH_POLICY_ALLOWED_TYPES
2123+
2124+ def setSharingPolicy(self, policy, user):
2125+ with person_logged_in(user):
2126+ result = self.distribution.setBranchSharingPolicy(policy)
2127+ return result
2128+
2129+ def getSharingPolicy(self):
2130+ return self.distribution.branch_sharing_policy
2131+
2132+ def test_setting_embargoed_creates_access_policy(self):
2133+ # Setting a policy that allows Embargoed creates a corresponding
2134+ # access policy and shares it with the maintainer.
2135+ self.factory.makeCommercialSubscription(pillar=self.distribution)
2136+ self.assertEqual(
2137+ [InformationType.PRIVATESECURITY, InformationType.USERDATA],
2138+ [policy.type for policy in
2139+ getUtility(IAccessPolicySource).findByPillar(
2140+ [self.distribution])])
2141+ self.setSharingPolicy(
2142+ self.enum.EMBARGOED_OR_PROPRIETARY,
2143+ self.commercial_admin)
2144+ self.assertEqual(
2145+ [InformationType.PRIVATESECURITY, InformationType.USERDATA,
2146+ InformationType.PROPRIETARY, InformationType.EMBARGOED],
2147+ [policy.type for policy in
2148+ getUtility(IAccessPolicySource).findByPillar(
2149+ [self.distribution])])
2150+ self.assertTrue(
2151+ getUtility(IService, 'sharing').checkPillarAccess(
2152+ [self.distribution], InformationType.PROPRIETARY,
2153+ self.distribution.owner))
2154+ self.assertTrue(
2155+ getUtility(IService, 'sharing').checkPillarAccess(
2156+ [self.distribution], InformationType.EMBARGOED,
2157+ self.distribution.owner))
2158+
2159+
2160+class TestDistributionSpecificationSharingPolicy(
2161+ BaseSharingPolicyTests, TestCaseWithFactory):
2162+ """Test Distribution.specification_sharing_policy."""
2163+
2164+ layer = DatabaseFunctionalLayer
2165+
2166+ enum = SpecificationSharingPolicy
2167+ public_policy = SpecificationSharingPolicy.PUBLIC
2168+ commercial_policies = (
2169+ SpecificationSharingPolicy.PUBLIC_OR_PROPRIETARY,
2170+ SpecificationSharingPolicy.PROPRIETARY_OR_PUBLIC,
2171+ SpecificationSharingPolicy.PROPRIETARY,
2172+ SpecificationSharingPolicy.EMBARGOED_OR_PROPRIETARY,
2173+ )
2174+ allowed_types = SPECIFICATION_POLICY_ALLOWED_TYPES
2175+
2176+ def setSharingPolicy(self, policy, user):
2177+ with person_logged_in(user):
2178+ result = self.distribution.setSpecificationSharingPolicy(policy)
2179+ return result
2180+
2181+ def getSharingPolicy(self):
2182+ return self.distribution.specification_sharing_policy
2183+
2184+
2185 class TestDistributionCurrentSourceReleases(
2186 CurrentSourceReleasesMixin, TestCase):
2187 """Test for Distribution.getCurrentSourceReleases().
2188diff --git a/lib/lp/registry/tests/test_product.py b/lib/lp/registry/tests/test_product.py
2189index 61f147a..89899e9 100644
2190--- a/lib/lp/registry/tests/test_product.py
2191+++ b/lib/lp/registry/tests/test_product.py
2192@@ -1761,7 +1761,7 @@ class BaseSharingPolicyTests:
2193 InformationType.PUBLIC in self.allowed_types[policy])
2194 for policy in policies_permitting_public:
2195 with ExpectedException(
2196- ProprietaryPillar, "The project is Proprietary."):
2197+ ProprietaryPillar, "The pillar is Proprietary."):
2198 self.setSharingPolicy(policy, owner)
2199
2200
2201diff --git a/lib/lp/scripts/garbo.py b/lib/lp/scripts/garbo.py
2202index f1c75ad..24c4d61 100644
2203--- a/lib/lp/scripts/garbo.py
2204+++ b/lib/lp/scripts/garbo.py
2205@@ -79,6 +79,7 @@ from lp.code.model.revision import (
2206 from lp.code.model.revisionstatus import RevisionStatusArtifact
2207 from lp.oci.model.ocirecipebuild import OCIFile
2208 from lp.registry.interfaces.person import IPersonSet
2209+from lp.registry.model.distribution import Distribution
2210 from lp.registry.model.person import Person
2211 from lp.registry.model.product import Product
2212 from lp.registry.model.sourcepackagename import SourcePackageName
2213@@ -1426,7 +1427,7 @@ class UnusedPOTMsgSetPruner(TunableLoop):
2214 transaction.commit()
2215
2216
2217-class UnusedAccessPolicyPruner(TunableLoop):
2218+class UnusedProductAccessPolicyPruner(TunableLoop):
2219 """Deletes unused AccessPolicy and AccessPolicyGrants for products."""
2220
2221 maximum_chunk_size = 5000
2222@@ -1451,6 +1452,32 @@ class UnusedAccessPolicyPruner(TunableLoop):
2223 transaction.commit()
2224
2225
2226+class UnusedDistributionAccessPolicyPruner(TunableLoop):
2227+ """Deletes unused AccessPolicy and AccessPolicyGrants for distributions."""
2228+
2229+ maximum_chunk_size = 5000
2230+
2231+ def __init__(self, log, abort_time=None):
2232+ super().__init__(log, abort_time)
2233+ self.start_at = 1
2234+ self.store = IMasterStore(Distribution)
2235+
2236+ def findDistributions(self):
2237+ return self.store.find(
2238+ Distribution,
2239+ Distribution.id >= self.start_at).order_by(Distribution.id)
2240+
2241+ def isDone(self):
2242+ return self.findDistributions().is_empty()
2243+
2244+ def __call__(self, chunk_size):
2245+ distributions = list(self.findDistributions()[:chunk_size])
2246+ for distribution in distributions:
2247+ distribution._pruneUnusedPolicies()
2248+ self.start_at = distributions[-1].id + 1
2249+ transaction.commit()
2250+
2251+
2252 class ProductVCSPopulator(TunableLoop):
2253 """Populates product.vcs from product.inferred_vcs if not set."""
2254
2255@@ -2087,8 +2114,9 @@ class DailyDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
2256 SuggestiveTemplatesCacheUpdater,
2257 TeamMembershipPruner,
2258 UnlinkedAccountPruner,
2259- UnusedAccessPolicyPruner,
2260+ UnusedDistributionAccessPolicyPruner,
2261 UnusedPOTMsgSetPruner,
2262+ UnusedProductAccessPolicyPruner,
2263 WebhookJobPruner,
2264 ]
2265 experimental_tunable_loops = [
2266diff --git a/lib/lp/scripts/tests/test_garbo.py b/lib/lp/scripts/tests/test_garbo.py
2267index 7f5844a..3df3b58 100644
2268--- a/lib/lp/scripts/tests/test_garbo.py
2269+++ b/lib/lp/scripts/tests/test_garbo.py
2270@@ -1355,9 +1355,10 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
2271 ap.type
2272 for ap in getUtility(IAccessPolicySource).findByPillar([pillar])]
2273
2274- def test_UnusedAccessPolicyPruner(self):
2275- # UnusedAccessPolicyPruner removes access policies that aren't
2276- # in use by artifacts or allowed by the project sharing policy.
2277+ def test_UnusedProductAccessPolicyPruner(self):
2278+ # UnusedProductAccessPolicyPruner removes access policies that
2279+ # aren't in use by artifacts or allowed by the project sharing
2280+ # policy.
2281 switch_dbuser('testadmin')
2282 product = self.factory.makeProduct()
2283 self.factory.makeCommercialSubscription(pillar=product)
2284@@ -1385,6 +1386,39 @@ class TestGarbo(FakeAdapterMixin, TestCaseWithFactory):
2285 [InformationType.PRIVATESECURITY, InformationType.PROPRIETARY],
2286 self.getAccessPolicyTypes(product))
2287
2288+ def test_UnusedDistributionAccessPolicyPruner(self):
2289+ # UnusedDistributionAccessPolicyPruner removes access policies that
2290+ # aren't in use by artifacts or allowed by the distribution sharing
2291+ # policy.
2292+ switch_dbuser('testadmin')
2293+ distribution = self.factory.makeProduct()
2294+ self.factory.makeCommercialSubscription(pillar=distribution)
2295+ self.factory.makeAccessPolicy(
2296+ distribution, InformationType.PROPRIETARY)
2297+ naked_distribution = removeSecurityProxy(distribution)
2298+ naked_distribution.bug_sharing_policy = BugSharingPolicy.PROPRIETARY
2299+ naked_distribution.branch_sharing_policy = (
2300+ BranchSharingPolicy.PROPRIETARY)
2301+ [ap] = getUtility(IAccessPolicySource).find(
2302+ [(distribution, InformationType.PRIVATESECURITY)])
2303+ self.factory.makeAccessPolicyArtifact(policy=ap)
2304+
2305+ # Private and Private Security were created with the distribution.
2306+ # Proprietary was created when the branch sharing policy was set.
2307+ self.assertContentEqual(
2308+ [InformationType.PRIVATESECURITY, InformationType.USERDATA,
2309+ InformationType.PROPRIETARY],
2310+ self.getAccessPolicyTypes(distribution))
2311+
2312+ self.runDaily()
2313+
2314+ # Proprietary is permitted by the sharing policy, and there's a
2315+ # Private Security artifact. But Private isn't in use or allowed
2316+ # by a sharing policy, so garbo deleted it.
2317+ self.assertContentEqual(
2318+ [InformationType.PRIVATESECURITY, InformationType.PROPRIETARY],
2319+ self.getAccessPolicyTypes(distribution))
2320+
2321 def test_ProductVCSPopulator(self):
2322 switch_dbuser('testadmin')
2323 product = self.factory.makeProduct()
2324diff --git a/lib/lp/security.py b/lib/lp/security.py
2325index 95d7a45..fbada1b 100644
2326--- a/lib/lp/security.py
2327+++ b/lib/lp/security.py
2328@@ -2402,14 +2402,15 @@ class EditBranch(AuthorizationBase):
2329
2330
2331 class ModerateBranch(EditBranch):
2332- """The owners, product owners, and admins can moderate branches."""
2333+ """The owners, pillar owners, and admins can moderate branches."""
2334 permission = 'launchpad.Moderate'
2335
2336 def checkAuthenticated(self, user):
2337 if super().checkAuthenticated(user):
2338 return True
2339 branch = self.obj
2340- if branch.product is not None and user.inTeam(branch.product.owner):
2341+ pillar = branch.product or branch.distribution
2342+ if pillar is not None and user.inTeam(pillar.owner):
2343 return True
2344 return user.in_commercial_admin
2345
2346@@ -2477,15 +2478,22 @@ class EditGitRepository(AuthorizationBase):
2347
2348
2349 class ModerateGitRepository(EditGitRepository):
2350- """The owners, project owners, and admins can moderate Git repositories."""
2351+ """The owners, pillar owners, and admins can moderate Git repositories."""
2352 permission = 'launchpad.Moderate'
2353
2354 def checkAuthenticated(self, user):
2355 if super().checkAuthenticated(user):
2356 return True
2357 target = self.obj.target
2358- if (target is not None and IProduct.providedBy(target) and
2359- user.inTeam(target.owner)):
2360+ if IProduct.providedBy(target):
2361+ pillar = target
2362+ elif IDistributionSourcePackage.providedBy(target):
2363+ pillar = target.distribution
2364+ elif IOCIProject.providedBy(target):
2365+ pillar = target.pillar
2366+ else:
2367+ raise AssertionError("Unknown target: %r" % target)
2368+ if pillar is not None and user.inTeam(pillar.owner):
2369 return True
2370 return user.in_commercial_admin
2371
2372diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
2373index 9e37780..8e0ec69 100644
2374--- a/lib/lp/testing/factory.py
2375+++ b/lib/lp/testing/factory.py
2376@@ -2709,7 +2709,10 @@ class BareLaunchpadObjectFactory(ObjectFactory):
2377 publish_root_dir=None, publish_base_url=None,
2378 publish_copy_base_url=None, no_pubconf=False,
2379 icon=None, summary=None, vcs=None,
2380- oci_project_admin=None, information_type=None):
2381+ oci_project_admin=None, bug_sharing_policy=None,
2382+ branch_sharing_policy=None,
2383+ specification_sharing_policy=None,
2384+ information_type=None):
2385 """Make a new distribution."""
2386 if name is None:
2387 name = self.getUniqueString(prefix="distribution")
2388@@ -2740,6 +2743,26 @@ class BareLaunchpadObjectFactory(ObjectFactory):
2389 naked_distro.bug_supervisor = bug_supervisor
2390 if oci_project_admin is not None:
2391 naked_distro.oci_project_admin = oci_project_admin
2392+ # makeProduct defaults licenses to [License.OTHER_PROPRIETARY] if
2393+ # any non-public sharing policy is set, which ensures a
2394+ # complimentary commercial subscription. However, Distribution
2395+ # doesn't have a licenses field, so deal with the commercial
2396+ # subscription directly here instead.
2397+ if ((bug_sharing_policy is not None and
2398+ bug_sharing_policy != BugSharingPolicy.PUBLIC) or
2399+ (branch_sharing_policy is not None and
2400+ branch_sharing_policy != BranchSharingPolicy.PUBLIC) or
2401+ (specification_sharing_policy is not None and
2402+ specification_sharing_policy !=
2403+ SpecificationSharingPolicy.PUBLIC)):
2404+ naked_distro._ensure_complimentary_subscription()
2405+ if branch_sharing_policy:
2406+ naked_distro.setBranchSharingPolicy(branch_sharing_policy)
2407+ if bug_sharing_policy:
2408+ naked_distro.setBugSharingPolicy(bug_sharing_policy)
2409+ if specification_sharing_policy:
2410+ naked_distro.setSpecificationSharingPolicy(
2411+ specification_sharing_policy)
2412 if not no_pubconf:
2413 self.makePublisherConfig(
2414 distro, publish_root_dir, publish_base_url,

Subscribers

People subscribed via source and target branches

to status/vote changes: