Merge lp:~cjwatson/launchpad/refactor-macaroon-testing into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18946
Proposed branch: lp:~cjwatson/launchpad/refactor-macaroon-testing
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/authserver-issue-macaroon
Diff against target: 429 lines (+140/-37)
7 files modified
lib/lp/code/model/codeimportjob.py (+4/-2)
lib/lp/code/model/tests/test_codeimportjob.py (+29/-14)
lib/lp/services/macaroons/interfaces.py (+3/-1)
lib/lp/services/macaroons/model.py (+24/-5)
lib/lp/services/macaroons/testing.py (+40/-0)
lib/lp/snappy/tests/test_snapbuild.py (+19/-8)
lib/lp/soyuz/tests/test_binarypackagebuild.py (+21/-7)
To merge this branch: bzr merge lp:~cjwatson/launchpad/refactor-macaroon-testing
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+365859@code.launchpad.net

Commit message

Add logging to macaroon verification, and refactor tests to check it.

Description of the change

It was all too easy for macaroon verification to fail for some reason other than the one that a test expected, and tests had no way to tell; indeed, I ran into and fixed exactly such a case while working on this branch. We now log details of verification failures, and I added a couple of test helpers to make it easy for tests to check these consistently.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

I wonder if it's better to refactor this to return a list of violations rather than directly logging them and returning False. Some callsites can then log without context if we really want, but the tests can do it less evilly.

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

I thought actually returning a list of violations was a bit too confusing (because it would mean inverting the sense of boolean tests of verifyMacaroon), but passing a list as an optional argument which verifyMacaroon appends to seems not too horrible and works fine, so I've done that.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/model/codeimportjob.py'
2--- lib/lp/code/model/codeimportjob.py 2019-04-24 12:50:20 +0000
3+++ lib/lp/code/model/codeimportjob.py 2019-04-25 22:19:14 +0000
4@@ -434,9 +434,11 @@
5
6 def checkVerificationContext(self, context):
7 """See `MacaroonIssuerBase`."""
8- if (not ICodeImportJob.providedBy(context) or
9- context.state != CodeImportJobState.RUNNING):
10+ if not ICodeImportJob.providedBy(context):
11 raise BadMacaroonContext(context)
12+ if context.state != CodeImportJobState.RUNNING:
13+ raise BadMacaroonContext(
14+ context, "%r is not in the RUNNING state." % context)
15 return context
16
17 def verifyPrimaryCaveat(self, caveat_value, context):
18
19=== modified file 'lib/lp/code/model/tests/test_codeimportjob.py'
20--- lib/lp/code/model/tests/test_codeimportjob.py 2019-04-25 09:34:40 +0000
21+++ lib/lp/code/model/tests/test_codeimportjob.py 2019-04-25 22:19:14 +0000
22@@ -65,6 +65,7 @@
23 BadMacaroonContext,
24 IMacaroonIssuer,
25 )
26+from lp.services.macaroons.testing import MacaroonTestMixin
27 from lp.services.webapp import canonical_url
28 from lp.testing import (
29 ANONYMOUS,
30@@ -1271,7 +1272,7 @@
31 get_feedback_messages(user_browser.contents))
32
33
34-class TestCodeImportJobMacaroonIssuer(TestCaseWithFactory):
35+class TestCodeImportJobMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
36 """Test CodeImportJob macaroon issuing and verification."""
37
38 layer = DatabaseFunctionalLayer
39@@ -1324,7 +1325,7 @@
40 issuer = getUtility(IMacaroonIssuer, "code-import-job")
41 getUtility(ICodeImportJobWorkflow).startJob(job, machine)
42 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
43- self.assertTrue(issuer.verifyMacaroon(macaroon, job))
44+ self.assertMacaroonVerifies(issuer, macaroon, job)
45
46 def test_verifyMacaroon_good_no_context(self):
47 machine = self.factory.makeCodeImportMachine(set_online=True)
48@@ -1332,8 +1333,8 @@
49 issuer = getUtility(IMacaroonIssuer, "code-import-job")
50 getUtility(ICodeImportJobWorkflow).startJob(job, machine)
51 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
52- self.assertTrue(
53- issuer.verifyMacaroon(macaroon, None, require_context=False))
54+ self.assertMacaroonVerifies(
55+ issuer, macaroon, job, require_context=False)
56
57 def test_verifyMacaroon_no_context_but_require_context(self):
58 machine = self.factory.makeCodeImportMachine(set_online=True)
59@@ -1341,7 +1342,9 @@
60 issuer = getUtility(IMacaroonIssuer, "code-import-job")
61 getUtility(ICodeImportJobWorkflow).startJob(job, machine)
62 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
63- self.assertFalse(issuer.verifyMacaroon(macaroon, None))
64+ self.assertMacaroonDoesNotVerify(
65+ ["Expected macaroon verification context but got None."],
66+ issuer, macaroon, None)
67
68 def test_verifyMacaroon_wrong_location(self):
69 machine = self.factory.makeCodeImportMachine(set_online=True)
70@@ -1351,9 +1354,12 @@
71 macaroon = Macaroon(
72 location="another-location",
73 key=removeSecurityProxy(issuer)._root_secret)
74- self.assertFalse(issuer.verifyMacaroon(macaroon, job))
75- self.assertFalse(
76- issuer.verifyMacaroon(macaroon, None, require_context=False))
77+ self.assertMacaroonDoesNotVerify(
78+ ["Macaroon has unknown location 'another-location'."],
79+ issuer, macaroon, job)
80+ self.assertMacaroonDoesNotVerify(
81+ ["Macaroon has unknown location 'another-location'."],
82+ issuer, macaroon, job, require_context=False)
83
84 def test_verifyMacaroon_wrong_key(self):
85 machine = self.factory.makeCodeImportMachine(set_online=True)
86@@ -1362,19 +1368,28 @@
87 getUtility(ICodeImportJobWorkflow).startJob(job, machine)
88 macaroon = Macaroon(
89 location=config.vhost.mainsite.hostname, key="another-secret")
90- self.assertFalse(issuer.verifyMacaroon(macaroon, job))
91- self.assertFalse(
92- issuer.verifyMacaroon(macaroon, None, require_context=False))
93+ self.assertMacaroonDoesNotVerify(
94+ ["Signatures do not match"], issuer, macaroon, job)
95+ self.assertMacaroonDoesNotVerify(
96+ ["Signatures do not match"],
97+ issuer, macaroon, job, require_context=False)
98
99 def test_verifyMacaroon_not_running(self):
100 job = self.makeJob()
101 issuer = getUtility(IMacaroonIssuer, "code-import-job")
102 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
103- self.assertFalse(issuer.verifyMacaroon(macaroon, job))
104+ self.assertMacaroonDoesNotVerify(
105+ ["%r is not in the RUNNING state." % job],
106+ issuer, macaroon, job)
107
108 def test_verifyMacaroon_wrong_job(self):
109+ machine = self.factory.makeCodeImportMachine(set_online=True)
110 job = self.makeJob()
111- other_job = self.factory.makeCodeImportJob(code_import=job.code_import)
112+ other_job = self.makeJob()
113 issuer = getUtility(IMacaroonIssuer, "code-import-job")
114+ getUtility(ICodeImportJobWorkflow).startJob(job, machine)
115 macaroon = removeSecurityProxy(issuer).issueMacaroon(other_job)
116- self.assertFalse(issuer.verifyMacaroon(macaroon, job))
117+ self.assertMacaroonDoesNotVerify(
118+ ["Caveat check for 'lp.code-import-job %s' failed." %
119+ other_job.id],
120+ issuer, macaroon, job)
121
122=== modified file 'lib/lp/services/macaroons/interfaces.py'
123--- lib/lp/services/macaroons/interfaces.py 2019-04-25 09:55:35 +0000
124+++ lib/lp/services/macaroons/interfaces.py 2019-04-25 22:19:14 +0000
125@@ -31,7 +31,7 @@
126 issuable_via_authserver = Bool(
127 "Does this issuer allow issuing macaroons via the authserver?")
128
129- def verifyMacaroon(macaroon, context, require_context=True):
130+ def verifyMacaroon(macaroon, context, require_context=True, errors=None):
131 """Verify that `macaroon` is valid for `context`.
132
133 :param macaroon: A `Macaroon`.
134@@ -41,6 +41,8 @@
135 verify that the macaroon could be valid for some context. Use
136 this in the authentication part of an
137 authentication/authorisation API.
138+ :param errors: If non-None, any verification error messages will be
139+ appended to this list.
140 :return: True if `macaroon` is valid for `context`, otherwise False.
141 """
142
143
144=== modified file 'lib/lp/services/macaroons/model.py'
145--- lib/lp/services/macaroons/model.py 2019-04-24 16:46:31 +0000
146+++ lib/lp/services/macaroons/model.py 2019-04-25 22:19:14 +0000
147@@ -95,22 +95,33 @@
148 """
149 raise NotImplementedError
150
151- def verifyMacaroon(self, macaroon, context, require_context=True):
152+ def verifyMacaroon(self, macaroon, context, require_context=True,
153+ errors=None):
154 """See `IMacaroonIssuer`."""
155 if macaroon.location != config.vhost.mainsite.hostname:
156+ if errors is not None:
157+ errors.append(
158+ "Macaroon has unknown location '%s'." % macaroon.location)
159 return False
160 if require_context and context is None:
161+ if errors is not None:
162+ errors.append(
163+ "Expected macaroon verification context but got None.")
164 return False
165 if context is not None:
166 try:
167 context = self.checkVerificationContext(context)
168- except BadMacaroonContext:
169+ except BadMacaroonContext as e:
170+ if errors is not None:
171+ errors.append(str(e))
172 return False
173
174 def verify(caveat):
175 try:
176 caveat_name, caveat_value = caveat.split(" ", 1)
177 except ValueError:
178+ if errors is not None:
179+ errors.append("Cannot parse caveat '%s'." % caveat)
180 return False
181 if caveat_name == self._primary_caveat_name:
182 checker = self.verifyPrimaryCaveat
183@@ -118,8 +129,14 @@
184 # XXX cjwatson 2019-04-09: For now we just fail closed if
185 # there are any other caveats, which is good enough for
186 # internal use.
187- return False
188- return checker(caveat_value, context)
189+ if errors is not None:
190+ errors.append("Unhandled caveat name '%s'." % caveat_name)
191+ return False
192+ if not checker(caveat_value, context):
193+ if errors is not None:
194+ errors.append("Caveat check for '%s' failed." % caveat)
195+ return False
196+ return True
197
198 try:
199 verifier = Verifier()
200@@ -130,5 +147,7 @@
201 # but most of them are too broad to reasonably catch so we let them
202 # turn into OOPSes for now. Revisit this once
203 # https://github.com/ecordell/pymacaroons/issues/51 is fixed.
204- except MacaroonVerificationFailedException:
205+ except MacaroonVerificationFailedException as e:
206+ if errors is not None and not errors:
207+ errors.append(str(e))
208 return False
209
210=== added file 'lib/lp/services/macaroons/testing.py'
211--- lib/lp/services/macaroons/testing.py 1970-01-01 00:00:00 +0000
212+++ lib/lp/services/macaroons/testing.py 2019-04-25 22:19:14 +0000
213@@ -0,0 +1,40 @@
214+# Copyright 2019 Canonical Ltd. This software is licensed under the
215+# GNU Affero General Public License version 3 (see the file LICENSE).
216+
217+"""Macaroon testing helpers."""
218+
219+from __future__ import absolute_import, print_function, unicode_literals
220+
221+__metaclass__ = type
222+__all__ = [
223+ 'find_caveats_by_name',
224+ 'MacaroonTestMixin',
225+ ]
226+
227+from testtools.content import text_content
228+
229+
230+def find_caveats_by_name(macaroon, caveat_name):
231+ return [
232+ caveat for caveat in macaroon.caveats
233+ if caveat.caveat_id.startswith(caveat_name + " ")]
234+
235+
236+class MacaroonTestMixin:
237+
238+ def assertMacaroonVerifies(self, issuer, macaroon, context, **kwargs):
239+ errors = []
240+ try:
241+ self.assertTrue(issuer.verifyMacaroon(
242+ macaroon, context, errors=errors, **kwargs))
243+ except Exception:
244+ if errors:
245+ self.addDetail("errors", text_content("\n".join(errors)))
246+ raise
247+
248+ def assertMacaroonDoesNotVerify(self, expected_errors, issuer, macaroon,
249+ context, **kwargs):
250+ errors = []
251+ self.assertFalse(issuer.verifyMacaroon(
252+ macaroon, context, errors=errors, **kwargs))
253+ self.assertEqual(expected_errors, errors)
254
255=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
256--- lib/lp/snappy/tests/test_snapbuild.py 2019-04-25 09:34:40 +0000
257+++ lib/lp/snappy/tests/test_snapbuild.py 2019-04-25 22:19:14 +0000
258@@ -46,6 +46,7 @@
259 BadMacaroonContext,
260 IMacaroonIssuer,
261 )
262+from lp.services.macaroons.testing import MacaroonTestMixin
263 from lp.services.propertycache import clear_property_cache
264 from lp.services.webapp.interfaces import OAuthPermission
265 from lp.services.webapp.publisher import canonical_url
266@@ -785,7 +786,7 @@
267 self.assertCanOpenRedirectedUrl(browser, file_url)
268
269
270-class TestSnapBuildMacaroonIssuer(TestCaseWithFactory):
271+class TestSnapBuildMacaroonIssuer(MacaroonTestMixin, TestCaseWithFactory):
272 """Test SnapBuild macaroon issuing and verification."""
273
274 layer = LaunchpadZopelessLayer
275@@ -840,7 +841,7 @@
276 issuer = removeSecurityProxy(
277 getUtility(IMacaroonIssuer, "snap-build"))
278 macaroon = issuer.issueMacaroon(build)
279- self.assertTrue(issuer.verifyMacaroon(macaroon, ref.repository))
280+ self.assertMacaroonVerifies(issuer, macaroon, ref.repository)
281
282 def test_verifyMacaroon_wrong_location(self):
283 [ref] = self.factory.makeGitRefs(
284@@ -852,7 +853,9 @@
285 getUtility(IMacaroonIssuer, "snap-build"))
286 macaroon = Macaroon(
287 location="another-location", key=issuer._root_secret)
288- self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository))
289+ self.assertMacaroonDoesNotVerify(
290+ ["Macaroon has unknown location 'another-location'."],
291+ issuer, macaroon, ref.repository)
292
293 def test_verifyMacaroon_wrong_key(self):
294 [ref] = self.factory.makeGitRefs(
295@@ -864,7 +867,8 @@
296 getUtility(IMacaroonIssuer, "snap-build"))
297 macaroon = Macaroon(
298 location=config.vhost.mainsite.hostname, key="another-secret")
299- self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository))
300+ self.assertMacaroonDoesNotVerify(
301+ ["Signatures do not match"], issuer, macaroon, ref.repository)
302
303 def test_verifyMacaroon_refuses_branch(self):
304 branch = self.factory.makeAnyBranch(
305@@ -875,7 +879,8 @@
306 issuer = removeSecurityProxy(
307 getUtility(IMacaroonIssuer, "snap-build"))
308 macaroon = issuer.issueMacaroon(build)
309- self.assertFalse(issuer.verifyMacaroon(macaroon, branch))
310+ self.assertMacaroonDoesNotVerify(
311+ ["Cannot handle context %r." % branch], issuer, macaroon, branch)
312
313 def test_verifyMacaroon_not_building(self):
314 [ref] = self.factory.makeGitRefs(
315@@ -885,7 +890,9 @@
316 issuer = removeSecurityProxy(
317 getUtility(IMacaroonIssuer, "snap-build"))
318 macaroon = issuer.issueMacaroon(build)
319- self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository))
320+ self.assertMacaroonDoesNotVerify(
321+ ["Caveat check for 'lp.snap-build %s' failed." % build.id],
322+ issuer, macaroon, ref.repository)
323
324 def test_verifyMacaroon_wrong_build(self):
325 [ref] = self.factory.makeGitRefs(
326@@ -899,7 +906,9 @@
327 issuer = removeSecurityProxy(
328 getUtility(IMacaroonIssuer, "snap-build"))
329 macaroon = issuer.issueMacaroon(other_build)
330- self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository))
331+ self.assertMacaroonDoesNotVerify(
332+ ["Caveat check for 'lp.snap-build %s' failed." % other_build.id],
333+ issuer, macaroon, ref.repository)
334
335 def test_verifyMacaroon_wrong_repository(self):
336 [ref] = self.factory.makeGitRefs(
337@@ -911,4 +920,6 @@
338 issuer = removeSecurityProxy(
339 getUtility(IMacaroonIssuer, "snap-build"))
340 macaroon = issuer.issueMacaroon(build)
341- self.assertFalse(issuer.verifyMacaroon(macaroon, other_repository))
342+ self.assertMacaroonDoesNotVerify(
343+ ["Caveat check for 'lp.snap-build %s' failed." % build.id],
344+ issuer, macaroon, other_repository)
345
346=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuild.py'
347--- lib/lp/soyuz/tests/test_binarypackagebuild.py 2019-04-25 09:34:40 +0000
348+++ lib/lp/soyuz/tests/test_binarypackagebuild.py 2019-04-25 22:19:14 +0000
349@@ -35,6 +35,7 @@
350 BadMacaroonContext,
351 IMacaroonIssuer,
352 )
353+from lp.services.macaroons.testing import MacaroonTestMixin
354 from lp.services.webapp.interaction import ANONYMOUS
355 from lp.services.webapp.interfaces import OAuthPermission
356 from lp.soyuz.enums import (
357@@ -913,7 +914,8 @@
358 archive, getUtility(ILaunchpadCelebrities).ppa_admin)
359
360
361-class TestBinaryPackageBuildMacaroonIssuer(TestCaseWithFactory):
362+class TestBinaryPackageBuildMacaroonIssuer(
363+ MacaroonTestMixin, TestCaseWithFactory):
364 """Test BinaryPackageBuild macaroon issuing and verification."""
365
366 layer = LaunchpadZopelessLayer
367@@ -961,7 +963,7 @@
368 issuer = removeSecurityProxy(
369 getUtility(IMacaroonIssuer, "binary-package-build"))
370 macaroon = issuer.issueMacaroon(build)
371- self.assertTrue(issuer.verifyMacaroon(macaroon, sprf.libraryfile))
372+ self.assertMacaroonVerifies(issuer, macaroon, sprf.libraryfile)
373
374 def test_verifyMacaroon_wrong_location(self):
375 build = self.factory.makeBinaryPackageBuild(
376@@ -973,7 +975,9 @@
377 getUtility(IMacaroonIssuer, "binary-package-build"))
378 macaroon = Macaroon(
379 location="another-location", key=issuer._root_secret)
380- self.assertFalse(issuer.verifyMacaroon(macaroon, sprf.libraryfile))
381+ self.assertMacaroonDoesNotVerify(
382+ ["Macaroon has unknown location 'another-location'."],
383+ issuer, macaroon, sprf.libraryfile)
384
385 def test_verifyMacaroon_wrong_key(self):
386 build = self.factory.makeBinaryPackageBuild(
387@@ -985,7 +989,8 @@
388 getUtility(IMacaroonIssuer, "binary-package-build"))
389 macaroon = Macaroon(
390 location=config.vhost.mainsite.hostname, key="another-secret")
391- self.assertFalse(issuer.verifyMacaroon(macaroon, sprf.libraryfile))
392+ self.assertMacaroonDoesNotVerify(
393+ ["Signatures do not match"], issuer, macaroon, sprf.libraryfile)
394
395 def test_verifyMacaroon_not_building(self):
396 build = self.factory.makeBinaryPackageBuild(
397@@ -995,7 +1000,10 @@
398 issuer = removeSecurityProxy(
399 getUtility(IMacaroonIssuer, "binary-package-build"))
400 macaroon = issuer.issueMacaroon(build)
401- self.assertFalse(issuer.verifyMacaroon(macaroon, sprf.libraryfile))
402+ self.assertMacaroonDoesNotVerify(
403+ ["Caveat check for 'lp.principal.binary-package-build %s' "
404+ "failed." % build.id],
405+ issuer, macaroon, sprf.libraryfile)
406
407 def test_verifyMacaroon_wrong_build(self):
408 build = self.factory.makeBinaryPackageBuild(
409@@ -1009,7 +1017,10 @@
410 issuer = removeSecurityProxy(
411 getUtility(IMacaroonIssuer, "binary-package-build"))
412 macaroon = issuer.issueMacaroon(other_build)
413- self.assertFalse(issuer.verifyMacaroon(macaroon, sprf.libraryfile))
414+ self.assertMacaroonDoesNotVerify(
415+ ["Caveat check for 'lp.principal.binary-package-build %s' "
416+ "failed." % other_build.id],
417+ issuer, macaroon, sprf.libraryfile)
418
419 def test_verifyMacaroon_wrong_file(self):
420 build = self.factory.makeBinaryPackageBuild(
421@@ -1021,4 +1032,7 @@
422 issuer = removeSecurityProxy(
423 getUtility(IMacaroonIssuer, "binary-package-build"))
424 macaroon = issuer.issueMacaroon(build)
425- self.assertFalse(issuer.verifyMacaroon(macaroon, lfa))
426+ self.assertMacaroonDoesNotVerify(
427+ ["Caveat check for 'lp.principal.binary-package-build %s' "
428+ "failed." % build.id],
429+ issuer, macaroon, lfa)