Merge ~cjwatson/launchpad:distribution-information-type into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: c390c788c73b540e7b0cb7ec08c1af5ea5966401
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:distribution-information-type
Merge into: launchpad:master
Diff against target: 1626 lines (+803/-104)
20 files modified
lib/lp/archivepublisher/tests/test_dominator.py (+7/-3)
lib/lp/archivepublisher/tests/test_publisher.py (+2/-1)
lib/lp/registry/configure.zcml (+4/-2)
lib/lp/registry/errors.py (+3/-3)
lib/lp/registry/interfaces/distribution.py (+24/-2)
lib/lp/registry/model/distribution.py (+307/-20)
lib/lp/registry/model/product.py (+3/-3)
lib/lp/registry/model/productrelease.py (+2/-2)
lib/lp/registry/model/productseries.py (+3/-3)
lib/lp/registry/services/sharingservice.py (+7/-1)
lib/lp/registry/services/tests/test_sharingservice.py (+29/-45)
lib/lp/registry/stories/webservice/xx-distribution.txt (+2/-0)
lib/lp/registry/tests/test_distribution.py (+375/-2)
lib/lp/registry/tests/test_pillaraffiliation.py (+5/-6)
lib/lp/registry/tests/test_product.py (+3/-3)
lib/lp/registry/tests/test_productrelease.py (+2/-2)
lib/lp/registry/tests/test_productseries.py (+2/-2)
lib/lp/security.py (+9/-1)
lib/lp/soyuz/tests/test_build_set.py (+11/-1)
lib/lp/testing/factory.py (+3/-2)
Reviewer Review Type Date Requested Status
William Grant code Approve
Jürgen Gmach Approve
Review via email: mp+415522@code.launchpad.net

Commit message

Add Distribution.information_type

Description of the change

This is mostly copied from `Product`, especially the validation logic and the test cases. There are no sharing policies yet, and no UI for setting the information type; these will come in future branches. Some of the logic also works slightly differently because, unlike projects, distributions don't have licences (well, conceptually they might, but they're typically an aggregation of software under many different licences so it's unlikely to ever make sense to model that directly).

To post a comment you must log in.
Revision history for this message
Jürgen Gmach (jugmac00) wrote :

LGTM - I am not super happy about using yet another test-addon, `testscenarios` for all the already mentioned reasons. Additionally skipping tests from within instead of using a decorator was unfamiliar to me. But it works :-) And I lack an immediate better solution.

The last meaningful contribution to this package was 7 years ago, but I saw you are listed as one the maintainers, so I think this is ok.

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

It may be worth noting that we're already using testscenarios elsewhere in Launchpad.

Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/archivepublisher/tests/test_dominator.py b/lib/lp/archivepublisher/tests/test_dominator.py
index aefd466..01c56e4 100755
--- a/lib/lp/archivepublisher/tests/test_dominator.py
+++ b/lib/lp/archivepublisher/tests/test_dominator.py
@@ -41,6 +41,7 @@ from lp.testing import (
41 StormStatementRecorder,41 StormStatementRecorder,
42 TestCaseWithFactory,42 TestCaseWithFactory,
43 )43 )
44from lp.testing.dbuser import lp_dbuser
44from lp.testing.fakemethod import FakeMethod45from lp.testing.fakemethod import FakeMethod
45from lp.testing.layers import ZopelessDatabaseLayer46from lp.testing.layers import ZopelessDatabaseLayer
46from lp.testing.matchers import HasQueryCount47from lp.testing.matchers import HasQueryCount
@@ -165,6 +166,8 @@ class TestDominator(TestNativePublishingBase):
165166
166 def test_dominateBinaries_rejects_empty_publication_list(self):167 def test_dominateBinaries_rejects_empty_publication_list(self):
167 """Domination asserts for non-empty input list."""168 """Domination asserts for non-empty input list."""
169 with lp_dbuser():
170 distroseries = self.factory.makeDistroArchSeries().distroseries
168 package = self.factory.makeBinaryPackageName()171 package = self.factory.makeBinaryPackageName()
169 dominator = Dominator(self.logger, self.ubuntutest.main_archive)172 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
170 dominator._sortPackages = FakeMethod({package.name: []})173 dominator._sortPackages = FakeMethod({package.name: []})
@@ -173,11 +176,12 @@ class TestDominator(TestNativePublishingBase):
173 self.assertRaises(176 self.assertRaises(
174 AssertionError,177 AssertionError,
175 dominator.dominateBinaries,178 dominator.dominateBinaries,
176 self.factory.makeDistroArchSeries().distroseries,179 distroseries, self.factory.getAnyPocket())
177 self.factory.getAnyPocket())
178180
179 def test_dominateSources_rejects_empty_publication_list(self):181 def test_dominateSources_rejects_empty_publication_list(self):
180 """Domination asserts for non-empty input list."""182 """Domination asserts for non-empty input list."""
183 with lp_dbuser():
184 distroseries = self.factory.makeDistroSeries()
181 package = self.factory.makeSourcePackageName()185 package = self.factory.makeSourcePackageName()
182 dominator = Dominator(self.logger, self.ubuntutest.main_archive)186 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
183 dominator._sortPackages = FakeMethod({package.name: []})187 dominator._sortPackages = FakeMethod({package.name: []})
@@ -186,7 +190,7 @@ class TestDominator(TestNativePublishingBase):
186 self.assertRaises(190 self.assertRaises(
187 AssertionError,191 AssertionError,
188 dominator.dominateSources,192 dominator.dominateSources,
189 self.factory.makeDistroSeries(), self.factory.getAnyPocket())193 distroseries, self.factory.getAnyPocket())
190194
191 def test_archall_domination(self):195 def test_archall_domination(self):
192 # Arch-all binaries should not be dominated when a new source196 # Arch-all binaries should not be dominated when a new source
diff --git a/lib/lp/archivepublisher/tests/test_publisher.py b/lib/lp/archivepublisher/tests/test_publisher.py
index c0891dd..754138b 100644
--- a/lib/lp/archivepublisher/tests/test_publisher.py
+++ b/lib/lp/archivepublisher/tests/test_publisher.py
@@ -1288,7 +1288,8 @@ class TestPublisher(TestPublisherBase):
1288 ubuntu = getUtility(IDistributionSet)['ubuntu']1288 ubuntu = getUtility(IDistributionSet)['ubuntu']
12891289
1290 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)1290 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
1291 copy_archive = self.factory.makeArchive(purpose=ArchivePurpose.COPY)1291 copy_archive = self.factory.makeArchive(
1292 distribution=self.ubuntu, purpose=ArchivePurpose.COPY)
1292 self.assertNotIn(ppa, ubuntu.getPendingPublicationPPAs())1293 self.assertNotIn(ppa, ubuntu.getPendingPublicationPPAs())
1293 self.assertNotIn(copy_archive, ubuntu.getPendingPublicationPPAs())1294 self.assertNotIn(copy_archive, ubuntu.getPendingPublicationPPAs())
1294 ppa.status = ArchiveStatus.DELETING1295 ppa.status = ArchiveStatus.DELETING
diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
index fbeaffd..10d677f 100644
--- a/lib/lp/registry/configure.zcml
+++ b/lib/lp/registry/configure.zcml
@@ -1821,7 +1821,8 @@
1821 class="lp.registry.model.distribution.Distribution">1821 class="lp.registry.model.distribution.Distribution">
1822 <allow1822 <allow
1823 interface="lp.registry.interfaces.distribution.IDistributionPublic1823 interface="lp.registry.interfaces.distribution.IDistributionPublic
1824 lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>1824 lp.bugs.interfaces.bugsummary.IBugSummaryDimension"
1825 attributes="information_type"/>
1825 <require1826 <require
1826 permission="launchpad.LimitedView"1827 permission="launchpad.LimitedView"
1827 interface="lp.registry.interfaces.distribution.IDistributionLimitedView"/>1828 interface="lp.registry.interfaces.distribution.IDistributionLimitedView"/>
@@ -1830,7 +1831,8 @@
1830 interface="lp.registry.interfaces.distribution.IDistributionView"/>1831 interface="lp.registry.interfaces.distribution.IDistributionView"/>
1831 <require1832 <require
1832 permission="launchpad.Edit"1833 permission="launchpad.Edit"
1833 interface="lp.registry.interfaces.distribution.IDistributionEditRestricted"/>1834 interface="lp.registry.interfaces.distribution.IDistributionEditRestricted"
1835 set_schema="lp.app.interfaces.informationtype.IInformationType"/>
1834 <require1836 <require
1835 permission="launchpad.Edit"1837 permission="launchpad.Edit"
1836 set_attributes="answers_usage blueprints_usage codehosting_usage1838 set_attributes="answers_usage blueprints_usage codehosting_usage
diff --git a/lib/lp/registry/errors.py b/lib/lp/registry/errors.py
index 4e1c52f..09acb08 100644
--- a/lib/lp/registry/errors.py
+++ b/lib/lp/registry/errors.py
@@ -27,7 +27,7 @@ __all__ = [
27 'InclusiveTeamLinkageError',27 'InclusiveTeamLinkageError',
28 'PPACreationError',28 'PPACreationError',
29 'PrivatePersonLinkageError',29 'PrivatePersonLinkageError',
30 'ProprietaryProduct',30 'ProprietaryPillar',
31 'TeamMembershipTransitionError',31 'TeamMembershipTransitionError',
32 'TeamMembershipPolicyError',32 'TeamMembershipPolicyError',
33 'UserCannotChangeMembershipSilently',33 'UserCannotChangeMembershipSilently',
@@ -100,8 +100,8 @@ class CommercialSubscribersOnly(Unauthorized):
100 """100 """
101101
102102
103class ProprietaryProduct(Exception):103class ProprietaryPillar(Exception):
104 """Cannot make the change because the project is proprietary."""104 """Cannot make the change because the pillar is proprietary."""
105105
106106
107class NoSuchSourcePackageName(NameLookupFailed):107class NoSuchSourcePackageName(NameLookupFailed):
diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py
index b102de8..7e943c1 100644
--- a/lib/lp/registry/interfaces/distribution.py
+++ b/lib/lp/registry/interfaces/distribution.py
@@ -59,6 +59,7 @@ from lp import _
59from lp.answers.interfaces.faqtarget import IFAQTarget59from lp.answers.interfaces.faqtarget import IFAQTarget
60from lp.answers.interfaces.questiontarget import IQuestionTarget60from lp.answers.interfaces.questiontarget import IQuestionTarget
61from lp.app.errors import NameLookupFailed61from lp.app.errors import NameLookupFailed
62from lp.app.interfaces.informationtype import IInformationType
62from lp.app.interfaces.launchpad import (63from lp.app.interfaces.launchpad import (
63 IHasIcon,64 IHasIcon,
64 IHasLogo,65 IHasLogo,
@@ -163,6 +164,14 @@ class IDistributionPublic(Interface):
163 def userCanLimitedView(user):164 def userCanLimitedView(user):
164 """True if the given user has limited access to this distribution."""165 """True if the given user has limited access to this distribution."""
165166
167 private = exported(
168 Bool(
169 title=_("Distribution is confidential"),
170 required=False, readonly=True, default=False,
171 description=_(
172 "If set, this distribution is visible only to those with "
173 "access grants.")))
174
166175
167class IDistributionLimitedView(IHasIcon, IHasLogo, IHasOwner, ILaunchpadUsage):176class IDistributionLimitedView(IHasIcon, IHasLogo, IHasOwner, ILaunchpadUsage):
168 """IDistribution attributes visible to people with artifact grants."""177 """IDistribution attributes visible to people with artifact grants."""
@@ -456,6 +465,9 @@ class IDistributionView(
456 "An object which contains the timeframe and the voucher code of a "465 "An object which contains the timeframe and the voucher code of a "
457 "subscription.")))466 "subscription.")))
458467
468 has_current_commercial_subscription = Attribute(
469 "Whether the distribution has a current commercial subscription.")
470
459 def getArchiveIDList(archive=None):471 def getArchiveIDList(archive=None):
460 """Return a list of archive IDs suitable for sqlvalues() or quote().472 """Return a list of archive IDs suitable for sqlvalues() or quote().
461473
@@ -768,6 +780,14 @@ class IDistributionView(
768class IDistributionEditRestricted(IOfficialBugTagTargetRestricted):780class IDistributionEditRestricted(IOfficialBugTagTargetRestricted):
769 """IDistribution properties requiring launchpad.Edit permission."""781 """IDistribution properties requiring launchpad.Edit permission."""
770782
783 def checkInformationType(value):
784 """Check whether the information type change should be permitted.
785
786 Iterate through exceptions explaining why the type should not be
787 changed. Has the side-effect of creating a commercial subscription
788 if permitted.
789 """
790
771 @call_with(registrant=REQUEST_USER)791 @call_with(registrant=REQUEST_USER)
772 @operation_parameters(792 @operation_parameters(
773 registry_url=TextLine(793 registry_url=TextLine(
@@ -802,7 +822,8 @@ class IDistributionEditRestricted(IOfficialBugTagTargetRestricted):
802class IDistribution(822class IDistribution(
803 IDistributionEditRestricted, IDistributionPublic,823 IDistributionEditRestricted, IDistributionPublic,
804 IDistributionLimitedView, IDistributionView, IHasBugSupervisor,824 IDistributionLimitedView, IDistributionView, IHasBugSupervisor,
805 IFAQTarget, IQuestionTarget, IStructuralSubscriptionTarget):825 IFAQTarget, IQuestionTarget, IStructuralSubscriptionTarget,
826 IInformationType):
806 """An operating system distribution.827 """An operating system distribution.
807828
808 Launchpadlib example: retrieving the current version of a package in a829 Launchpadlib example: retrieving the current version of a package in a
@@ -853,7 +874,8 @@ class IDistributionSet(Interface):
853 """Return the IDistribution with the given name or None."""874 """Return the IDistribution with the given name or None."""
854875
855 def new(name, display_name, title, description, summary, domainname,876 def new(name, display_name, title, description, summary, domainname,
856 members, owner, registrant, mugshot=None, logo=None, icon=None):877 members, owner, registrant, mugshot=None, logo=None, icon=None,
878 information_type=None):
857 """Create a new distribution."""879 """Create a new distribution."""
858880
859 def getCurrentSourceReleases(distro_to_source_packagenames):881 def getCurrentSourceReleases(distro_to_source_packagenames):
diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
index c6327d2..372bcbb 100644
--- a/lib/lp/registry/model/distribution.py
+++ b/lib/lp/registry/model/distribution.py
@@ -9,11 +9,17 @@ __all__ = [
9 ]9 ]
1010
11from collections import defaultdict11from collections import defaultdict
12from datetime import (
13 datetime,
14 timedelta,
15 )
12import itertools16import itertools
13from operator import itemgetter17from operator import itemgetter
1418
19import pytz
15from storm.expr import (20from storm.expr import (
16 And,21 And,
22 Coalesce,
17 Desc,23 Desc,
18 Exists,24 Exists,
19 Join,25 Join,
@@ -27,6 +33,7 @@ from storm.expr import (
27from storm.info import ClassAlias33from storm.info import ClassAlias
28from storm.locals import (34from storm.locals import (
29 Int,35 Int,
36 List,
30 Reference,37 Reference,
31 )38 )
32from storm.store import Store39from storm.store import Store
@@ -40,15 +47,23 @@ from lp.answers.model.faq import (
40 FAQSearch,47 FAQSearch,
41 )48 )
42from lp.answers.model.question import (49from lp.answers.model.question import (
50 Question,
43 QuestionTargetMixin,51 QuestionTargetMixin,
44 QuestionTargetSearch,52 QuestionTargetSearch,
45 )53 )
46from lp.app.enums import (54from lp.app.enums import (
47 FREE_INFORMATION_TYPES,55 FREE_INFORMATION_TYPES,
48 InformationType,56 InformationType,
57 PILLAR_INFORMATION_TYPES,
58 PRIVATE_INFORMATION_TYPES,
59 PROPRIETARY_INFORMATION_TYPES,
60 PUBLIC_INFORMATION_TYPES,
49 ServiceUsage,61 ServiceUsage,
50 )62 )
51from lp.app.errors import NotFoundError63from lp.app.errors import (
64 NotFoundError,
65 ServiceUsageForbidden,
66 )
52from lp.app.interfaces.launchpad import (67from lp.app.interfaces.launchpad import (
53 IHasIcon,68 IHasIcon,
54 IHasLogo,69 IHasLogo,
@@ -57,11 +72,14 @@ from lp.app.interfaces.launchpad import (
57 ILaunchpadUsage,72 ILaunchpadUsage,
58 IServiceUsage,73 IServiceUsage,
59 )74 )
75from lp.app.interfaces.services import IService
76from lp.app.model.launchpad import InformationTypeMixin
60from lp.app.validators.name import (77from lp.app.validators.name import (
61 sanitize_name,78 sanitize_name,
62 valid_name,79 valid_name,
63 )80 )
64from lp.archivepublisher.debversion import Version81from lp.archivepublisher.debversion import Version
82from lp.blueprints.enums import SpecificationFilter
65from lp.blueprints.model.specification import (83from lp.blueprints.model.specification import (
66 HasSpecificationsMixin,84 HasSpecificationsMixin,
67 Specification,85 Specification,
@@ -75,12 +93,14 @@ from lp.bugs.model.bugtarget import (
75 BugTargetBase,93 BugTargetBase,
76 OfficialBugTagTargetMixin,94 OfficialBugTagTargetMixin,
77 )95 )
96from lp.bugs.model.bugtaskflat import BugTaskFlat
78from lp.bugs.model.structuralsubscription import (97from lp.bugs.model.structuralsubscription import (
79 StructuralSubscriptionTargetMixin,98 StructuralSubscriptionTargetMixin,
80 )99 )
81from lp.code.interfaces.seriessourcepackagebranch import (100from lp.code.interfaces.seriessourcepackagebranch import (
82 IFindOfficialBranchLinks,101 IFindOfficialBranchLinks,
83 )102 )
103from lp.code.model.branch import Branch
84from lp.oci.interfaces.ociregistrycredentials import (104from lp.oci.interfaces.ociregistrycredentials import (
85 IOCIRegistryCredentialsSet,105 IOCIRegistryCredentialsSet,
86 )106 )
@@ -88,11 +108,20 @@ from lp.registry.enums import (
88 BranchSharingPolicy,108 BranchSharingPolicy,
89 BugSharingPolicy,109 BugSharingPolicy,
90 DistributionDefaultTraversalPolicy,110 DistributionDefaultTraversalPolicy,
111 INCLUSIVE_TEAM_POLICY,
91 SpecificationSharingPolicy,112 SpecificationSharingPolicy,
92 VCSType,113 VCSType,
93 )114 )
94from lp.registry.errors import NoSuchDistroSeries115from lp.registry.errors import (
95from lp.registry.interfaces.accesspolicy import IAccessPolicySource116 CannotChangeInformationType,
117 CommercialSubscribersOnly,
118 NoSuchDistroSeries,
119 ProprietaryPillar,
120 )
121from lp.registry.interfaces.accesspolicy import (
122 IAccessPolicyGrantSource,
123 IAccessPolicySource,
124 )
96from lp.registry.interfaces.distribution import (125from lp.registry.interfaces.distribution import (
97 IDistribution,126 IDistribution,
98 IDistributionSet,127 IDistributionSet,
@@ -119,6 +148,7 @@ from lp.registry.interfaces.pocket import suffixpocket
119from lp.registry.interfaces.role import IPersonRoles148from lp.registry.interfaces.role import IPersonRoles
120from lp.registry.interfaces.series import SeriesStatus149from lp.registry.interfaces.series import SeriesStatus
121from lp.registry.interfaces.sourcepackagename import ISourcePackageName150from lp.registry.interfaces.sourcepackagename import ISourcePackageName
151from lp.registry.model.accesspolicy import AccessPolicyGrantFlat
122from lp.registry.model.announcement import MakesAnnouncements152from lp.registry.model.announcement import MakesAnnouncements
123from lp.registry.model.commercialsubscription import CommercialSubscription153from lp.registry.model.commercialsubscription import CommercialSubscription
124from lp.registry.model.distributionmirror import (154from lp.registry.model.distributionmirror import (
@@ -142,6 +172,7 @@ from lp.registry.model.ociprojectname import OCIProjectName
142from lp.registry.model.oopsreferences import referenced_oops172from lp.registry.model.oopsreferences import referenced_oops
143from lp.registry.model.pillar import HasAliasMixin173from lp.registry.model.pillar import HasAliasMixin
144from lp.registry.model.sourcepackagename import SourcePackageName174from lp.registry.model.sourcepackagename import SourcePackageName
175from lp.registry.model.teammembership import TeamParticipation
145from lp.services.database.bulk import load_referencing176from lp.services.database.bulk import load_referencing
146from lp.services.database.constants import UTC_NOW177from lp.services.database.constants import UTC_NOW
147from lp.services.database.datetimecol import UtcDateTimeCol178from lp.services.database.datetimecol import UtcDateTimeCol
@@ -159,6 +190,8 @@ from lp.services.database.sqlobject import (
159 StringCol,190 StringCol,
160 )191 )
161from lp.services.database.stormexpr import (192from lp.services.database.stormexpr import (
193 ArrayAgg,
194 ArrayIntersects,
162 fti_search,195 fti_search,
163 rank_by_fti,196 rank_by_fti,
164 )197 )
@@ -203,6 +236,7 @@ from lp.translations.enums import TranslationPermission
203from lp.translations.model.hastranslationimports import (236from lp.translations.model.hastranslationimports import (
204 HasTranslationImportsMixin,237 HasTranslationImportsMixin,
205 )238 )
239from lp.translations.model.potemplate import POTemplate
206from lp.translations.model.translationpolicy import TranslationPolicyMixin240from lp.translations.model.translationpolicy import TranslationPolicyMixin
207241
208242
@@ -215,7 +249,8 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
215 HasTranslationImportsMixin, KarmaContextMixin,249 HasTranslationImportsMixin, KarmaContextMixin,
216 OfficialBugTagTargetMixin, QuestionTargetMixin,250 OfficialBugTagTargetMixin, QuestionTargetMixin,
217 StructuralSubscriptionTargetMixin, HasMilestonesMixin,251 StructuralSubscriptionTargetMixin, HasMilestonesMixin,
218 HasDriversMixin, TranslationPolicyMixin):252 HasDriversMixin, TranslationPolicyMixin,
253 InformationTypeMixin):
219 """A distribution of an operating system, e.g. Debian GNU/Linux."""254 """A distribution of an operating system, e.g. Debian GNU/Linux."""
220255
221 _table = 'Distribution'256 _table = 'Distribution'
@@ -281,9 +316,12 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
281 oci_registry_credentials = Reference(316 oci_registry_credentials = Reference(
282 oci_registry_credentials_id, "OCIRegistryCredentials.id")317 oci_registry_credentials_id, "OCIRegistryCredentials.id")
283318
319 _creating = False
320
284 def __init__(self, name, display_name, title, description, summary,321 def __init__(self, name, display_name, title, description, summary,
285 domainname, members, owner, registrant, mugshot=None,322 domainname, members, owner, registrant, mugshot=None,
286 logo=None, icon=None, vcs=None):323 logo=None, icon=None, vcs=None, information_type=None):
324 self._creating = True
287 try:325 try:
288 self.name = name326 self.name = name
289 self.display_name = display_name327 self.display_name = display_name
@@ -299,9 +337,11 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
299 self.logo = logo337 self.logo = logo
300 self.icon = icon338 self.icon = icon
301 self.vcs = vcs339 self.vcs = vcs
340 self.information_type = information_type
302 except Exception:341 except Exception:
303 IStore(self).remove(self)342 IStore(self).remove(self)
304 raise343 raise
344 del self._creating
305345
306 def __repr__(self):346 def __repr__(self):
307 display_name = backslashreplace(self.display_name)347 display_name = backslashreplace(self.display_name)
@@ -321,6 +361,109 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
321 """See `IBugTarget`."""361 """See `IBugTarget`."""
322 return self362 return self
323363
364 def _valid_distribution_information_type(self, attr, value):
365 for exception in self.checkInformationType(value):
366 raise exception
367 return value
368
369 def checkInformationType(self, value):
370 """See `IDistribution`."""
371 if value not in PILLAR_INFORMATION_TYPES:
372 yield CannotChangeInformationType(
373 'Not supported for distributions.')
374 if value in PROPRIETARY_INFORMATION_TYPES:
375 if self.answers_usage == ServiceUsage.LAUNCHPAD:
376 yield CannotChangeInformationType('Answers is enabled.')
377 if self._creating or value not in PROPRIETARY_INFORMATION_TYPES:
378 return
379 # Additional checks when transitioning an existing distribution to a
380 # proprietary type.
381 # All specs located by an ALL search are public.
382 public_specs = self.specifications(
383 None, filter=[SpecificationFilter.ALL])
384 if not public_specs.is_empty():
385 # Unlike bugs and branches, specifications cannot be USERDATA or a
386 # security type.
387 yield CannotChangeInformationType('Some blueprints are public.')
388 store = Store.of(self)
389 series_ids = [series.id for series in self.series]
390 non_proprietary_bugs = store.find(
391 BugTaskFlat,
392 BugTaskFlat.information_type.is_in(FREE_INFORMATION_TYPES),
393 Or(
394 BugTaskFlat.distribution == self.id,
395 BugTaskFlat.distroseries_id.is_in(series_ids)))
396 if not non_proprietary_bugs.is_empty():
397 yield CannotChangeInformationType(
398 'Some bugs are neither proprietary nor embargoed.')
399 # Default returns all public branches.
400 non_proprietary_branches = store.find(
401 Branch,
402 DistroSeries.distribution == self.id,
403 Branch.distroseries == DistroSeries.id,
404 Not(Branch.information_type.is_in(PROPRIETARY_INFORMATION_TYPES)))
405 if not non_proprietary_branches.is_empty():
406 yield CannotChangeInformationType(
407 'Some branches are neither proprietary nor embargoed.')
408 questions = store.find(Question, Question.distribution == self.id)
409 if not questions.is_empty():
410 yield CannotChangeInformationType(
411 'This distribution has questions.')
412 templates = store.find(
413 POTemplate, DistroSeries.distribution == self.id,
414 POTemplate.distroseries == DistroSeries.id)
415 if not templates.is_empty():
416 yield CannotChangeInformationType(
417 'This distribution has translations.')
418 if not self.getTranslationImportQueueEntries().is_empty():
419 yield CannotChangeInformationType(
420 'This distribution has queued translations.')
421 if self.translations_usage == ServiceUsage.LAUNCHPAD:
422 yield CannotChangeInformationType('Translations are enabled.')
423 bug_supervisor = self.bug_supervisor
424 if (bug_supervisor is not None and
425 bug_supervisor.membership_policy in INCLUSIVE_TEAM_POLICY):
426 yield CannotChangeInformationType(
427 'Bug supervisor has inclusive membership.')
428
429 # Proprietary check works only after creation, because during
430 # creation, has_current_commercial_subscription cannot give the
431 # right value and triggers an inappropriate DB flush.
432
433 # Create the complimentary commercial subscription for the
434 # distribution.
435 self._ensure_complimentary_subscription()
436
437 # If you have a commercial subscription, but it's not current, you
438 # cannot set the information type to a PROPRIETARY type.
439 if not self.has_current_commercial_subscription:
440 yield CommercialSubscribersOnly(
441 'A valid commercial subscription is required for private'
442 ' distributions.')
443
444 _information_type = DBEnum(
445 enum=InformationType, default=InformationType.PUBLIC,
446 name="information_type",
447 validator=_valid_distribution_information_type)
448
449 @property
450 def information_type(self):
451 return self._information_type or InformationType.PUBLIC
452
453 @information_type.setter
454 def information_type(self, value):
455 old_info_type = self._information_type
456 self._information_type = value
457 # Make sure that policies are updated to grant permission to the
458 # maintainer as required for the Distribution.
459 # However, only on edits. If this is a new Distribution it's
460 # handled already.
461 if not self._creating:
462 if (old_info_type == InformationType.PUBLIC and
463 value != InformationType.PUBLIC):
464 self._ensure_complimentary_subscription()
465 self._ensurePolicies([value])
466
324 @property467 @property
325 def pillar_category(self):468 def pillar_category(self):
326 """See `IPillar`."""469 """See `IPillar`."""
@@ -344,12 +487,77 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
344 # Sharing policy for distributions is always PUBLIC.487 # Sharing policy for distributions is always PUBLIC.
345 return SpecificationSharingPolicy.PUBLIC488 return SpecificationSharingPolicy.PUBLIC
346489
490 # Cache of AccessPolicy.ids that convey launchpad.LimitedView.
491 # Unlike artifacts' cached access_policies, an AccessArtifactGrant
492 # to an artifact in the policy is sufficient for access.
493 access_policies = List(type=Int())
494
495 def _ensurePolicies(self, information_types):
496 # Ensure that the distribution has access policies for the specified
497 # information types.
498 aps = getUtility(IAccessPolicySource)
499 existing_policies = aps.findByPillar([self])
500 existing_types = {
501 access_policy.type for access_policy in existing_policies}
502 # Create the missing policies.
503 required_types = set(information_types).difference(
504 existing_types).intersection(PRIVATE_INFORMATION_TYPES)
505 policies = itertools.product((self,), required_types)
506 policies = getUtility(IAccessPolicySource).create(policies)
507
508 # Add the maintainer to the policies.
509 grants = []
510 for p in policies:
511 grants.append((p, self.owner, self.owner))
512 getUtility(IAccessPolicyGrantSource).grant(grants)
513
514 self._cacheAccessPolicies()
515
516 def _cacheAccessPolicies(self):
517 # Update the cache of AccessPolicy.ids for which an
518 # AccessPolicyGrant or AccessArtifactGrant is sufficient to
519 # convey launchpad.LimitedView on this Distribution.
520 #
521 # We only need a cache for proprietary types, and it only
522 # includes proprietary policies in case a policy like Private
523 # Security was somehow left around when a project was
524 # transitioned to Proprietary.
525 if self.information_type in PROPRIETARY_INFORMATION_TYPES:
526 self.access_policies = [
527 policy.id for policy in
528 getUtility(IAccessPolicySource).find(
529 [(self, type) for type in PROPRIETARY_INFORMATION_TYPES])]
530 else:
531 self.access_policies = None
532
347 @cachedproperty533 @cachedproperty
348 def commercial_subscription(self):534 def commercial_subscription(self):
349 return IStore(CommercialSubscription).find(535 return IStore(CommercialSubscription).find(
350 CommercialSubscription, distribution=self).one()536 CommercialSubscription, distribution=self).one()
351537
352 @property538 @property
539 def has_current_commercial_subscription(self):
540 now = datetime.now(pytz.UTC)
541 return (self.commercial_subscription
542 and self.commercial_subscription.date_expires > now)
543
544 def _ensure_complimentary_subscription(self):
545 """Create a complementary commercial subscription for the distro."""
546 if not self.commercial_subscription:
547 lp_janitor = getUtility(ILaunchpadCelebrities).janitor
548 now = datetime.now(pytz.UTC)
549 date_expires = now + timedelta(days=30)
550 sales_system_id = "complimentary-30-day-%s" % now
551 whiteboard = (
552 "Complimentary 30 day subscription. -- Launchpad %s" %
553 now.date().isoformat())
554 subscription = CommercialSubscription(
555 pillar=self, date_starts=now, date_expires=date_expires,
556 registrant=lp_janitor, purchaser=lp_janitor,
557 sales_system_id=sales_system_id, whiteboard=whiteboard)
558 get_property_cache(self).commercial_subscription = subscription
559
560 @property
353 def uploaders(self):561 def uploaders(self):
354 """See `IDistribution`."""562 """See `IDistribution`."""
355 # Get all the distribution archives and find out the uploaders563 # Get all the distribution archives and find out the uploaders
@@ -397,6 +605,10 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
397 return self._answers_usage605 return self._answers_usage
398606
399 def _set_answers_usage(self, val):607 def _set_answers_usage(self, val):
608 if val == ServiceUsage.LAUNCHPAD:
609 if self.information_type in PROPRIETARY_INFORMATION_TYPES:
610 raise ServiceUsageForbidden(
611 "Answers not allowed for non-public distributions.")
400 self._answers_usage = val612 self._answers_usage = val
401 if val == ServiceUsage.LAUNCHPAD:613 if val == ServiceUsage.LAUNCHPAD:
402 self.official_answers = True614 self.official_answers = True
@@ -432,9 +644,17 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
432 _set_blueprints_usage,644 _set_blueprints_usage,
433 doc="Indicates if the product uses the blueprints service.")645 doc="Indicates if the product uses the blueprints service.")
434646
647 def validate_translations_usage(self, attr, value):
648 if value == ServiceUsage.LAUNCHPAD and self.private:
649 raise ProprietaryPillar(
650 "Translations are not supported for proprietary "
651 "distributions.")
652 return value
653
435 translations_usage = DBEnum(654 translations_usage = DBEnum(
436 name="translations_usage", allow_none=False,655 name="translations_usage", allow_none=False,
437 enum=ServiceUsage, default=ServiceUsage.UNKNOWN)656 enum=ServiceUsage, default=ServiceUsage.UNKNOWN,
657 validator=validate_translations_usage)
438658
439 @property659 @property
440 def codehosting_usage(self):660 def codehosting_usage(self):
@@ -1568,17 +1788,41 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
1568 self.oci_registry_credentials = None1788 self.oci_registry_credentials = None
1569 old_credentials.destroySelf()1789 old_credentials.destroySelf()
15701790
1791 @cachedproperty
1792 def _known_viewers(self):
1793 """A set of known persons able to view this distribution."""
1794 return set()
1795
1571 def userCanView(self, user):1796 def userCanView(self, user):
1572 """See `IDistributionPublic`."""1797 """See `IDistributionPublic`."""
1573 # All distributions are public until we finish introducing privacy1798 if self.information_type in PUBLIC_INFORMATION_TYPES:
1574 # support.1799 return True
1575 return True1800 if user is None:
1801 return False
1802 if user.id in self._known_viewers:
1803 return True
1804 if not IPersonRoles.providedBy(user):
1805 user = IPersonRoles(user)
1806 if user.in_commercial_admin or user.in_admin:
1807 self._known_viewers.add(user.id)
1808 return True
1809 if getUtility(IService, 'sharing').checkPillarAccess(
1810 [self], self.information_type, user):
1811 self._known_viewers.add(user.id)
1812 return True
1813 return False
15761814
1577 def userCanLimitedView(self, user):1815 def userCanLimitedView(self, user):
1578 """See `IDistributionPublic`."""1816 """See `IDistributionPublic`."""
1579 # All distributions are public until we finish introducing privacy1817 if self.userCanView(user):
1580 # support.1818 return True
1581 return True1819 if user is None:
1820 return False
1821 return not Store.of(self).find(
1822 Distribution,
1823 Distribution.id == self.id,
1824 DistributionSet.getDistributionPrivacyFilter(user.person),
1825 ).is_empty()
15821826
15831827
1584@implementer(IDistributionSet)1828@implementer(IDistributionSet)
@@ -1618,10 +1862,48 @@ class DistributionSet:
1618 return None1862 return None
1619 return pillar1863 return pillar
16201864
1865 @staticmethod
1866 def getDistributionPrivacyFilter(user):
1867 # Anonymous users can only see public distributions. This is also
1868 # sometimes used with an outer join with e.g. Product, so we let
1869 # NULL through too.
1870 public_filter = Or(
1871 Distribution._information_type == None,
1872 Distribution._information_type == InformationType.PUBLIC)
1873 if user is None:
1874 return public_filter
1875
1876 # (Commercial) admins can see any project.
1877 roles = IPersonRoles(user)
1878 if roles.in_admin or roles.in_commercial_admin:
1879 return True
1880
1881 # Normal users can see any project for which they can see either
1882 # an entire policy or an artifact.
1883 # XXX wgrant 2015-06-26: This is slower than ideal for people in
1884 # teams with lots of artifact grants, as there can be tens of
1885 # thousands of APGF rows for a single policy. But it's tens of
1886 # milliseconds at most.
1887 grant_filter = Coalesce(
1888 ArrayIntersects(
1889 SQL('Distribution.access_policies'),
1890 Select(
1891 ArrayAgg(AccessPolicyGrantFlat.policy_id),
1892 tables=(AccessPolicyGrantFlat,
1893 Join(TeamParticipation,
1894 TeamParticipation.teamID ==
1895 AccessPolicyGrantFlat.grantee_id)),
1896 where=(TeamParticipation.person == user)
1897 )),
1898 False)
1899 return Or(public_filter, grant_filter)
1900
1621 def new(self, name, display_name, title, description, summary, domainname,1901 def new(self, name, display_name, title, description, summary, domainname,
1622 members, owner, registrant, mugshot=None, logo=None, icon=None,1902 members, owner, registrant, mugshot=None, logo=None, icon=None,
1623 vcs=None):1903 vcs=None, information_type=None):
1624 """See `IDistributionSet`."""1904 """See `IDistributionSet`."""
1905 if information_type is None:
1906 information_type = InformationType.PUBLIC
1625 distro = Distribution(1907 distro = Distribution(
1626 name=name,1908 name=name,
1627 display_name=display_name,1909 display_name=display_name,
@@ -1635,14 +1917,19 @@ class DistributionSet:
1635 mugshot=mugshot,1917 mugshot=mugshot,
1636 logo=logo,1918 logo=logo,
1637 icon=icon,1919 icon=icon,
1638 vcs=vcs)1920 vcs=vcs,
1921 information_type=information_type)
1639 IStore(distro).add(distro)1922 IStore(distro).add(distro)
1640 getUtility(IArchiveSet).new(distribution=distro,1923 getUtility(IArchiveSet).new(
1641 owner=owner, purpose=ArchivePurpose.PRIMARY)1924 distribution=distro, owner=owner, purpose=ArchivePurpose.PRIMARY)
1642 policies = itertools.product(1925 if information_type != InformationType.PUBLIC:
1643 (distro,), (InformationType.USERDATA,1926 distro._ensure_complimentary_subscription()
1644 InformationType.PRIVATESECURITY))1927 # XXX cjwatson 2022-02-10: Replace this with sharing policies once
1645 getUtility(IAccessPolicySource).create(policies)1928 # those are defined here.
1929 distro._ensurePolicies(
1930 [information_type]
1931 if information_type == InformationType.PROPRIETARY
1932 else FREE_INFORMATION_TYPES)
1646 return distro1933 return distro
16471934
1648 def getCurrentSourceReleases(self, distro_source_packagenames):1935 def getCurrentSourceReleases(self, distro_source_packagenames):
diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py
index 0d9235c..8e8c0f2 100644
--- a/lib/lp/registry/model/product.py
+++ b/lib/lp/registry/model/product.py
@@ -128,7 +128,7 @@ from lp.registry.enums import (
128from lp.registry.errors import (128from lp.registry.errors import (
129 CannotChangeInformationType,129 CannotChangeInformationType,
130 CommercialSubscribersOnly,130 CommercialSubscribersOnly,
131 ProprietaryProduct,131 ProprietaryPillar,
132 )132 )
133from lp.registry.interfaces.accesspolicy import (133from lp.registry.interfaces.accesspolicy import (
134 IAccessPolicyArtifactSource,134 IAccessPolicyArtifactSource,
@@ -560,7 +560,7 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements,
560560
561 def validate_translations_usage(self, attr, value):561 def validate_translations_usage(self, attr, value):
562 if value == ServiceUsage.LAUNCHPAD and self.private:562 if value == ServiceUsage.LAUNCHPAD and self.private:
563 raise ProprietaryProduct(563 raise ProprietaryPillar(
564 "Translations are not supported for proprietary products.")564 "Translations are not supported for proprietary products.")
565 return value565 return value
566566
@@ -683,7 +683,7 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements,
683 "proprietary %s." % kind)683 "proprietary %s." % kind)
684 if self.information_type != InformationType.PUBLIC:684 if self.information_type != InformationType.PUBLIC:
685 if InformationType.PUBLIC in allowed_types[var]:685 if InformationType.PUBLIC in allowed_types[var]:
686 raise ProprietaryProduct(686 raise ProprietaryPillar(
687 "The project is %s." % self.information_type.title)687 "The project is %s." % self.information_type.title)
688 self._ensurePolicies(allowed_types[var])688 self._ensurePolicies(allowed_types[var])
689689
diff --git a/lib/lp/registry/model/productrelease.py b/lib/lp/registry/model/productrelease.py
index a1ae8fd..ba96c73 100644
--- a/lib/lp/registry/model/productrelease.py
+++ b/lib/lp/registry/model/productrelease.py
@@ -26,7 +26,7 @@ from lp.app.enums import InformationType
26from lp.app.errors import NotFoundError26from lp.app.errors import NotFoundError
27from lp.registry.errors import (27from lp.registry.errors import (
28 InvalidFilename,28 InvalidFilename,
29 ProprietaryProduct,29 ProprietaryPillar,
30 )30 )
31from lp.registry.interfaces.person import (31from lp.registry.interfaces.person import (
32 validate_person,32 validate_person,
@@ -151,7 +151,7 @@ class ProductRelease(SQLBase):
151 description=None, from_api=False):151 description=None, from_api=False):
152 """See `IProductRelease`."""152 """See `IProductRelease`."""
153 if not self.can_have_release_files:153 if not self.can_have_release_files:
154 raise ProprietaryProduct(154 raise ProprietaryPillar(
155 "Only public projects can have download files.")155 "Only public projects can have download files.")
156 if self.hasReleaseFile(filename):156 if self.hasReleaseFile(filename):
157 raise InvalidFilename157 raise InvalidFilename
diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py
index cf28a82..772373d 100644
--- a/lib/lp/registry/model/productseries.py
+++ b/lib/lp/registry/model/productseries.py
@@ -43,7 +43,7 @@ from lp.bugs.model.bugtarget import BugTargetBase
43from lp.bugs.model.structuralsubscription import (43from lp.bugs.model.structuralsubscription import (
44 StructuralSubscriptionTargetMixin,44 StructuralSubscriptionTargetMixin,
45 )45 )
46from lp.registry.errors import ProprietaryProduct46from lp.registry.errors import ProprietaryPillar
47from lp.registry.interfaces.packaging import PackagingType47from lp.registry.interfaces.packaging import PackagingType
48from lp.registry.interfaces.person import validate_person48from lp.registry.interfaces.person import validate_person
49from lp.registry.interfaces.productrelease import IProductReleaseSet49from lp.registry.interfaces.productrelease import IProductReleaseSet
@@ -141,8 +141,8 @@ class ProductSeries(SQLBase, BugTargetBase, HasMilestonesMixin,
141 return value141 return value
142 if (self.product.private and142 if (self.product.private and
143 value != TranslationsBranchImportMode.NO_IMPORT):143 value != TranslationsBranchImportMode.NO_IMPORT):
144 raise ProprietaryProduct('Translations are disabled for'144 raise ProprietaryPillar('Translations are disabled for'
145 ' proprietary projects.')145 ' proprietary projects.')
146 return value146 return value
147147
148 translations_autoimport_mode = DBEnum(148 translations_autoimport_mode = DBEnum(
diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
index dda0934..9480512 100644
--- a/lib/lp/registry/services/sharingservice.py
+++ b/lib/lp/registry/services/sharingservice.py
@@ -187,7 +187,13 @@ class SharingService:
187187
188 def getSharedDistributions(self, person, user):188 def getSharedDistributions(self, person, user):
189 """See `ISharingService`."""189 """See `ISharingService`."""
190 return self._getSharedPillars(person, user, Distribution)190 commercial_filter = None
191 if user and IPersonRoles(user).in_commercial_admin:
192 commercial_filter = Exists(Select(
193 1, tables=CommercialSubscription,
194 where=CommercialSubscription.distribution == Distribution.id))
195 return self._getSharedPillars(
196 person, user, Distribution, commercial_filter)
191197
192 def getArtifactGrantsForPersonOnPillar(self, pillar, person):198 def getArtifactGrantsForPersonOnPillar(self, pillar, person):
193 """Return the artifact grants for the given person and pillar."""199 """Return the artifact grants for the given person and pillar."""
diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py
index e7444fe..8ce3be1 100644
--- a/lib/lp/registry/services/tests/test_sharingservice.py
+++ b/lib/lp/registry/services/tests/test_sharingservice.py
@@ -93,8 +93,7 @@ class PillarScenariosMixin(WithScenarios):
93 def _makePillar(self, **kwargs):93 def _makePillar(self, **kwargs):
94 if ("bug_sharing_policy" in kwargs or94 if ("bug_sharing_policy" in kwargs or
95 "branch_sharing_policy" in kwargs or95 "branch_sharing_policy" in kwargs or
96 "specification_sharing_policy" in kwargs or96 "specification_sharing_policy" in kwargs):
97 "information_type" in kwargs):
98 self._skipUnlessProduct()97 self._skipUnlessProduct()
99 return getattr(self.factory, self.pillar_factory_name)(**kwargs)98 return getattr(self.factory, self.pillar_factory_name)(**kwargs)
10099
@@ -215,7 +214,6 @@ class TestSharingService(
215 [InformationType.PRIVATESECURITY, InformationType.USERDATA])214 [InformationType.PRIVATESECURITY, InformationType.USERDATA])
216215
217 def test_getInformationTypes_expired_commercial(self):216 def test_getInformationTypes_expired_commercial(self):
218 self._skipUnlessProduct()
219 pillar = self._makePillar()217 pillar = self._makePillar()
220 self.factory.makeCommercialSubscription(pillar, expired=True)218 self.factory.makeCommercialSubscription(pillar, expired=True)
221 self._assert_getAllowedInformationTypes(219 self._assert_getAllowedInformationTypes(
@@ -268,6 +266,7 @@ class TestSharingService(
268 def test_getBranchSharingPolicies_non_public(self):266 def test_getBranchSharingPolicies_non_public(self):
269 # When the pillar is non-public the policy options are limited to267 # When the pillar is non-public the policy options are limited to
270 # only proprietary or embargoed/proprietary.268 # only proprietary or embargoed/proprietary.
269 self._skipUnlessProduct()
271 owner = self.factory.makePerson()270 owner = self.factory.makePerson()
272 pillar = self._makePillar(271 pillar = self._makePillar(
273 information_type=InformationType.PROPRIETARY,272 information_type=InformationType.PROPRIETARY,
@@ -334,6 +333,7 @@ class TestSharingService(
334 def test_getSpecificationSharingPolicies_non_public(self):333 def test_getSpecificationSharingPolicies_non_public(self):
335 # When the pillar is non-public the policy options are limited to334 # When the pillar is non-public the policy options are limited to
336 # only proprietary or embargoed/proprietary.335 # only proprietary or embargoed/proprietary.
336 self._skipUnlessProduct()
337 owner = self.factory.makePerson()337 owner = self.factory.makePerson()
338 pillar = self._makePillar(338 pillar = self._makePillar(
339 information_type=InformationType.PROPRIETARY,339 information_type=InformationType.PROPRIETARY,
@@ -386,6 +386,7 @@ class TestSharingService(
386 def test_getBugSharingPolicies_non_public(self):386 def test_getBugSharingPolicies_non_public(self):
387 # When the pillar is non-public the policy options are limited to387 # When the pillar is non-public the policy options are limited to
388 # only proprietary or embargoed/proprietary.388 # only proprietary or embargoed/proprietary.
389 self._skipUnlessProduct()
389 owner = self.factory.makePerson()390 owner = self.factory.makePerson()
390 pillar = self._makePillar(391 pillar = self._makePillar(
391 information_type=InformationType.PROPRIETARY,392 information_type=InformationType.PROPRIETARY,
@@ -477,14 +478,13 @@ class TestSharingService(
477 self._makeGranteeData(478 self._makeGranteeData(
478 artifact_grant.grantee,479 artifact_grant.grantee,
479 [(InformationType.PROPRIETARY, SharingPermission.SOME)],480 [(InformationType.PROPRIETARY, SharingPermission.SOME)],
480 [InformationType.PROPRIETARY])]481 [InformationType.PROPRIETARY]),
481 if IProduct.providedBy(pillar):482 self._makeGranteeData(
482 owner_data = self._makeGranteeData(
483 pillar.owner,483 pillar.owner,
484 [(InformationType.USERDATA, SharingPermission.ALL),484 [(InformationType.USERDATA, SharingPermission.ALL),
485 (InformationType.PRIVATESECURITY, SharingPermission.ALL)],485 (InformationType.PRIVATESECURITY, SharingPermission.ALL)],
486 [])486 []),
487 expected_grantees.append(owner_data)487 ]
488 self.assertContentEqual(expected_grantees, grantees)488 self.assertContentEqual(expected_grantees, grantees)
489489
490 def test_getPillarGranteeData(self):490 def test_getPillarGranteeData(self):
@@ -503,7 +503,6 @@ class TestSharingService(
503503
504 Steps 2 and 3 are split out to allow batching on persons.504 Steps 2 and 3 are split out to allow batching on persons.
505 """505 """
506 self._skipUnlessProduct()
507 driver = self.factory.makePerson()506 driver = self.factory.makePerson()
508 pillar = self._makePillar(driver=driver)507 pillar = self._makePillar(driver=driver)
509 login_person(driver)508 login_person(driver)
@@ -577,19 +576,15 @@ class TestSharingService(
577 artifact=artifact_grant.abstract_artifact, policy=access_policy)576 artifact=artifact_grant.abstract_artifact, policy=access_policy)
578577
579 grantees = self.service.getPillarGrantees(pillar)578 grantees = self.service.getPillarGrantees(pillar)
579 policies = getUtility(IAccessPolicySource).findByPillar([pillar])
580 policies = [policy for policy in policies
581 if policy.type != InformationType.PROPRIETARY]
580 expected_grantees = [582 expected_grantees = [
581 (grantee, {access_policy: SharingPermission.ALL}, []),583 (grantee, {access_policy: SharingPermission.ALL}, []),
582 (artifact_grant.grantee, {access_policy: SharingPermission.SOME},584 (artifact_grant.grantee, {access_policy: SharingPermission.SOME},
583 [access_policy.type])]585 [access_policy.type]),
584 if IProduct.providedBy(pillar):586 (pillar.owner, dict.fromkeys(policies, SharingPermission.ALL), []),
585 policies = getUtility(IAccessPolicySource).findByPillar([pillar])587 ]
586 policies = [policy for policy in policies
587 if policy.type != InformationType.PROPRIETARY]
588 owner_data = (
589 pillar.owner,
590 dict.fromkeys(policies, SharingPermission.ALL),
591 [])
592 expected_grantees.append(owner_data)
593 self.assertContentEqual(expected_grantees, grantees)588 self.assertContentEqual(expected_grantees, grantees)
594589
595 def test_getPillarGrantees(self):590 def test_getPillarGrantees(self):
@@ -703,22 +698,13 @@ class TestSharingService(
703 self.assertContentEqual(698 self.assertContentEqual(
704 expected_grantee_data, grantee_data['grantee_entry'])699 expected_grantee_data, grantee_data['grantee_entry'])
705 # Check that getPillarGrantees returns what we expect.700 # Check that getPillarGrantees returns what we expect.
706 if IProduct.providedBy(pillar):701 expected_grantee_grants = [
707 expected_grantee_grants = [702 (grantee,
708 (grantee,703 {ud_policy: SharingPermission.SOME,
709 {ud_policy: SharingPermission.SOME,704 es_policy: SharingPermission.ALL},
710 es_policy: SharingPermission.ALL},705 [InformationType.PRIVATESECURITY,
711 [InformationType.PRIVATESECURITY,706 InformationType.USERDATA]),
712 InformationType.USERDATA]),707 ]
713 ]
714 else:
715 expected_grantee_grants = [
716 (grantee,
717 {es_policy: SharingPermission.ALL,
718 ud_policy: SharingPermission.SOME},
719 [InformationType.PRIVATESECURITY,
720 InformationType.USERDATA]),
721 ]
722708
723 grantee_grants = list(self.service.getPillarGrantees(pillar))709 grantee_grants = list(self.service.getPillarGrantees(pillar))
724 # Again, filter out the owner, if one exists.710 # Again, filter out the owner, if one exists.
@@ -842,11 +828,10 @@ class TestSharingService(
842 yet_another, policy_permissions,828 yet_another, policy_permissions,
843 [InformationType.PRIVATESECURITY, InformationType.USERDATA])829 [InformationType.PRIVATESECURITY, InformationType.USERDATA])
844 expected_data.append(yet_another_person_data)830 expected_data.append(yet_another_person_data)
845 if IProduct.providedBy(pillar):831 policy_permissions = {
846 policy_permissions = {832 policy: SharingPermission.ALL for policy in access_policies}
847 policy: SharingPermission.ALL for policy in access_policies}833 owner_data = (pillar.owner, policy_permissions, [])
848 owner_data = (pillar.owner, policy_permissions, [])834 expected_data.append(owner_data)
849 expected_data.append(owner_data)
850 self._assert_grantee_data(835 self._assert_grantee_data(
851 expected_data, self.service.getPillarGrantees(pillar))836 expected_data, self.service.getPillarGrantees(pillar))
852837
@@ -1516,8 +1501,9 @@ class TestSharingService(
1516 grant_access(branch, i == 9)1501 grant_access(branch, i == 9)
1517 for i, gitrepository in enumerate(gitrepositories):1502 for i, gitrepository in enumerate(gitrepositories):
1518 grant_access(gitrepository, i == 9)1503 grant_access(gitrepository, i == 9)
1519 getUtility(IService, 'sharing').ensureAccessGrants(1504 if snaps:
1520 [grantee], pillar.owner, snaps=snaps[:9])1505 getUtility(IService, 'sharing').ensureAccessGrants(
1506 [grantee], pillar.owner, snaps=snaps[:9])
1521 getUtility(IService, 'sharing').ensureAccessGrants(1507 getUtility(IService, 'sharing').ensureAccessGrants(
1522 [grantee], pillar.owner, specifications=specs[:9])1508 [grantee], pillar.owner, specifications=specs[:9])
1523 getUtility(IService, 'sharing').ensureAccessGrants(1509 getUtility(IService, 'sharing').ensureAccessGrants(
@@ -1593,7 +1579,6 @@ class TestSharingService(
15931579
1594 def test_getSharedPillars_commercial_admin_current(self):1580 def test_getSharedPillars_commercial_admin_current(self):
1595 # Commercial admins can see all current commercial pillars.1581 # Commercial admins can see all current commercial pillars.
1596 self._skipUnlessProduct()
1597 admin = getUtility(ILaunchpadCelebrities).commercial_admin.teamowner1582 admin = getUtility(ILaunchpadCelebrities).commercial_admin.teamowner
1598 pillar = self._makePillar()1583 pillar = self._makePillar()
1599 self.factory.makeCommercialSubscription(pillar)1584 self.factory.makeCommercialSubscription(pillar)
@@ -1601,7 +1586,6 @@ class TestSharingService(
16011586
1602 def test_getSharedPillars_commercial_admin_expired(self):1587 def test_getSharedPillars_commercial_admin_expired(self):
1603 # Commercial admins can see all expired commercial pillars.1588 # Commercial admins can see all expired commercial pillars.
1604 self._skipUnlessProduct()
1605 admin = getUtility(ILaunchpadCelebrities).commercial_admin.teamowner1589 admin = getUtility(ILaunchpadCelebrities).commercial_admin.teamowner
1606 pillar = self._makePillar()1590 pillar = self._makePillar()
1607 self.factory.makeCommercialSubscription(pillar, expired=True)1591 self.factory.makeCommercialSubscription(pillar, expired=True)
@@ -1684,6 +1668,7 @@ class TestSharingService(
16841668
1685 def test_getSharedSnaps(self):1669 def test_getSharedSnaps(self):
1686 # Test the getSharedSnaps method.1670 # Test the getSharedSnaps method.
1671 self._skipUnlessProduct()
1687 owner = self.factory.makePerson()1672 owner = self.factory.makePerson()
1688 pillar = self._makePillar(1673 pillar = self._makePillar(
1689 owner=owner, specification_sharing_policy=(1674 owner=owner, specification_sharing_policy=(
@@ -1981,7 +1966,6 @@ class TestSharingService(
1981 def test_getAccessPolicyGrantCounts(self):1966 def test_getAccessPolicyGrantCounts(self):
1982 # checkPillarAccess checks whether the user has full access to1967 # checkPillarAccess checks whether the user has full access to
1983 # an information type.1968 # an information type.
1984 self._skipUnlessProduct()
1985 pillar = self._makePillar()1969 pillar = self._makePillar()
1986 grantee = self.factory.makePerson()1970 grantee = self.factory.makePerson()
1987 with admin_logged_in():1971 with admin_logged_in():
diff --git a/lib/lp/registry/stories/webservice/xx-distribution.txt b/lib/lp/registry/stories/webservice/xx-distribution.txt
index 4f98323..0e64400 100644
--- a/lib/lp/registry/stories/webservice/xx-distribution.txt
+++ b/lib/lp/registry/stories/webservice/xx-distribution.txt
@@ -40,6 +40,7 @@ And for every distribution we publish most of its attributes.
40 driver_link: None40 driver_link: None
41 homepage_content: None41 homepage_content: None
42 icon_link: 'http://.../ubuntu/icon'42 icon_link: 'http://.../ubuntu/icon'
43 information_type: 'Public'
43 logo_link: 'http://.../ubuntu/logo'44 logo_link: 'http://.../ubuntu/logo'
44 main_archive_link: 'http://.../ubuntu/+archive/primary'45 main_archive_link: 'http://.../ubuntu/+archive/primary'
45 members_link: 'http://.../~ubuntu-team'46 members_link: 'http://.../~ubuntu-team'
@@ -54,6 +55,7 @@ And for every distribution we publish most of its attributes.
54 official_codehosting: False55 official_codehosting: False
55 official_packages: True56 official_packages: True
56 owner_link: 'http://.../~ubuntu-team'57 owner_link: 'http://.../~ubuntu-team'
58 private: False
57 redirect_default_traversal: False59 redirect_default_traversal: False
58 redirect_release_uploads: False60 redirect_release_uploads: False
59 registrant_link: 'http://.../~registry'61 registrant_link: 'http://.../~registry'
diff --git a/lib/lp/registry/tests/test_distribution.py b/lib/lp/registry/tests/test_distribution.py
index 427d95e..49f55df 100644
--- a/lib/lp/registry/tests/test_distribution.py
+++ b/lib/lp/registry/tests/test_distribution.py
@@ -21,10 +21,15 @@ from zope.security.interfaces import Unauthorized
21from zope.security.proxy import removeSecurityProxy21from zope.security.proxy import removeSecurityProxy
2222
23from lp.app.enums import (23from lp.app.enums import (
24 FREE_INFORMATION_TYPES,
24 InformationType,25 InformationType,
26 PILLAR_INFORMATION_TYPES,
25 ServiceUsage,27 ServiceUsage,
26 )28 )
27from lp.app.errors import NotFoundError29from lp.app.errors import (
30 NotFoundError,
31 ServiceUsageForbidden,
32 )
28from lp.app.interfaces.launchpad import ILaunchpadCelebrities33from lp.app.interfaces.launchpad import ILaunchpadCelebrities
29from lp.oci.tests.helpers import OCIConfigHelperMixin34from lp.oci.tests.helpers import OCIConfigHelperMixin
30from lp.registry.enums import (35from lp.registry.enums import (
@@ -33,12 +38,19 @@ from lp.registry.enums import (
33 DistributionDefaultTraversalPolicy,38 DistributionDefaultTraversalPolicy,
34 EXCLUSIVE_TEAM_POLICY,39 EXCLUSIVE_TEAM_POLICY,
35 INCLUSIVE_TEAM_POLICY,40 INCLUSIVE_TEAM_POLICY,
41 TeamMembershipPolicy,
36 )42 )
37from lp.registry.errors import (43from lp.registry.errors import (
44 CannotChangeInformationType,
45 CommercialSubscribersOnly,
38 InclusiveTeamLinkageError,46 InclusiveTeamLinkageError,
39 NoSuchDistroSeries,47 NoSuchDistroSeries,
48 ProprietaryPillar,
49 )
50from lp.registry.interfaces.accesspolicy import (
51 IAccessPolicyGrantSource,
52 IAccessPolicySource,
40 )53 )
41from lp.registry.interfaces.accesspolicy import IAccessPolicySource
42from lp.registry.interfaces.distribution import (54from lp.registry.interfaces.distribution import (
43 IDistribution,55 IDistribution,
44 IDistributionSet,56 IDistributionSet,
@@ -46,7 +58,9 @@ from lp.registry.interfaces.distribution import (
46from lp.registry.interfaces.oopsreferences import IHasOOPSReferences58from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
47from lp.registry.interfaces.person import IPersonSet59from lp.registry.interfaces.person import IPersonSet
48from lp.registry.interfaces.series import SeriesStatus60from lp.registry.interfaces.series import SeriesStatus
61from lp.registry.model.distribution import Distribution
49from lp.registry.tests.test_distroseries import CurrentSourceReleasesMixin62from lp.registry.tests.test_distroseries import CurrentSourceReleasesMixin
63from lp.services.librarianserver.testing.fake import FakeLibrarian
50from lp.services.propertycache import get_property_cache64from lp.services.propertycache import get_property_cache
51from lp.services.webapp import canonical_url65from lp.services.webapp import canonical_url
52from lp.services.webapp.interfaces import OAuthPermission66from lp.services.webapp.interfaces import OAuthPermission
@@ -73,6 +87,9 @@ from lp.testing.views import create_initialized_view
73from lp.translations.enums import TranslationPermission87from lp.translations.enums import TranslationPermission
7488
7589
90PRIVATE_DISTRIBUTION_TYPES = [InformationType.PROPRIETARY]
91
92
76class TestDistribution(TestCaseWithFactory):93class TestDistribution(TestCaseWithFactory):
7794
78 layer = DatabaseFunctionalLayer95 layer = DatabaseFunctionalLayer
@@ -370,6 +387,362 @@ class TestDistribution(TestCaseWithFactory):
370 DistributionDefaultTraversalPolicy.SERIES)387 DistributionDefaultTraversalPolicy.SERIES)
371 distro.redirect_default_traversal = True388 distro.redirect_default_traversal = True
372389
390 def test_creation_grants_maintainer_access(self):
391 # Creating a new distribution creates an access grant for the
392 # maintainer for all default policies.
393 distribution = self.factory.makeDistribution()
394 policies = getUtility(IAccessPolicySource).findByPillar(
395 (distribution,))
396 grants = getUtility(IAccessPolicyGrantSource).findByPolicy(policies)
397 expected_grantess = {distribution.owner}
398 grantees = {grant.grantee for grant in grants}
399 self.assertEqual(expected_grantess, grantees)
400
401 def test_change_info_type_proprietary_check_artifacts(self):
402 # Cannot change distribution information_type if any artifacts are
403 # public.
404 # XXX cjwatson 2022-02-11: Make this use
405 # artifact.transitionToInformationType once sharing policies are in
406 # place.
407 distribution = self.factory.makeDistribution()
408 self.useContext(person_logged_in(distribution.owner))
409 spec = self.factory.makeSpecification(distribution=distribution)
410 for info_type in PRIVATE_DISTRIBUTION_TYPES:
411 with ExpectedException(
412 CannotChangeInformationType,
413 "Some blueprints are public."):
414 distribution.information_type = info_type
415 removeSecurityProxy(spec).information_type = (
416 InformationType.PROPRIETARY)
417 dsp = self.factory.makeDistributionSourcePackage(
418 distribution=distribution)
419 bug = self.factory.makeBug(target=dsp)
420 for bug_info_type in FREE_INFORMATION_TYPES:
421 removeSecurityProxy(bug).information_type = bug_info_type
422 for info_type in PRIVATE_DISTRIBUTION_TYPES:
423 with ExpectedException(
424 CannotChangeInformationType,
425 "Some bugs are neither proprietary nor embargoed."):
426 distribution.information_type = info_type
427 removeSecurityProxy(bug).information_type = InformationType.PROPRIETARY
428 distroseries = self.factory.makeDistroSeries(distribution=distribution)
429 sp = self.factory.makeSourcePackage(distroseries=distroseries)
430 branch = self.factory.makeBranch(sourcepackage=sp)
431 for branch_info_type in FREE_INFORMATION_TYPES:
432 removeSecurityProxy(branch).information_type = branch_info_type
433 for info_type in PRIVATE_DISTRIBUTION_TYPES:
434 with ExpectedException(
435 CannotChangeInformationType,
436 "Some branches are neither proprietary nor "
437 "embargoed."):
438 distribution.information_type = info_type
439 removeSecurityProxy(branch).information_type = (
440 InformationType.PROPRIETARY)
441 for info_type in PRIVATE_DISTRIBUTION_TYPES:
442 distribution.information_type = info_type
443
444 def test_change_info_type_proprietary_check_translations(self):
445 distribution = self.factory.makeDistribution()
446 with person_logged_in(distribution.owner):
447 for usage in ServiceUsage:
448 distribution.information_type = InformationType.PUBLIC
449 distribution.translations_usage = usage.value
450 for info_type in PRIVATE_DISTRIBUTION_TYPES:
451 if (distribution.translations_usage ==
452 ServiceUsage.LAUNCHPAD):
453 with ExpectedException(
454 CannotChangeInformationType,
455 "Translations are enabled."):
456 distribution.information_type = info_type
457 else:
458 distribution.information_type = info_type
459
460 def test_cacheAccessPolicies(self):
461 # Distribution.access_policies is a list caching AccessPolicy.ids
462 # for which an AccessPolicyGrant or AccessArtifactGrant gives a
463 # principal LimitedView on the Distribution.
464 aps = getUtility(IAccessPolicySource)
465
466 # Public distributions don't need a cache.
467 distribution = self.factory.makeDistribution()
468 naked_distribution = removeSecurityProxy(distribution)
469 self.assertContentEqual(
470 [InformationType.USERDATA, InformationType.PRIVATESECURITY],
471 [p.type for p in aps.findByPillar([distribution])])
472 self.assertIsNone(naked_distribution.access_policies)
473
474 # A private distribution normally just allows the Proprietary
475 # policy, even if there is still another policy like Private
476 # Security.
477 naked_distribution.information_type = InformationType.PROPRIETARY
478 [prop_policy] = aps.find([(distribution, InformationType.PROPRIETARY)])
479 self.assertEqual([prop_policy.id], naked_distribution.access_policies)
480
481 # If we switch it back to public, the cache is no longer
482 # required.
483 naked_distribution.information_type = InformationType.PUBLIC
484 self.assertIsNone(naked_distribution.access_policies)
485
486 def test_checkInformationType_bug_supervisor(self):
487 # Bug supervisors of proprietary distributions must not have
488 # inclusive membership policies.
489 team = self.factory.makeTeam()
490 distribution = self.factory.makeDistribution(bug_supervisor=team)
491 for policy in (token.value for token in TeamMembershipPolicy):
492 with person_logged_in(team.teamowner):
493 team.membership_policy = policy
494 for info_type in PRIVATE_DISTRIBUTION_TYPES:
495 with person_logged_in(distribution.owner):
496 errors = list(distribution.checkInformationType(info_type))
497 if policy in EXCLUSIVE_TEAM_POLICY:
498 self.assertEqual([], errors)
499 else:
500 with ExpectedException(
501 CannotChangeInformationType,
502 "Bug supervisor has inclusive membership."):
503 raise errors[0]
504
505 def test_checkInformationType_questions(self):
506 # Proprietary distributions must not have questions.
507 distribution = self.factory.makeDistribution()
508 for info_type in PRIVATE_DISTRIBUTION_TYPES:
509 with person_logged_in(distribution.owner):
510 self.assertEqual([],
511 list(distribution.checkInformationType(info_type)))
512 self.factory.makeQuestion(target=distribution)
513 for info_type in PRIVATE_DISTRIBUTION_TYPES:
514 with person_logged_in(distribution.owner):
515 error, = list(distribution.checkInformationType(info_type))
516 with ExpectedException(
517 CannotChangeInformationType,
518 "This distribution has questions."):
519 raise error
520
521 def test_checkInformationType_translations(self):
522 # Proprietary distributions must not have translations.
523 distroseries = self.factory.makeDistroSeries()
524 distribution = distroseries.distribution
525 for info_type in PRIVATE_DISTRIBUTION_TYPES:
526 with person_logged_in(distribution.owner):
527 self.assertEqual(
528 [], list(distribution.checkInformationType(info_type)))
529 self.factory.makePOTemplate(distroseries=distroseries)
530 for info_type in PRIVATE_DISTRIBUTION_TYPES:
531 with person_logged_in(distribution.owner):
532 error, = list(distribution.checkInformationType(info_type))
533 with ExpectedException(
534 CannotChangeInformationType,
535 "This distribution has translations."):
536 raise error
537
538 def test_checkInformationType_queued_translations(self):
539 # Proprietary distributions must not have queued translations.
540 self.useFixture(FakeLibrarian())
541 distroseries = self.factory.makeDistroSeries()
542 distribution = distroseries.distribution
543 entry = self.factory.makeTranslationImportQueueEntry(
544 distroseries=distroseries)
545 for info_type in PRIVATE_DISTRIBUTION_TYPES:
546 with person_logged_in(distribution.owner):
547 error, = list(distribution.checkInformationType(info_type))
548 with ExpectedException(
549 CannotChangeInformationType,
550 "This distribution has queued translations."):
551 raise error
552 Store.of(entry).remove(entry)
553 with person_logged_in(distribution.owner):
554 for info_type in PRIVATE_DISTRIBUTION_TYPES:
555 self.assertContentEqual(
556 [], distribution.checkInformationType(info_type))
557
558 def test_checkInformationType_series_only_bugs(self):
559 # A distribution with bugtasks that are only targeted to a series
560 # cannot change information type.
561 series = self.factory.makeDistroSeries()
562 bug = self.factory.makeBug(target=series.distribution)
563 with person_logged_in(series.owner):
564 bug.addTask(series.owner, series)
565 bug.default_bugtask.delete()
566 for info_type in PRIVATE_DISTRIBUTION_TYPES:
567 error, = list(series.distribution.checkInformationType(
568 info_type))
569 with ExpectedException(
570 CannotChangeInformationType,
571 "Some bugs are neither proprietary nor embargoed."):
572 raise error
573
574 def test_private_forbids_translations(self):
575 owner = self.factory.makePerson()
576 distribution = self.factory.makeDistribution(owner=owner)
577 self.useContext(person_logged_in(owner))
578 for info_type in PRIVATE_DISTRIBUTION_TYPES:
579 distribution.information_type = info_type
580 with ExpectedException(
581 ProprietaryPillar,
582 "Translations are not supported for proprietary "
583 "distributions."):
584 distribution.translations_usage = ServiceUsage.LAUNCHPAD
585 for usage in ServiceUsage.items:
586 if usage == ServiceUsage.LAUNCHPAD:
587 continue
588 distribution.translations_usage = usage
589
590 def createDistribution(self, information_type=None):
591 # Convenience method for testing IDistributionSet.new rather than
592 # self.factory.makeDistribution.
593 owner = self.factory.makePerson()
594 members = self.factory.makeTeam(owner=owner)
595 kwargs = {}
596 if information_type is not None:
597 kwargs['information_type'] = information_type
598 with person_logged_in(owner):
599 return getUtility(IDistributionSet).new(
600 name=self.factory.getUniqueUnicode("distro"),
601 display_name="Fnord", title="Fnord",
602 description="test 1", summary="test 2",
603 domainname="distro.example.org",
604 members=members, owner=owner, registrant=owner, **kwargs)
605
606 def test_information_type(self):
607 # Distribution is created with specified information_type.
608 distribution = self.createDistribution(
609 information_type=InformationType.PROPRIETARY)
610 self.assertEqual(
611 InformationType.PROPRIETARY, distribution.information_type)
612 # The owner can set information_type.
613 with person_logged_in(removeSecurityProxy(distribution).owner):
614 distribution.information_type = InformationType.PUBLIC
615 self.assertEqual(InformationType.PUBLIC, distribution.information_type)
616 # The database persists the value of information_type.
617 store = Store.of(distribution)
618 store.flush()
619 store.reset()
620 distribution = store.get(Distribution, distribution.id)
621 self.assertEqual(InformationType.PUBLIC, distribution.information_type)
622 self.assertFalse(distribution.private)
623
624 def test_switching_to_public_does_not_create_policy(self):
625 # Creating a Proprietary distribution and switching it to Public
626 # does not create a PUBLIC AccessPolicy.
627 distribution = self.createDistribution(
628 information_type=InformationType.PROPRIETARY)
629 aps = getUtility(IAccessPolicySource).findByPillar([distribution])
630 self.assertContentEqual(
631 [InformationType.PROPRIETARY],
632 [ap.type for ap in aps])
633 removeSecurityProxy(distribution).information_type = (
634 InformationType.PUBLIC)
635 aps = getUtility(IAccessPolicySource).findByPillar([distribution])
636 self.assertContentEqual(
637 [InformationType.PROPRIETARY],
638 [ap.type for ap in aps])
639
640 def test_information_type_default(self):
641 # The default information_type is PUBLIC.
642 distribution = self.createDistribution()
643 self.assertEqual(InformationType.PUBLIC, distribution.information_type)
644 self.assertFalse(distribution.private)
645
646 invalid_information_types = [
647 info_type for info_type in InformationType.items
648 if info_type not in PILLAR_INFORMATION_TYPES]
649
650 def test_information_type_init_invalid_values(self):
651 # Cannot create Distribution.information_type with invalid values.
652 for info_type in self.invalid_information_types:
653 with ExpectedException(
654 CannotChangeInformationType,
655 "Not supported for distributions."):
656 self.createDistribution(information_type=info_type)
657
658 def test_information_type_set_invalid_values(self):
659 # Cannot set Distribution.information_type to invalid values.
660 distribution = self.factory.makeDistribution()
661 for info_type in self.invalid_information_types:
662 with ExpectedException(
663 CannotChangeInformationType,
664 "Not supported for distributions."):
665 with person_logged_in(distribution.owner):
666 distribution.information_type = info_type
667
668 def test_set_proprietary_gets_commercial_subscription(self):
669 # Changing a Distribution to Proprietary will auto-generate a
670 # complimentary subscription just as choosing a proprietary
671 # information type at creation time.
672 owner = self.factory.makePerson()
673 distribution = self.factory.makeDistribution(owner=owner)
674 self.useContext(person_logged_in(owner))
675 self.assertIsNone(distribution.commercial_subscription)
676
677 distribution.information_type = InformationType.PROPRIETARY
678 self.assertEqual(
679 InformationType.PROPRIETARY, distribution.information_type)
680 self.assertIsNotNone(distribution.commercial_subscription)
681
682 def test_set_proprietary_fails_expired_commercial_subscription(self):
683 # Cannot set information type to proprietary with an expired
684 # complimentary subscription.
685 owner = self.factory.makePerson()
686 distribution = self.factory.makeDistribution(
687 information_type=InformationType.PROPRIETARY, owner=owner)
688 self.useContext(person_logged_in(owner))
689
690 # The Distribution now has a complimentary commercial subscription.
691 new_expires_date = (
692 datetime.datetime.now(pytz.UTC) - datetime.timedelta(1))
693 naked_subscription = removeSecurityProxy(
694 distribution.commercial_subscription)
695 naked_subscription.date_expires = new_expires_date
696
697 # We can make the distribution PUBLIC.
698 distribution.information_type = InformationType.PUBLIC
699 self.assertEqual(InformationType.PUBLIC, distribution.information_type)
700
701 # However we can't change it back to Proprietary because our
702 # commercial subscription has expired.
703 with ExpectedException(
704 CommercialSubscribersOnly,
705 "A valid commercial subscription is required for private"
706 " distributions."):
707 distribution.information_type = InformationType.PROPRIETARY
708
709 def test_no_answers_for_proprietary(self):
710 # Enabling Answers is forbidden while information_type is proprietary.
711 distribution = self.factory.makeDistribution(
712 information_type=InformationType.PROPRIETARY)
713 with person_logged_in(removeSecurityProxy(distribution).owner):
714 self.assertEqual(ServiceUsage.UNKNOWN, distribution.answers_usage)
715 for usage in ServiceUsage.items:
716 if usage == ServiceUsage.LAUNCHPAD:
717 with ExpectedException(
718 ServiceUsageForbidden,
719 "Answers not allowed for non-public "
720 "distributions."):
721 distribution.answers_usage = ServiceUsage.LAUNCHPAD
722 else:
723 # All other values are permitted.
724 distribution.answers_usage = usage
725
726 def test_answers_for_public(self):
727 # Enabling answers is permitted while information_type is PUBLIC.
728 distribution = self.factory.makeDistribution(
729 information_type=InformationType.PUBLIC)
730 self.assertEqual(ServiceUsage.UNKNOWN, distribution.answers_usage)
731 with person_logged_in(distribution.owner):
732 for usage in ServiceUsage.items:
733 # All values are permitted.
734 distribution.answers_usage = usage
735
736 def test_no_proprietary_if_answers(self):
737 # Information type cannot be set to proprietary while Answers are
738 # enabled.
739 distribution = self.factory.makeDistribution()
740 with person_logged_in(distribution.owner):
741 distribution.answers_usage = ServiceUsage.LAUNCHPAD
742 with ExpectedException(
743 CannotChangeInformationType, "Answers is enabled."):
744 distribution.information_type = InformationType.PROPRIETARY
745
373746
374class TestDistributionCurrentSourceReleases(747class TestDistributionCurrentSourceReleases(
375 CurrentSourceReleasesMixin, TestCase):748 CurrentSourceReleasesMixin, TestCase):
diff --git a/lib/lp/registry/tests/test_pillaraffiliation.py b/lib/lp/registry/tests/test_pillaraffiliation.py
index 0098e28..e757d0e 100644
--- a/lib/lp/registry/tests/test_pillaraffiliation.py
+++ b/lib/lp/registry/tests/test_pillaraffiliation.py
@@ -3,11 +3,11 @@
33
4"""Tests for adapters."""4"""Tests for adapters."""
55
6from storm.store import Store
7from testtools.matchers import Equals6from testtools.matchers import Equals
8from zope.component import getUtility7from zope.component import getUtility
98
10from lp.registry.model.pillaraffiliation import IHasAffiliation9from lp.registry.model.pillaraffiliation import IHasAffiliation
10from lp.services.database.sqlbase import flush_database_caches
11from lp.services.worlddata.interfaces.language import ILanguageSet11from lp.services.worlddata.interfaces.language import ILanguageSet
12from lp.testing import (12from lp.testing import (
13 person_logged_in,13 person_logged_in,
@@ -146,21 +146,20 @@ class TestPillarAffiliation(TestCaseWithFactory):
146 # - Product, Person146 # - Product, Person
147 person = self.factory.makePerson()147 person = self.factory.makePerson()
148 product = self.factory.makeProduct(owner=person, name='pting')148 product = self.factory.makeProduct(owner=person, name='pting')
149 Store.of(product).invalidate()149 flush_database_caches()
150 with StormStatementRecorder() as recorder:150 with StormStatementRecorder() as recorder:
151 IHasAffiliation(product).getAffiliationBadges([person])151 IHasAffiliation(product).getAffiliationBadges([person])
152 self.assertThat(recorder, HasQueryCount(Equals(4)))152 self.assertThat(recorder, HasQueryCount(Equals(2)))
153153
154 def test_distro_affiliation_query_count(self):154 def test_distro_affiliation_query_count(self):
155 # Only 2 business queries are expected, selects from:155 # Only 2 business queries are expected, selects from:
156 # - Distribution, Person156 # - Distribution, Person
157 # plus an additional query to create a PublisherConfig record.
158 person = self.factory.makePerson()157 person = self.factory.makePerson()
159 distro = self.factory.makeDistribution(owner=person, name='pting')158 distro = self.factory.makeDistribution(owner=person, name='pting')
160 Store.of(distro).invalidate()159 flush_database_caches()
161 with StormStatementRecorder() as recorder:160 with StormStatementRecorder() as recorder:
162 IHasAffiliation(distro).getAffiliationBadges([person])161 IHasAffiliation(distro).getAffiliationBadges([person])
163 self.assertThat(recorder, HasQueryCount(Equals(3)))162 self.assertThat(recorder, HasQueryCount(Equals(2)))
164163
165164
166class _TestBugTaskorBranchMixin:165class _TestBugTaskorBranchMixin:
diff --git a/lib/lp/registry/tests/test_product.py b/lib/lp/registry/tests/test_product.py
index c037549..61f147a 100644
--- a/lib/lp/registry/tests/test_product.py
+++ b/lib/lp/registry/tests/test_product.py
@@ -68,7 +68,7 @@ from lp.registry.errors import (
68 CannotChangeInformationType,68 CannotChangeInformationType,
69 CommercialSubscribersOnly,69 CommercialSubscribersOnly,
70 InclusiveTeamLinkageError,70 InclusiveTeamLinkageError,
71 ProprietaryProduct,71 ProprietaryPillar,
72 )72 )
73from lp.registry.interfaces.accesspolicy import (73from lp.registry.interfaces.accesspolicy import (
74 IAccessPolicyGrantSource,74 IAccessPolicyGrantSource,
@@ -635,7 +635,7 @@ class TestProduct(TestCaseWithFactory):
635 for info_type in PRIVATE_PROJECT_TYPES:635 for info_type in PRIVATE_PROJECT_TYPES:
636 product.information_type = info_type636 product.information_type = info_type
637 with ExpectedException(637 with ExpectedException(
638 ProprietaryProduct,638 ProprietaryPillar,
639 "Translations are not supported for proprietary products."):639 "Translations are not supported for proprietary products."):
640 product.translations_usage = ServiceUsage.LAUNCHPAD640 product.translations_usage = ServiceUsage.LAUNCHPAD
641 for usage in ServiceUsage.items:641 for usage in ServiceUsage.items:
@@ -1761,7 +1761,7 @@ class BaseSharingPolicyTests:
1761 InformationType.PUBLIC in self.allowed_types[policy])1761 InformationType.PUBLIC in self.allowed_types[policy])
1762 for policy in policies_permitting_public:1762 for policy in policies_permitting_public:
1763 with ExpectedException(1763 with ExpectedException(
1764 ProprietaryProduct, "The project is Proprietary."):1764 ProprietaryPillar, "The project is Proprietary."):
1765 self.setSharingPolicy(policy, owner)1765 self.setSharingPolicy(policy, owner)
17661766
17671767
diff --git a/lib/lp/registry/tests/test_productrelease.py b/lib/lp/registry/tests/test_productrelease.py
index 39c8222..85ecbca 100644
--- a/lib/lp/registry/tests/test_productrelease.py
+++ b/lib/lp/registry/tests/test_productrelease.py
@@ -8,7 +8,7 @@ from zope.component import getUtility
8from lp.app.enums import InformationType8from lp.app.enums import InformationType
9from lp.registry.errors import (9from lp.registry.errors import (
10 InvalidFilename,10 InvalidFilename,
11 ProprietaryProduct,11 ProprietaryPillar,
12 )12 )
13from lp.registry.interfaces.productrelease import (13from lp.registry.interfaces.productrelease import (
14 IProductReleaseSet,14 IProductReleaseSet,
@@ -101,5 +101,5 @@ class ProductReleaseFileTestcase(TestCaseWithFactory):
101 release = self.factory.makeProductRelease(product=product)101 release = self.factory.makeProductRelease(product=product)
102 self.assertFalse(release.can_have_release_files)102 self.assertFalse(release.can_have_release_files)
103 self.assertRaises(103 self.assertRaises(
104 ProprietaryProduct, release.addReleaseFile,104 ProprietaryPillar, release.addReleaseFile,
105 'README', b'test', 'text/plain', owner)105 'README', b'test', 'text/plain', owner)
diff --git a/lib/lp/registry/tests/test_productseries.py b/lib/lp/registry/tests/test_productseries.py
index da6f828..e80452e 100644
--- a/lib/lp/registry/tests/test_productseries.py
+++ b/lib/lp/registry/tests/test_productseries.py
@@ -20,7 +20,7 @@ from lp.app.interfaces.services import IService
20from lp.registry.enums import SharingPermission20from lp.registry.enums import SharingPermission
21from lp.registry.errors import (21from lp.registry.errors import (
22 CannotPackageProprietaryProduct,22 CannotPackageProprietaryProduct,
23 ProprietaryProduct,23 ProprietaryPillar,
24 )24 )
25from lp.registry.interfaces.distribution import IDistributionSet25from lp.registry.interfaces.distribution import IDistributionSet
26from lp.registry.interfaces.distroseries import IDistroSeriesSet26from lp.registry.interfaces.distroseries import IDistroSeriesSet
@@ -73,7 +73,7 @@ class TestProductSeries(TestCaseWithFactory):
73 for mode in TranslationsBranchImportMode.items:73 for mode in TranslationsBranchImportMode.items:
74 if mode == TranslationsBranchImportMode.NO_IMPORT:74 if mode == TranslationsBranchImportMode.NO_IMPORT:
75 continue75 continue
76 with ExpectedException(ProprietaryProduct,76 with ExpectedException(ProprietaryPillar,
77 'Translations are disabled for proprietary'77 'Translations are disabled for proprietary'
78 ' projects.'):78 ' projects.'):
79 series.translations_autoimport_mode = mode79 series.translations_autoimport_mode = mode
diff --git a/lib/lp/security.py b/lib/lp/security.py
index 22bc140..95d7a45 100644
--- a/lib/lp/security.py
+++ b/lib/lp/security.py
@@ -1282,7 +1282,11 @@ class EditDistributionByDistroOwnersOrAdmins(AuthorizationBase):
1282 usedfor = IDistribution1282 usedfor = IDistribution
12831283
1284 def checkAuthenticated(self, user):1284 def checkAuthenticated(self, user):
1285 return user.isOwner(self.obj) or user.in_admin1285 # Commercial admins may help setup commercial distributions.
1286 return (
1287 user.isOwner(self.obj)
1288 or is_commercial_case(self.obj, user)
1289 or user.in_admin)
12861290
12871291
1288class ModerateDistributionByDriversOrOwnersOrAdmins(AuthorizationBase):1292class ModerateDistributionByDriversOrOwnersOrAdmins(AuthorizationBase):
@@ -3266,6 +3270,10 @@ class ViewPublisherConfig(AdminByAdminsTeam):
3266 usedfor = IPublisherConfig3270 usedfor = IPublisherConfig
32673271
32683272
3273class ViewSourcePackage(AnonymousAuthorization):
3274 usedfor = ISourcePackage
3275
3276
3269class EditSourcePackage(EditDistributionSourcePackage):3277class EditSourcePackage(EditDistributionSourcePackage):
3270 usedfor = ISourcePackage3278 usedfor = ISourcePackage
32713279
diff --git a/lib/lp/soyuz/tests/test_build_set.py b/lib/lp/soyuz/tests/test_build_set.py
index 6dcf9fd..b2a795c 100644
--- a/lib/lp/soyuz/tests/test_build_set.py
+++ b/lib/lp/soyuz/tests/test_build_set.py
@@ -29,7 +29,10 @@ from lp.testing import (
29 person_logged_in,29 person_logged_in,
30 TestCaseWithFactory,30 TestCaseWithFactory,
31 )31 )
32from lp.testing.dbuser import lp_dbuser32from lp.testing.dbuser import (
33 lp_dbuser,
34 switch_dbuser,
35 )
33from lp.testing.layers import (36from lp.testing.layers import (
34 LaunchpadFunctionalLayer,37 LaunchpadFunctionalLayer,
35 ZopelessDatabaseLayer,38 ZopelessDatabaseLayer,
@@ -348,6 +351,13 @@ class BuildRecordCreationTests(TestNativePublishingBase):
348351
349 def setUp(self):352 def setUp(self):
350 super().setUp()353 super().setUp()
354
355 # TestNativePublishingBase switches to the archive publisher's
356 # database user, but the publisher doesn't create build records so
357 # we aren't really interested in its database permissions here.
358 # Just use the webapp's database user instead.
359 switch_dbuser("launchpad")
360
351 self.distro = self.factory.makeDistribution()361 self.distro = self.factory.makeDistribution()
352 self.avr = self.factory.makeProcessor(362 self.avr = self.factory.makeProcessor(
353 name="avr2001", supports_virtualized=True)363 name="avr2001", supports_virtualized=True)
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 492f57b..041cb89 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -2709,7 +2709,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
2709 publish_root_dir=None, publish_base_url=None,2709 publish_root_dir=None, publish_base_url=None,
2710 publish_copy_base_url=None, no_pubconf=False,2710 publish_copy_base_url=None, no_pubconf=False,
2711 icon=None, summary=None, vcs=None,2711 icon=None, summary=None, vcs=None,
2712 oci_project_admin=None):2712 oci_project_admin=None, information_type=None):
2713 """Make a new distribution."""2713 """Make a new distribution."""
2714 if name is None:2714 if name is None:
2715 name = self.getUniqueString(prefix="distribution")2715 name = self.getUniqueString(prefix="distribution")
@@ -2729,7 +2729,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
2729 members = self.makeTeam(owner)2729 members = self.makeTeam(owner)
2730 distro = getUtility(IDistributionSet).new(2730 distro = getUtility(IDistributionSet).new(
2731 name, displayname, title, description, summary, domainname,2731 name, displayname, title, description, summary, domainname,
2732 members, owner, registrant, icon=icon, vcs=vcs)2732 members, owner, registrant, icon=icon, vcs=vcs,
2733 information_type=information_type)
2733 naked_distro = removeSecurityProxy(distro)2734 naked_distro = removeSecurityProxy(distro)
2734 if aliases is not None:2735 if aliases is not None:
2735 naked_distro.setAliases(aliases)2736 naked_distro.setAliases(aliases)

Subscribers

People subscribed via source and target branches

to status/vote changes: