Merge ~cjwatson/launchpad:distribution-information-type into launchpad:master
- Git
- lp:~cjwatson/launchpad
- distribution-information-type
- Merge into 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) |
Related bugs: |
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.
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
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
1 | diff --git a/lib/lp/archivepublisher/tests/test_dominator.py b/lib/lp/archivepublisher/tests/test_dominator.py |
2 | index 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 |
46 | diff --git a/lib/lp/archivepublisher/tests/test_publisher.py b/lib/lp/archivepublisher/tests/test_publisher.py |
47 | index 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 |
60 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml |
61 | index 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 |
84 | diff --git a/lib/lp/registry/errors.py b/lib/lp/registry/errors.py |
85 | index 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): |
108 | diff --git a/lib/lp/registry/interfaces/distribution.py b/lib/lp/registry/interfaces/distribution.py |
109 | index 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): |
180 | diff --git a/lib/lp/registry/model/distribution.py b/lib/lp/registry/model/distribution.py |
181 | index 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): |
700 | diff --git a/lib/lp/registry/model/product.py b/lib/lp/registry/model/product.py |
701 | index 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 | |
731 | diff --git a/lib/lp/registry/model/productrelease.py b/lib/lp/registry/model/productrelease.py |
732 | index 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 |
753 | diff --git a/lib/lp/registry/model/productseries.py b/lib/lp/registry/model/productseries.py |
754 | index 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( |
777 | diff --git a/lib/lp/registry/services/sharingservice.py b/lib/lp/registry/services/sharingservice.py |
778 | index 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.""" |
796 | diff --git a/lib/lp/registry/services/tests/test_sharingservice.py b/lib/lp/registry/services/tests/test_sharingservice.py |
797 | index 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(): |
985 | diff --git a/lib/lp/registry/stories/webservice/xx-distribution.txt b/lib/lp/registry/stories/webservice/xx-distribution.txt |
986 | index 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' |
1005 | diff --git a/lib/lp/registry/tests/test_distribution.py b/lib/lp/registry/tests/test_distribution.py |
1006 | index 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): |
1430 | diff --git a/lib/lp/registry/tests/test_pillaraffiliation.py b/lib/lp/registry/tests/test_pillaraffiliation.py |
1431 | index 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: |
1473 | diff --git a/lib/lp/registry/tests/test_product.py b/lib/lp/registry/tests/test_product.py |
1474 | index 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 | |
1504 | diff --git a/lib/lp/registry/tests/test_productrelease.py b/lib/lp/registry/tests/test_productrelease.py |
1505 | index 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) |
1524 | diff --git a/lib/lp/registry/tests/test_productseries.py b/lib/lp/registry/tests/test_productseries.py |
1525 | index 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 |
1546 | diff --git a/lib/lp/security.py b/lib/lp/security.py |
1547 | index 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 | |
1574 | diff --git a/lib/lp/soyuz/tests/test_build_set.py b/lib/lp/soyuz/tests/test_build_set.py |
1575 | index 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) |
1604 | diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py |
1605 | index 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) |
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.