Merge lp:~cjwatson/launchpad/authserver-issue-macaroon into lp:launchpad
- authserver-issue-macaroon
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 18943 | ||||
Proposed branch: | lp:~cjwatson/launchpad/authserver-issue-macaroon | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/snap-build-macaroon | ||||
Diff against target: |
479 lines (+176/-28) 9 files modified
lib/lp/code/model/tests/test_codeimportjob.py (+12/-0) lib/lp/services/authserver/interfaces.py (+17/-2) lib/lp/services/authserver/tests/test_authserver.py (+49/-10) lib/lp/services/authserver/xmlrpc.py (+56/-9) lib/lp/services/macaroons/interfaces.py (+5/-1) lib/lp/services/macaroons/model.py (+2/-0) lib/lp/snappy/model/snapbuild.py (+3/-6) lib/lp/snappy/tests/test_snapbuild.py (+18/-0) lib/lp/soyuz/tests/test_binarypackagebuild.py (+14/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/authserver-issue-macaroon | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+364353@code.launchpad.net |
Commit message
Add an authserver method to issue macaroons (currently only snap-build).
Description of the change
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/code/model/tests/test_codeimportjob.py' |
2 | --- lib/lp/code/model/tests/test_codeimportjob.py 2019-04-24 12:50:20 +0000 |
3 | +++ lib/lp/code/model/tests/test_codeimportjob.py 2019-04-25 09:56:06 +0000 |
4 | @@ -25,6 +25,7 @@ |
5 | ) |
6 | import transaction |
7 | from zope.component import getUtility |
8 | +from zope.publisher.xmlrpc import TestRequest |
9 | from zope.security.proxy import removeSecurityProxy |
10 | |
11 | from lp.app.enums import InformationType |
12 | @@ -53,6 +54,7 @@ |
13 | make_running_import, |
14 | ) |
15 | from lp.code.tests.helpers import GitHostingFixture |
16 | +from lp.services.authserver.xmlrpc import AuthServerAPIView |
17 | from lp.services.config import config |
18 | from lp.services.database.constants import UTC_NOW |
19 | from lp.services.database.interfaces import IStore |
20 | @@ -78,6 +80,8 @@ |
21 | LaunchpadFunctionalLayer, |
22 | ) |
23 | from lp.testing.pages import get_feedback_messages |
24 | +from lp.xmlrpc import faults |
25 | +from lp.xmlrpc.interfaces import IPrivateApplication |
26 | |
27 | |
28 | def login_for_code_imports(): |
29 | @@ -1306,6 +1310,14 @@ |
30 | self.pushConfig("codeimport", macaroon_secret_key="some-secret") |
31 | self.test_issueMacaroon_good() |
32 | |
33 | + def test_issueMacaroon_not_via_authserver(self): |
34 | + job = self.makeJob() |
35 | + private_root = getUtility(IPrivateApplication) |
36 | + authserver = AuthServerAPIView(private_root.authserver, TestRequest()) |
37 | + self.assertEqual( |
38 | + faults.PermissionDenied(), |
39 | + authserver.issueMacaroon("code-import-job", "CodeImportJob", job)) |
40 | + |
41 | def test_verifyMacaroon_good(self): |
42 | machine = self.factory.makeCodeImportMachine(set_online=True) |
43 | job = self.makeJob() |
44 | |
45 | === modified file 'lib/lp/services/authserver/interfaces.py' |
46 | --- lib/lp/services/authserver/interfaces.py 2019-04-23 13:50:35 +0000 |
47 | +++ lib/lp/services/authserver/interfaces.py 2019-04-25 09:56:06 +0000 |
48 | @@ -1,4 +1,4 @@ |
49 | -# Copyright 2009-2018 Canonical Ltd. This software is licensed under the |
50 | +# Copyright 2009-2019 Canonical Ltd. This software is licensed under the |
51 | # GNU Affero General Public License version 3 (see the file LICENSE). |
52 | |
53 | """Interface for the XML-RPC authentication server.""" |
54 | @@ -27,12 +27,27 @@ |
55 | person with the given name. |
56 | """ |
57 | |
58 | + def issueMacaroon(issuer_name, context_type, context): |
59 | + """Issue a macaroon of type `issuer_name` for `context`. |
60 | + |
61 | + :param issuer_name: An `IMacaroonIssuer` name. Only issuers where |
62 | + `issuable_via_authserver` is True are permitted. |
63 | + :param context_type: A string identifying the type of context for |
64 | + which to issue the macaroon. Currently only 'LibraryFileAlias' |
65 | + and 'SnapBuild' are supported. |
66 | + :param context: The context for which to issue the macaroon. Note |
67 | + that this is passed over XML-RPC, so it should be plain data |
68 | + (e.g. an ID) rather than a database object. |
69 | + :return: A serialised macaroon or a fault. |
70 | + """ |
71 | + |
72 | def verifyMacaroon(macaroon_raw, context_type, context): |
73 | """Verify that `macaroon_raw` grants access to `context`. |
74 | |
75 | :param macaroon_raw: A serialised macaroon. |
76 | :param context_type: A string identifying the type of context to |
77 | - check. Currently only 'LibraryFileAlias' is supported. |
78 | + check. Currently only 'LibraryFileAlias' and 'SnapBuild' are |
79 | + supported. |
80 | :param context: The context to check. Note that this is passed over |
81 | XML-RPC, so it should be plain data (e.g. an ID) rather than a |
82 | database object. |
83 | |
84 | === modified file 'lib/lp/services/authserver/tests/test_authserver.py' |
85 | --- lib/lp/services/authserver/tests/test_authserver.py 2019-04-24 12:50:20 +0000 |
86 | +++ lib/lp/services/authserver/tests/test_authserver.py 2019-04-25 09:56:06 +0000 |
87 | @@ -7,7 +7,12 @@ |
88 | |
89 | from pymacaroons import Macaroon |
90 | from storm.sqlobject import SQLObjectNotFound |
91 | -from testtools.matchers import Is |
92 | +from testtools.matchers import ( |
93 | + Equals, |
94 | + Is, |
95 | + MatchesListwise, |
96 | + MatchesStructure, |
97 | + ) |
98 | from zope.component import getUtility |
99 | from zope.interface import implementer |
100 | from zope.publisher.xmlrpc import TestRequest |
101 | @@ -25,7 +30,6 @@ |
102 | from lp.services.macaroons.model import MacaroonIssuerBase |
103 | from lp.testing import ( |
104 | person_logged_in, |
105 | - TestCase, |
106 | TestCaseWithFactory, |
107 | ) |
108 | from lp.testing.fixture import ZopeUtilityFixture |
109 | @@ -82,6 +86,7 @@ |
110 | class DummyMacaroonIssuer(MacaroonIssuerBase): |
111 | |
112 | identifier = 'test' |
113 | + issuable_via_authserver = True |
114 | _root_secret = 'test' |
115 | |
116 | def checkIssuingContext(self, context): |
117 | @@ -101,12 +106,12 @@ |
118 | return caveat_value == str(context.id) |
119 | |
120 | |
121 | -class VerifyMacaroonTests(TestCase): |
122 | +class MacaroonTests(TestCaseWithFactory): |
123 | |
124 | layer = ZopelessDatabaseLayer |
125 | |
126 | def setUp(self): |
127 | - super(VerifyMacaroonTests, self).setUp() |
128 | + super(MacaroonTests, self).setUp() |
129 | self.issuer = DummyMacaroonIssuer() |
130 | self.useFixture(ZopeUtilityFixture( |
131 | self.issuer, IMacaroonIssuer, name='test')) |
132 | @@ -114,12 +119,46 @@ |
133 | self.authserver = AuthServerAPIView( |
134 | private_root.authserver, TestRequest()) |
135 | |
136 | - def test_nonsense_macaroon(self): |
137 | + def test_issue_unknown_issuer(self): |
138 | + self.assertEqual( |
139 | + faults.PermissionDenied(), |
140 | + self.authserver.issueMacaroon( |
141 | + 'unknown-issuer', 'LibraryFileAlias', 1)) |
142 | + |
143 | + def test_issue_wrong_context_type(self): |
144 | + self.assertEqual( |
145 | + faults.PermissionDenied(), |
146 | + self.authserver.issueMacaroon( |
147 | + 'unknown-issuer', 'nonsense', 1)) |
148 | + |
149 | + def test_issue_not_issuable_via_authserver(self): |
150 | + self.issuer.issuable_via_authserver = False |
151 | + self.assertEqual( |
152 | + faults.PermissionDenied(), |
153 | + self.authserver.issueMacaroon('test', 'LibraryFileAlias', 1)) |
154 | + |
155 | + def test_issue_bad_context(self): |
156 | + build = self.factory.makeSnapBuild() |
157 | + self.assertEqual( |
158 | + faults.PermissionDenied(), |
159 | + self.authserver.issueMacaroon('test', 'SnapBuild', build.id)) |
160 | + |
161 | + def test_issue_success(self): |
162 | + macaroon = Macaroon.deserialize( |
163 | + self.authserver.issueMacaroon('test', 'LibraryFileAlias', 1)) |
164 | + self.assertThat(macaroon, MatchesStructure( |
165 | + location=Equals(config.vhost.mainsite.hostname), |
166 | + identifier=Equals('test'), |
167 | + caveats=MatchesListwise([ |
168 | + MatchesStructure.byEquality(caveat_id='lp.test 1'), |
169 | + ]))) |
170 | + |
171 | + def test_verify_nonsense_macaroon(self): |
172 | self.assertEqual( |
173 | faults.Unauthorized(), |
174 | self.authserver.verifyMacaroon('nonsense', 'LibraryFileAlias', 1)) |
175 | |
176 | - def test_unknown_issuer(self): |
177 | + def test_verify_unknown_issuer(self): |
178 | macaroon = Macaroon( |
179 | location=config.vhost.mainsite.hostname, |
180 | identifier='unknown-issuer', key='test') |
181 | @@ -128,7 +167,7 @@ |
182 | self.authserver.verifyMacaroon( |
183 | macaroon.serialize(), 'LibraryFileAlias', 1)) |
184 | |
185 | - def test_wrong_context_type(self): |
186 | + def test_verify_wrong_context_type(self): |
187 | lfa = getUtility(ILibraryFileAliasSet)[1] |
188 | macaroon = self.issuer.issueMacaroon(lfa) |
189 | self.assertEqual( |
190 | @@ -136,7 +175,7 @@ |
191 | self.authserver.verifyMacaroon( |
192 | macaroon.serialize(), 'nonsense', lfa.id)) |
193 | |
194 | - def test_wrong_context(self): |
195 | + def test_verify_wrong_context(self): |
196 | lfa = getUtility(ILibraryFileAliasSet)[1] |
197 | macaroon = self.issuer.issueMacaroon(lfa) |
198 | self.assertEqual( |
199 | @@ -144,7 +183,7 @@ |
200 | self.authserver.verifyMacaroon( |
201 | macaroon.serialize(), 'LibraryFileAlias', 2)) |
202 | |
203 | - def test_nonexistent_lfa(self): |
204 | + def test_verify_nonexistent_lfa(self): |
205 | macaroon = self.issuer.issueMacaroon( |
206 | getUtility(ILibraryFileAliasSet)[1]) |
207 | # Pick a large ID that doesn't exist in sampledata. |
208 | @@ -157,7 +196,7 @@ |
209 | self.authserver.verifyMacaroon( |
210 | macaroon.serialize(), 'LibraryFileAlias', lfa_id)) |
211 | |
212 | - def test_success(self): |
213 | + def test_verify_success(self): |
214 | lfa = getUtility(ILibraryFileAliasSet)[1] |
215 | macaroon = self.issuer.issueMacaroon(lfa) |
216 | self.assertThat( |
217 | |
218 | === modified file 'lib/lp/services/authserver/xmlrpc.py' |
219 | --- lib/lp/services/authserver/xmlrpc.py 2019-04-23 13:50:35 +0000 |
220 | +++ lib/lp/services/authserver/xmlrpc.py 2019-04-25 09:56:06 +0000 |
221 | @@ -17,6 +17,7 @@ |
222 | getUtility, |
223 | ) |
224 | from zope.interface import implementer |
225 | +from zope.security.proxy import removeSecurityProxy |
226 | |
227 | from lp.registry.interfaces.person import IPersonSet |
228 | from lp.services.authserver.interfaces import ( |
229 | @@ -24,8 +25,12 @@ |
230 | IAuthServerApplication, |
231 | ) |
232 | from lp.services.librarian.interfaces import ILibraryFileAliasSet |
233 | -from lp.services.macaroons.interfaces import IMacaroonIssuer |
234 | +from lp.services.macaroons.interfaces import ( |
235 | + BadMacaroonContext, |
236 | + IMacaroonIssuer, |
237 | + ) |
238 | from lp.services.webapp import LaunchpadXMLRPCView |
239 | +from lp.snappy.interfaces.snapbuild import ISnapBuildSet |
240 | from lp.xmlrpc import faults |
241 | |
242 | |
243 | @@ -45,6 +50,53 @@ |
244 | for key in person.sshkeys], |
245 | } |
246 | |
247 | + def _resolveContext(self, context_type, context): |
248 | + """Resolve a serialised context. |
249 | + |
250 | + :param context_type: A string identifying the type of context. |
251 | + Currently only 'LibraryFileAlias' and 'SnapBuild' are supported. |
252 | + :param context: The context as plain data (e.g. an ID). |
253 | + :return: The resolved context, or None. |
254 | + """ |
255 | + if context_type == 'LibraryFileAlias': |
256 | + # The context is a `LibraryFileAlias` ID. |
257 | + try: |
258 | + return getUtility(ILibraryFileAliasSet)[context] |
259 | + except SQLObjectNotFound: |
260 | + return None |
261 | + elif context_type == 'SnapBuild': |
262 | + # The context is a `SnapBuild` ID. |
263 | + return getUtility(ISnapBuildSet).getByID(context) |
264 | + else: |
265 | + return None |
266 | + |
267 | + def issueMacaroon(self, issuer_name, context_type, context): |
268 | + """See `IAuthServer.issueMacaroon`.""" |
269 | + try: |
270 | + issuer = getUtility(IMacaroonIssuer, issuer_name) |
271 | + except ComponentLookupError: |
272 | + return faults.PermissionDenied() |
273 | + # Only permit issuers that have been specifically designed for use |
274 | + # with the authserver: they must need to be issued by parts of |
275 | + # Launchpad other than appservers but be verified by appservers, |
276 | + # they must take parameters that can be passed over XML-RPC, and |
277 | + # they must issue macaroons with carefully-designed constraints to |
278 | + # minimise privilege-escalation attacks. |
279 | + if not issuer.issuable_via_authserver: |
280 | + return faults.PermissionDenied() |
281 | + # The context is plain data, since we can't pass general objects over |
282 | + # the XML-RPC interface. Look it up so that we can verify it. |
283 | + context = self._resolveContext(context_type, context) |
284 | + if context is None: |
285 | + return faults.PermissionDenied() |
286 | + try: |
287 | + # issueMacaroon isn't normally public, but we clearly need it |
288 | + # here. |
289 | + macaroon = removeSecurityProxy(issuer).issueMacaroon(context) |
290 | + except BadMacaroonContext: |
291 | + return faults.PermissionDenied() |
292 | + return macaroon.serialize() |
293 | + |
294 | def verifyMacaroon(self, macaroon_raw, context_type, context): |
295 | """See `IAuthServer.verifyMacaroon`.""" |
296 | try: |
297 | @@ -59,15 +111,10 @@ |
298 | return faults.Unauthorized() |
299 | # The context is plain data, since we can't pass general objects over |
300 | # the XML-RPC interface. Look it up so that we can verify it. |
301 | - if context_type == 'LibraryFileAlias': |
302 | - # The context is a `LibraryFileAlias` ID. |
303 | - try: |
304 | - lfa = getUtility(ILibraryFileAliasSet)[context] |
305 | - except SQLObjectNotFound: |
306 | - return faults.Unauthorized() |
307 | - else: |
308 | + context = self._resolveContext(context_type, context) |
309 | + if context is None: |
310 | return faults.Unauthorized() |
311 | - if not issuer.verifyMacaroon(macaroon, lfa): |
312 | + if not issuer.verifyMacaroon(macaroon, context): |
313 | return faults.Unauthorized() |
314 | return True |
315 | |
316 | |
317 | === modified file 'lib/lp/services/macaroons/interfaces.py' |
318 | --- lib/lp/services/macaroons/interfaces.py 2019-04-24 12:50:20 +0000 |
319 | +++ lib/lp/services/macaroons/interfaces.py 2019-04-25 09:56:06 +0000 |
320 | @@ -12,6 +12,7 @@ |
321 | ] |
322 | |
323 | from zope.interface import Interface |
324 | +from zope.schema import Bool |
325 | |
326 | |
327 | class BadMacaroonContext(Exception): |
328 | @@ -27,6 +28,9 @@ |
329 | class IMacaroonIssuerPublic(Interface): |
330 | """Public interface to a policy for verifying macaroons.""" |
331 | |
332 | + issuable_via_authserver = Bool( |
333 | + "Does this issuer allow issuing macaroons via the authserver?") |
334 | + |
335 | def verifyMacaroon(macaroon, context, require_context=True): |
336 | """Verify that `macaroon` is valid for `context`. |
337 | |
338 | @@ -49,6 +53,6 @@ |
339 | |
340 | :param context: The context that the returned macaroon should relate |
341 | to. |
342 | - :raises ValueError: if the context is unsuitable. |
343 | + :raises BadMacaroonContext: if the context is unsuitable. |
344 | :return: A macaroon. |
345 | """ |
346 | |
347 | === modified file 'lib/lp/services/macaroons/model.py' |
348 | --- lib/lp/services/macaroons/model.py 2019-04-24 16:15:47 +0000 |
349 | +++ lib/lp/services/macaroons/model.py 2019-04-25 09:56:06 +0000 |
350 | @@ -23,6 +23,8 @@ |
351 | class MacaroonIssuerBase: |
352 | """See `IMacaroonIssuer`.""" |
353 | |
354 | + issuable_via_authserver = False |
355 | + |
356 | @property |
357 | def identifier(self): |
358 | """An identifying name for this issuer.""" |
359 | |
360 | === modified file 'lib/lp/snappy/model/snapbuild.py' |
361 | --- lib/lp/snappy/model/snapbuild.py 2019-04-24 12:50:20 +0000 |
362 | +++ lib/lp/snappy/model/snapbuild.py 2019-04-25 09:56:06 +0000 |
363 | @@ -597,17 +597,14 @@ |
364 | class SnapBuildMacaroonIssuer(MacaroonIssuerBase): |
365 | |
366 | identifier = "snap-build" |
367 | + issuable_via_authserver = True |
368 | |
369 | def checkIssuingContext(self, context): |
370 | """See `MacaroonIssuerBase`. |
371 | |
372 | - For issuing, the context is an `ISnapBuild` or its ID. |
373 | + For issuing, the context is an `ISnapBuild`. |
374 | """ |
375 | - if ISnapBuild.providedBy(context): |
376 | - pass |
377 | - elif isinstance(context, int): |
378 | - context = getUtility(ISnapBuildSet).getByID(context) |
379 | - else: |
380 | + if not ISnapBuild.providedBy(context): |
381 | raise BadMacaroonContext(context) |
382 | if not removeSecurityProxy(context).is_private: |
383 | raise BadMacaroonContext( |
384 | |
385 | === modified file 'lib/lp/snappy/tests/test_snapbuild.py' |
386 | --- lib/lp/snappy/tests/test_snapbuild.py 2019-04-24 12:50:20 +0000 |
387 | +++ lib/lp/snappy/tests/test_snapbuild.py 2019-04-25 09:56:06 +0000 |
388 | @@ -26,6 +26,7 @@ |
389 | MatchesStructure, |
390 | ) |
391 | from zope.component import getUtility |
392 | +from zope.publisher.xmlrpc import TestRequest |
393 | from zope.security.proxy import removeSecurityProxy |
394 | |
395 | from lp.app.enums import InformationType |
396 | @@ -36,6 +37,7 @@ |
397 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
398 | from lp.buildmaster.interfaces.processor import IProcessorSet |
399 | from lp.registry.enums import PersonVisibility |
400 | +from lp.services.authserver.xmlrpc import AuthServerAPIView |
401 | from lp.services.config import config |
402 | from lp.services.features.testing import FeatureFixture |
403 | from lp.services.job.interfaces.job import JobStatus |
404 | @@ -73,6 +75,7 @@ |
405 | from lp.testing.mail_helpers import pop_notifications |
406 | from lp.testing.matchers import HasQueryCount |
407 | from lp.testing.pages import webservice_for_person |
408 | +from lp.xmlrpc.interfaces import IPrivateApplication |
409 | |
410 | |
411 | expected_body = """\ |
412 | @@ -813,6 +816,21 @@ |
413 | caveat_id="lp.snap-build %s" % build.id), |
414 | ]))) |
415 | |
416 | + def test_issueMacaroon_via_authserver(self): |
417 | + build = self.factory.makeSnapBuild( |
418 | + snap=self.factory.makeSnap(private=True)) |
419 | + private_root = getUtility(IPrivateApplication) |
420 | + authserver = AuthServerAPIView(private_root.authserver, TestRequest()) |
421 | + macaroon = Macaroon.deserialize( |
422 | + authserver.issueMacaroon("snap-build", "SnapBuild", build.id)) |
423 | + self.assertThat(macaroon, MatchesStructure( |
424 | + location=Equals("launchpad.dev"), |
425 | + identifier=Equals("snap-build"), |
426 | + caveats=MatchesListwise([ |
427 | + MatchesStructure.byEquality( |
428 | + caveat_id="lp.snap-build %s" % build.id), |
429 | + ]))) |
430 | + |
431 | def test_verifyMacaroon_good(self): |
432 | [ref] = self.factory.makeGitRefs( |
433 | information_type=InformationType.USERDATA) |
434 | |
435 | === modified file 'lib/lp/soyuz/tests/test_binarypackagebuild.py' |
436 | --- lib/lp/soyuz/tests/test_binarypackagebuild.py 2019-04-24 12:50:20 +0000 |
437 | +++ lib/lp/soyuz/tests/test_binarypackagebuild.py 2019-04-25 09:56:06 +0000 |
438 | @@ -18,6 +18,7 @@ |
439 | MatchesStructure, |
440 | ) |
441 | from zope.component import getUtility |
442 | +from zope.publisher.xmlrpc import TestRequest |
443 | from zope.security.proxy import removeSecurityProxy |
444 | |
445 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
446 | @@ -27,6 +28,7 @@ |
447 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
448 | from lp.registry.interfaces.series import SeriesStatus |
449 | from lp.registry.interfaces.sourcepackage import SourcePackageUrgency |
450 | +from lp.services.authserver.xmlrpc import AuthServerAPIView |
451 | from lp.services.config import config |
452 | from lp.services.log.logger import DevNullLogger |
453 | from lp.services.macaroons.interfaces import ( |
454 | @@ -67,6 +69,8 @@ |
455 | LaunchpadZopelessLayer, |
456 | ) |
457 | from lp.testing.pages import webservice_for_person |
458 | +from lp.xmlrpc import faults |
459 | +from lp.xmlrpc.interfaces import IPrivateApplication |
460 | |
461 | |
462 | class TestBinaryPackageBuild(TestCaseWithFactory): |
463 | @@ -938,6 +942,16 @@ |
464 | caveat_id="lp.principal.binary-package-build %s" % build.id), |
465 | ])) |
466 | |
467 | + def test_issueMacaroon_not_via_authserver(self): |
468 | + build = self.factory.makeBinaryPackageBuild( |
469 | + archive=self.factory.makeArchive(private=True)) |
470 | + private_root = getUtility(IPrivateApplication) |
471 | + authserver = AuthServerAPIView(private_root.authserver, TestRequest()) |
472 | + self.assertEqual( |
473 | + faults.PermissionDenied(), |
474 | + authserver.issueMacaroon( |
475 | + "binary-package-build", "BinaryPackageBuild", build)) |
476 | + |
477 | def test_verifyMacaroon_good(self): |
478 | build = self.factory.makeBinaryPackageBuild( |
479 | archive=self.factory.makeArchive(private=True)) |