Merge ~cjwatson/launchpad:issue-private-archive-macaroons into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: c7054367d530f8d2188f5330a9ebbf0163ce88fe
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:issue-private-archive-macaroons
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:get-sources-list-accept-behaviour
Diff against target: 402 lines (+147/-24)
10 files modified
lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py (+9/-0)
lib/lp/buildmaster/model/buildfarmjobbehaviour.py (+4/-0)
lib/lp/oci/model/ocirecipebuildbehaviour.py (+9/-5)
lib/lp/snappy/model/snapbuildbehaviour.py (+8/-4)
lib/lp/snappy/tests/test_snapbuildbehaviour.py (+55/-0)
lib/lp/soyuz/adapters/archivedependencies.py (+11/-8)
lib/lp/soyuz/model/binarypackagebuildbehaviour.py (+10/-0)
lib/lp/soyuz/model/livefsbuildbehaviour.py (+9/-0)
lib/lp/soyuz/tests/test_archive.py (+29/-7)
system-packages.txt (+3/-0)
Reviewer Review Type Date Requested Status
Ioana Lasc (community) Approve
Review via email: mp+401323@code.launchpad.net

Commit message

Dispatch sources.list entries for private archives using macaroons

Description of the change

This allows snap base archive dependencies on private archives to work (for 16.04 ESM), and also takes a significant step towards abolishing `Archive.buildd_secret`. These tokens are only valid while the build is running, and so are much less vulnerable to accidental leakage.

The old buildd secret is still used in the file map passed to builders to fetch the source package for binary package builds in private archives, so it can't yet be removed entirely.

To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py b/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py
2index 4d768d8..d210d64 100644
3--- a/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py
4+++ b/lib/lp/buildmaster/interfaces/buildfarmjobbehaviour.py
5@@ -46,6 +46,15 @@ class IBuildFarmJobBehaviour(Interface):
6 'password' (optional): password to authenticate with
7 """
8
9+ def issueMacaroon():
10+ """Issue a macaroon to access private resources for this build.
11+
12+ :raises NotImplementedError: if the build type does not support
13+ accessing private resources.
14+ :return: A Deferred that calls back with a serialized macaroon or a
15+ fault.
16+ """
17+
18 def extraBuildArgs(logger=None):
19 """Return extra arguments required by the builder for this build.
20
21diff --git a/lib/lp/buildmaster/model/buildfarmjobbehaviour.py b/lib/lp/buildmaster/model/buildfarmjobbehaviour.py
22index f8b5034..e0d5380 100644
23--- a/lib/lp/buildmaster/model/buildfarmjobbehaviour.py
24+++ b/lib/lp/buildmaster/model/buildfarmjobbehaviour.py
25@@ -89,6 +89,10 @@ class BuildFarmJobBehaviourBase:
26 """The default behaviour is to send no files."""
27 return {}
28
29+ def issueMacaroon(self):
30+ raise NotImplementedError(
31+ "This build type does not support accessing private resources.")
32+
33 def extraBuildArgs(self, logger=None):
34 """The default behaviour is to send only common extra arguments."""
35 args = {}
36diff --git a/lib/lp/oci/model/ocirecipebuildbehaviour.py b/lib/lp/oci/model/ocirecipebuildbehaviour.py
37index e8a60f5..0ca766d 100644
38--- a/lib/lp/oci/model/ocirecipebuildbehaviour.py
39+++ b/lib/lp/oci/model/ocirecipebuildbehaviour.py
40@@ -80,6 +80,14 @@ class OCIRecipeBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase):
41 raise CannotBuild(
42 "Missing chroot for %s" % build.distro_arch_series.displayname)
43
44+ def issueMacaroon(self):
45+ """See `IBuildFarmJobBehaviour`."""
46+ return cancel_on_timeout(
47+ self._authserver.callRemote(
48+ "issueMacaroon",
49+ "oci-recipe-build", "OCIRecipeBuild", self.build.id),
50+ config.builddmaster.authentication_timeout)
51+
52 def _getBuildInfoArgs(self):
53 def format_user(user):
54 if user is None:
55@@ -142,11 +150,7 @@ class OCIRecipeBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase):
56
57 if build.recipe.git_ref is not None:
58 if build.recipe.git_repository.private:
59- macaroon_raw = yield cancel_on_timeout(
60- self._authserver.callRemote(
61- "issueMacaroon",
62- "oci-recipe-build", "OCIRecipeBuild", build.id),
63- config.builddmaster.authentication_timeout)
64+ macaroon_raw = yield self.issueMacaroon()
65 url = build.recipe.git_repository.getCodebrowseUrl(
66 username=None, password=macaroon_raw)
67 args["git_repository"] = url
68diff --git a/lib/lp/snappy/model/snapbuildbehaviour.py b/lib/lp/snappy/model/snapbuildbehaviour.py
69index dda949b..0057670 100644
70--- a/lib/lp/snappy/model/snapbuildbehaviour.py
71+++ b/lib/lp/snappy/model/snapbuildbehaviour.py
72@@ -145,6 +145,13 @@ class SnapBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase):
73 raise CannotBuild(
74 "Missing chroot for %s" % build.distro_arch_series.displayname)
75
76+ def issueMacaroon(self):
77+ """See `IBuildFarmJobBehaviour`."""
78+ return cancel_on_timeout(
79+ self._authserver.callRemote(
80+ "issueMacaroon", "snap-build", "SnapBuild", self.build.id),
81+ config.builddmaster.authentication_timeout)
82+
83 @defer.inlineCallbacks
84 def extraBuildArgs(self, logger=None):
85 """
86@@ -186,10 +193,7 @@ class SnapBuildBehaviour(SnapProxyMixin, BuildFarmJobBehaviourBase):
87 if build.snap.git_ref.repository_url is not None:
88 args["git_repository"] = build.snap.git_ref.repository_url
89 elif build.snap.git_repository.private:
90- macaroon_raw = yield cancel_on_timeout(
91- self._authserver.callRemote(
92- "issueMacaroon", "snap-build", "SnapBuild", build.id),
93- config.builddmaster.authentication_timeout)
94+ macaroon_raw = yield self.issueMacaroon()
95 url = build.snap.git_repository.getCodebrowseUrl(
96 username=None, password=macaroon_raw)
97 args["git_repository"] = url
98diff --git a/lib/lp/snappy/tests/test_snapbuildbehaviour.py b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
99index 1d80326..0deebed 100644
100--- a/lib/lp/snappy/tests/test_snapbuildbehaviour.py
101+++ b/lib/lp/snappy/tests/test_snapbuildbehaviour.py
102@@ -14,6 +14,7 @@ from textwrap import dedent
103 import time
104 import uuid
105
106+from aptsources.sourceslist import SourceEntry
107 import fixtures
108 from pymacaroons import Macaroon
109 import pytz
110@@ -77,6 +78,7 @@ from lp.services.log.logger import (
111 BufferLogger,
112 DevNullLogger,
113 )
114+from lp.services.macaroons.testing import MacaroonVerifies
115 from lp.services.statsd.tests import StatsMixin
116 from lp.services.webapp import canonical_url
117 from lp.snappy.interfaces.snap import (
118@@ -790,6 +792,59 @@ class TestAsyncSnapBuildBehaviour(StatsMixin, TestSnapBuildBehaviourBase):
119 self.assertEqual(expected_archives, args["archives"])
120
121 @defer.inlineCallbacks
122+ def test_extraBuildArgs_snap_base_with_private_archive_dependencies(self):
123+ # If the build is using a snap base that has archive dependencies on
124+ # private PPAs, extraBuildArgs sends them.
125+ self.useFixture(InProcessAuthServerFixture())
126+ self.pushConfig(
127+ "launchpad", internal_macaroon_secret_key="some-secret")
128+ snap_base = self.factory.makeSnapBase()
129+ job = self.makeJob(snap_base=snap_base)
130+ dependency = self.factory.makeArchive(
131+ distribution=job.archive.distribution, private=True)
132+ snap_base.addArchiveDependency(
133+ dependency, PackagePublishingPocket.RELEASE,
134+ getUtility(IComponentSet)["main"])
135+ self.factory.makeBinaryPackagePublishingHistory(
136+ archive=dependency, distroarchseries=job.build.distro_arch_series,
137+ pocket=PackagePublishingPocket.RELEASE,
138+ status=PackagePublishingStatus.PUBLISHED)
139+ with dbuser(config.builddmaster.dbuser):
140+ args = yield job.extraBuildArgs()
141+ job.build.updateStatus(BuildStatus.BUILDING)
142+ self.assertThat(
143+ [SourceEntry(item) for item in args["archives"]],
144+ MatchesListwise([
145+ MatchesStructure(
146+ type=Equals("deb"),
147+ uri=AfterPreprocessing(urlsplit, MatchesStructure(
148+ scheme=Equals("http"),
149+ username=Equals("buildd"),
150+ password=MacaroonVerifies("snap-build", dependency),
151+ hostname=Equals("private-ppa.launchpad.test"),
152+ path=Equals("/%s/%s/%s" % (
153+ dependency.owner.name, dependency.name,
154+ dependency.distribution.name)))),
155+ dist=Equals(job.build.distro_series.name),
156+ comps=Equals(["main"])),
157+ MatchesStructure.byEquality(
158+ type="deb",
159+ uri=job.archive.archive_url,
160+ dist=job.build.distro_series.name,
161+ comps=["main", "universe"]),
162+ MatchesStructure.byEquality(
163+ type="deb",
164+ uri=job.archive.archive_url,
165+ dist="%s-security" % job.build.distro_series.name,
166+ comps=["main", "universe"]),
167+ MatchesStructure.byEquality(
168+ type="deb",
169+ uri=job.archive.archive_url,
170+ dist="%s-updates" % job.build.distro_series.name,
171+ comps=["main", "universe"]),
172+ ]))
173+
174+ @defer.inlineCallbacks
175 def test_extraBuildArgs_ppa_and_snap_base_with_archive_dependencies(self):
176 # If the build is using a PPA and a snap base that both have archive
177 # dependencies, extraBuildArgs sends them all.
178diff --git a/lib/lp/soyuz/adapters/archivedependencies.py b/lib/lp/soyuz/adapters/archivedependencies.py
179index ff1d117..685a82e 100644
180--- a/lib/lp/soyuz/adapters/archivedependencies.py
181+++ b/lib/lp/soyuz/adapters/archivedependencies.py
182@@ -290,7 +290,8 @@ def get_sources_list_for_building(behaviour, distroarchseries,
183 tools_source=tools_source, tools_fingerprint=tools_fingerprint,
184 logger=logger)
185 sources_list_lines, trusted_keys = (
186- yield _get_sources_list_for_dependencies(deps, logger=logger))
187+ yield _get_sources_list_for_dependencies(
188+ behaviour, deps, logger=logger))
189
190 external_dep_lines = []
191 # Append external sources.list lines for this build if specified. No
192@@ -343,7 +344,8 @@ def _has_published_binaries(archive, distroarchseries, pocket):
193 return not published_binaries.is_empty()
194
195
196-def _get_binary_sources_list_line(archive, distroarchseries, pocket,
197+@defer.inlineCallbacks
198+def _get_binary_sources_list_line(behaviour, archive, distroarchseries, pocket,
199 components):
200 """Return the correponding binary sources_list line."""
201 # Encode the private PPA repository password in the
202@@ -351,23 +353,24 @@ def _get_binary_sources_list_line(archive, distroarchseries, pocket,
203 # sanitized to not expose it.
204 if archive.private:
205 uri = URI(archive.archive_url)
206- uri = uri.replace(
207- userinfo="buildd:%s" % archive.buildd_secret)
208+ macaroon_raw = yield behaviour.issueMacaroon()
209+ uri = uri.replace(userinfo="buildd:%s" % macaroon_raw)
210 url = str(uri)
211 else:
212 url = archive.archive_url
213
214 suite = distroarchseries.distroseries.name + pocketsuffix[pocket]
215- return 'deb %s %s %s' % (url, suite, ' '.join(components))
216+ defer.returnValue('deb %s %s %s' % (url, suite, ' '.join(components)))
217
218
219 @defer.inlineCallbacks
220-def _get_sources_list_for_dependencies(dependencies, logger=None):
221+def _get_sources_list_for_dependencies(behaviour, dependencies, logger=None):
222 """Return sources.list entries and keys.
223
224 Process the given list of dependency tuples for the given
225 `DistroArchseries`.
226
227+ :param behaviour: the build's `IBuildFarmJobBehaviour`.
228 :param dependencies: list of 3 elements tuples as:
229 (`IArchive`, `IDistroArchSeries`, `PackagePublishingPocket`,
230 list of `IComponent` names)
231@@ -399,8 +402,8 @@ def _get_sources_list_for_dependencies(dependencies, logger=None):
232 components = [
233 component for component in components
234 if component in archive_components]
235- sources_list_line = _get_binary_sources_list_line(
236- archive, distro_arch_series, pocket, components)
237+ sources_list_line = yield _get_binary_sources_list_line(
238+ behaviour, archive, distro_arch_series, pocket, components)
239 sources_list_lines.append(sources_list_line)
240 fingerprint = archive.signing_key_fingerprint
241 if fingerprint is not None and fingerprint not in trusted_keys:
242diff --git a/lib/lp/soyuz/model/binarypackagebuildbehaviour.py b/lib/lp/soyuz/model/binarypackagebuildbehaviour.py
243index 6b74f46..2876ac0 100644
244--- a/lib/lp/soyuz/model/binarypackagebuildbehaviour.py
245+++ b/lib/lp/soyuz/model/binarypackagebuildbehaviour.py
246@@ -22,6 +22,8 @@ from lp.buildmaster.model.buildfarmjobbehaviour import (
247 BuildFarmJobBehaviourBase,
248 )
249 from lp.registry.interfaces.pocket import PackagePublishingPocket
250+from lp.services.config import config
251+from lp.services.twistedsupport import cancel_on_timeout
252 from lp.services.webapp import urlappend
253 from lp.soyuz.adapters.archivedependencies import (
254 get_primary_current_component,
255@@ -131,6 +133,14 @@ class BinaryPackageBuildBehaviour(BuildFarmJobBehaviourBase):
256 (build.title, build.id, build.pocket.name,
257 build.distro_series.name))
258
259+ def issueMacaroon(self):
260+ """See `IBuildFarmJobBehaviour`."""
261+ return cancel_on_timeout(
262+ self._authserver.callRemote(
263+ "issueMacaroon", "binary-package-build", "BinaryPackageBuild",
264+ self.build.id),
265+ config.builddmaster.authentication_timeout)
266+
267 @defer.inlineCallbacks
268 def extraBuildArgs(self, logger=None):
269 """
270diff --git a/lib/lp/soyuz/model/livefsbuildbehaviour.py b/lib/lp/soyuz/model/livefsbuildbehaviour.py
271index c439559..0f93d39 100644
272--- a/lib/lp/soyuz/model/livefsbuildbehaviour.py
273+++ b/lib/lp/soyuz/model/livefsbuildbehaviour.py
274@@ -25,6 +25,8 @@ from lp.buildmaster.model.buildfarmjobbehaviour import (
275 BuildFarmJobBehaviourBase,
276 )
277 from lp.registry.interfaces.series import SeriesStatus
278+from lp.services.config import config
279+from lp.services.twistedsupport import cancel_on_timeout
280 from lp.soyuz.adapters.archivedependencies import (
281 get_sources_list_for_building,
282 )
283@@ -78,6 +80,13 @@ class LiveFSBuildBehaviour(BuildFarmJobBehaviourBase):
284 raise CannotBuild(
285 "Missing chroot for %s" % build.distro_arch_series.displayname)
286
287+ def issueMacaroon(self):
288+ """See `IBuildFarmJobBehaviour`."""
289+ return cancel_on_timeout(
290+ self._authserver.callRemote(
291+ "issueMacaroon", "livefs-build", "LiveFSBuild", self.build.id),
292+ config.builddmaster.authentication_timeout)
293+
294 @defer.inlineCallbacks
295 def extraBuildArgs(self, logger=None):
296 """
297diff --git a/lib/lp/soyuz/tests/test_archive.py b/lib/lp/soyuz/tests/test_archive.py
298index a49ccd3..2d53207 100644
299--- a/lib/lp/soyuz/tests/test_archive.py
300+++ b/lib/lp/soyuz/tests/test_archive.py
301@@ -13,22 +13,28 @@ from datetime import (
302 import doctest
303 import os.path
304
305+from aptsources.sourceslist import SourceEntry
306 from pytz import UTC
307 import responses
308 import six
309 from six.moves import http_client
310+from six.moves.urllib.parse import urlsplit
311 from storm.store import Store
312 from testtools.matchers import (
313+ AfterPreprocessing,
314 AllMatch,
315 DocTestMatches,
316+ Equals,
317 LessThan,
318 MatchesListwise,
319 MatchesPredicate,
320- MatchesRegex,
321 MatchesStructure,
322 )
323 from testtools.testcase import ExpectedException
324-from testtools.twistedsupport import AsynchronousDeferredRunTest
325+from testtools.twistedsupport import (
326+ AsynchronousDeferredRunTest,
327+ AsynchronousDeferredRunTestForBrokenTwisted,
328+ )
329 import transaction
330 from twisted.internet import defer
331 from zope.component import getUtility
332@@ -57,6 +63,7 @@ from lp.registry.interfaces.person import IPersonSet
333 from lp.registry.interfaces.pocket import PackagePublishingPocket
334 from lp.registry.interfaces.series import SeriesStatus
335 from lp.registry.interfaces.teammembership import TeamMembershipStatus
336+from lp.services.authserver.testing import InProcessAuthServerFixture
337 from lp.services.database.interfaces import IStore
338 from lp.services.database.sqlbase import sqlvalues
339 from lp.services.features import getFeatureFlag
340@@ -67,6 +74,7 @@ from lp.services.gpg.interfaces import (
341 IGPGHandler,
342 )
343 from lp.services.job.interfaces.job import JobStatus
344+from lp.services.macaroons.testing import MacaroonVerifies
345 from lp.services.propertycache import (
346 clear_property_cache,
347 get_property_cache,
348@@ -1866,11 +1874,15 @@ class TestAddArchiveDependencies(TestCaseWithFactory):
349 class TestArchiveDependencies(TestCaseWithFactory):
350
351 layer = LaunchpadZopelessLayer
352- run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
353+ run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
354+ timeout=30)
355
356 @defer.inlineCallbacks
357 def test_private_sources_list(self):
358 """Entries for private dependencies include credentials."""
359+ self.useFixture(InProcessAuthServerFixture())
360+ self.pushConfig(
361+ "launchpad", internal_macaroon_secret_key="some-secret")
362 p3a = self.factory.makeArchive(name='p3a', private=True)
363 yield self.useFixture(InProcessKeyServerFixture()).start()
364 key_path = os.path.join(gpgkeysdir, 'ppa-sample@canonical.com.sec')
365@@ -1890,10 +1902,20 @@ class TestArchiveDependencies(TestCaseWithFactory):
366 sources_list, trusted_keys = yield get_sources_list_for_building(
367 behaviour, build.distro_arch_series,
368 build.source_package_release.name)
369- matches = MatchesRegex(
370- "deb http://buildd:sekrit@private-ppa.launchpad.test/"
371- "person-name-.*/dependency/ubuntu distroseries-.* main")
372- self.assertThat(sources_list[0], matches)
373+ # Mark the build as building so that we can verify its macaroons.
374+ build.updateStatus(BuildStatus.BUILDING)
375+ self.assertThat(SourceEntry(sources_list[0]), MatchesStructure(
376+ type=Equals("deb"),
377+ uri=AfterPreprocessing(urlsplit, MatchesStructure(
378+ scheme=Equals("http"),
379+ username=Equals("buildd"),
380+ password=MacaroonVerifies("binary-package-build", p3a),
381+ hostname=Equals("private-ppa.launchpad.test"),
382+ path=Equals("/%s/dependency/ubuntu" % p3a.owner.name),
383+ )),
384+ dist=Equals(build.distro_series.getSuite(build.pocket)),
385+ comps=Equals(["main"]),
386+ ))
387 self.assertThat(trusted_keys, MatchesListwise([
388 Base64KeyMatches("0D57E99656BEFB0897606EE9A022DD1F5001B46D"),
389 ]))
390diff --git a/system-packages.txt b/system-packages.txt
391index 84a8548..56a1291 100644
392--- a/system-packages.txt
393+++ b/system-packages.txt
394@@ -19,6 +19,9 @@ apt
395 apt_inst
396 apt_pkg
397
398+# Used by tests to parse sources.list entries.
399+aptsources
400+
401 # utilities/js-deps
402 convoy
403

Subscribers

People subscribed via source and target branches

to status/vote changes: