Merge lp:~cjwatson/launchpad/snap-build-record-code into lp:launchpad

Proposed by Colin Watson
Status: Rejected
Rejected by: Colin Watson
Proposed branch: lp:~cjwatson/launchpad/snap-build-record-code
Merge into: lp:launchpad
Diff against target: 524 lines (+244/-23)
11 files modified
lib/lp/code/model/branch.py (+17/-4)
lib/lp/code/model/gitrepository.py (+15/-3)
lib/lp/code/model/tests/test_branch.py (+3/-1)
lib/lp/code/model/tests/test_gitrepository.py (+3/-1)
lib/lp/security.py (+25/-4)
lib/lp/snappy/interfaces/snapbuild.py (+36/-0)
lib/lp/snappy/model/snap.py (+2/-1)
lib/lp/snappy/model/snapbuild.py (+49/-5)
lib/lp/snappy/model/snapbuildbehaviour.py (+2/-0)
lib/lp/snappy/tests/test_snapbuild.py (+91/-3)
lib/lp/testing/factory.py (+1/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-build-record-code
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+365356@code.launchpad.net

Commit message

Record the code object from which a SnapBuild was created, for use by security adapters.

Description of the change

This is annoying, but I don't see any other way to cope with privacy changes of code objects that have snaps attached to them without leaking information about old builds.

We shouldn't land this until https://code.launchpad.net/~cjwatson/launchpad/branch-delete-job/+merge/364907 and https://code.launchpad.net/~cjwatson/launchpad/git-repository-delete-job/+merge/364910 have landed, as it'll make deletions slower; and, assuming all of these are approved, we'll need to add SELECT and DELETE permissions for the snap-build-job user on public.snapbuild before landing this.

https://code.launchpad.net/~cjwatson/launchpad/db-snap-build-record-code/+merge/365355 is the corresponding database patch.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

There are a couple of known problems with this, discussed on today's LP team call:

(1) When the snap build is detached, it will no longer have a private code artifact attached to it and thus may become public. Oops.
(2) It's not obvious that these are quite the semantics we want. Unlike source packages, Git repositories include history, but the history can be mutated (e.g. via git filter-branch), and the process of making a private repository public might well include redacting its history. If old snap builds automatically become public then that could be a problem.

We may need a private flag on the build, but it probably can't just be that because we need some way of knowing who can see it. Perhaps we could detach from public builds (thus keeping logs for old builds that are on public Ubuntu images, etc.) but delete private builds. Perhaps only the snap owner could see old detached private builds, or maybe even only admins. Or something else ...

18920. By Colin Watson

Add an explicitly_private flag to SnapBuild.

This lets us avoid accidental information leaks.

18921. By Colin Watson

Check privacy of code objects associated with snap builds.

18922. By Colin Watson

Merge devel.

18923. By Colin Watson

Add warning for future developers.

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

I've tightened up the privacy rules here as best I can. How's this?

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

Superseded by Thiago's work on project-based snap recipes with privacy controlled by the pillar.

Unmerged revisions

18923. By Colin Watson

Add warning for future developers.

18922. By Colin Watson

Merge devel.

18921. By Colin Watson

Check privacy of code objects associated with snap builds.

18920. By Colin Watson

Add an explicitly_private flag to SnapBuild.

This lets us avoid accidental information leaks.

18919. By Colin Watson

Record the code object from which a SnapBuild was created, for later use by security adapters.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2019-04-17 10:34:13 +0000
+++ lib/lp/code/model/branch.py 2019-05-28 16:37:20 +0000
@@ -196,6 +196,7 @@
196from lp.services.webhooks.interfaces import IWebhookSet196from lp.services.webhooks.interfaces import IWebhookSet
197from lp.services.webhooks.model import WebhookTargetMixin197from lp.services.webhooks.model import WebhookTargetMixin
198from lp.snappy.interfaces.snap import ISnapSet198from lp.snappy.interfaces.snap import ISnapSet
199from lp.snappy.interfaces.snapbuild import ISnapBuildSet
199200
200201
201@implementer(IBranch, IPrivacy, IInformationType)202@implementer(IBranch, IPrivacy, IInformationType)
@@ -923,10 +924,9 @@
923 DeletionCallable(924 DeletionCallable(
924 recipe, _('This recipe uses this branch.'), recipe.destroySelf)925 recipe, _('This recipe uses this branch.'), recipe.destroySelf)
925 for recipe in recipes)926 for recipe in recipes)
926 if not getUtility(ISnapSet).findByBranch(self).is_empty():927 if (not getUtility(ISnapSet).findByBranch(self).is_empty() or
927 alteration_operations.append(DeletionCallable(928 not getUtility(ISnapBuildSet).findByBranch(self).is_empty()):
928 None, _('Some snap packages build from this branch.'),929 alteration_operations.append(DetachSnaps(self))
929 getUtility(ISnapSet).detachFromBranch, self))
930 return (alteration_operations, deletion_operations)930 return (alteration_operations, deletion_operations)
931931
932 def deletionRequirements(self, eager_load=False):932 def deletionRequirements(self, eager_load=False):
@@ -1644,6 +1644,19 @@
1644 getUtility(ICodeImportSet).delete(self.affected_object)1644 getUtility(ICodeImportSet).delete(self.affected_object)
16451645
16461646
1647class DetachSnaps(DeletionOperation):
1648 """Deletion operation that detaches snaps from a branch."""
1649
1650 def __init__(self, branch):
1651 super(DetachSnaps, self).__init__(
1652 None, _('Some snap packages build from this branch.'))
1653 self.branch = branch
1654
1655 def __call__(self):
1656 getUtility(ISnapSet).detachFromBranch(self.branch)
1657 getUtility(ISnapBuildSet).detachFromBranch(self.branch)
1658
1659
1647@implementer(IBranchSet)1660@implementer(IBranchSet)
1648class BranchSet:1661class BranchSet:
1649 """The set of all branches."""1662 """The set of all branches."""
16501663
=== modified file 'lib/lp/code/model/gitrepository.py'
--- lib/lp/code/model/gitrepository.py 2019-05-03 13:18:52 +0000
+++ lib/lp/code/model/gitrepository.py 2019-05-28 16:37:20 +0000
@@ -202,6 +202,7 @@
202from lp.services.webhooks.interfaces import IWebhookSet202from lp.services.webhooks.interfaces import IWebhookSet
203from lp.services.webhooks.model import WebhookTargetMixin203from lp.services.webhooks.model import WebhookTargetMixin
204from lp.snappy.interfaces.snap import ISnapSet204from lp.snappy.interfaces.snap import ISnapSet
205from lp.snappy.interfaces.snapbuild import ISnapBuildSet
205206
206207
207object_type_map = {208object_type_map = {
@@ -1476,9 +1477,7 @@
1476 recipe.destroySelf)1477 recipe.destroySelf)
1477 for recipe in recipes)1478 for recipe in recipes)
1478 if not getUtility(ISnapSet).findByGitRepository(self).is_empty():1479 if not getUtility(ISnapSet).findByGitRepository(self).is_empty():
1479 alteration_operations.append(DeletionCallable(1480 alteration_operations.append(DetachSnaps(self))
1480 None, msg("Some snap packages build from this repository."),
1481 getUtility(ISnapSet).detachFromGitRepository, self))
14821481
1483 return (alteration_operations, deletion_operations)1482 return (alteration_operations, deletion_operations)
14841483
@@ -1630,6 +1629,19 @@
1630 getUtility(ICodeImportSet).delete(self.affected_object)1629 getUtility(ICodeImportSet).delete(self.affected_object)
16311630
16321631
1632class DetachSnaps(DeletionOperation):
1633 """Deletion operation that detaches snaps from a repository."""
1634
1635 def __init__(self, repository):
1636 super(DetachSnaps, self).__init__(
1637 None, msg("Some snap packages build from this repository."))
1638 self.repository = repository
1639
1640 def __call__(self):
1641 getUtility(ISnapSet).detachFromGitRepository(self.repository)
1642 getUtility(ISnapBuildSet).detachFromGitRepository(self.repository)
1643
1644
1633@implementer(IGitRepositorySet)1645@implementer(IGitRepositorySet)
1634class GitRepositorySet:1646class GitRepositorySet:
1635 """See `IGitRepositorySet`."""1647 """See `IGitRepositorySet`."""
16361648
=== modified file 'lib/lp/code/model/tests/test_branch.py'
--- lib/lp/code/model/tests/test_branch.py 2019-01-28 18:09:21 +0000
+++ lib/lp/code/model/tests/test_branch.py 2019-05-28 16:37:20 +0000
@@ -1697,7 +1697,7 @@
1697 def test_snap_requirements(self):1697 def test_snap_requirements(self):
1698 # If a branch is used by a snap package, the deletion requirements1698 # If a branch is used by a snap package, the deletion requirements
1699 # indicate this.1699 # indicate this.
1700 self.factory.makeSnap(branch=self.branch)1700 self.factory.makeSnapBuild(branch=self.branch)
1701 self.assertEqual(1701 self.assertEqual(
1702 {None: ('alter', _('Some snap packages build from this branch.'))},1702 {None: ('alter', _('Some snap packages build from this branch.'))},
1703 self.branch.deletionRequirements())1703 self.branch.deletionRequirements())
@@ -1705,7 +1705,9 @@
1705 def test_snap_deletion(self):1705 def test_snap_deletion(self):
1706 # break_references allows deleting a branch used by a snap package.1706 # break_references allows deleting a branch used by a snap package.
1707 snap1 = self.factory.makeSnap(branch=self.branch)1707 snap1 = self.factory.makeSnap(branch=self.branch)
1708 self.factory.makeSnapBuild(snap=snap1)
1708 snap2 = self.factory.makeSnap(branch=self.branch)1709 snap2 = self.factory.makeSnap(branch=self.branch)
1710 self.factory.makeSnapBuild(snap=snap2)
1709 self.branch.destroySelf(break_references=True)1711 self.branch.destroySelf(break_references=True)
1710 transaction.commit()1712 transaction.commit()
1711 self.assertIsNone(snap1.branch)1713 self.assertIsNone(snap1.branch)
17121714
=== modified file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py 2019-05-03 13:18:52 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py 2019-05-28 16:37:20 +0000
@@ -989,7 +989,7 @@
989 # If a repository is used by a snap package, the deletion989 # If a repository is used by a snap package, the deletion
990 # requirements indicate this.990 # requirements indicate this.
991 [ref] = self.factory.makeGitRefs()991 [ref] = self.factory.makeGitRefs()
992 self.factory.makeSnap(git_ref=ref)992 self.factory.makeSnapBuild(git_ref=ref)
993 self.assertEqual(993 self.assertEqual(
994 {None:994 {None:
995 ("alter", _("Some snap packages build from this repository."))},995 ("alter", _("Some snap packages build from this repository."))},
@@ -1001,7 +1001,9 @@
1001 [ref1, ref2] = self.factory.makeGitRefs(1001 [ref1, ref2] = self.factory.makeGitRefs(
1002 repository=repository, paths=["refs/heads/1", "refs/heads/2"])1002 repository=repository, paths=["refs/heads/1", "refs/heads/2"])
1003 snap1 = self.factory.makeSnap(git_ref=ref1)1003 snap1 = self.factory.makeSnap(git_ref=ref1)
1004 self.factory.makeSnapBuild(snap=snap1)
1004 snap2 = self.factory.makeSnap(git_ref=ref2)1005 snap2 = self.factory.makeSnap(git_ref=ref2)
1006 self.factory.makeSnapBuild(snap=snap2)
1005 repository.destroySelf(break_references=True)1007 repository.destroySelf(break_references=True)
1006 transaction.commit()1008 transaction.commit()
1007 self.assertIsNone(snap1.git_repository)1009 self.assertIsNone(snap1.git_repository)
10081010
=== modified file 'lib/lp/security.py'
--- lib/lp/security.py 2019-03-26 20:51:38 +0000
+++ lib/lp/security.py 2019-05-28 16:37:20 +0000
@@ -3363,13 +3363,34 @@
3363 obj, obj.snap, 'launchpad.View')3363 obj, obj.snap, 'launchpad.View')
33643364
33653365
3366class ViewSnapBuild(DelegatedAuthorization):3366class ViewSnapBuild(AuthorizationBase):
3367 permission = 'launchpad.View'3367 permission = 'launchpad.View'
3368 usedfor = ISnapBuild3368 usedfor = ISnapBuild
33693369
3370 def iter_objects(self):3370 def checkUnauthenticated(self):
3371 yield self.obj.snap3371 return not self.obj.is_private
3372 yield self.obj.archive3372
3373 def checkAuthenticated(self, user):
3374 if not self.obj.is_private:
3375 return True
3376
3377 for item in (
3378 self.obj.snap, self.obj.archive,
3379 self.obj.branch, self.obj.git_repository):
3380 if (item is not None and
3381 not self.forwardCheckAuthenticated(user, item)):
3382 return False
3383
3384 # Only the snap owner and admins can see explicitly-private builds,
3385 # on top of any restrictions associated with the individual
3386 # components above. This may be overly-restrictive but is at least
3387 # relatively safe; if need be we can investigate loosening this
3388 # later, but be careful of the case where private code objects have
3389 # been detached.
3390 if self.obj.explicitly_private:
3391 return EditSnap(self.obj.snap).checkAuthenticated(user)
3392 else:
3393 return True
33733394
33743395
3375class EditSnapBuild(AdminByBuilddAdmin):3396class EditSnapBuild(AdminByBuilddAdmin):
33763397
=== modified file 'lib/lp/snappy/interfaces/snapbuild.py'
--- lib/lp/snappy/interfaces/snapbuild.py 2018-12-12 10:31:40 +0000
+++ lib/lp/snappy/interfaces/snapbuild.py 2019-05-28 16:37:20 +0000
@@ -51,6 +51,8 @@
51from lp import _51from lp import _
52from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource52from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource
53from lp.buildmaster.interfaces.packagebuild import IPackageBuild53from lp.buildmaster.interfaces.packagebuild import IPackageBuild
54from lp.code.interfaces.branch import IBranch
55from lp.code.interfaces.gitrepository import IGitRepository
54from lp.registry.interfaces.person import IPerson56from lp.registry.interfaces.person import IPerson
55from lp.registry.interfaces.pocket import PackagePublishingPocket57from lp.registry.interfaces.pocket import PackagePublishingPocket
56from lp.services.database.constants import DEFAULT58from lp.services.database.constants import DEFAULT
@@ -168,6 +170,14 @@
168 "supported."),170 "supported."),
169 key_type=TextLine()))171 key_type=TextLine()))
170172
173 explicitly_private = Bool(
174 title=_("Whether this build is explicitly private."),
175 description=_(
176 "An explicitly-private build remains private even if its snap and "
177 "other associated resources are public. This allows snaps to "
178 "transition between private and public states without "
179 "inadvertently leaking old private information."))
180
171 virtualized = Bool(181 virtualized = Bool(
172 title=_("If True, this build is virtualized."), readonly=True)182 title=_("If True, this build is virtualized."), readonly=True)
173183
@@ -248,6 +258,20 @@
248 store_upload_metadata = Attribute(258 store_upload_metadata = Attribute(
249 _("A dict of data about store upload progress."))259 _("A dict of data about store upload progress."))
250260
261 branch = Reference(
262 IBranch,
263 title=_("Bazaar branch"), required=False, readonly=True)
264
265 git_repository = Reference(
266 IGitRepository,
267 title=_("Git repository"), required=False, readonly=True)
268
269 git_repository_url = TextLine(
270 title=_("Git repository URL"), required=False, readonly=True)
271
272 git_path = TextLine(
273 title=_("Git branch path"), required=False, readonly=True)
274
251 def getFiles():275 def getFiles():
252 """Retrieve the build's `ISnapFile` records.276 """Retrieve the build's `ISnapFile` records.
253277
@@ -343,3 +367,15 @@
343367
344 def preloadBuildsData(builds):368 def preloadBuildsData(builds):
345 """Load the data related to a list of snap builds."""369 """Load the data related to a list of snap builds."""
370
371 def findByBranch(branch):
372 """Return all snap builds for the given Bazaar branch."""
373
374 def findByGitRepository(repository, paths=None):
375 """Return all snap builds for the given Git repository."""
376
377 def detachFromBranch(branch):
378 """Detach all snap builds from the given Bazaar branch."""
379
380 def detachFromGitRepository(repository):
381 """Detach all snap builds from the given Git repository."""
346382
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2019-05-16 10:21:14 +0000
+++ lib/lp/snappy/model/snap.py 2019-05-28 16:37:20 +0000
@@ -650,7 +650,8 @@
650650
651 build = getUtility(ISnapBuildSet).new(651 build = getUtility(ISnapBuildSet).new(
652 requester, self, archive, distro_arch_series, pocket,652 requester, self, archive, distro_arch_series, pocket,
653 channels=channels, build_request=build_request)653 channels=channels, build_request=build_request,
654 explicitly_private=self.private)
654 build.queueBuild()655 build.queueBuild()
655 notify(ObjectCreatedEvent(build, user=requester))656 notify(ObjectCreatedEvent(build, user=requester))
656 return build657 return build
657658
=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 2019-05-09 15:44:59 +0000
+++ lib/lp/snappy/model/snapbuild.py 2019-05-28 16:37:20 +0000
@@ -154,6 +154,8 @@
154154
155 channels = JSON('channels', allow_none=True)155 channels = JSON('channels', allow_none=True)
156156
157 explicitly_private = Bool(name='explicitly_private')
158
157 processor_id = Int(name='processor', allow_none=False)159 processor_id = Int(name='processor', allow_none=False)
158 processor = Reference(processor_id, 'Processor.id')160 processor = Reference(processor_id, 'Processor.id')
159 virtualized = Bool(name='virtualized')161 virtualized = Bool(name='virtualized')
@@ -184,9 +186,20 @@
184186
185 store_upload_metadata = JSON('store_upload_json_data', allow_none=True)187 store_upload_metadata = JSON('store_upload_json_data', allow_none=True)
186188
189 branch_id = Int(name='branch', allow_none=True)
190 branch = Reference(branch_id, 'Branch.id')
191
192 git_repository_id = Int(name='git_repository', allow_none=True)
193 git_repository = Reference(git_repository_id, 'GitRepository.id')
194
195 git_repository_url = Unicode(name='git_repository_url', allow_none=True)
196
197 git_path = Unicode(name='git_path', allow_none=True)
198
187 def __init__(self, build_farm_job, requester, snap, archive,199 def __init__(self, build_farm_job, requester, snap, archive,
188 distro_arch_series, pocket, channels, processor, virtualized,200 distro_arch_series, pocket, channels, processor, virtualized,
189 date_created, store_upload_metadata=None, build_request=None):201 date_created, store_upload_metadata=None, build_request=None,
202 explicitly_private=False):
190 """Construct a `SnapBuild`."""203 """Construct a `SnapBuild`."""
191 super(SnapBuild, self).__init__()204 super(SnapBuild, self).__init__()
192 self.build_farm_job = build_farm_job205 self.build_farm_job = build_farm_job
@@ -202,7 +215,14 @@
202 self.store_upload_metadata = store_upload_metadata215 self.store_upload_metadata = store_upload_metadata
203 if build_request is not None:216 if build_request is not None:
204 self.build_request_id = build_request.id217 self.build_request_id = build_request.id
218 self.explicitly_private = explicitly_private
205 self.status = BuildStatus.NEEDSBUILD219 self.status = BuildStatus.NEEDSBUILD
220 # Copy branch/repository information from the snap, for future
221 # privacy checks on this build.
222 self.branch = snap.branch
223 self.git_repository = snap.git_repository
224 self.git_repository_url = snap.git_repository_url
225 self.git_path = snap.git_path
206226
207 @property227 @property
208 def build_request(self):228 def build_request(self):
@@ -214,10 +234,15 @@
214 def is_private(self):234 def is_private(self):
215 """See `IBuildFarmJob`."""235 """See `IBuildFarmJob`."""
216 return (236 return (
237 self.explicitly_private or
217 self.snap.private or238 self.snap.private or
218 self.snap.owner.private or239 self.snap.owner.private or
219 self.archive.private240 # We must check individual components here, since the Snap's
220 )241 # configuration may have changed since this build was created.
242 self.archive.private or
243 (self.branch is not None and self.branch.private) or
244 (self.git_repository is not None and self.git_repository.private)
245 )
221246
222 @property247 @property
223 def title(self):248 def title(self):
@@ -518,7 +543,8 @@
518543
519 def new(self, requester, snap, archive, distro_arch_series, pocket,544 def new(self, requester, snap, archive, distro_arch_series, pocket,
520 channels=None, date_created=DEFAULT,545 channels=None, date_created=DEFAULT,
521 store_upload_metadata=None, build_request=None):546 store_upload_metadata=None, build_request=None,
547 explicitly_private=False):
522 """See `ISnapBuildSet`."""548 """See `ISnapBuildSet`."""
523 store = IMasterStore(SnapBuild)549 store = IMasterStore(SnapBuild)
524 build_farm_job = getUtility(IBuildFarmJobSource).new(550 build_farm_job = getUtility(IBuildFarmJobSource).new(
@@ -530,7 +556,7 @@
530 not distro_arch_series.processor.supports_nonvirtualized556 not distro_arch_series.processor.supports_nonvirtualized
531 or snap.require_virtualized or archive.require_virtualized,557 or snap.require_virtualized or archive.require_virtualized,
532 date_created, store_upload_metadata=store_upload_metadata,558 date_created, store_upload_metadata=store_upload_metadata,
533 build_request=build_request)559 build_request=build_request, explicitly_private=explicitly_private)
534 store.add(snapbuild)560 store.add(snapbuild)
535 return snapbuild561 return snapbuild
536562
@@ -592,6 +618,24 @@
592 bfj.id for bfj in build_farm_jobs))618 bfj.id for bfj in build_farm_jobs))
593 return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)619 return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)
594620
621 def findByBranch(self, branch):
622 """See `ISnapBuildSet`."""
623 return IStore(SnapBuild).find(SnapBuild, SnapBuild.branch == branch)
624
625 def findByGitRepository(self, repository):
626 """See `ISnapBuildSet`."""
627 return IStore(SnapBuild).find(
628 SnapBuild, SnapBuild.git_repository == repository)
629
630 def detachFromBranch(self, branch):
631 """See `ISnapBuildSet`."""
632 self.findByBranch(branch).set(branch_id=None)
633
634 def detachFromGitRepository(self, repository):
635 """See `ISnapBuildSet`."""
636 self.findByGitRepository(repository).set(
637 git_repository_id=None, git_path=None)
638
595639
596@implementer(IMacaroonIssuer)640@implementer(IMacaroonIssuer)
597class SnapBuildMacaroonIssuer(MacaroonIssuerBase):641class SnapBuildMacaroonIssuer(MacaroonIssuerBase):
598642
=== modified file 'lib/lp/snappy/model/snapbuildbehaviour.py'
--- lib/lp/snappy/model/snapbuildbehaviour.py 2019-05-22 16:54:09 +0000
+++ lib/lp/snappy/model/snapbuildbehaviour.py 2019-05-28 16:37:20 +0000
@@ -139,6 +139,8 @@
139 # dict, since otherwise we'll be unable to serialise it to139 # dict, since otherwise we'll be unable to serialise it to
140 # XML-RPC.140 # XML-RPC.
141 args["channels"] = removeSecurityProxy(channels)141 args["channels"] = removeSecurityProxy(channels)
142 # XXX cjwatson 2019-04-01: Once new SnapBuilds are being created on
143 # production with code object columns, we should use them here.
142 if build.snap.branch is not None:144 if build.snap.branch is not None:
143 args["branch"] = build.snap.branch.bzr_identity145 args["branch"] = build.snap.branch.bzr_identity
144 elif build.snap.git_ref is not None:146 elif build.snap.git_ref is not None:
145147
=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py 2019-05-09 15:44:59 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py 2019-05-28 16:37:20 +0000
@@ -60,6 +60,7 @@
60from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource60from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource
61from lp.soyuz.enums import ArchivePurpose61from lp.soyuz.enums import ArchivePurpose
62from lp.testing import (62from lp.testing import (
63 admin_logged_in,
63 ANONYMOUS,64 ANONYMOUS,
64 api_url,65 api_url,
65 login,66 login,
@@ -158,20 +159,91 @@
158 build = self.factory.makeSnapBuild(archive=self.factory.makeArchive())159 build = self.factory.makeSnapBuild(archive=self.factory.makeArchive())
159 self.assertEqual("main", build.current_component.name)160 self.assertEqual("main", build.current_component.name)
160161
161 def test_is_private(self):162 def test_public(self):
162 # A SnapBuild is private iff its Snap and archive are.163 # A SnapBuild is public if it has no associated private resources.
163 self.assertFalse(self.build.is_private)164 self.assertFalse(self.build.is_private)
165
166 def test_explicitly_private(self):
167 # A SnapBuild is private if it is marked explicitly private due to
168 # its Snap being private. It remains private even if the Snap is
169 # later made public, in order to avoid accidental information leaks.
170 with person_logged_in(self.factory.makePerson()) as owner:
171 build = self.factory.makeSnapBuild(
172 requester=owner, owner=owner, private=True)
173 self.assertTrue(build.explicitly_private)
174 self.assertTrue(build.is_private)
175 with admin_logged_in():
176 build.snap.private = False
177 self.assertTrue(build.is_private)
178
179 def test_private_if_owner_is_private(self):
180 # A SnapBuild is private if its owner is private.
164 private_team = self.factory.makeTeam(181 private_team = self.factory.makeTeam(
165 visibility=PersonVisibility.PRIVATE)182 visibility=PersonVisibility.PRIVATE)
166 with person_logged_in(private_team.teamowner):183 with person_logged_in(private_team.teamowner):
167 build = self.factory.makeSnapBuild(184 build = self.factory.makeSnapBuild(
168 requester=private_team.teamowner, owner=private_team,185 requester=private_team.teamowner, owner=private_team,
169 private=True)186 private=True)
187 self.assertTrue(build.explicitly_private)
170 self.assertTrue(build.is_private)188 self.assertTrue(build.is_private)
189
190 def test_private_if_archive_is_private(self):
191 # A SnapBuild is private if its archive is private.
171 private_archive = self.factory.makeArchive(private=True)192 private_archive = self.factory.makeArchive(private=True)
172 with person_logged_in(private_archive.owner):193 with person_logged_in(private_archive.owner):
173 build = self.factory.makeSnapBuild(archive=private_archive)194 build = self.factory.makeSnapBuild(archive=private_archive)
174 self.assertTrue(build.is_private)195 self.assertFalse(build.explicitly_private)
196 self.assertTrue(build.is_private)
197
198 def test_private_if_branch_is_private(self):
199 # A SnapBuild is private if its Bazaar branch is private, even if
200 # the Snap is later reconfigured to use a public branch.
201 private_branch = self.factory.makeAnyBranch(
202 information_type=InformationType.USERDATA)
203 with person_logged_in(private_branch.owner):
204 build = self.factory.makeSnapBuild(
205 branch=private_branch, private=True)
206 self.assertTrue(build.explicitly_private)
207 self.assertTrue(build.is_private)
208 build.snap.branch = self.factory.makeAnyBranch()
209 self.assertTrue(build.is_private)
210
211 def test_private_if_git_repository_is_private(self):
212 # A SnapBuild is private if its Git repository is private, even if
213 # the Snap is later reconfigured to use a public repository.
214 [private_ref] = self.factory.makeGitRefs(
215 information_type=InformationType.USERDATA)
216 with person_logged_in(private_ref.owner):
217 build = self.factory.makeSnapBuild(
218 git_ref=private_ref, private=True)
219 self.assertTrue(build.explicitly_private)
220 self.assertTrue(build.is_private)
221 build.snap.git_ref = self.factory.makeGitRefs()[0]
222 self.assertTrue(build.is_private)
223
224 def test_copies_code_information(self):
225 # Creating a SnapBuild copies code information from its parent Snap.
226 for args in (
227 {"branch": self.factory.makeAnyBranch()},
228 {"git_ref": self.factory.makeGitRefs()[0]},
229 {"git_ref": self.factory.makeGitRefRemote()},
230 ):
231 snap = self.factory.makeSnap(**args)
232 build = self.factory.makeSnapBuild(snap=snap)
233 snap.branch = self.factory.makeAnyBranch()
234 snap.git_ref = None
235 if "branch" in args:
236 self.assertThat(build, MatchesStructure(
237 branch=Equals(args["branch"]),
238 git_repository=Is(None),
239 git_repository_url=Is(None),
240 git_path=Is(None)))
241 else:
242 self.assertThat(build, MatchesStructure(
243 branch=Is(None),
244 git_repository=Equals(args["git_ref"].repository),
245 git_repository_url=Equals(args["git_ref"].repository_url),
246 git_path=Equals(args["git_ref"].path)))
175247
176 def test_can_be_cancelled(self):248 def test_can_be_cancelled(self):
177 # For all states that can be cancelled, can_be_cancelled returns True.249 # For all states that can be cancelled, can_be_cancelled returns True.
@@ -693,6 +765,22 @@
693 # 404 since we aren't allowed to know that the private team exists.765 # 404 since we aren't allowed to know that the private team exists.
694 self.assertEqual(404, unpriv_webservice.get(build_url).status)766 self.assertEqual(404, unpriv_webservice.get(build_url).status)
695767
768 def test_private_snap_made_public(self):
769 # A SnapBuild with a Snap that was initially private but then made
770 # public is still private.
771 with person_logged_in(self.person):
772 db_build = self.factory.makeSnapBuild(
773 requester=self.person, owner=self.person, private=True)
774 build_url = api_url(db_build)
775 with admin_logged_in():
776 db_build.snap.private = False
777 unpriv_webservice = webservice_for_person(
778 self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC)
779 unpriv_webservice.default_api_version = "devel"
780 logout()
781 self.assertEqual(200, self.webservice.get(build_url).status)
782 self.assertEqual(401, unpriv_webservice.get(build_url).status)
783
696 def test_private_archive(self):784 def test_private_archive(self):
697 # A SnapBuild with a private archive is private.785 # A SnapBuild with a private archive is private.
698 db_archive = self.factory.makeArchive(owner=self.person, private=True)786 db_archive = self.factory.makeArchive(owner=self.person, private=True)
699787
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py 2019-05-06 14:22:34 +0000
+++ lib/lp/testing/factory.py 2019-05-28 16:37:20 +0000
@@ -4800,7 +4800,7 @@
4800 snapbuild = getUtility(ISnapBuildSet).new(4800 snapbuild = getUtility(ISnapBuildSet).new(
4801 requester, snap, archive, distroarchseries, pocket,4801 requester, snap, archive, distroarchseries, pocket,
4802 channels=channels, date_created=date_created,4802 channels=channels, date_created=date_created,
4803 build_request=build_request)4803 build_request=build_request, explicitly_private=snap.private)
4804 if duration is not None:4804 if duration is not None:
4805 removeSecurityProxy(snapbuild).updateStatus(4805 removeSecurityProxy(snapbuild).updateStatus(
4806 BuildStatus.BUILDING, builder=builder,4806 BuildStatus.BUILDING, builder=builder,