Merge lp:~cjwatson/launchpad/snap-build-record-code into lp:launchpad
- snap-build-record-code
- Merge into devel
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 | ||||
Related bugs: |
|
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:/
https:/
Colin Watson (cjwatson) wrote : | # |
- 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.
Colin Watson (cjwatson) wrote : | # |
I've tightened up the privacy rules here as best I can. How's this?
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
1 | === modified file 'lib/lp/code/model/branch.py' | |||
2 | --- lib/lp/code/model/branch.py 2019-04-17 10:34:13 +0000 | |||
3 | +++ lib/lp/code/model/branch.py 2019-05-28 16:37:20 +0000 | |||
4 | @@ -196,6 +196,7 @@ | |||
5 | 196 | from lp.services.webhooks.interfaces import IWebhookSet | 196 | from lp.services.webhooks.interfaces import IWebhookSet |
6 | 197 | from lp.services.webhooks.model import WebhookTargetMixin | 197 | from lp.services.webhooks.model import WebhookTargetMixin |
7 | 198 | from lp.snappy.interfaces.snap import ISnapSet | 198 | from lp.snappy.interfaces.snap import ISnapSet |
8 | 199 | from lp.snappy.interfaces.snapbuild import ISnapBuildSet | ||
9 | 199 | 200 | ||
10 | 200 | 201 | ||
11 | 201 | @implementer(IBranch, IPrivacy, IInformationType) | 202 | @implementer(IBranch, IPrivacy, IInformationType) |
12 | @@ -923,10 +924,9 @@ | |||
13 | 923 | DeletionCallable( | 924 | DeletionCallable( |
14 | 924 | recipe, _('This recipe uses this branch.'), recipe.destroySelf) | 925 | recipe, _('This recipe uses this branch.'), recipe.destroySelf) |
15 | 925 | for recipe in recipes) | 926 | for recipe in recipes) |
20 | 926 | if not getUtility(ISnapSet).findByBranch(self).is_empty(): | 927 | if (not getUtility(ISnapSet).findByBranch(self).is_empty() or |
21 | 927 | alteration_operations.append(DeletionCallable( | 928 | not getUtility(ISnapBuildSet).findByBranch(self).is_empty()): |
22 | 928 | None, _('Some snap packages build from this branch.'), | 929 | alteration_operations.append(DetachSnaps(self)) |
19 | 929 | getUtility(ISnapSet).detachFromBranch, self)) | ||
23 | 930 | return (alteration_operations, deletion_operations) | 930 | return (alteration_operations, deletion_operations) |
24 | 931 | 931 | ||
25 | 932 | def deletionRequirements(self, eager_load=False): | 932 | def deletionRequirements(self, eager_load=False): |
26 | @@ -1644,6 +1644,19 @@ | |||
27 | 1644 | getUtility(ICodeImportSet).delete(self.affected_object) | 1644 | getUtility(ICodeImportSet).delete(self.affected_object) |
28 | 1645 | 1645 | ||
29 | 1646 | 1646 | ||
30 | 1647 | class DetachSnaps(DeletionOperation): | ||
31 | 1648 | """Deletion operation that detaches snaps from a branch.""" | ||
32 | 1649 | |||
33 | 1650 | def __init__(self, branch): | ||
34 | 1651 | super(DetachSnaps, self).__init__( | ||
35 | 1652 | None, _('Some snap packages build from this branch.')) | ||
36 | 1653 | self.branch = branch | ||
37 | 1654 | |||
38 | 1655 | def __call__(self): | ||
39 | 1656 | getUtility(ISnapSet).detachFromBranch(self.branch) | ||
40 | 1657 | getUtility(ISnapBuildSet).detachFromBranch(self.branch) | ||
41 | 1658 | |||
42 | 1659 | |||
43 | 1647 | @implementer(IBranchSet) | 1660 | @implementer(IBranchSet) |
44 | 1648 | class BranchSet: | 1661 | class BranchSet: |
45 | 1649 | """The set of all branches.""" | 1662 | """The set of all branches.""" |
46 | 1650 | 1663 | ||
47 | === modified file 'lib/lp/code/model/gitrepository.py' | |||
48 | --- lib/lp/code/model/gitrepository.py 2019-05-03 13:18:52 +0000 | |||
49 | +++ lib/lp/code/model/gitrepository.py 2019-05-28 16:37:20 +0000 | |||
50 | @@ -202,6 +202,7 @@ | |||
51 | 202 | from lp.services.webhooks.interfaces import IWebhookSet | 202 | from lp.services.webhooks.interfaces import IWebhookSet |
52 | 203 | from lp.services.webhooks.model import WebhookTargetMixin | 203 | from lp.services.webhooks.model import WebhookTargetMixin |
53 | 204 | from lp.snappy.interfaces.snap import ISnapSet | 204 | from lp.snappy.interfaces.snap import ISnapSet |
54 | 205 | from lp.snappy.interfaces.snapbuild import ISnapBuildSet | ||
55 | 205 | 206 | ||
56 | 206 | 207 | ||
57 | 207 | object_type_map = { | 208 | object_type_map = { |
58 | @@ -1476,9 +1477,7 @@ | |||
59 | 1476 | recipe.destroySelf) | 1477 | recipe.destroySelf) |
60 | 1477 | for recipe in recipes) | 1478 | for recipe in recipes) |
61 | 1478 | if not getUtility(ISnapSet).findByGitRepository(self).is_empty(): | 1479 | if not getUtility(ISnapSet).findByGitRepository(self).is_empty(): |
65 | 1479 | alteration_operations.append(DeletionCallable( | 1480 | alteration_operations.append(DetachSnaps(self)) |
63 | 1480 | None, msg("Some snap packages build from this repository."), | ||
64 | 1481 | getUtility(ISnapSet).detachFromGitRepository, self)) | ||
66 | 1482 | 1481 | ||
67 | 1483 | return (alteration_operations, deletion_operations) | 1482 | return (alteration_operations, deletion_operations) |
68 | 1484 | 1483 | ||
69 | @@ -1630,6 +1629,19 @@ | |||
70 | 1630 | getUtility(ICodeImportSet).delete(self.affected_object) | 1629 | getUtility(ICodeImportSet).delete(self.affected_object) |
71 | 1631 | 1630 | ||
72 | 1632 | 1631 | ||
73 | 1632 | class DetachSnaps(DeletionOperation): | ||
74 | 1633 | """Deletion operation that detaches snaps from a repository.""" | ||
75 | 1634 | |||
76 | 1635 | def __init__(self, repository): | ||
77 | 1636 | super(DetachSnaps, self).__init__( | ||
78 | 1637 | None, msg("Some snap packages build from this repository.")) | ||
79 | 1638 | self.repository = repository | ||
80 | 1639 | |||
81 | 1640 | def __call__(self): | ||
82 | 1641 | getUtility(ISnapSet).detachFromGitRepository(self.repository) | ||
83 | 1642 | getUtility(ISnapBuildSet).detachFromGitRepository(self.repository) | ||
84 | 1643 | |||
85 | 1644 | |||
86 | 1633 | @implementer(IGitRepositorySet) | 1645 | @implementer(IGitRepositorySet) |
87 | 1634 | class GitRepositorySet: | 1646 | class GitRepositorySet: |
88 | 1635 | """See `IGitRepositorySet`.""" | 1647 | """See `IGitRepositorySet`.""" |
89 | 1636 | 1648 | ||
90 | === modified file 'lib/lp/code/model/tests/test_branch.py' | |||
91 | --- lib/lp/code/model/tests/test_branch.py 2019-01-28 18:09:21 +0000 | |||
92 | +++ lib/lp/code/model/tests/test_branch.py 2019-05-28 16:37:20 +0000 | |||
93 | @@ -1697,7 +1697,7 @@ | |||
94 | 1697 | def test_snap_requirements(self): | 1697 | def test_snap_requirements(self): |
95 | 1698 | # If a branch is used by a snap package, the deletion requirements | 1698 | # If a branch is used by a snap package, the deletion requirements |
96 | 1699 | # indicate this. | 1699 | # indicate this. |
98 | 1700 | self.factory.makeSnap(branch=self.branch) | 1700 | self.factory.makeSnapBuild(branch=self.branch) |
99 | 1701 | self.assertEqual( | 1701 | self.assertEqual( |
100 | 1702 | {None: ('alter', _('Some snap packages build from this branch.'))}, | 1702 | {None: ('alter', _('Some snap packages build from this branch.'))}, |
101 | 1703 | self.branch.deletionRequirements()) | 1703 | self.branch.deletionRequirements()) |
102 | @@ -1705,7 +1705,9 @@ | |||
103 | 1705 | def test_snap_deletion(self): | 1705 | def test_snap_deletion(self): |
104 | 1706 | # break_references allows deleting a branch used by a snap package. | 1706 | # break_references allows deleting a branch used by a snap package. |
105 | 1707 | snap1 = self.factory.makeSnap(branch=self.branch) | 1707 | snap1 = self.factory.makeSnap(branch=self.branch) |
106 | 1708 | self.factory.makeSnapBuild(snap=snap1) | ||
107 | 1708 | snap2 = self.factory.makeSnap(branch=self.branch) | 1709 | snap2 = self.factory.makeSnap(branch=self.branch) |
108 | 1710 | self.factory.makeSnapBuild(snap=snap2) | ||
109 | 1709 | self.branch.destroySelf(break_references=True) | 1711 | self.branch.destroySelf(break_references=True) |
110 | 1710 | transaction.commit() | 1712 | transaction.commit() |
111 | 1711 | self.assertIsNone(snap1.branch) | 1713 | self.assertIsNone(snap1.branch) |
112 | 1712 | 1714 | ||
113 | === modified file 'lib/lp/code/model/tests/test_gitrepository.py' | |||
114 | --- lib/lp/code/model/tests/test_gitrepository.py 2019-05-03 13:18:52 +0000 | |||
115 | +++ lib/lp/code/model/tests/test_gitrepository.py 2019-05-28 16:37:20 +0000 | |||
116 | @@ -989,7 +989,7 @@ | |||
117 | 989 | # If a repository is used by a snap package, the deletion | 989 | # If a repository is used by a snap package, the deletion |
118 | 990 | # requirements indicate this. | 990 | # requirements indicate this. |
119 | 991 | [ref] = self.factory.makeGitRefs() | 991 | [ref] = self.factory.makeGitRefs() |
121 | 992 | self.factory.makeSnap(git_ref=ref) | 992 | self.factory.makeSnapBuild(git_ref=ref) |
122 | 993 | self.assertEqual( | 993 | self.assertEqual( |
123 | 994 | {None: | 994 | {None: |
124 | 995 | ("alter", _("Some snap packages build from this repository."))}, | 995 | ("alter", _("Some snap packages build from this repository."))}, |
125 | @@ -1001,7 +1001,9 @@ | |||
126 | 1001 | [ref1, ref2] = self.factory.makeGitRefs( | 1001 | [ref1, ref2] = self.factory.makeGitRefs( |
127 | 1002 | repository=repository, paths=["refs/heads/1", "refs/heads/2"]) | 1002 | repository=repository, paths=["refs/heads/1", "refs/heads/2"]) |
128 | 1003 | snap1 = self.factory.makeSnap(git_ref=ref1) | 1003 | snap1 = self.factory.makeSnap(git_ref=ref1) |
129 | 1004 | self.factory.makeSnapBuild(snap=snap1) | ||
130 | 1004 | snap2 = self.factory.makeSnap(git_ref=ref2) | 1005 | snap2 = self.factory.makeSnap(git_ref=ref2) |
131 | 1006 | self.factory.makeSnapBuild(snap=snap2) | ||
132 | 1005 | repository.destroySelf(break_references=True) | 1007 | repository.destroySelf(break_references=True) |
133 | 1006 | transaction.commit() | 1008 | transaction.commit() |
134 | 1007 | self.assertIsNone(snap1.git_repository) | 1009 | self.assertIsNone(snap1.git_repository) |
135 | 1008 | 1010 | ||
136 | === modified file 'lib/lp/security.py' | |||
137 | --- lib/lp/security.py 2019-03-26 20:51:38 +0000 | |||
138 | +++ lib/lp/security.py 2019-05-28 16:37:20 +0000 | |||
139 | @@ -3363,13 +3363,34 @@ | |||
140 | 3363 | obj, obj.snap, 'launchpad.View') | 3363 | obj, obj.snap, 'launchpad.View') |
141 | 3364 | 3364 | ||
142 | 3365 | 3365 | ||
144 | 3366 | class ViewSnapBuild(DelegatedAuthorization): | 3366 | class ViewSnapBuild(AuthorizationBase): |
145 | 3367 | permission = 'launchpad.View' | 3367 | permission = 'launchpad.View' |
146 | 3368 | usedfor = ISnapBuild | 3368 | usedfor = ISnapBuild |
147 | 3369 | 3369 | ||
151 | 3370 | def iter_objects(self): | 3370 | def checkUnauthenticated(self): |
152 | 3371 | yield self.obj.snap | 3371 | return not self.obj.is_private |
153 | 3372 | yield self.obj.archive | 3372 | |
154 | 3373 | def checkAuthenticated(self, user): | ||
155 | 3374 | if not self.obj.is_private: | ||
156 | 3375 | return True | ||
157 | 3376 | |||
158 | 3377 | for item in ( | ||
159 | 3378 | self.obj.snap, self.obj.archive, | ||
160 | 3379 | self.obj.branch, self.obj.git_repository): | ||
161 | 3380 | if (item is not None and | ||
162 | 3381 | not self.forwardCheckAuthenticated(user, item)): | ||
163 | 3382 | return False | ||
164 | 3383 | |||
165 | 3384 | # Only the snap owner and admins can see explicitly-private builds, | ||
166 | 3385 | # on top of any restrictions associated with the individual | ||
167 | 3386 | # components above. This may be overly-restrictive but is at least | ||
168 | 3387 | # relatively safe; if need be we can investigate loosening this | ||
169 | 3388 | # later, but be careful of the case where private code objects have | ||
170 | 3389 | # been detached. | ||
171 | 3390 | if self.obj.explicitly_private: | ||
172 | 3391 | return EditSnap(self.obj.snap).checkAuthenticated(user) | ||
173 | 3392 | else: | ||
174 | 3393 | return True | ||
175 | 3373 | 3394 | ||
176 | 3374 | 3395 | ||
177 | 3375 | class EditSnapBuild(AdminByBuilddAdmin): | 3396 | class EditSnapBuild(AdminByBuilddAdmin): |
178 | 3376 | 3397 | ||
179 | === modified file 'lib/lp/snappy/interfaces/snapbuild.py' | |||
180 | --- lib/lp/snappy/interfaces/snapbuild.py 2018-12-12 10:31:40 +0000 | |||
181 | +++ lib/lp/snappy/interfaces/snapbuild.py 2019-05-28 16:37:20 +0000 | |||
182 | @@ -51,6 +51,8 @@ | |||
183 | 51 | from lp import _ | 51 | from lp import _ |
184 | 52 | from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource | 52 | from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource |
185 | 53 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild | 53 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
186 | 54 | from lp.code.interfaces.branch import IBranch | ||
187 | 55 | from lp.code.interfaces.gitrepository import IGitRepository | ||
188 | 54 | from lp.registry.interfaces.person import IPerson | 56 | from lp.registry.interfaces.person import IPerson |
189 | 55 | from lp.registry.interfaces.pocket import PackagePublishingPocket | 57 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
190 | 56 | from lp.services.database.constants import DEFAULT | 58 | from lp.services.database.constants import DEFAULT |
191 | @@ -168,6 +170,14 @@ | |||
192 | 168 | "supported."), | 170 | "supported."), |
193 | 169 | key_type=TextLine())) | 171 | key_type=TextLine())) |
194 | 170 | 172 | ||
195 | 173 | explicitly_private = Bool( | ||
196 | 174 | title=_("Whether this build is explicitly private."), | ||
197 | 175 | description=_( | ||
198 | 176 | "An explicitly-private build remains private even if its snap and " | ||
199 | 177 | "other associated resources are public. This allows snaps to " | ||
200 | 178 | "transition between private and public states without " | ||
201 | 179 | "inadvertently leaking old private information.")) | ||
202 | 180 | |||
203 | 171 | virtualized = Bool( | 181 | virtualized = Bool( |
204 | 172 | title=_("If True, this build is virtualized."), readonly=True) | 182 | title=_("If True, this build is virtualized."), readonly=True) |
205 | 173 | 183 | ||
206 | @@ -248,6 +258,20 @@ | |||
207 | 248 | store_upload_metadata = Attribute( | 258 | store_upload_metadata = Attribute( |
208 | 249 | _("A dict of data about store upload progress.")) | 259 | _("A dict of data about store upload progress.")) |
209 | 250 | 260 | ||
210 | 261 | branch = Reference( | ||
211 | 262 | IBranch, | ||
212 | 263 | title=_("Bazaar branch"), required=False, readonly=True) | ||
213 | 264 | |||
214 | 265 | git_repository = Reference( | ||
215 | 266 | IGitRepository, | ||
216 | 267 | title=_("Git repository"), required=False, readonly=True) | ||
217 | 268 | |||
218 | 269 | git_repository_url = TextLine( | ||
219 | 270 | title=_("Git repository URL"), required=False, readonly=True) | ||
220 | 271 | |||
221 | 272 | git_path = TextLine( | ||
222 | 273 | title=_("Git branch path"), required=False, readonly=True) | ||
223 | 274 | |||
224 | 251 | def getFiles(): | 275 | def getFiles(): |
225 | 252 | """Retrieve the build's `ISnapFile` records. | 276 | """Retrieve the build's `ISnapFile` records. |
226 | 253 | 277 | ||
227 | @@ -343,3 +367,15 @@ | |||
228 | 343 | 367 | ||
229 | 344 | def preloadBuildsData(builds): | 368 | def preloadBuildsData(builds): |
230 | 345 | """Load the data related to a list of snap builds.""" | 369 | """Load the data related to a list of snap builds.""" |
231 | 370 | |||
232 | 371 | def findByBranch(branch): | ||
233 | 372 | """Return all snap builds for the given Bazaar branch.""" | ||
234 | 373 | |||
235 | 374 | def findByGitRepository(repository, paths=None): | ||
236 | 375 | """Return all snap builds for the given Git repository.""" | ||
237 | 376 | |||
238 | 377 | def detachFromBranch(branch): | ||
239 | 378 | """Detach all snap builds from the given Bazaar branch.""" | ||
240 | 379 | |||
241 | 380 | def detachFromGitRepository(repository): | ||
242 | 381 | """Detach all snap builds from the given Git repository.""" | ||
243 | 346 | 382 | ||
244 | === modified file 'lib/lp/snappy/model/snap.py' | |||
245 | --- lib/lp/snappy/model/snap.py 2019-05-16 10:21:14 +0000 | |||
246 | +++ lib/lp/snappy/model/snap.py 2019-05-28 16:37:20 +0000 | |||
247 | @@ -650,7 +650,8 @@ | |||
248 | 650 | 650 | ||
249 | 651 | build = getUtility(ISnapBuildSet).new( | 651 | build = getUtility(ISnapBuildSet).new( |
250 | 652 | requester, self, archive, distro_arch_series, pocket, | 652 | requester, self, archive, distro_arch_series, pocket, |
252 | 653 | channels=channels, build_request=build_request) | 653 | channels=channels, build_request=build_request, |
253 | 654 | explicitly_private=self.private) | ||
254 | 654 | build.queueBuild() | 655 | build.queueBuild() |
255 | 655 | notify(ObjectCreatedEvent(build, user=requester)) | 656 | notify(ObjectCreatedEvent(build, user=requester)) |
256 | 656 | return build | 657 | return build |
257 | 657 | 658 | ||
258 | === modified file 'lib/lp/snappy/model/snapbuild.py' | |||
259 | --- lib/lp/snappy/model/snapbuild.py 2019-05-09 15:44:59 +0000 | |||
260 | +++ lib/lp/snappy/model/snapbuild.py 2019-05-28 16:37:20 +0000 | |||
261 | @@ -154,6 +154,8 @@ | |||
262 | 154 | 154 | ||
263 | 155 | channels = JSON('channels', allow_none=True) | 155 | channels = JSON('channels', allow_none=True) |
264 | 156 | 156 | ||
265 | 157 | explicitly_private = Bool(name='explicitly_private') | ||
266 | 158 | |||
267 | 157 | processor_id = Int(name='processor', allow_none=False) | 159 | processor_id = Int(name='processor', allow_none=False) |
268 | 158 | processor = Reference(processor_id, 'Processor.id') | 160 | processor = Reference(processor_id, 'Processor.id') |
269 | 159 | virtualized = Bool(name='virtualized') | 161 | virtualized = Bool(name='virtualized') |
270 | @@ -184,9 +186,20 @@ | |||
271 | 184 | 186 | ||
272 | 185 | store_upload_metadata = JSON('store_upload_json_data', allow_none=True) | 187 | store_upload_metadata = JSON('store_upload_json_data', allow_none=True) |
273 | 186 | 188 | ||
274 | 189 | branch_id = Int(name='branch', allow_none=True) | ||
275 | 190 | branch = Reference(branch_id, 'Branch.id') | ||
276 | 191 | |||
277 | 192 | git_repository_id = Int(name='git_repository', allow_none=True) | ||
278 | 193 | git_repository = Reference(git_repository_id, 'GitRepository.id') | ||
279 | 194 | |||
280 | 195 | git_repository_url = Unicode(name='git_repository_url', allow_none=True) | ||
281 | 196 | |||
282 | 197 | git_path = Unicode(name='git_path', allow_none=True) | ||
283 | 198 | |||
284 | 187 | def __init__(self, build_farm_job, requester, snap, archive, | 199 | def __init__(self, build_farm_job, requester, snap, archive, |
285 | 188 | distro_arch_series, pocket, channels, processor, virtualized, | 200 | distro_arch_series, pocket, channels, processor, virtualized, |
287 | 189 | date_created, store_upload_metadata=None, build_request=None): | 201 | date_created, store_upload_metadata=None, build_request=None, |
288 | 202 | explicitly_private=False): | ||
289 | 190 | """Construct a `SnapBuild`.""" | 203 | """Construct a `SnapBuild`.""" |
290 | 191 | super(SnapBuild, self).__init__() | 204 | super(SnapBuild, self).__init__() |
291 | 192 | self.build_farm_job = build_farm_job | 205 | self.build_farm_job = build_farm_job |
292 | @@ -202,7 +215,14 @@ | |||
293 | 202 | self.store_upload_metadata = store_upload_metadata | 215 | self.store_upload_metadata = store_upload_metadata |
294 | 203 | if build_request is not None: | 216 | if build_request is not None: |
295 | 204 | self.build_request_id = build_request.id | 217 | self.build_request_id = build_request.id |
296 | 218 | self.explicitly_private = explicitly_private | ||
297 | 205 | self.status = BuildStatus.NEEDSBUILD | 219 | self.status = BuildStatus.NEEDSBUILD |
298 | 220 | # Copy branch/repository information from the snap, for future | ||
299 | 221 | # privacy checks on this build. | ||
300 | 222 | self.branch = snap.branch | ||
301 | 223 | self.git_repository = snap.git_repository | ||
302 | 224 | self.git_repository_url = snap.git_repository_url | ||
303 | 225 | self.git_path = snap.git_path | ||
304 | 206 | 226 | ||
305 | 207 | @property | 227 | @property |
306 | 208 | def build_request(self): | 228 | def build_request(self): |
307 | @@ -214,10 +234,15 @@ | |||
308 | 214 | def is_private(self): | 234 | def is_private(self): |
309 | 215 | """See `IBuildFarmJob`.""" | 235 | """See `IBuildFarmJob`.""" |
310 | 216 | return ( | 236 | return ( |
311 | 237 | self.explicitly_private or | ||
312 | 217 | self.snap.private or | 238 | self.snap.private or |
313 | 218 | self.snap.owner.private or | 239 | self.snap.owner.private or |
316 | 219 | self.archive.private | 240 | # We must check individual components here, since the Snap's |
317 | 220 | ) | 241 | # configuration may have changed since this build was created. |
318 | 242 | self.archive.private or | ||
319 | 243 | (self.branch is not None and self.branch.private) or | ||
320 | 244 | (self.git_repository is not None and self.git_repository.private) | ||
321 | 245 | ) | ||
322 | 221 | 246 | ||
323 | 222 | @property | 247 | @property |
324 | 223 | def title(self): | 248 | def title(self): |
325 | @@ -518,7 +543,8 @@ | |||
326 | 518 | 543 | ||
327 | 519 | def new(self, requester, snap, archive, distro_arch_series, pocket, | 544 | def new(self, requester, snap, archive, distro_arch_series, pocket, |
328 | 520 | channels=None, date_created=DEFAULT, | 545 | channels=None, date_created=DEFAULT, |
330 | 521 | store_upload_metadata=None, build_request=None): | 546 | store_upload_metadata=None, build_request=None, |
331 | 547 | explicitly_private=False): | ||
332 | 522 | """See `ISnapBuildSet`.""" | 548 | """See `ISnapBuildSet`.""" |
333 | 523 | store = IMasterStore(SnapBuild) | 549 | store = IMasterStore(SnapBuild) |
334 | 524 | build_farm_job = getUtility(IBuildFarmJobSource).new( | 550 | build_farm_job = getUtility(IBuildFarmJobSource).new( |
335 | @@ -530,7 +556,7 @@ | |||
336 | 530 | not distro_arch_series.processor.supports_nonvirtualized | 556 | not distro_arch_series.processor.supports_nonvirtualized |
337 | 531 | or snap.require_virtualized or archive.require_virtualized, | 557 | or snap.require_virtualized or archive.require_virtualized, |
338 | 532 | date_created, store_upload_metadata=store_upload_metadata, | 558 | date_created, store_upload_metadata=store_upload_metadata, |
340 | 533 | build_request=build_request) | 559 | build_request=build_request, explicitly_private=explicitly_private) |
341 | 534 | store.add(snapbuild) | 560 | store.add(snapbuild) |
342 | 535 | return snapbuild | 561 | return snapbuild |
343 | 536 | 562 | ||
344 | @@ -592,6 +618,24 @@ | |||
345 | 592 | bfj.id for bfj in build_farm_jobs)) | 618 | bfj.id for bfj in build_farm_jobs)) |
346 | 593 | return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData) | 619 | return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData) |
347 | 594 | 620 | ||
348 | 621 | def findByBranch(self, branch): | ||
349 | 622 | """See `ISnapBuildSet`.""" | ||
350 | 623 | return IStore(SnapBuild).find(SnapBuild, SnapBuild.branch == branch) | ||
351 | 624 | |||
352 | 625 | def findByGitRepository(self, repository): | ||
353 | 626 | """See `ISnapBuildSet`.""" | ||
354 | 627 | return IStore(SnapBuild).find( | ||
355 | 628 | SnapBuild, SnapBuild.git_repository == repository) | ||
356 | 629 | |||
357 | 630 | def detachFromBranch(self, branch): | ||
358 | 631 | """See `ISnapBuildSet`.""" | ||
359 | 632 | self.findByBranch(branch).set(branch_id=None) | ||
360 | 633 | |||
361 | 634 | def detachFromGitRepository(self, repository): | ||
362 | 635 | """See `ISnapBuildSet`.""" | ||
363 | 636 | self.findByGitRepository(repository).set( | ||
364 | 637 | git_repository_id=None, git_path=None) | ||
365 | 638 | |||
366 | 595 | 639 | ||
367 | 596 | @implementer(IMacaroonIssuer) | 640 | @implementer(IMacaroonIssuer) |
368 | 597 | class SnapBuildMacaroonIssuer(MacaroonIssuerBase): | 641 | class SnapBuildMacaroonIssuer(MacaroonIssuerBase): |
369 | 598 | 642 | ||
370 | === modified file 'lib/lp/snappy/model/snapbuildbehaviour.py' | |||
371 | --- lib/lp/snappy/model/snapbuildbehaviour.py 2019-05-22 16:54:09 +0000 | |||
372 | +++ lib/lp/snappy/model/snapbuildbehaviour.py 2019-05-28 16:37:20 +0000 | |||
373 | @@ -139,6 +139,8 @@ | |||
374 | 139 | # dict, since otherwise we'll be unable to serialise it to | 139 | # dict, since otherwise we'll be unable to serialise it to |
375 | 140 | # XML-RPC. | 140 | # XML-RPC. |
376 | 141 | args["channels"] = removeSecurityProxy(channels) | 141 | args["channels"] = removeSecurityProxy(channels) |
377 | 142 | # XXX cjwatson 2019-04-01: Once new SnapBuilds are being created on | ||
378 | 143 | # production with code object columns, we should use them here. | ||
379 | 142 | if build.snap.branch is not None: | 144 | if build.snap.branch is not None: |
380 | 143 | args["branch"] = build.snap.branch.bzr_identity | 145 | args["branch"] = build.snap.branch.bzr_identity |
381 | 144 | elif build.snap.git_ref is not None: | 146 | elif build.snap.git_ref is not None: |
382 | 145 | 147 | ||
383 | === modified file 'lib/lp/snappy/tests/test_snapbuild.py' | |||
384 | --- lib/lp/snappy/tests/test_snapbuild.py 2019-05-09 15:44:59 +0000 | |||
385 | +++ lib/lp/snappy/tests/test_snapbuild.py 2019-05-28 16:37:20 +0000 | |||
386 | @@ -60,6 +60,7 @@ | |||
387 | 60 | from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource | 60 | from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource |
388 | 61 | from lp.soyuz.enums import ArchivePurpose | 61 | from lp.soyuz.enums import ArchivePurpose |
389 | 62 | from lp.testing import ( | 62 | from lp.testing import ( |
390 | 63 | admin_logged_in, | ||
391 | 63 | ANONYMOUS, | 64 | ANONYMOUS, |
392 | 64 | api_url, | 65 | api_url, |
393 | 65 | login, | 66 | login, |
394 | @@ -158,20 +159,91 @@ | |||
395 | 158 | build = self.factory.makeSnapBuild(archive=self.factory.makeArchive()) | 159 | build = self.factory.makeSnapBuild(archive=self.factory.makeArchive()) |
396 | 159 | self.assertEqual("main", build.current_component.name) | 160 | self.assertEqual("main", build.current_component.name) |
397 | 160 | 161 | ||
400 | 161 | def test_is_private(self): | 162 | def test_public(self): |
401 | 162 | # A SnapBuild is private iff its Snap and archive are. | 163 | # A SnapBuild is public if it has no associated private resources. |
402 | 163 | self.assertFalse(self.build.is_private) | 164 | self.assertFalse(self.build.is_private) |
403 | 165 | |||
404 | 166 | def test_explicitly_private(self): | ||
405 | 167 | # A SnapBuild is private if it is marked explicitly private due to | ||
406 | 168 | # its Snap being private. It remains private even if the Snap is | ||
407 | 169 | # later made public, in order to avoid accidental information leaks. | ||
408 | 170 | with person_logged_in(self.factory.makePerson()) as owner: | ||
409 | 171 | build = self.factory.makeSnapBuild( | ||
410 | 172 | requester=owner, owner=owner, private=True) | ||
411 | 173 | self.assertTrue(build.explicitly_private) | ||
412 | 174 | self.assertTrue(build.is_private) | ||
413 | 175 | with admin_logged_in(): | ||
414 | 176 | build.snap.private = False | ||
415 | 177 | self.assertTrue(build.is_private) | ||
416 | 178 | |||
417 | 179 | def test_private_if_owner_is_private(self): | ||
418 | 180 | # A SnapBuild is private if its owner is private. | ||
419 | 164 | private_team = self.factory.makeTeam( | 181 | private_team = self.factory.makeTeam( |
420 | 165 | visibility=PersonVisibility.PRIVATE) | 182 | visibility=PersonVisibility.PRIVATE) |
421 | 166 | with person_logged_in(private_team.teamowner): | 183 | with person_logged_in(private_team.teamowner): |
422 | 167 | build = self.factory.makeSnapBuild( | 184 | build = self.factory.makeSnapBuild( |
423 | 168 | requester=private_team.teamowner, owner=private_team, | 185 | requester=private_team.teamowner, owner=private_team, |
424 | 169 | private=True) | 186 | private=True) |
425 | 187 | self.assertTrue(build.explicitly_private) | ||
426 | 170 | self.assertTrue(build.is_private) | 188 | self.assertTrue(build.is_private) |
427 | 189 | |||
428 | 190 | def test_private_if_archive_is_private(self): | ||
429 | 191 | # A SnapBuild is private if its archive is private. | ||
430 | 171 | private_archive = self.factory.makeArchive(private=True) | 192 | private_archive = self.factory.makeArchive(private=True) |
431 | 172 | with person_logged_in(private_archive.owner): | 193 | with person_logged_in(private_archive.owner): |
432 | 173 | build = self.factory.makeSnapBuild(archive=private_archive) | 194 | build = self.factory.makeSnapBuild(archive=private_archive) |
434 | 174 | self.assertTrue(build.is_private) | 195 | self.assertFalse(build.explicitly_private) |
435 | 196 | self.assertTrue(build.is_private) | ||
436 | 197 | |||
437 | 198 | def test_private_if_branch_is_private(self): | ||
438 | 199 | # A SnapBuild is private if its Bazaar branch is private, even if | ||
439 | 200 | # the Snap is later reconfigured to use a public branch. | ||
440 | 201 | private_branch = self.factory.makeAnyBranch( | ||
441 | 202 | information_type=InformationType.USERDATA) | ||
442 | 203 | with person_logged_in(private_branch.owner): | ||
443 | 204 | build = self.factory.makeSnapBuild( | ||
444 | 205 | branch=private_branch, private=True) | ||
445 | 206 | self.assertTrue(build.explicitly_private) | ||
446 | 207 | self.assertTrue(build.is_private) | ||
447 | 208 | build.snap.branch = self.factory.makeAnyBranch() | ||
448 | 209 | self.assertTrue(build.is_private) | ||
449 | 210 | |||
450 | 211 | def test_private_if_git_repository_is_private(self): | ||
451 | 212 | # A SnapBuild is private if its Git repository is private, even if | ||
452 | 213 | # the Snap is later reconfigured to use a public repository. | ||
453 | 214 | [private_ref] = self.factory.makeGitRefs( | ||
454 | 215 | information_type=InformationType.USERDATA) | ||
455 | 216 | with person_logged_in(private_ref.owner): | ||
456 | 217 | build = self.factory.makeSnapBuild( | ||
457 | 218 | git_ref=private_ref, private=True) | ||
458 | 219 | self.assertTrue(build.explicitly_private) | ||
459 | 220 | self.assertTrue(build.is_private) | ||
460 | 221 | build.snap.git_ref = self.factory.makeGitRefs()[0] | ||
461 | 222 | self.assertTrue(build.is_private) | ||
462 | 223 | |||
463 | 224 | def test_copies_code_information(self): | ||
464 | 225 | # Creating a SnapBuild copies code information from its parent Snap. | ||
465 | 226 | for args in ( | ||
466 | 227 | {"branch": self.factory.makeAnyBranch()}, | ||
467 | 228 | {"git_ref": self.factory.makeGitRefs()[0]}, | ||
468 | 229 | {"git_ref": self.factory.makeGitRefRemote()}, | ||
469 | 230 | ): | ||
470 | 231 | snap = self.factory.makeSnap(**args) | ||
471 | 232 | build = self.factory.makeSnapBuild(snap=snap) | ||
472 | 233 | snap.branch = self.factory.makeAnyBranch() | ||
473 | 234 | snap.git_ref = None | ||
474 | 235 | if "branch" in args: | ||
475 | 236 | self.assertThat(build, MatchesStructure( | ||
476 | 237 | branch=Equals(args["branch"]), | ||
477 | 238 | git_repository=Is(None), | ||
478 | 239 | git_repository_url=Is(None), | ||
479 | 240 | git_path=Is(None))) | ||
480 | 241 | else: | ||
481 | 242 | self.assertThat(build, MatchesStructure( | ||
482 | 243 | branch=Is(None), | ||
483 | 244 | git_repository=Equals(args["git_ref"].repository), | ||
484 | 245 | git_repository_url=Equals(args["git_ref"].repository_url), | ||
485 | 246 | git_path=Equals(args["git_ref"].path))) | ||
486 | 175 | 247 | ||
487 | 176 | def test_can_be_cancelled(self): | 248 | def test_can_be_cancelled(self): |
488 | 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. |
489 | @@ -693,6 +765,22 @@ | |||
490 | 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. |
491 | 694 | self.assertEqual(404, unpriv_webservice.get(build_url).status) | 766 | self.assertEqual(404, unpriv_webservice.get(build_url).status) |
492 | 695 | 767 | ||
493 | 768 | def test_private_snap_made_public(self): | ||
494 | 769 | # A SnapBuild with a Snap that was initially private but then made | ||
495 | 770 | # public is still private. | ||
496 | 771 | with person_logged_in(self.person): | ||
497 | 772 | db_build = self.factory.makeSnapBuild( | ||
498 | 773 | requester=self.person, owner=self.person, private=True) | ||
499 | 774 | build_url = api_url(db_build) | ||
500 | 775 | with admin_logged_in(): | ||
501 | 776 | db_build.snap.private = False | ||
502 | 777 | unpriv_webservice = webservice_for_person( | ||
503 | 778 | self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC) | ||
504 | 779 | unpriv_webservice.default_api_version = "devel" | ||
505 | 780 | logout() | ||
506 | 781 | self.assertEqual(200, self.webservice.get(build_url).status) | ||
507 | 782 | self.assertEqual(401, unpriv_webservice.get(build_url).status) | ||
508 | 783 | |||
509 | 696 | def test_private_archive(self): | 784 | def test_private_archive(self): |
510 | 697 | # A SnapBuild with a private archive is private. | 785 | # A SnapBuild with a private archive is private. |
511 | 698 | db_archive = self.factory.makeArchive(owner=self.person, private=True) | 786 | db_archive = self.factory.makeArchive(owner=self.person, private=True) |
512 | 699 | 787 | ||
513 | === modified file 'lib/lp/testing/factory.py' | |||
514 | --- lib/lp/testing/factory.py 2019-05-06 14:22:34 +0000 | |||
515 | +++ lib/lp/testing/factory.py 2019-05-28 16:37:20 +0000 | |||
516 | @@ -4800,7 +4800,7 @@ | |||
517 | 4800 | snapbuild = getUtility(ISnapBuildSet).new( | 4800 | snapbuild = getUtility(ISnapBuildSet).new( |
518 | 4801 | requester, snap, archive, distroarchseries, pocket, | 4801 | requester, snap, archive, distroarchseries, pocket, |
519 | 4802 | channels=channels, date_created=date_created, | 4802 | channels=channels, date_created=date_created, |
521 | 4803 | build_request=build_request) | 4803 | build_request=build_request, explicitly_private=snap.private) |
522 | 4804 | if duration is not None: | 4804 | if duration is not None: |
523 | 4805 | removeSecurityProxy(snapbuild).updateStatus( | 4805 | removeSecurityProxy(snapbuild).updateStatus( |
524 | 4806 | BuildStatus.BUILDING, builder=builder, | 4806 | BuildStatus.BUILDING, builder=builder, |
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 ...