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
1diff --git a/lib/lp/archivepublisher/tests/test_dominator.py b/lib/lp/archivepublisher/tests/test_dominator.py
2index aefd466..01c56e4 100755
3--- a/lib/lp/archivepublisher/tests/test_dominator.py
4+++ b/lib/lp/archivepublisher/tests/test_dominator.py
5@@ -41,6 +41,7 @@ from lp.testing import (
6 StormStatementRecorder,
7 TestCaseWithFactory,
8 )
9+from lp.testing.dbuser import lp_dbuser
10 from lp.testing.fakemethod import FakeMethod
11 from lp.testing.layers import ZopelessDatabaseLayer
12 from lp.testing.matchers import HasQueryCount
13@@ -165,6 +166,8 @@ class TestDominator(TestNativePublishingBase):
14
15 def test_dominateBinaries_rejects_empty_publication_list(self):
16 """Domination asserts for non-empty input list."""
17+ with lp_dbuser():
18+ distroseries = self.factory.makeDistroArchSeries().distroseries
19 package = self.factory.makeBinaryPackageName()
20 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
21 dominator._sortPackages = FakeMethod({package.name: []})
22@@ -173,11 +176,12 @@ class TestDominator(TestNativePublishingBase):
23 self.assertRaises(
24 AssertionError,
25 dominator.dominateBinaries,
26- self.factory.makeDistroArchSeries().distroseries,
27- self.factory.getAnyPocket())
28+ distroseries, self.factory.getAnyPocket())
29
30 def test_dominateSources_rejects_empty_publication_list(self):
31 """Domination asserts for non-empty input list."""
32+ with lp_dbuser():
33+ distroseries = self.factory.makeDistroSeries()
34 package = self.factory.makeSourcePackageName()
35 dominator = Dominator(self.logger, self.ubuntutest.main_archive)
36 dominator._sortPackages = FakeMethod({package.name: []})
37@@ -186,7 +190,7 @@ class TestDominator(TestNativePublishingBase):
38 self.assertRaises(
39 AssertionError,
40 dominator.dominateSources,
41- self.factory.makeDistroSeries(), self.factory.getAnyPocket())
42+ distroseries, self.factory.getAnyPocket())
43
44 def test_archall_domination(self):
45 # Arch-all binaries should not be dominated when a new source
46diff --git a/lib/lp/archivepublisher/tests/test_publisher.py b/lib/lp/archivepublisher/tests/test_publisher.py
47index c0891dd..754138b 100644
48--- a/lib/lp/archivepublisher/tests/test_publisher.py
49+++ b/lib/lp/archivepublisher/tests/test_publisher.py
50@@ -1288,7 +1288,8 @@ class TestPublisher(TestPublisherBase):
51 ubuntu = getUtility(IDistributionSet)['ubuntu']
52
53 ppa = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
54- copy_archive = self.factory.makeArchive(purpose=ArchivePurpose.COPY)
55+ copy_archive = self.factory.makeArchive(
56+ distribution=self.ubuntu, purpose=ArchivePurpose.COPY)
57 self.assertNotIn(ppa, ubuntu.getPendingPublicationPPAs())
58 self.assertNotIn(copy_archive, ubuntu.getPendingPublicationPPAs())
59 ppa.status = ArchiveStatus.DELETING
60diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
61index fbeaffd..10d677f 100644
62--- a/lib/lp/registry/configure.zcml
63+++ b/lib/lp/registry/configure.zcml
64@@ -1821,7 +1821,8 @@
65 class="lp.registry.model.distribution.Distribution">
66 <allow
67 interface="lp.registry.interfaces.distribution.IDistributionPublic
68- lp.bugs.interfaces.bugsummary.IBugSummaryDimension"/>
69+ lp.bugs.interfaces.bugsummary.IBugSummaryDimension"
70+ attributes="information_type"/>
71 <require
72 permission="launchpad.LimitedView"
73 interface="lp.registry.interfaces.distribution.IDistributionLimitedView"/>
74@@ -1830,7 +1831,8 @@
75 interface="lp.registry.interfaces.distribution.IDistributionView"/>
76 <require
77 permission="launchpad.Edit"
78- interface="lp.registry.interfaces.distribution.IDistributionEditRestricted"/>
79+ interface="lp.registry.interfaces.distribution.IDistributionEditRestricted"
80+ set_schema="lp.app.interfaces.informationtype.IInformationType"/>
81 <require
82 permission="launchpad.Edit"
83 set_attributes="answers_usage blueprints_usage codehosting_usage
84diff --git a/lib/lp/registry/errors.py b/lib/lp/registry/errors.py
85index 4e1c52f..09acb08 100644
86--- a/lib/lp/registry/errors.py
87+++ b/lib/lp/registry/errors.py
88@@ -27,7 +27,7 @@ __all__ = [
89 'InclusiveTeamLinkageError',
90 'PPACreationError',
91 'PrivatePersonLinkageError',
92- 'ProprietaryProduct',
93+ 'ProprietaryPillar',
94 'TeamMembershipTransitionError',
95 'TeamMembershipPolicyError',
96 'UserCannotChangeMembershipSilently',
97@@ -100,8 +100,8 @@ class CommercialSubscribersOnly(Unauthorized):
98 """
99
100
101-class ProprietaryProduct(Exception):
102- """Cannot make the change because the project is proprietary."""
103+class ProprietaryPillar(Exception):
104+ """Cannot make the change because the pillar is proprietary."""
105
106
107 class NoSuchSourcePackageName(NameLookupFailed):
108diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py
109index b102de8..7e943c1 100644
110--- a/lib/lp/registry/interfaces/distribution.py
111+++ b/lib/lp/registry/interfaces/distribution.py
112@@ -59,6 +59,7 @@ from lp import _
113 from lp.answers.interfaces.faqtarget import IFAQTarget
114 from lp.answers.interfaces.questiontarget import IQuestionTarget
115 from lp.app.errors import NameLookupFailed
116+from lp.app.interfaces.informationtype import IInformationType
117 from lp.app.interfaces.launchpad import (
118 IHasIcon,
119 IHasLogo,
120@@ -163,6 +164,14 @@ class IDistributionPublic(Interface):
121 def userCanLimitedView(user):
122 """True if the given user has limited access to this distribution."""
123
124+ private = exported(
125+ Bool(
126+ title=_("Distribution is confidential"),
127+ required=False, readonly=True, default=False,
128+ description=_(
129+ "If set, this distribution is visible only to those with "
130+ "access grants.")))
131+
132
133 class IDistributionLimitedView(IHasIcon, IHasLogo, IHasOwner, ILaunchpadUsage):
134 """IDistribution attributes visible to people with artifact grants."""
135@@ -456,6 +465,9 @@ class IDistributionView(
136 "An object which contains the timeframe and the voucher code of a "
137 "subscription.")))
138
139+ has_current_commercial_subscription = Attribute(
140+ "Whether the distribution has a current commercial subscription.")
141+
142 def getArchiveIDList(archive=None):
143 """Return a list of archive IDs suitable for sqlvalues() or quote().
144
145@@ -768,6 +780,14 @@ class IDistributionView(
146 class IDistributionEditRestricted(IOfficialBugTagTargetRestricted):
147 """IDistribution properties requiring launchpad.Edit permission."""
148
149+ def checkInformationType(value):
150+ """Check whether the information type change should be permitted.
151+
152+ Iterate through exceptions explaining why the type should not be
153+ changed. Has the side-effect of creating a commercial subscription
154+ if permitted.
155+ """
156+
157 @call_with(registrant=REQUEST_USER)
158 @operation_parameters(
159 registry_url=TextLine(
160@@ -802,7 +822,8 @@ class IDistributionEditRestricted(IOfficialBugTagTargetRestricted):
161 class IDistribution(
162 IDistributionEditRestricted, IDistributionPublic,
163 IDistributionLimitedView, IDistributionView, IHasBugSupervisor,
164- IFAQTarget, IQuestionTarget, IStructuralSubscriptionTarget):
165+ IFAQTarget, IQuestionTarget, IStructuralSubscriptionTarget,
166+ IInformationType):
167 """An operating system distribution.
168
169 Launchpadlib example: retrieving the current version of a package in a
170@@ -853,7 +874,8 @@ class IDistributionSet(Interface):
171 """Return the IDistribution with the given name or None."""
172
173 def new(name, display_name, title, description, summary, domainname,
174- members, owner, registrant, mugshot=None, logo=None, icon=None):
175+ members, owner, registrant, mugshot=None, logo=None, icon=None,
176+ information_type=None):
177 """Create a new distribution."""
178
179 def getCurrentSourceReleases(distro_to_source_packagenames):
180diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py
181index c6327d2..372bcbb 100644
182--- a/lib/lp/registry/model/distribution.py
183+++ b/lib/lp/registry/model/distribution.py
184@@ -9,11 +9,17 @@ __all__ = [
185 ]
186
187 from collections import defaultdict
188+from datetime import (
189+ datetime,
190+ timedelta,
191+ )
192 import itertools
193 from operator import itemgetter
194
195+import pytz
196 from storm.expr import (
197 And,
198+ Coalesce,
199 Desc,
200 Exists,
201 Join,
202@@ -27,6 +33,7 @@ from storm.expr import (
203 from storm.info import ClassAlias
204 from storm.locals import (
205 Int,
206+ List,
207 Reference,
208 )
209 from storm.store import Store
210@@ -40,15 +47,23 @@ from lp.answers.model.faq import (
211 FAQSearch,
212 )
213 from lp.answers.model.question import (
214+ Question,
215 QuestionTargetMixin,
216 QuestionTargetSearch,
217 )
218 from lp.app.enums import (
219 FREE_INFORMATION_TYPES,
220 InformationType,
221+ PILLAR_INFORMATION_TYPES,
222+ PRIVATE_INFORMATION_TYPES,
223+ PROPRIETARY_INFORMATION_TYPES,
224+ PUBLIC_INFORMATION_TYPES,
225 ServiceUsage,
226 )
227-from lp.app.errors import NotFoundError
228+from lp.app.errors import (
229+ NotFoundError,
230+ ServiceUsageForbidden,
231+ )
232 from lp.app.interfaces.launchpad import (
233 IHasIcon,
234 IHasLogo,
235@@ -57,11 +72,14 @@ from lp.app.interfaces.launchpad import (
236 ILaunchpadUsage,
237 IServiceUsage,
238 )
239+from lp.app.interfaces.services import IService
240+from lp.app.model.launchpad import InformationTypeMixin
241 from lp.app.validators.name import (
242 sanitize_name,
243 valid_name,
244 )
245 from lp.archivepublisher.debversion import Version
246+from lp.blueprints.enums import SpecificationFilter
247 from lp.blueprints.model.specification import (
248 HasSpecificationsMixin,
249 Specification,
250@@ -75,12 +93,14 @@ from lp.bugs.model.bugtarget import (
251 BugTargetBase,
252 OfficialBugTagTargetMixin,
253 )
254+from lp.bugs.model.bugtaskflat import BugTaskFlat
255 from lp.bugs.model.structuralsubscription import (
256 StructuralSubscriptionTargetMixin,
257 )
258 from lp.code.interfaces.seriessourcepackagebranch import (
259 IFindOfficialBranchLinks,
260 )
261+from lp.code.model.branch import Branch
262 from lp.oci.interfaces.ociregistrycredentials import (
263 IOCIRegistryCredentialsSet,
264 )
265@@ -88,11 +108,20 @@ from lp.registry.enums import (
266 BranchSharingPolicy,
267 BugSharingPolicy,
268 DistributionDefaultTraversalPolicy,
269+ INCLUSIVE_TEAM_POLICY,
270 SpecificationSharingPolicy,
271 VCSType,
272 )
273-from lp.registry.errors import NoSuchDistroSeries
274-from lp.registry.interfaces.accesspolicy import IAccessPolicySource
275+from lp.registry.errors import (
276+ CannotChangeInformationType,
277+ CommercialSubscribersOnly,
278+ NoSuchDistroSeries,
279+ ProprietaryPillar,
280+ )
281+from lp.registry.interfaces.accesspolicy import (
282+ IAccessPolicyGrantSource,
283+ IAccessPolicySource,
284+ )
285 from lp.registry.interfaces.distribution import (
286 IDistribution,
287 IDistributionSet,
288@@ -119,6 +148,7 @@ from lp.registry.interfaces.pocket import suffixpocket
289 from lp.registry.interfaces.role import IPersonRoles
290 from lp.registry.interfaces.series import SeriesStatus
291 from lp.registry.interfaces.sourcepackagename import ISourcePackageName
292+from lp.registry.model.accesspolicy import AccessPolicyGrantFlat
293 from lp.registry.model.announcement import MakesAnnouncements
294 from lp.registry.model.commercialsubscription import CommercialSubscription
295 from lp.registry.model.distributionmirror import (
296@@ -142,6 +172,7 @@ from lp.registry.model.ociprojectname import OCIProjectName
297 from lp.registry.model.oopsreferences import referenced_oops
298 from lp.registry.model.pillar import HasAliasMixin
299 from lp.registry.model.sourcepackagename import SourcePackageName
300+from lp.registry.model.teammembership import TeamParticipation
301 from lp.services.database.bulk import load_referencing
302 from lp.services.database.constants import UTC_NOW
303 from lp.services.database.datetimecol import UtcDateTimeCol
304@@ -159,6 +190,8 @@ from lp.services.database.sqlobject import (
305 StringCol,
306 )
307 from lp.services.database.stormexpr import (
308+ ArrayAgg,
309+ ArrayIntersects,
310 fti_search,
311 rank_by_fti,
312 )
313@@ -203,6 +236,7 @@ from lp.translations.enums import TranslationPermission
314 from lp.translations.model.hastranslationimports import (
315 HasTranslationImportsMixin,
316 )
317+from lp.translations.model.potemplate import POTemplate
318 from lp.translations.model.translationpolicy import TranslationPolicyMixin
319
320
321@@ -215,7 +249,8 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
322 HasTranslationImportsMixin, KarmaContextMixin,
323 OfficialBugTagTargetMixin, QuestionTargetMixin,
324 StructuralSubscriptionTargetMixin, HasMilestonesMixin,
325- HasDriversMixin, TranslationPolicyMixin):
326+ HasDriversMixin, TranslationPolicyMixin,
327+ InformationTypeMixin):
328 """A distribution of an operating system, e.g. Debian GNU/Linux."""
329
330 _table = 'Distribution'
331@@ -281,9 +316,12 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
332 oci_registry_credentials = Reference(
333 oci_registry_credentials_id, "OCIRegistryCredentials.id")
334
335+ _creating = False
336+
337 def __init__(self, name, display_name, title, description, summary,
338 domainname, members, owner, registrant, mugshot=None,
339- logo=None, icon=None, vcs=None):
340+ logo=None, icon=None, vcs=None, information_type=None):
341+ self._creating = True
342 try:
343 self.name = name
344 self.display_name = display_name
345@@ -299,9 +337,11 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
346 self.logo = logo
347 self.icon = icon
348 self.vcs = vcs
349+ self.information_type = information_type
350 except Exception:
351 IStore(self).remove(self)
352 raise
353+ del self._creating
354
355 def __repr__(self):
356 display_name = backslashreplace(self.display_name)
357@@ -321,6 +361,109 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
358 """See `IBugTarget`."""
359 return self
360
361+ def _valid_distribution_information_type(self, attr, value):
362+ for exception in self.checkInformationType(value):
363+ raise exception
364+ return value
365+
366+ def checkInformationType(self, value):
367+ """See `IDistribution`."""
368+ if value not in PILLAR_INFORMATION_TYPES:
369+ yield CannotChangeInformationType(
370+ 'Not supported for distributions.')
371+ if value in PROPRIETARY_INFORMATION_TYPES:
372+ if self.answers_usage == ServiceUsage.LAUNCHPAD:
373+ yield CannotChangeInformationType('Answers is enabled.')
374+ if self._creating or value not in PROPRIETARY_INFORMATION_TYPES:
375+ return
376+ # Additional checks when transitioning an existing distribution to a
377+ # proprietary type.
378+ # All specs located by an ALL search are public.
379+ public_specs = self.specifications(
380+ None, filter=[SpecificationFilter.ALL])
381+ if not public_specs.is_empty():
382+ # Unlike bugs and branches, specifications cannot be USERDATA or a
383+ # security type.
384+ yield CannotChangeInformationType('Some blueprints are public.')
385+ store = Store.of(self)
386+ series_ids = [series.id for series in self.series]
387+ non_proprietary_bugs = store.find(
388+ BugTaskFlat,
389+ BugTaskFlat.information_type.is_in(FREE_INFORMATION_TYPES),
390+ Or(
391+ BugTaskFlat.distribution == self.id,
392+ BugTaskFlat.distroseries_id.is_in(series_ids)))
393+ if not non_proprietary_bugs.is_empty():
394+ yield CannotChangeInformationType(
395+ 'Some bugs are neither proprietary nor embargoed.')
396+ # Default returns all public branches.
397+ non_proprietary_branches = store.find(
398+ Branch,
399+ DistroSeries.distribution == self.id,
400+ Branch.distroseries == DistroSeries.id,
401+ Not(Branch.information_type.is_in(PROPRIETARY_INFORMATION_TYPES)))
402+ if not non_proprietary_branches.is_empty():
403+ yield CannotChangeInformationType(
404+ 'Some branches are neither proprietary nor embargoed.')
405+ questions = store.find(Question, Question.distribution == self.id)
406+ if not questions.is_empty():
407+ yield CannotChangeInformationType(
408+ 'This distribution has questions.')
409+ templates = store.find(
410+ POTemplate, DistroSeries.distribution == self.id,
411+ POTemplate.distroseries == DistroSeries.id)
412+ if not templates.is_empty():
413+ yield CannotChangeInformationType(
414+ 'This distribution has translations.')
415+ if not self.getTranslationImportQueueEntries().is_empty():
416+ yield CannotChangeInformationType(
417+ 'This distribution has queued translations.')
418+ if self.translations_usage == ServiceUsage.LAUNCHPAD:
419+ yield CannotChangeInformationType('Translations are enabled.')
420+ bug_supervisor = self.bug_supervisor
421+ if (bug_supervisor is not None and
422+ bug_supervisor.membership_policy in INCLUSIVE_TEAM_POLICY):
423+ yield CannotChangeInformationType(
424+ 'Bug supervisor has inclusive membership.')
425+
426+ # Proprietary check works only after creation, because during
427+ # creation, has_current_commercial_subscription cannot give the
428+ # right value and triggers an inappropriate DB flush.
429+
430+ # Create the complimentary commercial subscription for the
431+ # distribution.
432+ self._ensure_complimentary_subscription()
433+
434+ # If you have a commercial subscription, but it's not current, you
435+ # cannot set the information type to a PROPRIETARY type.
436+ if not self.has_current_commercial_subscription:
437+ yield CommercialSubscribersOnly(
438+ 'A valid commercial subscription is required for private'
439+ ' distributions.')
440+
441+ _information_type = DBEnum(
442+ enum=InformationType, default=InformationType.PUBLIC,
443+ name="information_type",
444+ validator=_valid_distribution_information_type)
445+
446+ @property
447+ def information_type(self):
448+ return self._information_type or InformationType.PUBLIC
449+
450+ @information_type.setter
451+ def information_type(self, value):
452+ old_info_type = self._information_type
453+ self._information_type = value
454+ # Make sure that policies are updated to grant permission to the
455+ # maintainer as required for the Distribution.
456+ # However, only on edits. If this is a new Distribution it's
457+ # handled already.
458+ if not self._creating:
459+ if (old_info_type == InformationType.PUBLIC and
460+ value != InformationType.PUBLIC):
461+ self._ensure_complimentary_subscription()
462+ self._ensurePolicies([value])
463+
464 @property
465 def pillar_category(self):
466 """See `IPillar`."""
467@@ -344,12 +487,77 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
468 # Sharing policy for distributions is always PUBLIC.
469 return SpecificationSharingPolicy.PUBLIC
470
471+ # Cache of AccessPolicy.ids that convey launchpad.LimitedView.
472+ # Unlike artifacts' cached access_policies, an AccessArtifactGrant
473+ # to an artifact in the policy is sufficient for access.
474+ access_policies = List(type=Int())
475+
476+ def _ensurePolicies(self, information_types):
477+ # Ensure that the distribution has access policies for the specified
478+ # information types.
479+ aps = getUtility(IAccessPolicySource)
480+ existing_policies = aps.findByPillar([self])
481+ existing_types = {
482+ access_policy.type for access_policy in existing_policies}
483+ # Create the missing policies.
484+ required_types = set(information_types).difference(
485+ existing_types).intersection(PRIVATE_INFORMATION_TYPES)
486+ policies = itertools.product((self,), required_types)
487+ policies = getUtility(IAccessPolicySource).create(policies)
488+
489+ # Add the maintainer to the policies.
490+ grants = []
491+ for p in policies:
492+ grants.append((p, self.owner, self.owner))
493+ getUtility(IAccessPolicyGrantSource).grant(grants)
494+
495+ self._cacheAccessPolicies()
496+
497+ def _cacheAccessPolicies(self):
498+ # Update the cache of AccessPolicy.ids for which an
499+ # AccessPolicyGrant or AccessArtifactGrant is sufficient to
500+ # convey launchpad.LimitedView on this Distribution.
501+ #
502+ # We only need a cache for proprietary types, and it only
503+ # includes proprietary policies in case a policy like Private
504+ # Security was somehow left around when a project was
505+ # transitioned to Proprietary.
506+ if self.information_type in PROPRIETARY_INFORMATION_TYPES:
507+ self.access_policies = [
508+ policy.id for policy in
509+ getUtility(IAccessPolicySource).find(
510+ [(self, type) for type in PROPRIETARY_INFORMATION_TYPES])]
511+ else:
512+ self.access_policies = None
513+
514 @cachedproperty
515 def commercial_subscription(self):
516 return IStore(CommercialSubscription).find(
517 CommercialSubscription, distribution=self).one()
518
519 @property
520+ def has_current_commercial_subscription(self):
521+ now = datetime.now(pytz.UTC)
522+ return (self.commercial_subscription
523+ and self.commercial_subscription.date_expires > now)
524+
525+ def _ensure_complimentary_subscription(self):
526+ """Create a complementary commercial subscription for the distro."""
527+ if not self.commercial_subscription:
528+ lp_janitor = getUtility(ILaunchpadCelebrities).janitor
529+ now = datetime.now(pytz.UTC)
530+ date_expires = now + timedelta(days=30)
531+ sales_system_id = "complimentary-30-day-%s" % now
532+ whiteboard = (
533+ "Complimentary 30 day subscription. -- Launchpad %s" %
534+ now.date().isoformat())
535+ subscription = CommercialSubscription(
536+ pillar=self, date_starts=now, date_expires=date_expires,
537+ registrant=lp_janitor, purchaser=lp_janitor,
538+ sales_system_id=sales_system_id, whiteboard=whiteboard)
539+ get_property_cache(self).commercial_subscription = subscription
540+
541+ @property
542 def uploaders(self):
543 """See `IDistribution`."""
544 # Get all the distribution archives and find out the uploaders
545@@ -397,6 +605,10 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
546 return self._answers_usage
547
548 def _set_answers_usage(self, val):
549+ if val == ServiceUsage.LAUNCHPAD:
550+ if self.information_type in PROPRIETARY_INFORMATION_TYPES:
551+ raise ServiceUsageForbidden(
552+ "Answers not allowed for non-public distributions.")
553 self._answers_usage = val
554 if val == ServiceUsage.LAUNCHPAD:
555 self.official_answers = True
556@@ -432,9 +644,17 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
557 _set_blueprints_usage,
558 doc="Indicates if the product uses the blueprints service.")
559
560+ def validate_translations_usage(self, attr, value):
561+ if value == ServiceUsage.LAUNCHPAD and self.private:
562+ raise ProprietaryPillar(
563+ "Translations are not supported for proprietary "
564+ "distributions.")
565+ return value
566+
567 translations_usage = DBEnum(
568 name="translations_usage", allow_none=False,
569- enum=ServiceUsage, default=ServiceUsage.UNKNOWN)
570+ enum=ServiceUsage, default=ServiceUsage.UNKNOWN,
571+ validator=validate_translations_usage)
572
573 @property
574 def codehosting_usage(self):
575@@ -1568,17 +1788,41 @@ class Distribution(SQLBase, BugTargetBase, MakesAnnouncements,
576 self.oci_registry_credentials = None
577 old_credentials.destroySelf()
578
579+ @cachedproperty
580+ def _known_viewers(self):
581+ """A set of known persons able to view this distribution."""
582+ return set()
583+
584 def userCanView(self, user):
585 """See `IDistributionPublic`."""
586- # All distributions are public until we finish introducing privacy
587- # support.
588- return True
589+ if self.information_type in PUBLIC_INFORMATION_TYPES:
590+ return True
591+ if user is None:
592+ return False
593+ if user.id in self._known_viewers:
594+ return True
595+ if not IPersonRoles.providedBy(user):
596+ user = IPersonRoles(user)
597+ if user.in_commercial_admin or user.in_admin:
598+ self._known_viewers.add(user.id)
599+ return True
600+ if getUtility(IService, 'sharing').checkPillarAccess(
601+ [self], self.information_type, user):
602+ self._known_viewers.add(user.id)
603+ return True
604+ return False
605
606 def userCanLimitedView(self, user):
607 """See `IDistributionPublic`."""
608- # All distributions are public until we finish introducing privacy
609- # support.
610- return True
611+ if self.userCanView(user):
612+ return True
613+ if user is None:
614+ return False
615+ return not Store.of(self).find(
616+ Distribution,
617+ Distribution.id == self.id,
618+ DistributionSet.getDistributionPrivacyFilter(user.person),
619+ ).is_empty()
620
621
622 @implementer(IDistributionSet)
623@@ -1618,10 +1862,48 @@ class DistributionSet:
624 return None
625 return pillar
626
627+ @staticmethod
628+ def getDistributionPrivacyFilter(user):
629+ # Anonymous users can only see public distributions. This is also
630+ # sometimes used with an outer join with e.g. Product, so we let
631+ # NULL through too.
632+ public_filter = Or(
633+ Distribution._information_type == None,
634+ Distribution._information_type == InformationType.PUBLIC)
635+ if user is None:
636+ return public_filter
637+
638+ # (Commercial) admins can see any project.
639+ roles = IPersonRoles(user)
640+ if roles.in_admin or roles.in_commercial_admin:
641+ return True
642+
643+ # Normal users can see any project for which they can see either
644+ # an entire policy or an artifact.
645+ # XXX wgrant 2015-06-26: This is slower than ideal for people in
646+ # teams with lots of artifact grants, as there can be tens of
647+ # thousands of APGF rows for a single policy. But it's tens of
648+ # milliseconds at most.
649+ grant_filter = Coalesce(
650+ ArrayIntersects(
651+ SQL('Distribution.access_policies'),
652+ Select(
653+ ArrayAgg(AccessPolicyGrantFlat.policy_id),
654+ tables=(AccessPolicyGrantFlat,
655+ Join(TeamParticipation,
656+ TeamParticipation.teamID ==
657+ AccessPolicyGrantFlat.grantee_id)),
658+ where=(TeamParticipation.person == user)
659+ )),
660+ False)
661+ return Or(public_filter, grant_filter)
662+
663 def new(self, name, display_name, title, description, summary, domainname,
664 members, owner, registrant, mugshot=None, logo=None, icon=None,
665- vcs=None):
666+ vcs=None, information_type=None):
667 """See `IDistributionSet`."""
668+ if information_type is None:
669+ information_type = InformationType.PUBLIC
670 distro = Distribution(
671 name=name,
672 display_name=display_name,
673@@ -1635,14 +1917,19 @@ class DistributionSet:
674 mugshot=mugshot,
675 logo=logo,
676 icon=icon,
677- vcs=vcs)
678+ vcs=vcs,
679+ information_type=information_type)
680 IStore(distro).add(distro)
681- getUtility(IArchiveSet).new(distribution=distro,
682- owner=owner, purpose=ArchivePurpose.PRIMARY)
683- policies = itertools.product(
684- (distro,), (InformationType.USERDATA,
685- InformationType.PRIVATESECURITY))
686- getUtility(IAccessPolicySource).create(policies)
687+ getUtility(IArchiveSet).new(
688+ distribution=distro, owner=owner, purpose=ArchivePurpose.PRIMARY)
689+ if information_type != InformationType.PUBLIC:
690+ distro._ensure_complimentary_subscription()
691+ # XXX cjwatson 2022-02-10: Replace this with sharing policies once
692+ # those are defined here.
693+ distro._ensurePolicies(
694+ [information_type]
695+ if information_type == InformationType.PROPRIETARY
696+ else FREE_INFORMATION_TYPES)
697 return distro
698
699 def getCurrentSourceReleases(self, distro_source_packagenames):
700diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py
701index 0d9235c..8e8c0f2 100644
702--- a/lib/lp/registry/model/product.py
703+++ b/lib/lp/registry/model/product.py
704@@ -128,7 +128,7 @@ from lp.registry.enums import (
705 from lp.registry.errors import (
706 CannotChangeInformationType,
707 CommercialSubscribersOnly,
708- ProprietaryProduct,
709+ ProprietaryPillar,
710 )
711 from lp.registry.interfaces.accesspolicy import (
712 IAccessPolicyArtifactSource,
713@@ -560,7 +560,7 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements,
714
715 def validate_translations_usage(self, attr, value):
716 if value == ServiceUsage.LAUNCHPAD and self.private:
717- raise ProprietaryProduct(
718+ raise ProprietaryPillar(
719 "Translations are not supported for proprietary products.")
720 return value
721
722@@ -683,7 +683,7 @@ class Product(SQLBase, BugTargetBase, MakesAnnouncements,
723 "proprietary %s." % kind)
724 if self.information_type != InformationType.PUBLIC:
725 if InformationType.PUBLIC in allowed_types[var]:
726- raise ProprietaryProduct(
727+ raise ProprietaryPillar(
728 "The project is %s." % self.information_type.title)
729 self._ensurePolicies(allowed_types[var])
730
731diff --git a/lib/lp/registry/model/productrelease.py b/lib/lp/registry/model/productrelease.py
732index a1ae8fd..ba96c73 100644
733--- a/lib/lp/registry/model/productrelease.py
734+++ b/lib/lp/registry/model/productrelease.py
735@@ -26,7 +26,7 @@ from lp.app.enums import InformationType
736 from lp.app.errors import NotFoundError
737 from lp.registry.errors import (
738 InvalidFilename,
739- ProprietaryProduct,
740+ ProprietaryPillar,
741 )
742 from lp.registry.interfaces.person import (
743 validate_person,
744@@ -151,7 +151,7 @@ class ProductRelease(SQLBase):
745 description=None, from_api=False):
746 """See `IProductRelease`."""
747 if not self.can_have_release_files:
748- raise ProprietaryProduct(
749+ raise ProprietaryPillar(
750 "Only public projects can have download files.")
751 if self.hasReleaseFile(filename):
752 raise InvalidFilename
753diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py
754index cf28a82..772373d 100644
755--- a/lib/lp/registry/model/productseries.py
756+++ b/lib/lp/registry/model/productseries.py
757@@ -43,7 +43,7 @@ from lp.bugs.model.bugtarget import BugTargetBase
758 from lp.bugs.model.structuralsubscription import (
759 StructuralSubscriptionTargetMixin,
760 )
761-from lp.registry.errors import ProprietaryProduct
762+from lp.registry.errors import ProprietaryPillar
763 from lp.registry.interfaces.packaging import PackagingType
764 from lp.registry.interfaces.person import validate_person
765 from lp.registry.interfaces.productrelease import IProductReleaseSet
766@@ -141,8 +141,8 @@ class ProductSeries(SQLBase, BugTargetBase, HasMilestonesMixin,
767 return value
768 if (self.product.private and
769 value != TranslationsBranchImportMode.NO_IMPORT):
770- raise ProprietaryProduct('Translations are disabled for'
771- ' proprietary projects.')
772+ raise ProprietaryPillar('Translations are disabled for'
773+ ' proprietary projects.')
774 return value
775
776 translations_autoimport_mode = DBEnum(
777diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py
778index dda0934..9480512 100644
779--- a/lib/lp/registry/services/sharingservice.py
780+++ b/lib/lp/registry/services/sharingservice.py
781@@ -187,7 +187,13 @@ class SharingService:
782
783 def getSharedDistributions(self, person, user):
784 """See `ISharingService`."""
785- return self._getSharedPillars(person, user, Distribution)
786+ commercial_filter = None
787+ if user and IPersonRoles(user).in_commercial_admin:
788+ commercial_filter = Exists(Select(
789+ 1, tables=CommercialSubscription,
790+ where=CommercialSubscription.distribution == Distribution.id))
791+ return self._getSharedPillars(
792+ person, user, Distribution, commercial_filter)
793
794 def getArtifactGrantsForPersonOnPillar(self, pillar, person):
795 """Return the artifact grants for the given person and pillar."""
796diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py
797index e7444fe..8ce3be1 100644
798--- a/lib/lp/registry/services/tests/test_sharingservice.py
799+++ b/lib/lp/registry/services/tests/test_sharingservice.py
800@@ -93,8 +93,7 @@ class PillarScenariosMixin(WithScenarios):
801 def _makePillar(self, **kwargs):
802 if ("bug_sharing_policy" in kwargs or
803 "branch_sharing_policy" in kwargs or
804- "specification_sharing_policy" in kwargs or
805- "information_type" in kwargs):
806+ "specification_sharing_policy" in kwargs):
807 self._skipUnlessProduct()
808 return getattr(self.factory, self.pillar_factory_name)(**kwargs)
809
810@@ -215,7 +214,6 @@ class TestSharingService(
811 [InformationType.PRIVATESECURITY, InformationType.USERDATA])
812
813 def test_getInformationTypes_expired_commercial(self):
814- self._skipUnlessProduct()
815 pillar = self._makePillar()
816 self.factory.makeCommercialSubscription(pillar, expired=True)
817 self._assert_getAllowedInformationTypes(
818@@ -268,6 +266,7 @@ class TestSharingService(
819 def test_getBranchSharingPolicies_non_public(self):
820 # When the pillar is non-public the policy options are limited to
821 # only proprietary or embargoed/proprietary.
822+ self._skipUnlessProduct()
823 owner = self.factory.makePerson()
824 pillar = self._makePillar(
825 information_type=InformationType.PROPRIETARY,
826@@ -334,6 +333,7 @@ class TestSharingService(
827 def test_getSpecificationSharingPolicies_non_public(self):
828 # When the pillar is non-public the policy options are limited to
829 # only proprietary or embargoed/proprietary.
830+ self._skipUnlessProduct()
831 owner = self.factory.makePerson()
832 pillar = self._makePillar(
833 information_type=InformationType.PROPRIETARY,
834@@ -386,6 +386,7 @@ class TestSharingService(
835 def test_getBugSharingPolicies_non_public(self):
836 # When the pillar is non-public the policy options are limited to
837 # only proprietary or embargoed/proprietary.
838+ self._skipUnlessProduct()
839 owner = self.factory.makePerson()
840 pillar = self._makePillar(
841 information_type=InformationType.PROPRIETARY,
842@@ -477,14 +478,13 @@ class TestSharingService(
843 self._makeGranteeData(
844 artifact_grant.grantee,
845 [(InformationType.PROPRIETARY, SharingPermission.SOME)],
846- [InformationType.PROPRIETARY])]
847- if IProduct.providedBy(pillar):
848- owner_data = self._makeGranteeData(
849+ [InformationType.PROPRIETARY]),
850+ self._makeGranteeData(
851 pillar.owner,
852 [(InformationType.USERDATA, SharingPermission.ALL),
853 (InformationType.PRIVATESECURITY, SharingPermission.ALL)],
854- [])
855- expected_grantees.append(owner_data)
856+ []),
857+ ]
858 self.assertContentEqual(expected_grantees, grantees)
859
860 def test_getPillarGranteeData(self):
861@@ -503,7 +503,6 @@ class TestSharingService(
862
863 Steps 2 and 3 are split out to allow batching on persons.
864 """
865- self._skipUnlessProduct()
866 driver = self.factory.makePerson()
867 pillar = self._makePillar(driver=driver)
868 login_person(driver)
869@@ -577,19 +576,15 @@ class TestSharingService(
870 artifact=artifact_grant.abstract_artifact, policy=access_policy)
871
872 grantees = self.service.getPillarGrantees(pillar)
873+ policies = getUtility(IAccessPolicySource).findByPillar([pillar])
874+ policies = [policy for policy in policies
875+ if policy.type != InformationType.PROPRIETARY]
876 expected_grantees = [
877 (grantee, {access_policy: SharingPermission.ALL}, []),
878 (artifact_grant.grantee, {access_policy: SharingPermission.SOME},
879- [access_policy.type])]
880- if IProduct.providedBy(pillar):
881- policies = getUtility(IAccessPolicySource).findByPillar([pillar])
882- policies = [policy for policy in policies
883- if policy.type != InformationType.PROPRIETARY]
884- owner_data = (
885- pillar.owner,
886- dict.fromkeys(policies, SharingPermission.ALL),
887- [])
888- expected_grantees.append(owner_data)
889+ [access_policy.type]),
890+ (pillar.owner, dict.fromkeys(policies, SharingPermission.ALL), []),
891+ ]
892 self.assertContentEqual(expected_grantees, grantees)
893
894 def test_getPillarGrantees(self):
895@@ -703,22 +698,13 @@ class TestSharingService(
896 self.assertContentEqual(
897 expected_grantee_data, grantee_data['grantee_entry'])
898 # Check that getPillarGrantees returns what we expect.
899- if IProduct.providedBy(pillar):
900- expected_grantee_grants = [
901- (grantee,
902- {ud_policy: SharingPermission.SOME,
903- es_policy: SharingPermission.ALL},
904- [InformationType.PRIVATESECURITY,
905- InformationType.USERDATA]),
906- ]
907- else:
908- expected_grantee_grants = [
909- (grantee,
910- {es_policy: SharingPermission.ALL,
911- ud_policy: SharingPermission.SOME},
912- [InformationType.PRIVATESECURITY,
913- InformationType.USERDATA]),
914- ]
915+ expected_grantee_grants = [
916+ (grantee,
917+ {ud_policy: SharingPermission.SOME,
918+ es_policy: SharingPermission.ALL},
919+ [InformationType.PRIVATESECURITY,
920+ InformationType.USERDATA]),
921+ ]
922
923 grantee_grants = list(self.service.getPillarGrantees(pillar))
924 # Again, filter out the owner, if one exists.
925@@ -842,11 +828,10 @@ class TestSharingService(
926 yet_another, policy_permissions,
927 [InformationType.PRIVATESECURITY, InformationType.USERDATA])
928 expected_data.append(yet_another_person_data)
929- if IProduct.providedBy(pillar):
930- policy_permissions = {
931- policy: SharingPermission.ALL for policy in access_policies}
932- owner_data = (pillar.owner, policy_permissions, [])
933- expected_data.append(owner_data)
934+ policy_permissions = {
935+ policy: SharingPermission.ALL for policy in access_policies}
936+ owner_data = (pillar.owner, policy_permissions, [])
937+ expected_data.append(owner_data)
938 self._assert_grantee_data(
939 expected_data, self.service.getPillarGrantees(pillar))
940
941@@ -1516,8 +1501,9 @@ class TestSharingService(
942 grant_access(branch, i == 9)
943 for i, gitrepository in enumerate(gitrepositories):
944 grant_access(gitrepository, i == 9)
945- getUtility(IService, 'sharing').ensureAccessGrants(
946- [grantee], pillar.owner, snaps=snaps[:9])
947+ if snaps:
948+ getUtility(IService, 'sharing').ensureAccessGrants(
949+ [grantee], pillar.owner, snaps=snaps[:9])
950 getUtility(IService, 'sharing').ensureAccessGrants(
951 [grantee], pillar.owner, specifications=specs[:9])
952 getUtility(IService, 'sharing').ensureAccessGrants(
953@@ -1593,7 +1579,6 @@ class TestSharingService(
954
955 def test_getSharedPillars_commercial_admin_current(self):
956 # Commercial admins can see all current commercial pillars.
957- self._skipUnlessProduct()
958 admin = getUtility(ILaunchpadCelebrities).commercial_admin.teamowner
959 pillar = self._makePillar()
960 self.factory.makeCommercialSubscription(pillar)
961@@ -1601,7 +1586,6 @@ class TestSharingService(
962
963 def test_getSharedPillars_commercial_admin_expired(self):
964 # Commercial admins can see all expired commercial pillars.
965- self._skipUnlessProduct()
966 admin = getUtility(ILaunchpadCelebrities).commercial_admin.teamowner
967 pillar = self._makePillar()
968 self.factory.makeCommercialSubscription(pillar, expired=True)
969@@ -1684,6 +1668,7 @@ class TestSharingService(
970
971 def test_getSharedSnaps(self):
972 # Test the getSharedSnaps method.
973+ self._skipUnlessProduct()
974 owner = self.factory.makePerson()
975 pillar = self._makePillar(
976 owner=owner, specification_sharing_policy=(
977@@ -1981,7 +1966,6 @@ class TestSharingService(
978 def test_getAccessPolicyGrantCounts(self):
979 # checkPillarAccess checks whether the user has full access to
980 # an information type.
981- self._skipUnlessProduct()
982 pillar = self._makePillar()
983 grantee = self.factory.makePerson()
984 with admin_logged_in():
985diff --git a/lib/lp/registry/stories/webservice/xx-distribution.txt b/lib/lp/registry/stories/webservice/xx-distribution.txt
986index 4f98323..0e64400 100644
987--- a/lib/lp/registry/stories/webservice/xx-distribution.txt
988+++ b/lib/lp/registry/stories/webservice/xx-distribution.txt
989@@ -40,6 +40,7 @@ And for every distribution we publish most of its attributes.
990 driver_link: None
991 homepage_content: None
992 icon_link: 'http://.../ubuntu/icon'
993+ information_type: 'Public'
994 logo_link: 'http://.../ubuntu/logo'
995 main_archive_link: 'http://.../ubuntu/+archive/primary'
996 members_link: 'http://.../~ubuntu-team'
997@@ -54,6 +55,7 @@ And for every distribution we publish most of its attributes.
998 official_codehosting: False
999 official_packages: True
1000 owner_link: 'http://.../~ubuntu-team'
1001+ private: False
1002 redirect_default_traversal: False
1003 redirect_release_uploads: False
1004 registrant_link: 'http://.../~registry'
1005diff --git a/lib/lp/registry/tests/test_distribution.py b/lib/lp/registry/tests/test_distribution.py
1006index 427d95e..49f55df 100644
1007--- a/lib/lp/registry/tests/test_distribution.py
1008+++ b/lib/lp/registry/tests/test_distribution.py
1009@@ -21,10 +21,15 @@ from zope.security.interfaces import Unauthorized
1010 from zope.security.proxy import removeSecurityProxy
1011
1012 from lp.app.enums import (
1013+ FREE_INFORMATION_TYPES,
1014 InformationType,
1015+ PILLAR_INFORMATION_TYPES,
1016 ServiceUsage,
1017 )
1018-from lp.app.errors import NotFoundError
1019+from lp.app.errors import (
1020+ NotFoundError,
1021+ ServiceUsageForbidden,
1022+ )
1023 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
1024 from lp.oci.tests.helpers import OCIConfigHelperMixin
1025 from lp.registry.enums import (
1026@@ -33,12 +38,19 @@ from lp.registry.enums import (
1027 DistributionDefaultTraversalPolicy,
1028 EXCLUSIVE_TEAM_POLICY,
1029 INCLUSIVE_TEAM_POLICY,
1030+ TeamMembershipPolicy,
1031 )
1032 from lp.registry.errors import (
1033+ CannotChangeInformationType,
1034+ CommercialSubscribersOnly,
1035 InclusiveTeamLinkageError,
1036 NoSuchDistroSeries,
1037+ ProprietaryPillar,
1038+ )
1039+from lp.registry.interfaces.accesspolicy import (
1040+ IAccessPolicyGrantSource,
1041+ IAccessPolicySource,
1042 )
1043-from lp.registry.interfaces.accesspolicy import IAccessPolicySource
1044 from lp.registry.interfaces.distribution import (
1045 IDistribution,
1046 IDistributionSet,
1047@@ -46,7 +58,9 @@ from lp.registry.interfaces.distribution import (
1048 from lp.registry.interfaces.oopsreferences import IHasOOPSReferences
1049 from lp.registry.interfaces.person import IPersonSet
1050 from lp.registry.interfaces.series import SeriesStatus
1051+from lp.registry.model.distribution import Distribution
1052 from lp.registry.tests.test_distroseries import CurrentSourceReleasesMixin
1053+from lp.services.librarianserver.testing.fake import FakeLibrarian
1054 from lp.services.propertycache import get_property_cache
1055 from lp.services.webapp import canonical_url
1056 from lp.services.webapp.interfaces import OAuthPermission
1057@@ -73,6 +87,9 @@ from lp.testing.views import create_initialized_view
1058 from lp.translations.enums import TranslationPermission
1059
1060
1061+PRIVATE_DISTRIBUTION_TYPES = [InformationType.PROPRIETARY]
1062+
1063+
1064 class TestDistribution(TestCaseWithFactory):
1065
1066 layer = DatabaseFunctionalLayer
1067@@ -370,6 +387,362 @@ class TestDistribution(TestCaseWithFactory):
1068 DistributionDefaultTraversalPolicy.SERIES)
1069 distro.redirect_default_traversal = True
1070
1071+ def test_creation_grants_maintainer_access(self):
1072+ # Creating a new distribution creates an access grant for the
1073+ # maintainer for all default policies.
1074+ distribution = self.factory.makeDistribution()
1075+ policies = getUtility(IAccessPolicySource).findByPillar(
1076+ (distribution,))
1077+ grants = getUtility(IAccessPolicyGrantSource).findByPolicy(policies)
1078+ expected_grantess = {distribution.owner}
1079+ grantees = {grant.grantee for grant in grants}
1080+ self.assertEqual(expected_grantess, grantees)
1081+
1082+ def test_change_info_type_proprietary_check_artifacts(self):
1083+ # Cannot change distribution information_type if any artifacts are
1084+ # public.
1085+ # XXX cjwatson 2022-02-11: Make this use
1086+ # artifact.transitionToInformationType once sharing policies are in
1087+ # place.
1088+ distribution = self.factory.makeDistribution()
1089+ self.useContext(person_logged_in(distribution.owner))
1090+ spec = self.factory.makeSpecification(distribution=distribution)
1091+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1092+ with ExpectedException(
1093+ CannotChangeInformationType,
1094+ "Some blueprints are public."):
1095+ distribution.information_type = info_type
1096+ removeSecurityProxy(spec).information_type = (
1097+ InformationType.PROPRIETARY)
1098+ dsp = self.factory.makeDistributionSourcePackage(
1099+ distribution=distribution)
1100+ bug = self.factory.makeBug(target=dsp)
1101+ for bug_info_type in FREE_INFORMATION_TYPES:
1102+ removeSecurityProxy(bug).information_type = bug_info_type
1103+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1104+ with ExpectedException(
1105+ CannotChangeInformationType,
1106+ "Some bugs are neither proprietary nor embargoed."):
1107+ distribution.information_type = info_type
1108+ removeSecurityProxy(bug).information_type = InformationType.PROPRIETARY
1109+ distroseries = self.factory.makeDistroSeries(distribution=distribution)
1110+ sp = self.factory.makeSourcePackage(distroseries=distroseries)
1111+ branch = self.factory.makeBranch(sourcepackage=sp)
1112+ for branch_info_type in FREE_INFORMATION_TYPES:
1113+ removeSecurityProxy(branch).information_type = branch_info_type
1114+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1115+ with ExpectedException(
1116+ CannotChangeInformationType,
1117+ "Some branches are neither proprietary nor "
1118+ "embargoed."):
1119+ distribution.information_type = info_type
1120+ removeSecurityProxy(branch).information_type = (
1121+ InformationType.PROPRIETARY)
1122+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1123+ distribution.information_type = info_type
1124+
1125+ def test_change_info_type_proprietary_check_translations(self):
1126+ distribution = self.factory.makeDistribution()
1127+ with person_logged_in(distribution.owner):
1128+ for usage in ServiceUsage:
1129+ distribution.information_type = InformationType.PUBLIC
1130+ distribution.translations_usage = usage.value
1131+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1132+ if (distribution.translations_usage ==
1133+ ServiceUsage.LAUNCHPAD):
1134+ with ExpectedException(
1135+ CannotChangeInformationType,
1136+ "Translations are enabled."):
1137+ distribution.information_type = info_type
1138+ else:
1139+ distribution.information_type = info_type
1140+
1141+ def test_cacheAccessPolicies(self):
1142+ # Distribution.access_policies is a list caching AccessPolicy.ids
1143+ # for which an AccessPolicyGrant or AccessArtifactGrant gives a
1144+ # principal LimitedView on the Distribution.
1145+ aps = getUtility(IAccessPolicySource)
1146+
1147+ # Public distributions don't need a cache.
1148+ distribution = self.factory.makeDistribution()
1149+ naked_distribution = removeSecurityProxy(distribution)
1150+ self.assertContentEqual(
1151+ [InformationType.USERDATA, InformationType.PRIVATESECURITY],
1152+ [p.type for p in aps.findByPillar([distribution])])
1153+ self.assertIsNone(naked_distribution.access_policies)
1154+
1155+ # A private distribution normally just allows the Proprietary
1156+ # policy, even if there is still another policy like Private
1157+ # Security.
1158+ naked_distribution.information_type = InformationType.PROPRIETARY
1159+ [prop_policy] = aps.find([(distribution, InformationType.PROPRIETARY)])
1160+ self.assertEqual([prop_policy.id], naked_distribution.access_policies)
1161+
1162+ # If we switch it back to public, the cache is no longer
1163+ # required.
1164+ naked_distribution.information_type = InformationType.PUBLIC
1165+ self.assertIsNone(naked_distribution.access_policies)
1166+
1167+ def test_checkInformationType_bug_supervisor(self):
1168+ # Bug supervisors of proprietary distributions must not have
1169+ # inclusive membership policies.
1170+ team = self.factory.makeTeam()
1171+ distribution = self.factory.makeDistribution(bug_supervisor=team)
1172+ for policy in (token.value for token in TeamMembershipPolicy):
1173+ with person_logged_in(team.teamowner):
1174+ team.membership_policy = policy
1175+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1176+ with person_logged_in(distribution.owner):
1177+ errors = list(distribution.checkInformationType(info_type))
1178+ if policy in EXCLUSIVE_TEAM_POLICY:
1179+ self.assertEqual([], errors)
1180+ else:
1181+ with ExpectedException(
1182+ CannotChangeInformationType,
1183+ "Bug supervisor has inclusive membership."):
1184+ raise errors[0]
1185+
1186+ def test_checkInformationType_questions(self):
1187+ # Proprietary distributions must not have questions.
1188+ distribution = self.factory.makeDistribution()
1189+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1190+ with person_logged_in(distribution.owner):
1191+ self.assertEqual([],
1192+ list(distribution.checkInformationType(info_type)))
1193+ self.factory.makeQuestion(target=distribution)
1194+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1195+ with person_logged_in(distribution.owner):
1196+ error, = list(distribution.checkInformationType(info_type))
1197+ with ExpectedException(
1198+ CannotChangeInformationType,
1199+ "This distribution has questions."):
1200+ raise error
1201+
1202+ def test_checkInformationType_translations(self):
1203+ # Proprietary distributions must not have translations.
1204+ distroseries = self.factory.makeDistroSeries()
1205+ distribution = distroseries.distribution
1206+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1207+ with person_logged_in(distribution.owner):
1208+ self.assertEqual(
1209+ [], list(distribution.checkInformationType(info_type)))
1210+ self.factory.makePOTemplate(distroseries=distroseries)
1211+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1212+ with person_logged_in(distribution.owner):
1213+ error, = list(distribution.checkInformationType(info_type))
1214+ with ExpectedException(
1215+ CannotChangeInformationType,
1216+ "This distribution has translations."):
1217+ raise error
1218+
1219+ def test_checkInformationType_queued_translations(self):
1220+ # Proprietary distributions must not have queued translations.
1221+ self.useFixture(FakeLibrarian())
1222+ distroseries = self.factory.makeDistroSeries()
1223+ distribution = distroseries.distribution
1224+ entry = self.factory.makeTranslationImportQueueEntry(
1225+ distroseries=distroseries)
1226+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1227+ with person_logged_in(distribution.owner):
1228+ error, = list(distribution.checkInformationType(info_type))
1229+ with ExpectedException(
1230+ CannotChangeInformationType,
1231+ "This distribution has queued translations."):
1232+ raise error
1233+ Store.of(entry).remove(entry)
1234+ with person_logged_in(distribution.owner):
1235+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1236+ self.assertContentEqual(
1237+ [], distribution.checkInformationType(info_type))
1238+
1239+ def test_checkInformationType_series_only_bugs(self):
1240+ # A distribution with bugtasks that are only targeted to a series
1241+ # cannot change information type.
1242+ series = self.factory.makeDistroSeries()
1243+ bug = self.factory.makeBug(target=series.distribution)
1244+ with person_logged_in(series.owner):
1245+ bug.addTask(series.owner, series)
1246+ bug.default_bugtask.delete()
1247+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1248+ error, = list(series.distribution.checkInformationType(
1249+ info_type))
1250+ with ExpectedException(
1251+ CannotChangeInformationType,
1252+ "Some bugs are neither proprietary nor embargoed."):
1253+ raise error
1254+
1255+ def test_private_forbids_translations(self):
1256+ owner = self.factory.makePerson()
1257+ distribution = self.factory.makeDistribution(owner=owner)
1258+ self.useContext(person_logged_in(owner))
1259+ for info_type in PRIVATE_DISTRIBUTION_TYPES:
1260+ distribution.information_type = info_type
1261+ with ExpectedException(
1262+ ProprietaryPillar,
1263+ "Translations are not supported for proprietary "
1264+ "distributions."):
1265+ distribution.translations_usage = ServiceUsage.LAUNCHPAD
1266+ for usage in ServiceUsage.items:
1267+ if usage == ServiceUsage.LAUNCHPAD:
1268+ continue
1269+ distribution.translations_usage = usage
1270+
1271+ def createDistribution(self, information_type=None):
1272+ # Convenience method for testing IDistributionSet.new rather than
1273+ # self.factory.makeDistribution.
1274+ owner = self.factory.makePerson()
1275+ members = self.factory.makeTeam(owner=owner)
1276+ kwargs = {}
1277+ if information_type is not None:
1278+ kwargs['information_type'] = information_type
1279+ with person_logged_in(owner):
1280+ return getUtility(IDistributionSet).new(
1281+ name=self.factory.getUniqueUnicode("distro"),
1282+ display_name="Fnord", title="Fnord",
1283+ description="test 1", summary="test 2",
1284+ domainname="distro.example.org",
1285+ members=members, owner=owner, registrant=owner, **kwargs)
1286+
1287+ def test_information_type(self):
1288+ # Distribution is created with specified information_type.
1289+ distribution = self.createDistribution(
1290+ information_type=InformationType.PROPRIETARY)
1291+ self.assertEqual(
1292+ InformationType.PROPRIETARY, distribution.information_type)
1293+ # The owner can set information_type.
1294+ with person_logged_in(removeSecurityProxy(distribution).owner):
1295+ distribution.information_type = InformationType.PUBLIC
1296+ self.assertEqual(InformationType.PUBLIC, distribution.information_type)
1297+ # The database persists the value of information_type.
1298+ store = Store.of(distribution)
1299+ store.flush()
1300+ store.reset()
1301+ distribution = store.get(Distribution, distribution.id)
1302+ self.assertEqual(InformationType.PUBLIC, distribution.information_type)
1303+ self.assertFalse(distribution.private)
1304+
1305+ def test_switching_to_public_does_not_create_policy(self):
1306+ # Creating a Proprietary distribution and switching it to Public
1307+ # does not create a PUBLIC AccessPolicy.
1308+ distribution = self.createDistribution(
1309+ information_type=InformationType.PROPRIETARY)
1310+ aps = getUtility(IAccessPolicySource).findByPillar([distribution])
1311+ self.assertContentEqual(
1312+ [InformationType.PROPRIETARY],
1313+ [ap.type for ap in aps])
1314+ removeSecurityProxy(distribution).information_type = (
1315+ InformationType.PUBLIC)
1316+ aps = getUtility(IAccessPolicySource).findByPillar([distribution])
1317+ self.assertContentEqual(
1318+ [InformationType.PROPRIETARY],
1319+ [ap.type for ap in aps])
1320+
1321+ def test_information_type_default(self):
1322+ # The default information_type is PUBLIC.
1323+ distribution = self.createDistribution()
1324+ self.assertEqual(InformationType.PUBLIC, distribution.information_type)
1325+ self.assertFalse(distribution.private)
1326+
1327+ invalid_information_types = [
1328+ info_type for info_type in InformationType.items
1329+ if info_type not in PILLAR_INFORMATION_TYPES]
1330+
1331+ def test_information_type_init_invalid_values(self):
1332+ # Cannot create Distribution.information_type with invalid values.
1333+ for info_type in self.invalid_information_types:
1334+ with ExpectedException(
1335+ CannotChangeInformationType,
1336+ "Not supported for distributions."):
1337+ self.createDistribution(information_type=info_type)
1338+
1339+ def test_information_type_set_invalid_values(self):
1340+ # Cannot set Distribution.information_type to invalid values.
1341+ distribution = self.factory.makeDistribution()
1342+ for info_type in self.invalid_information_types:
1343+ with ExpectedException(
1344+ CannotChangeInformationType,
1345+ "Not supported for distributions."):
1346+ with person_logged_in(distribution.owner):
1347+ distribution.information_type = info_type
1348+
1349+ def test_set_proprietary_gets_commercial_subscription(self):
1350+ # Changing a Distribution to Proprietary will auto-generate a
1351+ # complimentary subscription just as choosing a proprietary
1352+ # information type at creation time.
1353+ owner = self.factory.makePerson()
1354+ distribution = self.factory.makeDistribution(owner=owner)
1355+ self.useContext(person_logged_in(owner))
1356+ self.assertIsNone(distribution.commercial_subscription)
1357+
1358+ distribution.information_type = InformationType.PROPRIETARY
1359+ self.assertEqual(
1360+ InformationType.PROPRIETARY, distribution.information_type)
1361+ self.assertIsNotNone(distribution.commercial_subscription)
1362+
1363+ def test_set_proprietary_fails_expired_commercial_subscription(self):
1364+ # Cannot set information type to proprietary with an expired
1365+ # complimentary subscription.
1366+ owner = self.factory.makePerson()
1367+ distribution = self.factory.makeDistribution(
1368+ information_type=InformationType.PROPRIETARY, owner=owner)
1369+ self.useContext(person_logged_in(owner))
1370+
1371+ # The Distribution now has a complimentary commercial subscription.
1372+ new_expires_date = (
1373+ datetime.datetime.now(pytz.UTC) - datetime.timedelta(1))
1374+ naked_subscription = removeSecurityProxy(
1375+ distribution.commercial_subscription)
1376+ naked_subscription.date_expires = new_expires_date
1377+
1378+ # We can make the distribution PUBLIC.
1379+ distribution.information_type = InformationType.PUBLIC
1380+ self.assertEqual(InformationType.PUBLIC, distribution.information_type)
1381+
1382+ # However we can't change it back to Proprietary because our
1383+ # commercial subscription has expired.
1384+ with ExpectedException(
1385+ CommercialSubscribersOnly,
1386+ "A valid commercial subscription is required for private"
1387+ " distributions."):
1388+ distribution.information_type = InformationType.PROPRIETARY
1389+
1390+ def test_no_answers_for_proprietary(self):
1391+ # Enabling Answers is forbidden while information_type is proprietary.
1392+ distribution = self.factory.makeDistribution(
1393+ information_type=InformationType.PROPRIETARY)
1394+ with person_logged_in(removeSecurityProxy(distribution).owner):
1395+ self.assertEqual(ServiceUsage.UNKNOWN, distribution.answers_usage)
1396+ for usage in ServiceUsage.items:
1397+ if usage == ServiceUsage.LAUNCHPAD:
1398+ with ExpectedException(
1399+ ServiceUsageForbidden,
1400+ "Answers not allowed for non-public "
1401+ "distributions."):
1402+ distribution.answers_usage = ServiceUsage.LAUNCHPAD
1403+ else:
1404+ # All other values are permitted.
1405+ distribution.answers_usage = usage
1406+
1407+ def test_answers_for_public(self):
1408+ # Enabling answers is permitted while information_type is PUBLIC.
1409+ distribution = self.factory.makeDistribution(
1410+ information_type=InformationType.PUBLIC)
1411+ self.assertEqual(ServiceUsage.UNKNOWN, distribution.answers_usage)
1412+ with person_logged_in(distribution.owner):
1413+ for usage in ServiceUsage.items:
1414+ # All values are permitted.
1415+ distribution.answers_usage = usage
1416+
1417+ def test_no_proprietary_if_answers(self):
1418+ # Information type cannot be set to proprietary while Answers are
1419+ # enabled.
1420+ distribution = self.factory.makeDistribution()
1421+ with person_logged_in(distribution.owner):
1422+ distribution.answers_usage = ServiceUsage.LAUNCHPAD
1423+ with ExpectedException(
1424+ CannotChangeInformationType, "Answers is enabled."):
1425+ distribution.information_type = InformationType.PROPRIETARY
1426+
1427
1428 class TestDistributionCurrentSourceReleases(
1429 CurrentSourceReleasesMixin, TestCase):
1430diff --git a/lib/lp/registry/tests/test_pillaraffiliation.py b/lib/lp/registry/tests/test_pillaraffiliation.py
1431index 0098e28..e757d0e 100644
1432--- a/lib/lp/registry/tests/test_pillaraffiliation.py
1433+++ b/lib/lp/registry/tests/test_pillaraffiliation.py
1434@@ -3,11 +3,11 @@
1435
1436 """Tests for adapters."""
1437
1438-from storm.store import Store
1439 from testtools.matchers import Equals
1440 from zope.component import getUtility
1441
1442 from lp.registry.model.pillaraffiliation import IHasAffiliation
1443+from lp.services.database.sqlbase import flush_database_caches
1444 from lp.services.worlddata.interfaces.language import ILanguageSet
1445 from lp.testing import (
1446 person_logged_in,
1447@@ -146,21 +146,20 @@ class TestPillarAffiliation(TestCaseWithFactory):
1448 # - Product, Person
1449 person = self.factory.makePerson()
1450 product = self.factory.makeProduct(owner=person, name='pting')
1451- Store.of(product).invalidate()
1452+ flush_database_caches()
1453 with StormStatementRecorder() as recorder:
1454 IHasAffiliation(product).getAffiliationBadges([person])
1455- self.assertThat(recorder, HasQueryCount(Equals(4)))
1456+ self.assertThat(recorder, HasQueryCount(Equals(2)))
1457
1458 def test_distro_affiliation_query_count(self):
1459 # Only 2 business queries are expected, selects from:
1460 # - Distribution, Person
1461- # plus an additional query to create a PublisherConfig record.
1462 person = self.factory.makePerson()
1463 distro = self.factory.makeDistribution(owner=person, name='pting')
1464- Store.of(distro).invalidate()
1465+ flush_database_caches()
1466 with StormStatementRecorder() as recorder:
1467 IHasAffiliation(distro).getAffiliationBadges([person])
1468- self.assertThat(recorder, HasQueryCount(Equals(3)))
1469+ self.assertThat(recorder, HasQueryCount(Equals(2)))
1470
1471
1472 class _TestBugTaskorBranchMixin:
1473diff --git a/lib/lp/registry/tests/test_product.py b/lib/lp/registry/tests/test_product.py
1474index c037549..61f147a 100644
1475--- a/lib/lp/registry/tests/test_product.py
1476+++ b/lib/lp/registry/tests/test_product.py
1477@@ -68,7 +68,7 @@ from lp.registry.errors import (
1478 CannotChangeInformationType,
1479 CommercialSubscribersOnly,
1480 InclusiveTeamLinkageError,
1481- ProprietaryProduct,
1482+ ProprietaryPillar,
1483 )
1484 from lp.registry.interfaces.accesspolicy import (
1485 IAccessPolicyGrantSource,
1486@@ -635,7 +635,7 @@ class TestProduct(TestCaseWithFactory):
1487 for info_type in PRIVATE_PROJECT_TYPES:
1488 product.information_type = info_type
1489 with ExpectedException(
1490- ProprietaryProduct,
1491+ ProprietaryPillar,
1492 "Translations are not supported for proprietary products."):
1493 product.translations_usage = ServiceUsage.LAUNCHPAD
1494 for usage in ServiceUsage.items:
1495@@ -1761,7 +1761,7 @@ class BaseSharingPolicyTests:
1496 InformationType.PUBLIC in self.allowed_types[policy])
1497 for policy in policies_permitting_public:
1498 with ExpectedException(
1499- ProprietaryProduct, "The project is Proprietary."):
1500+ ProprietaryPillar, "The project is Proprietary."):
1501 self.setSharingPolicy(policy, owner)
1502
1503
1504diff --git a/lib/lp/registry/tests/test_productrelease.py b/lib/lp/registry/tests/test_productrelease.py
1505index 39c8222..85ecbca 100644
1506--- a/lib/lp/registry/tests/test_productrelease.py
1507+++ b/lib/lp/registry/tests/test_productrelease.py
1508@@ -8,7 +8,7 @@ from zope.component import getUtility
1509 from lp.app.enums import InformationType
1510 from lp.registry.errors import (
1511 InvalidFilename,
1512- ProprietaryProduct,
1513+ ProprietaryPillar,
1514 )
1515 from lp.registry.interfaces.productrelease import (
1516 IProductReleaseSet,
1517@@ -101,5 +101,5 @@ class ProductReleaseFileTestcase(TestCaseWithFactory):
1518 release = self.factory.makeProductRelease(product=product)
1519 self.assertFalse(release.can_have_release_files)
1520 self.assertRaises(
1521- ProprietaryProduct, release.addReleaseFile,
1522+ ProprietaryPillar, release.addReleaseFile,
1523 'README', b'test', 'text/plain', owner)
1524diff --git a/lib/lp/registry/tests/test_productseries.py b/lib/lp/registry/tests/test_productseries.py
1525index da6f828..e80452e 100644
1526--- a/lib/lp/registry/tests/test_productseries.py
1527+++ b/lib/lp/registry/tests/test_productseries.py
1528@@ -20,7 +20,7 @@ from lp.app.interfaces.services import IService
1529 from lp.registry.enums import SharingPermission
1530 from lp.registry.errors import (
1531 CannotPackageProprietaryProduct,
1532- ProprietaryProduct,
1533+ ProprietaryPillar,
1534 )
1535 from lp.registry.interfaces.distribution import IDistributionSet
1536 from lp.registry.interfaces.distroseries import IDistroSeriesSet
1537@@ -73,7 +73,7 @@ class TestProductSeries(TestCaseWithFactory):
1538 for mode in TranslationsBranchImportMode.items:
1539 if mode == TranslationsBranchImportMode.NO_IMPORT:
1540 continue
1541- with ExpectedException(ProprietaryProduct,
1542+ with ExpectedException(ProprietaryPillar,
1543 'Translations are disabled for proprietary'
1544 ' projects.'):
1545 series.translations_autoimport_mode = mode
1546diff --git a/lib/lp/security.py b/lib/lp/security.py
1547index 22bc140..95d7a45 100644
1548--- a/lib/lp/security.py
1549+++ b/lib/lp/security.py
1550@@ -1282,7 +1282,11 @@ class EditDistributionByDistroOwnersOrAdmins(AuthorizationBase):
1551 usedfor = IDistribution
1552
1553 def checkAuthenticated(self, user):
1554- return user.isOwner(self.obj) or user.in_admin
1555+ # Commercial admins may help setup commercial distributions.
1556+ return (
1557+ user.isOwner(self.obj)
1558+ or is_commercial_case(self.obj, user)
1559+ or user.in_admin)
1560
1561
1562 class ModerateDistributionByDriversOrOwnersOrAdmins(AuthorizationBase):
1563@@ -3266,6 +3270,10 @@ class ViewPublisherConfig(AdminByAdminsTeam):
1564 usedfor = IPublisherConfig
1565
1566
1567+class ViewSourcePackage(AnonymousAuthorization):
1568+ usedfor = ISourcePackage
1569+
1570+
1571 class EditSourcePackage(EditDistributionSourcePackage):
1572 usedfor = ISourcePackage
1573
1574diff --git a/lib/lp/soyuz/tests/test_build_set.py b/lib/lp/soyuz/tests/test_build_set.py
1575index 6dcf9fd..b2a795c 100644
1576--- a/lib/lp/soyuz/tests/test_build_set.py
1577+++ b/lib/lp/soyuz/tests/test_build_set.py
1578@@ -29,7 +29,10 @@ from lp.testing import (
1579 person_logged_in,
1580 TestCaseWithFactory,
1581 )
1582-from lp.testing.dbuser import lp_dbuser
1583+from lp.testing.dbuser import (
1584+ lp_dbuser,
1585+ switch_dbuser,
1586+ )
1587 from lp.testing.layers import (
1588 LaunchpadFunctionalLayer,
1589 ZopelessDatabaseLayer,
1590@@ -348,6 +351,13 @@ class BuildRecordCreationTests(TestNativePublishingBase):
1591
1592 def setUp(self):
1593 super().setUp()
1594+
1595+ # TestNativePublishingBase switches to the archive publisher's
1596+ # database user, but the publisher doesn't create build records so
1597+ # we aren't really interested in its database permissions here.
1598+ # Just use the webapp's database user instead.
1599+ switch_dbuser("launchpad")
1600+
1601 self.distro = self.factory.makeDistribution()
1602 self.avr = self.factory.makeProcessor(
1603 name="avr2001", supports_virtualized=True)
1604diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
1605index 492f57b..041cb89 100644
1606--- a/lib/lp/testing/factory.py
1607+++ b/lib/lp/testing/factory.py
1608@@ -2709,7 +2709,7 @@ class BareLaunchpadObjectFactory(ObjectFactory):
1609 publish_root_dir=None, publish_base_url=None,
1610 publish_copy_base_url=None, no_pubconf=False,
1611 icon=None, summary=None, vcs=None,
1612- oci_project_admin=None):
1613+ oci_project_admin=None, information_type=None):
1614 """Make a new distribution."""
1615 if name is None:
1616 name = self.getUniqueString(prefix="distribution")
1617@@ -2729,7 +2729,8 @@ class BareLaunchpadObjectFactory(ObjectFactory):
1618 members = self.makeTeam(owner)
1619 distro = getUtility(IDistributionSet).new(
1620 name, displayname, title, description, summary, domainname,
1621- members, owner, registrant, icon=icon, vcs=vcs)
1622+ members, owner, registrant, icon=icon, vcs=vcs,
1623+ information_type=information_type)
1624 naked_distro = removeSecurityProxy(distro)
1625 if aliases is not None:
1626 naked_distro.setAliases(aliases)

Subscribers

People subscribed via source and target branches

to status/vote changes: