Merge lp:~cjwatson/launchpad/librarian-accept-macaroon into lp:launchpad

Proposed by Colin Watson on 2018-05-04
Status: Needs review
Proposed branch: lp:~cjwatson/launchpad/librarian-accept-macaroon
Merge into: lp:launchpad
Diff against target: 1052 lines (+574/-80)
14 files modified
configs/development/launchpad-lazr.conf (+2/-1)
configs/testrunner-appserver/launchpad-lazr.conf (+1/-3)
lib/lp/code/model/codeimportjob.py (+8/-2)
lib/lp/code/model/tests/test_codeimportjob.py (+9/-2)
lib/lp/code/xmlrpc/tests/test_git.py (+10/-5)
lib/lp/codehosting/codeimport/tests/test_workermonitor.py (+2/-2)
lib/lp/services/config/schema-lazr.conf (+15/-0)
lib/lp/services/librarianserver/db.py (+57/-10)
lib/lp/services/librarianserver/tests/test_db.py (+102/-6)
lib/lp/services/librarianserver/tests/test_web.py (+128/-46)
lib/lp/services/librarianserver/web.py (+7/-0)
lib/lp/soyuz/configure.zcml (+10/-1)
lib/lp/soyuz/model/binarypackagebuild.py (+90/-2)
lib/lp/soyuz/tests/test_binarypackagebuild.py (+133/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/librarian-accept-macaroon
Reviewer Review Type Date Requested Status
William Grant 2018-05-04 Needs Information on 2018-05-09
Review via email: mp+345079@code.launchpad.net

Commit message

Allow macaroon authentication to the librarian for BPBs' source files.

Description of the change

This will later allow us to dispatch private builds immediately, rather than having to wait for the source to be published first.

In this case we accept the macaroon as the password part of basic authentication with an empty username, similar to the git authentication method used by code import jobs. This is a bit weird, but it avoids needing to change launchpad-buildd, and we could always switch to a more conventional "Authorization: Macaroon ..." scheme later. (Alternatively, I suppose we could put the macaroon in a query parameter instead?)

The only things I can see that we'll need to do deployment-wise are to set up *.restricted.dogfood.launchpadlibrarian.net (both DNS and listening to it on a different IP address from the public librarian) and to configure librarian.macaroon_secret_key.

To post a comment you must log in.
William Grant (wgrant) wrote :

This seems like a massive layering violation. I really think we should just issue macaroons with LFA.id caveats directly, even though that loses us the BuildStatus.BUILDING restriction.

review: Needs Information
Colin Watson (cjwatson) wrote :

I don't understand the principle here. Can you explain why it's any more of a layering violation for a BPB issuer to grant permission to access its associated LFAs than it is for a CodeImportJob issuer to grant permission to access its associated GitRepositories?

In general, my intent in designing the IMacaroonIssuer interface was that various subsystems could grant the specific permissions they needed, and the verifying code could then use the getUtility(IMacaroonIssuer, token.identifier) idiom to get the appropriate verifier without having to have specific knowledge of what the verifier in question does. Apart from the security.cfg changes, nothing about the librarian changes here is at all specific to BPBs, and it would be perfectly possible to write a different verifier that grants access based on some other criteria; furthermore, if (hypothetically) a BPB needed access to some other private resource, the same macaroon could be used for both by generalising the verifier.

The only thing here that feels at all like a layering violation to me is that BPBMacaroonIssuer.verifyMacaroon takes an LFA ID rather than a build. But, firstly, that's something that it's free to generalise later without reference to the librarian (although I suppose that would be easier if it took an actual LFA; that would require a little bit of extra work in the librarian to avoid an extra query), and, secondly, it's OK for BPBs to know about LFAs, just not vice versa.

William Grant (wgrant) wrote :

On 09/05/18 23:24, Colin Watson wrote:
> I don't understand the principle here. Can you explain why it's any
> more of a layering violation for a BPB issuer to grant permission to
> access its associated LFAs than it is for a CodeImportJob issuer to
> grant permission to access its associated GitRepositories?

It was less obviously wrong there because the GitRepository
authorisation code already touched CodeImport and they were part of the
same Launchpad application. But in this case we have the librarian
touching complex bits of the Soyuz schema, which is ugly from a code
structure perspective and also means we have to be very careful around
synchronising schema changes and librarian deployments.

librarian today only examines non-librarian tables when GC introspects
the schema to work out which FKs there are to LFA.id, and that doesn't
seem like an architectural separation that we should abandon lightly.

> In general, my intent in designing the IMacaroonIssuer interface was
> that various subsystems could grant the specific permissions they
> needed, and the verifying code could then use the
> getUtility(IMacaroonIssuer, token.identifier) idiom to get the
> appropriate verifier without having to have specific knowledge of
> what the verifier in question does. Apart from the security.cfg
> changes, nothing about the librarian changes here is at all specific
> to BPBs, and it would be perfectly possible to write a different
> verifier that grants access based on some other criteria;
> furthermore, if (hypothetically) a BPB needed access to some other
> private resource, the same macaroon could be used for both by
> generalising the verifier.

I agree that sharing macaroons would be ideal, but having the librarian
itself validate macaroons that may require arbitrarily broad and complex
database logic (particularly given the librarian is a Twisted app which
really shouldn't be doing synchronous DB calls at all) doesn't seem right.

Colin Watson (cjwatson) wrote :

Would it be better if the librarian talked to the authserver for this?
I'd avoided that approach because it seemed like unnecessary complexity,
but it could easily be done asynchronously and it would allow preserving
the fine-grained permissions, which I'd really like to find a way to do.
We could do it only for the macaroon auth case, which I expect to
continue being comparatively rare.

William Grant (wgrant) wrote :

I think that sounds fine. It's a bit of a change from where we are today, but not a huge way off and not clearly in the wrong direction.

18632. By Colin Watson on 2018-10-31

Merge devel.

18633. By Colin Watson on 2018-11-01

Generalise codeimport.macaroon_secret_key to launchpad.internal_macaroon_secret_key so that it can be used for some other macaroons.

18634. By Colin Watson on 2018-11-01

Make the librarian verify macaroons using the authserver rather than directly.

Colin Watson (cjwatson) wrote :

I've updated this to use the authserver. I've also split out https://code.launchpad.net/~cjwatson/launchpad/librarianserver-test-web-requests/+merge/358189, which might be worth reviewing first to make the review diff size here more manageable.

18635. By Colin Watson on 2018-11-02

Merge devel.

Colin Watson (cjwatson) wrote :

OK, that other MP is merged now, so this is a bit more readable.

Unmerged revisions

18635. By Colin Watson on 2018-11-02

Merge devel.

18634. By Colin Watson on 2018-11-01

Make the librarian verify macaroons using the authserver rather than directly.

18633. By Colin Watson on 2018-11-01

Generalise codeimport.macaroon_secret_key to launchpad.internal_macaroon_secret_key so that it can be used for some other macaroons.

18632. By Colin Watson on 2018-10-31

Merge devel.

18631. By Colin Watson on 2018-05-04

Allow macaroon authentication to the librarian for BPBs' source files.

This will later allow us to dispatch private builds immediately, rather than
having to wait for the source to be published first.

In this case we accept the macaroon as the password part of basic
authentication with an empty username, similar to the git authentication
method used by code import jobs. This is a bit weird, but it avoids needing
to change launchpad-buildd, and we could always switch to a more
conventional "Authorization: Macaroon ..." scheme later.

18630. By Colin Watson on 2018-05-04

Make CodeImportJobMacaroonIssuer.verifyMacaroon check that the context is the right type, just in case.

18629. By Colin Watson on 2018-05-04

Convert lp.services.librarianserver.tests.test_web to requests.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configs/development/launchpad-lazr.conf'
2--- configs/development/launchpad-lazr.conf 2018-06-14 16:28:45 +0000
3+++ configs/development/launchpad-lazr.conf 2018-11-02 11:53:31 +0000
4@@ -121,7 +121,8 @@
5 restricted_upload_port: 58095
6 restricted_download_port: 58085
7 restricted_download_url: http://launchpad.dev:58085/
8-use_https = False
9+use_https: False
10+authentication_endpoint: http://xmlrpc-private.launchpad.dev:8087/authserver
11
12 [librarian_server]
13 root: /var/tmp/fatsam
14
15=== modified file 'configs/testrunner-appserver/launchpad-lazr.conf'
16--- configs/testrunner-appserver/launchpad-lazr.conf 2018-05-21 20:30:16 +0000
17+++ configs/testrunner-appserver/launchpad-lazr.conf 2018-11-02 11:53:31 +0000
18@@ -8,14 +8,12 @@
19 [codehosting]
20 launch: False
21
22-[codeimport]
23-macaroon_secret_key: dev-macaroon-secret
24-
25 [bing_test_service]
26 launch: False
27
28 [launchpad]
29 openid_provider_root: http://testopenid.dev:8085/
30+internal_macaroon_secret_key: internal-dev-macaroon-secret
31
32 [librarian_server]
33 launch: False
34
35=== modified file 'lib/lp/code/model/codeimportjob.py'
36--- lib/lp/code/model/codeimportjob.py 2018-03-15 20:44:04 +0000
37+++ lib/lp/code/model/codeimportjob.py 2018-11-02 11:53:31 +0000
38@@ -413,10 +413,14 @@
39
40 @property
41 def _root_secret(self):
42- secret = config.codeimport.macaroon_secret_key
43+ secret = config.launchpad.internal_macaroon_secret_key
44+ if not secret:
45+ # XXX cjwatson 2018-11-01: Remove this once it is no longer in
46+ # production configs.
47+ secret = config.codeimport.macaroon_secret_key
48 if not secret:
49 raise RuntimeError(
50- "codeimport.macaroon_secret_key not configured.")
51+ "launchpad.internal_macaroon_secret_key not configured.")
52 return secret
53
54 def issueMacaroon(self, context):
55@@ -442,6 +446,8 @@
56
57 def verifyMacaroon(self, macaroon, context):
58 """See `IMacaroonIssuer`."""
59+ if not ICodeImportJob.providedBy(context):
60+ return False
61 if not self.checkMacaroonIssuer(macaroon):
62 return False
63 try:
64
65=== modified file 'lib/lp/code/model/tests/test_codeimportjob.py'
66--- lib/lp/code/model/tests/test_codeimportjob.py 2018-04-19 00:02:19 +0000
67+++ lib/lp/code/model/tests/test_codeimportjob.py 2018-11-02 11:53:31 +0000
68@@ -131,7 +131,8 @@
69 'git://git.example.com/project.git']))
70
71 def test_git_to_git_arguments(self):
72- self.pushConfig('codeimport', macaroon_secret_key='some-secret')
73+ self.pushConfig(
74+ 'launchpad', internal_macaroon_secret_key='some-secret')
75 self.useFixture(GitHostingFixture())
76 code_import = self.factory.makeCodeImport(
77 git_repo_url="git://git.example.com/project.git",
78@@ -1271,7 +1272,8 @@
79 def setUp(self):
80 super(TestCodeImportJobMacaroonIssuer, self).setUp()
81 login_for_code_imports()
82- self.pushConfig("codeimport", macaroon_secret_key="some-secret")
83+ self.pushConfig(
84+ "launchpad", internal_macaroon_secret_key="some-secret")
85 self.useFixture(GitHostingFixture())
86
87 def makeJob(self, target_rcs_type=TargetRevisionControlSystems.GIT):
88@@ -1296,6 +1298,11 @@
89 caveat_id="lp.code-import-job %s" % job.id),
90 ]))
91
92+ def test_issueMacaroon_good_old_config(self):
93+ self.pushConfig("launchpad", internal_macaroon_secret_key="")
94+ self.pushConfig("codeimport", macaroon_secret_key="some-secret")
95+ self.test_issueMacaroon_good()
96+
97 def test_checkMacaroonIssuer_good(self):
98 job = self.makeJob()
99 issuer = getUtility(IMacaroonIssuer, "code-import-job")
100
101=== modified file 'lib/lp/code/xmlrpc/tests/test_git.py'
102--- lib/lp/code/xmlrpc/tests/test_git.py 2018-10-24 00:42:39 +0000
103+++ lib/lp/code/xmlrpc/tests/test_git.py 2018-11-02 11:53:31 +0000
104@@ -891,7 +891,8 @@
105 def test_translatePath_code_import(self):
106 # A code import worker with a suitable macaroon can write to a
107 # repository associated with a running code import job.
108- self.pushConfig("codeimport", macaroon_secret_key="some-secret")
109+ self.pushConfig(
110+ "launchpad", internal_macaroon_secret_key="some-secret")
111 machine = self.factory.makeCodeImportMachine(set_online=True)
112 code_imports = [
113 self.factory.makeCodeImport(
114@@ -927,7 +928,8 @@
115 def test_translatePath_private_code_import(self):
116 # A code import worker with a suitable macaroon can write to a
117 # repository associated with a running private code import job.
118- self.pushConfig("codeimport", macaroon_secret_key="some-secret")
119+ self.pushConfig(
120+ "launchpad", internal_macaroon_secret_key="some-secret")
121 machine = self.factory.makeCodeImportMachine(set_online=True)
122 code_imports = [
123 self.factory.makeCodeImport(
124@@ -999,7 +1001,8 @@
125 faults.Unauthorized)
126
127 def test_authenticateWithPassword_code_import(self):
128- self.pushConfig("codeimport", macaroon_secret_key="some-secret")
129+ self.pushConfig(
130+ "launchpad", internal_macaroon_secret_key="some-secret")
131 code_import = self.factory.makeCodeImport(
132 target_rcs_type=TargetRevisionControlSystems.GIT)
133 with celebrity_logged_in("vcs_imports"):
134@@ -1022,7 +1025,8 @@
135 # A code import worker with a suitable macaroon has repository owner
136 # privileges on a repository associated with a running code import
137 # job.
138- self.pushConfig("codeimport", macaroon_secret_key="some-secret")
139+ self.pushConfig(
140+ "launchpad", internal_macaroon_secret_key="some-secret")
141 machine = self.factory.makeCodeImportMachine(set_online=True)
142 code_imports = [
143 self.factory.makeCodeImport(
144@@ -1062,7 +1066,8 @@
145 # A code import worker with a suitable macaroon has repository owner
146 # privileges on a repository associated with a running private code
147 # import job.
148- self.pushConfig("codeimport", macaroon_secret_key="some-secret")
149+ self.pushConfig(
150+ "launchpad", internal_macaroon_secret_key="some-secret")
151 machine = self.factory.makeCodeImportMachine(set_online=True)
152 code_imports = [
153 self.factory.makeCodeImport(
154
155=== modified file 'lib/lp/codehosting/codeimport/tests/test_workermonitor.py'
156--- lib/lp/codehosting/codeimport/tests/test_workermonitor.py 2018-03-26 07:37:26 +0000
157+++ lib/lp/codehosting/codeimport/tests/test_workermonitor.py 2018-11-02 11:53:31 +0000
158@@ -819,8 +819,8 @@
159 "[codehosting]",
160 "git_browse_root: %s" % self.target_git_server.get_url(""),
161 "",
162- "[codeimport]",
163- "macaroon_secret_key: some-secret",
164+ "[launchpad]",
165+ "internal_macaroon_secret_key: some-secret",
166 ]
167 config_fixture.add_section("\n" + "\n".join(setting_lines))
168 self.useFixture(ConfigUseFixture(config_name))
169
170=== modified file 'lib/lp/services/config/schema-lazr.conf'
171--- lib/lp/services/config/schema-lazr.conf 2018-07-30 18:09:23 +0000
172+++ lib/lp/services/config/schema-lazr.conf 2018-11-02 11:53:31 +0000
173@@ -425,6 +425,7 @@
174 svn_revisions_import_limit: 500
175
176 # Secret key for macaroons used to grant git push permission to workers.
177+# Deprecated in favour of launchpad.internal_macaroon_secret_key.
178 macaroon_secret_key: none
179
180 [codeimportdispatcher]
181@@ -1098,6 +1099,10 @@
182 # datatype: string
183 feature_flags_endpoint:
184
185+# Secret key for macaroons used to grant permissions to various internal
186+# components of Launchpad.
187+internal_macaroon_secret_key: none
188+
189 [launchpad_session]
190 # The database connection string.
191 # datatype: pgconnection
192@@ -1180,6 +1185,16 @@
193
194 use_https = True
195
196+# The URL of the XML-RPC endpoint that handles verifying macaroons. This
197+# should implement IAuthServer.
198+#
199+# datatype: string
200+authentication_endpoint: none
201+
202+# The time in seconds that the librarian will wait for a reply from the
203+# authserver.
204+authentication_timeout: 5
205+
206
207 [librarian_gc]
208 # The database user which will be used by this process.
209
210=== modified file 'lib/lp/services/librarianserver/db.py'
211--- lib/lp/services/librarianserver/db.py 2016-01-10 22:37:40 +0000
212+++ lib/lp/services/librarianserver/db.py 2018-11-02 11:53:31 +0000
213@@ -1,4 +1,4 @@
214-# Copyright 2009 Canonical Ltd. This software is licensed under the
215+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
216 # GNU Affero General Public License version 3 (see the file LICENSE).
217
218 """Database access layer for the Librarian."""
219@@ -11,11 +11,20 @@
220 import hashlib
221 import urllib
222
223+from pymacaroons import Macaroon
224+from six.moves.xmlrpc_client import Fault
225 from storm.expr import (
226 And,
227 SQL,
228 )
229+from twisted.internet import (
230+ defer,
231+ reactor as default_reactor,
232+ threads,
233+ )
234+from twisted.web import xmlrpc
235
236+from lp.services.config import config
237 from lp.services.database.interfaces import IStore
238 from lp.services.database.sqlbase import session_store
239 from lp.services.librarian.model import (
240@@ -23,6 +32,8 @@
241 LibraryFileContent,
242 TimeLimitedToken,
243 )
244+from lp.services.twistedsupport import cancel_on_timeout
245+from lp.xmlrpc import faults
246
247
248 class Library:
249@@ -36,18 +47,46 @@
250 Files created in this library will marked as restricted.
251 """
252 self.restricted = restricted
253+ self._authserver = xmlrpc.Proxy(
254+ config.librarian.authentication_endpoint,
255+ connectTimeout=config.librarian.authentication_timeout)
256
257 # The following methods are read-only queries.
258
259 def lookupBySHA1(self, digest):
260 return [fc.id for fc in LibraryFileContent.selectBy(sha1=digest)]
261
262+ @defer.inlineCallbacks
263+ def _verifyMacaroon(self, macaroon, aliasid):
264+ """Verify an LFA-authorising macaroon with the authserver.
265+
266+ This must be called in the reactor thread.
267+
268+ :param macaroon: A `Macaroon`.
269+ :param aliasid: A `LibraryFileAlias` ID.
270+ :return: True if the authserver reports that `macaroon` authorises
271+ access to `aliasid`; False if it reports that it does not.
272+ :raises Fault: if the authserver request fails.
273+ """
274+ try:
275+ yield cancel_on_timeout(
276+ self._authserver.callRemote(
277+ "verifyMacaroon", macaroon.serialize(), aliasid),
278+ config.librarian.authentication_timeout)
279+ defer.returnValue(True)
280+ except Fault as fault:
281+ if fault.faultCode == faults.Unauthorized.error_code:
282+ defer.returnValue(False)
283+ else:
284+ raise
285+
286 def getAlias(self, aliasid, token, path):
287 """Returns a LibraryFileAlias, or raises LookupError.
288
289 A LookupError is raised if no record with the given ID exists
290 or if not related LibraryFileContent exists.
291
292+ :param aliasid: A `LibraryFileAlias` ID.
293 :param token: The token for the file. If None no token is present.
294 When a token is supplied, it is looked up with path.
295 :param path: The path the request is for, unused unless a token
296@@ -69,16 +108,24 @@
297 #
298 # This needs to match url_path_quote.
299 normalised_path = urllib.quote(urllib.unquote(path), safe='/~+')
300- store = session_store()
301- token_found = store.find(TimeLimitedToken,
302- SQL("age(created) < interval '1 day'"),
303- TimeLimitedToken.token == hashlib.sha256(token).hexdigest(),
304- TimeLimitedToken.path == normalised_path).is_empty()
305- store.reset()
306- if token_found:
307+ if isinstance(token, Macaroon):
308+ # Macaroons have enough other constraints that they don't
309+ # need to be path-specific; it's simpler and faster to just
310+ # check the alias ID.
311+ token_ok = threads.blockingCallFromThread(
312+ default_reactor, self._verifyMacaroon, token, aliasid)
313+ else:
314+ store = session_store()
315+ token_ok = not store.find(TimeLimitedToken,
316+ SQL("age(created) < interval '1 day'"),
317+ TimeLimitedToken.token ==
318+ hashlib.sha256(token).hexdigest(),
319+ TimeLimitedToken.path == normalised_path).is_empty()
320+ store.reset()
321+ if token_ok:
322+ restricted = True
323+ else:
324 raise LookupError("Token stale/pruned/path mismatch")
325- else:
326- restricted = True
327 alias = LibraryFileAlias.selectOne(And(
328 LibraryFileAlias.id == aliasid,
329 LibraryFileAlias.contentID == LibraryFileContent.q.id,
330
331=== modified file 'lib/lp/services/librarianserver/tests/test_db.py'
332--- lib/lp/services/librarianserver/tests/test_db.py 2013-06-20 05:50:00 +0000
333+++ lib/lp/services/librarianserver/tests/test_db.py 2018-11-02 11:53:31 +0000
334@@ -1,21 +1,37 @@
335-# Copyright 2009 Canonical Ltd. This software is licensed under the
336+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
337 # GNU Affero General Public License version 3 (see the file LICENSE).
338
339-import unittest
340+__metaclass__ = type
341
342+from fixtures import MockPatchObject
343+from pymacaroons import Macaroon
344+from testtools.testcase import ExpectedException
345+from testtools.twistedsupport import AsynchronousDeferredRunTest
346 import transaction
347+from twisted.internet import (
348+ defer,
349+ reactor,
350+ )
351+from twisted.internet.threads import deferToThread
352+from twisted.web import (
353+ server,
354+ xmlrpc,
355+ )
356
357 from lp.services.database.interfaces import IStore
358 from lp.services.librarian.model import LibraryFileContent
359 from lp.services.librarianserver import db
360+from lp.testing import TestCase
361 from lp.testing.dbuser import switch_dbuser
362 from lp.testing.layers import LaunchpadZopelessLayer
363-
364-
365-class DBTestCase(unittest.TestCase):
366+from lp.xmlrpc import faults
367+
368+
369+class DBTestCase(TestCase):
370 layer = LaunchpadZopelessLayer
371
372 def setUp(self):
373+ super(DBTestCase, self).setUp()
374 switch_dbuser('librarian')
375
376 def test_lookupByDigest(self):
377@@ -43,12 +59,34 @@
378 self.assertEqual('text/unknown', alias.mimetype)
379
380
381-class TestLibrarianStuff(unittest.TestCase):
382+class FakeAuthServer(xmlrpc.XMLRPC):
383+ """A fake authserver.
384+
385+ This saves us from needing to start an appserver in tests.
386+ """
387+
388+ def __init__(self):
389+ xmlrpc.XMLRPC.__init__(self)
390+ self.macaroons = set()
391+
392+ def registerMacaroon(self, macaroon, context):
393+ self.macaroons.add((macaroon.serialize(), context))
394+
395+ def xmlrpc_verifyMacaroon(self, macaroon_raw, context):
396+ if (macaroon_raw, context) in self.macaroons:
397+ return True
398+ else:
399+ return faults.Unauthorized()
400+
401+
402+class TestLibrarianStuff(TestCase):
403 """Tests for the librarian."""
404
405 layer = LaunchpadZopelessLayer
406+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=10)
407
408 def setUp(self):
409+ super(TestLibrarianStuff, self).setUp()
410 switch_dbuser('librarian')
411 self.store = IStore(LibraryFileContent)
412 self.content_id = db.Library().add('deadbeef', 1234, 'abababab', 'ba')
413@@ -102,6 +140,64 @@
414 self.assertRaises(
415 LookupError, unrestricted_library.getAlias, 1, None, '/')
416
417+ def setUpAuthServer(self):
418+ authserver = FakeAuthServer()
419+ authserver_listener = reactor.listenTCP(0, server.Site(authserver))
420+ self.addCleanup(authserver_listener.stopListening)
421+ authserver_port = authserver_listener.getHost().port
422+ authserver_url = 'http://localhost:%d/' % authserver_port
423+ self.pushConfig('librarian', authentication_endpoint=authserver_url)
424+ return authserver
425+
426+ @defer.inlineCallbacks
427+ def test_getAlias_with_macaroon(self):
428+ # Library.getAlias() uses the authserver to verify macaroons.
429+ authserver = self.setUpAuthServer()
430+ unrestricted_library = db.Library(restricted=False)
431+ alias = unrestricted_library.getAlias(1, None, '/')
432+ alias.restricted = True
433+ transaction.commit()
434+ restricted_library = db.Library(restricted=True)
435+ macaroon = Macaroon()
436+ with ExpectedException(LookupError):
437+ yield deferToThread(restricted_library.getAlias, 1, macaroon, '/')
438+ authserver.registerMacaroon(macaroon, 1)
439+ alias = yield deferToThread(
440+ restricted_library.getAlias, 1, macaroon, '/')
441+ self.assertEqual(1, alias.id)
442+
443+ @defer.inlineCallbacks
444+ def test_getAlias_with_wrong_macaroon(self):
445+ # A macaroon for a different LFA doesn't work.
446+ authserver = self.setUpAuthServer()
447+ unrestricted_library = db.Library(restricted=False)
448+ alias = unrestricted_library.getAlias(1, None, '/')
449+ alias.restricted = True
450+ transaction.commit()
451+ macaroon = Macaroon()
452+ authserver.registerMacaroon(macaroon, 2)
453+ restricted_library = db.Library(restricted=True)
454+ with ExpectedException(LookupError):
455+ yield deferToThread(restricted_library.getAlias, 1, macaroon, '/')
456+
457+ @defer.inlineCallbacks
458+ def test_getAlias_with_macaroon_timeout(self):
459+ # The authserver call is cancelled after a timeout period.
460+ unrestricted_library = db.Library(restricted=False)
461+ alias = unrestricted_library.getAlias(1, None, '/')
462+ alias.restricted = True
463+ transaction.commit()
464+ macaroon = Macaroon()
465+ restricted_library = db.Library(restricted=True)
466+ self.useFixture(MockPatchObject(
467+ restricted_library._authserver, 'callRemote',
468+ return_value=defer.Deferred()))
469+ # XXX cjwatson 2018-11-01: We should use a Clock instead, but I had
470+ # trouble getting that working in conjunction with deferToThread.
471+ self.pushConfig('librarian', authentication_timeout=1)
472+ with ExpectedException(defer.CancelledError):
473+ yield deferToThread(restricted_library.getAlias, 1, macaroon, '/')
474+
475 def test_getAliases(self):
476 # Library.getAliases() returns a sequence
477 # [(LFA.id, LFA.filename, LFA.mimetype), ...] where LFA are
478
479=== modified file 'lib/lp/services/librarianserver/tests/test_web.py'
480--- lib/lp/services/librarianserver/tests/test_web.py 2018-11-02 10:14:23 +0000
481+++ lib/lp/services/librarianserver/tests/test_web.py 2018-11-02 11:53:31 +0000
482@@ -1,6 +1,8 @@
483 # Copyright 2009-2018 Canonical Ltd. This software is licensed under the
484 # GNU Affero General Public License version 3 (see the file LICENSE).
485
486+__metaclass__ = type
487+
488 from datetime import datetime
489 from gzip import GzipFile
490 import hashlib
491@@ -14,12 +16,14 @@
492 import pytz
493 import requests
494 from storm.expr import SQL
495-import testtools
496 from testtools.matchers import EndsWith
497 import transaction
498 from zope.component import getUtility
499+from zope.security.proxy import removeSecurityProxy
500
501+from lp.buildmaster.enums import BuildStatus
502 from lp.services.config import config
503+from lp.services.config.fixture import ConfigUseFixture
504 from lp.services.database.interfaces import IMasterStore
505 from lp.services.database.sqlbase import (
506 cursor,
507@@ -37,10 +41,17 @@
508 TimeLimitedToken,
509 )
510 from lp.services.librarianserver.storage import LibrarianStorage
511-from lp.testing.dbuser import switch_dbuser
512+from lp.services.macaroons.interfaces import IMacaroonIssuer
513+from lp.testing import TestCaseWithFactory
514+from lp.testing.dbuser import (
515+ dbuser,
516+ switch_dbuser,
517+ )
518 from lp.testing.layers import (
519+ AppServerLayer,
520 LaunchpadFunctionalLayer,
521 LaunchpadZopelessLayer,
522+ ZopelessAppServerLayer,
523 )
524
525
526@@ -50,19 +61,62 @@
527 return str(parsed.replace(path=parsed.path.replace(old, new)))
528
529
530-class LibrarianWebTestCase(testtools.TestCase):
531+class LibrarianWebTestMixin:
532+
533+ layer = LaunchpadFunctionalLayer
534+
535+ def commit(self):
536+ """Synchronize database state."""
537+ flush_database_updates()
538+ transaction.commit()
539+
540+ def get_restricted_file_and_public_url(self, filename='sample'):
541+ # Use a regular LibrarianClient to ensure we speak to the
542+ # nonrestricted port on the librarian which is where secured
543+ # restricted files are served from.
544+ client = LibrarianClient()
545+ fileAlias = client.addFile(
546+ filename, 12, BytesIO(b'a' * 12), contentType='text/plain')
547+ # Note: We're deliberately using the wrong url here: we should be
548+ # passing secure=True to getURLForAlias, but to use the returned URL
549+ # we would need a wildcard DNS facility patched into requests; instead
550+ # we use the *deliberate* choice of having the path of secure and
551+ # insecure urls be the same, so that we can test it: the server code
552+ # doesn't need to know about the fancy wildcard domains.
553+ url = client.getURLForAlias(fileAlias)
554+ # Now that we have a url which talks to the public librarian, make the
555+ # file restricted.
556+ IMasterStore(LibraryFileAlias).find(
557+ LibraryFileAlias, LibraryFileAlias.id == fileAlias).set(
558+ restricted=True)
559+ self.commit()
560+ return fileAlias, url
561+
562+ def require404(self, url, **kwargs):
563+ """Assert that opening `url` raises a 404."""
564+ response = requests.get(url, **kwargs)
565+ self.assertEqual(404, response.status_code)
566+
567+
568+class LibrarianZopelessWebTestMixin(LibrarianWebTestMixin):
569+
570+ layer = LaunchpadZopelessLayer
571+
572+ def setUp(self):
573+ super(LibrarianZopelessWebTestMixin, self).setUp()
574+ switch_dbuser(config.librarian.dbuser)
575+
576+ def commit(self):
577+ LaunchpadZopelessLayer.commit()
578+
579+
580+class LibrarianWebTestCase(LibrarianWebTestMixin, TestCaseWithFactory):
581 """Test the librarian's web interface."""
582- layer = LaunchpadFunctionalLayer
583
584 # Add stuff to a librarian via the upload port, then check that it's
585 # immediately visible on the web interface. (in an attempt to test ddaa's
586 # 500-error issue).
587
588- def commit(self):
589- """Synchronize database state."""
590- flush_database_updates()
591- transaction.commit()
592-
593 def test_uploadThenDownload(self):
594 client = LibrarianClient()
595
596@@ -286,28 +340,6 @@
597 self.assertNotIn('Last-Modified', response.headers)
598 self.assertNotIn('Cache-Control', response.headers)
599
600- def get_restricted_file_and_public_url(self, filename='sample'):
601- # Use a regular LibrarianClient to ensure we speak to the
602- # nonrestricted port on the librarian which is where secured
603- # restricted files are served from.
604- client = LibrarianClient()
605- fileAlias = client.addFile(
606- filename, 12, BytesIO(b'a' * 12), contentType='text/plain')
607- # Note: We're deliberately using the wrong url here: we should be
608- # passing secure=True to getURLForAlias, but to use the returned URL
609- # we would need a wildcard DNS facility patched into requests; instead
610- # we use the *deliberate* choice of having the path of secure and
611- # insecure urls be the same, so that we can test it: the server code
612- # doesn't need to know about the fancy wildcard domains.
613- url = client.getURLForAlias(fileAlias)
614- # Now that we have a url which talks to the public librarian, make the
615- # file restricted.
616- IMasterStore(LibraryFileAlias).find(
617- LibraryFileAlias, LibraryFileAlias.id == fileAlias).set(
618- restricted=True)
619- self.commit()
620- return fileAlias, url
621-
622 def test_restricted_subdomain_must_match_file_alias(self):
623 # IFF there is a .restricted. in the host, then the library file alias
624 # in the subdomain must match that in the path.
625@@ -436,21 +468,9 @@
626 last_modified_header, 'Tue, 30 Jan 2001 13:45:59 GMT')
627 # Perhaps we should also set Expires to the Last-Modified.
628
629- def require404(self, url, **kwargs):
630- """Assert that opening `url` raises a 404."""
631- response = requests.get(url, **kwargs)
632- self.assertEqual(404, response.status_code)
633-
634-
635-class LibrarianZopelessWebTestCase(LibrarianWebTestCase):
636- layer = LaunchpadZopelessLayer
637-
638- def setUp(self):
639- super(LibrarianZopelessWebTestCase, self).setUp()
640- switch_dbuser(config.librarian.dbuser)
641-
642- def commit(self):
643- LaunchpadZopelessLayer.commit()
644+
645+class LibrarianZopelessWebTestCase(
646+ LibrarianZopelessWebTestMixin, LibrarianWebTestCase):
647
648 def test_getURLForAliasObject(self):
649 # getURLForAliasObject returns the same URL as getURLForAlias.
650@@ -467,6 +487,68 @@
651 client.getURLForAliasObject(alias))
652
653
654+class LibrarianWebMacaroonTestCase(LibrarianWebTestMixin, TestCaseWithFactory):
655+
656+ layer = AppServerLayer
657+
658+ def setUp(self):
659+ super(LibrarianWebMacaroonTestCase, self).setUp()
660+ # Copy launchpad.internal_macaroon_secret_key from the appserver
661+ # config so that we can issue macaroons using it.
662+ with ConfigUseFixture(self.layer.appserver_config_name):
663+ key = config.launchpad.internal_macaroon_secret_key
664+ self.pushConfig("launchpad", internal_macaroon_secret_key=key)
665+
666+ def test_restricted_with_macaroon(self):
667+ fileAlias, url = self.get_restricted_file_and_public_url()
668+ lfa = IMasterStore(LibraryFileAlias).get(LibraryFileAlias, fileAlias)
669+ with dbuser('testadmin'):
670+ build = self.factory.makeBinaryPackageBuild(
671+ archive=self.factory.makeArchive(private=True))
672+ naked_build = removeSecurityProxy(build)
673+ self.factory.makeSourcePackageReleaseFile(
674+ sourcepackagerelease=naked_build.source_package_release,
675+ library_file=lfa)
676+ naked_build.updateStatus(BuildStatus.BUILDING)
677+ issuer = getUtility(IMacaroonIssuer, "binary-package-build")
678+ macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
679+ self.commit()
680+ response = requests.get(url, auth=("", macaroon.serialize()))
681+ response.raise_for_status()
682+ self.assertEqual(b"a" * 12, response.content)
683+
684+ def test_restricted_with_invalid_macaroon(self):
685+ fileAlias, url = self.get_restricted_file_and_public_url()
686+ lfa = IMasterStore(LibraryFileAlias).get(LibraryFileAlias, fileAlias)
687+ with dbuser('testadmin'):
688+ build = self.factory.makeBinaryPackageBuild(
689+ archive=self.factory.makeArchive(private=True))
690+ naked_build = removeSecurityProxy(build)
691+ self.factory.makeSourcePackageReleaseFile(
692+ sourcepackagerelease=naked_build.source_package_release,
693+ library_file=lfa)
694+ naked_build.updateStatus(BuildStatus.BUILDING)
695+ self.commit()
696+ self.require404(url, auth=("", "not-a-macaroon"))
697+
698+ def test_restricted_with_unverifiable_macaroon(self):
699+ fileAlias, url = self.get_restricted_file_and_public_url()
700+ with dbuser('testadmin'):
701+ build = self.factory.makeBinaryPackageBuild(
702+ archive=self.factory.makeArchive(private=True))
703+ removeSecurityProxy(build).updateStatus(BuildStatus.BUILDING)
704+ issuer = getUtility(IMacaroonIssuer, "binary-package-build")
705+ macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
706+ self.commit()
707+ self.require404(url, auth=("", macaroon.serialize()))
708+
709+
710+class LibrarianZopelessWebMacaroonTestCase(
711+ LibrarianZopelessWebTestMixin, LibrarianWebMacaroonTestCase):
712+
713+ layer = ZopelessAppServerLayer
714+
715+
716 class DeletedContentTestCase(unittest.TestCase):
717
718 layer = LaunchpadZopelessLayer
719
720=== modified file 'lib/lp/services/librarianserver/web.py'
721--- lib/lp/services/librarianserver/web.py 2018-05-09 16:49:58 +0000
722+++ lib/lp/services/librarianserver/web.py 2018-11-02 11:53:31 +0000
723@@ -7,6 +7,7 @@
724 import time
725 from urlparse import urlparse
726
727+from pymacaroons import Macaroon
728 from storm.exceptions import DisconnectionError
729 from twisted.internet import (
730 abstract,
731@@ -126,6 +127,12 @@
732 return fourOhFour
733
734 token = request.args.get('token', [None])[0]
735+ if token is None:
736+ if not request.getUser() and request.getPassword():
737+ try:
738+ token = Macaroon.deserialize(request.getPassword())
739+ except Exception:
740+ pass
741 path = request.path
742 deferred = deferToThread(
743 self._getFileAlias, self.aliasID, token, path)
744
745=== modified file 'lib/lp/soyuz/configure.zcml'
746--- lib/lp/soyuz/configure.zcml 2017-07-18 16:22:03 +0000
747+++ lib/lp/soyuz/configure.zcml 2018-11-02 11:53:31 +0000
748@@ -1,4 +1,4 @@
749-<!-- Copyright 2009-2017 Canonical Ltd. This software is licensed under the
750+<!-- Copyright 2009-2018 Canonical Ltd. This software is licensed under the
751 GNU Affero General Public License version 3 (see the file LICENSE).
752 -->
753
754@@ -480,6 +480,15 @@
755 <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource"/>
756 </securedutility>
757
758+ <!-- BinaryPackageBuildMacaroonIssuer -->
759+
760+ <securedutility
761+ class="lp.soyuz.model.binarypackagebuild.BinaryPackageBuildMacaroonIssuer"
762+ provides="lp.services.macaroons.interfaces.IMacaroonIssuer"
763+ name="binary-package-build">
764+ <allow interface="lp.services.macaroons.interfaces.IMacaroonIssuerPublic"/>
765+ </securedutility>
766+
767 <!-- DistroArchSeriesBinaryPackage -->
768
769 <class
770
771=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
772--- lib/lp/soyuz/model/binarypackagebuild.py 2017-03-29 09:28:09 +0000
773+++ lib/lp/soyuz/model/binarypackagebuild.py 2018-11-02 11:53:31 +0000
774@@ -1,4 +1,4 @@
775-# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
776+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
777 # GNU Affero General Public License version 3 (see the file LICENSE).
778
779 __metaclass__ = type
780@@ -21,6 +21,10 @@
781
782 import apt_pkg
783 from debian.deb822 import PkgRelation
784+from pymacaroons import (
785+ Macaroon,
786+ Verifier,
787+ )
788 import pytz
789 from sqlobject import SQLObjectNotFound
790 from storm.expr import (
791@@ -81,6 +85,7 @@
792 LibraryFileAlias,
793 LibraryFileContent,
794 )
795+from lp.services.macaroons.interfaces import IMacaroonIssuer
796 from lp.soyuz.adapters.buildarch import determine_architectures_to_build
797 from lp.soyuz.enums import (
798 ArchivePurpose,
799@@ -102,7 +107,10 @@
800 from lp.soyuz.mail.binarypackagebuild import BinaryPackageBuildMailer
801 from lp.soyuz.model.binarypackagename import BinaryPackageName
802 from lp.soyuz.model.binarypackagerelease import BinaryPackageRelease
803-from lp.soyuz.model.files import BinaryPackageFile
804+from lp.soyuz.model.files import (
805+ BinaryPackageFile,
806+ SourcePackageReleaseFile,
807+ )
808 from lp.soyuz.model.packageset import Packageset
809 from lp.soyuz.model.queue import (
810 PackageUpload,
811@@ -1362,3 +1370,83 @@
812 % (build.title, build.id, build.archive.displayname,
813 build_queue.lastscore))
814 return new_builds
815+
816+
817+@implementer(IMacaroonIssuer)
818+class BinaryPackageBuildMacaroonIssuer:
819+
820+ @property
821+ def _root_secret(self):
822+ secret = config.launchpad.internal_macaroon_secret_key
823+ if not secret:
824+ raise RuntimeError(
825+ "launchpad.internal_macaroon_secret_key not configured.")
826+ return secret
827+
828+ def issueMacaroon(self, context):
829+ """See `IMacaroonIssuer`.
830+
831+ For issuing, the context is an `IBinaryPackageBuild`.
832+ """
833+ if not removeSecurityProxy(context).archive.private:
834+ raise AssertionError(
835+ "Refusing to issue macaroon for public build.")
836+ macaroon = Macaroon(
837+ location=config.vhost.mainsite.hostname,
838+ identifier="binary-package-build", key=self._root_secret)
839+ macaroon.add_first_party_caveat(
840+ "lp.binary-package-build %s" % removeSecurityProxy(context).id)
841+ return macaroon
842+
843+ def checkMacaroonIssuer(self, macaroon):
844+ """See `IMacaroonIssuer`."""
845+ if macaroon.location != config.vhost.mainsite.hostname:
846+ return False
847+ try:
848+ verifier = Verifier()
849+ verifier.satisfy_general(
850+ lambda caveat: caveat.startswith("lp.binary-package-build "))
851+ return verifier.verify(macaroon, self._root_secret)
852+ except Exception:
853+ return False
854+
855+ def verifyMacaroon(self, macaroon, context):
856+ """See `IMacaroonIssuer`.
857+
858+ For verification, the context is a `LibraryFileAlias` ID. We check
859+ that the file is one of those required to build the
860+ `IBinaryPackageBuild` that is the context of the macaroon, and that
861+ the context build is currently building.
862+ """
863+ # Circular import.
864+ from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
865+
866+ if not isinstance(context, int):
867+ return False
868+ if not self.checkMacaroonIssuer(macaroon):
869+ return False
870+
871+ def verify_build(caveat):
872+ prefix = "lp.binary-package-build "
873+ if not caveat.startswith(prefix):
874+ return False
875+ try:
876+ build_id = int(caveat[len(prefix):])
877+ except ValueError:
878+ return False
879+ return not IStore(BinaryPackageBuild).find(
880+ BinaryPackageBuild,
881+ BinaryPackageBuild.id == build_id,
882+ BinaryPackageBuild.source_package_release_id ==
883+ SourcePackageRelease.id,
884+ SourcePackageReleaseFile.sourcepackagereleaseID ==
885+ SourcePackageRelease.id,
886+ SourcePackageReleaseFile.libraryfileID == context,
887+ BinaryPackageBuild.status == BuildStatus.BUILDING).is_empty()
888+
889+ try:
890+ verifier = Verifier()
891+ verifier.satisfy_general(verify_build)
892+ return verifier.verify(macaroon, self._root_secret)
893+ except Exception:
894+ return False
895
896=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuild.py'
897--- lib/lp/soyuz/tests/test_binarypackagebuild.py 2018-02-09 14:56:43 +0000
898+++ lib/lp/soyuz/tests/test_binarypackagebuild.py 2018-11-02 11:53:31 +0000
899@@ -10,8 +10,13 @@
900 timedelta,
901 )
902
903+from pymacaroons import Macaroon
904 import pytz
905 from simplejson import dumps
906+from testtools.matchers import (
907+ MatchesListwise,
908+ MatchesStructure,
909+ )
910 from zope.component import getUtility
911 from zope.security.proxy import removeSecurityProxy
912
913@@ -22,7 +27,9 @@
914 from lp.registry.interfaces.pocket import PackagePublishingPocket
915 from lp.registry.interfaces.series import SeriesStatus
916 from lp.registry.interfaces.sourcepackage import SourcePackageUrgency
917+from lp.services.config import config
918 from lp.services.log.logger import DevNullLogger
919+from lp.services.macaroons.interfaces import IMacaroonIssuer
920 from lp.services.webapp.interaction import ANONYMOUS
921 from lp.services.webapp.interfaces import OAuthPermission
922 from lp.soyuz.enums import (
923@@ -897,3 +904,129 @@
924 with anonymous_logged_in():
925 self.assertScoreWriteableByTeam(
926 archive, getUtility(ILaunchpadCelebrities).ppa_admin)
927+
928+
929+class TestBinaryPackageBuildMacaroonIssuer(TestCaseWithFactory):
930+ """Test BinaryPackageBuild macaroon issuing and verification."""
931+
932+ layer = LaunchpadZopelessLayer
933+
934+ def setUp(self):
935+ super(TestBinaryPackageBuildMacaroonIssuer, self).setUp()
936+ self.pushConfig(
937+ "launchpad", internal_macaroon_secret_key="some-secret")
938+
939+ def test_issueMacaroon_refuses_public_archive(self):
940+ build = self.factory.makeBinaryPackageBuild()
941+ issuer = getUtility(IMacaroonIssuer, "binary-package-build")
942+ self.assertRaises(
943+ AssertionError, removeSecurityProxy(issuer).issueMacaroon, build)
944+
945+ def test_issueMacaroon_good(self):
946+ build = self.factory.makeBinaryPackageBuild(
947+ archive=self.factory.makeArchive(private=True))
948+ issuer = getUtility(IMacaroonIssuer, "binary-package-build")
949+ macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
950+ self.assertEqual("launchpad.dev", macaroon.location)
951+ self.assertEqual("binary-package-build", macaroon.identifier)
952+ self.assertThat(macaroon.caveats, MatchesListwise([
953+ MatchesStructure.byEquality(
954+ caveat_id="lp.binary-package-build %s" % build.id),
955+ ]))
956+
957+ def test_checkMacaroonIssuer_good(self):
958+ build = self.factory.makeBinaryPackageBuild(
959+ archive=self.factory.makeArchive(private=True))
960+ issuer = getUtility(IMacaroonIssuer, "binary-package-build")
961+ macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
962+ self.assertTrue(issuer.checkMacaroonIssuer(macaroon))
963+
964+ def test_checkMacaroonIssuer_wrong_location(self):
965+ issuer = getUtility(IMacaroonIssuer, "binary-package-build")
966+ macaroon = Macaroon(
967+ location="another-location",
968+ key=removeSecurityProxy(issuer)._root_secret)
969+ self.assertFalse(issuer.checkMacaroonIssuer(macaroon))
970+
971+ def test_checkMacaroonIssuer_wrong_key(self):
972+ issuer = getUtility(IMacaroonIssuer, "binary-package-build")
973+ macaroon = Macaroon(
974+ location=config.vhost.mainsite.hostname, key="another-secret")
975+ self.assertFalse(issuer.checkMacaroonIssuer(macaroon))
976+
977+ def test_verifyMacaroon_good(self):
978+ build = self.factory.makeBinaryPackageBuild(
979+ archive=self.factory.makeArchive(private=True))
980+ sprf = self.factory.makeSourcePackageReleaseFile(
981+ sourcepackagerelease=build.source_package_release)
982+ lfa_id = sprf.libraryfile.id
983+ build.updateStatus(BuildStatus.BUILDING)
984+ issuer = removeSecurityProxy(
985+ getUtility(IMacaroonIssuer, "binary-package-build"))
986+ macaroon = issuer.issueMacaroon(build)
987+ self.assertTrue(issuer.verifyMacaroon(macaroon, lfa_id))
988+
989+ def test_verifyMacaroon_wrong_location(self):
990+ build = self.factory.makeBinaryPackageBuild(
991+ archive=self.factory.makeArchive(private=True))
992+ sprf = self.factory.makeSourcePackageReleaseFile(
993+ sourcepackagerelease=build.source_package_release)
994+ lfa_id = sprf.libraryfile.id
995+ build.updateStatus(BuildStatus.BUILDING)
996+ issuer = removeSecurityProxy(
997+ getUtility(IMacaroonIssuer, "binary-package-build"))
998+ macaroon = Macaroon(
999+ location="another-location", key=issuer._root_secret)
1000+ self.assertFalse(issuer.verifyMacaroon(macaroon, lfa_id))
1001+
1002+ def test_verifyMacaroon_wrong_key(self):
1003+ build = self.factory.makeBinaryPackageBuild(
1004+ archive=self.factory.makeArchive(private=True))
1005+ sprf = self.factory.makeSourcePackageReleaseFile(
1006+ sourcepackagerelease=build.source_package_release)
1007+ lfa_id = sprf.libraryfile.id
1008+ build.updateStatus(BuildStatus.BUILDING)
1009+ issuer = removeSecurityProxy(
1010+ getUtility(IMacaroonIssuer, "binary-package-build"))
1011+ macaroon = Macaroon(
1012+ location=config.vhost.mainsite.hostname, key="another-secret")
1013+ self.assertFalse(issuer.verifyMacaroon(macaroon, lfa_id))
1014+
1015+ def test_verifyMacaroon_not_building(self):
1016+ build = self.factory.makeBinaryPackageBuild(
1017+ archive=self.factory.makeArchive(private=True))
1018+ sprf = self.factory.makeSourcePackageReleaseFile(
1019+ sourcepackagerelease=build.source_package_release)
1020+ lfa_id = sprf.libraryfile.id
1021+ issuer = removeSecurityProxy(
1022+ getUtility(IMacaroonIssuer, "binary-package-build"))
1023+ macaroon = issuer.issueMacaroon(build)
1024+ self.assertFalse(issuer.verifyMacaroon(macaroon, lfa_id))
1025+
1026+ def test_verifyMacaroon_wrong_build(self):
1027+ build = self.factory.makeBinaryPackageBuild(
1028+ archive=self.factory.makeArchive(private=True))
1029+ sprf = self.factory.makeSourcePackageReleaseFile(
1030+ sourcepackagerelease=build.source_package_release)
1031+ lfa_id = sprf.libraryfile.id
1032+ build.updateStatus(BuildStatus.BUILDING)
1033+ other_build = self.factory.makeBinaryPackageBuild(
1034+ archive=self.factory.makeArchive(private=True))
1035+ other_build.updateStatus(BuildStatus.BUILDING)
1036+ issuer = removeSecurityProxy(
1037+ getUtility(IMacaroonIssuer, "binary-package-build"))
1038+ macaroon = issuer.issueMacaroon(other_build)
1039+ self.assertFalse(issuer.verifyMacaroon(macaroon, lfa_id))
1040+
1041+ def test_verifyMacaroon_wrong_file(self):
1042+ build = self.factory.makeBinaryPackageBuild(
1043+ archive=self.factory.makeArchive(private=True))
1044+ self.factory.makeSourcePackageReleaseFile(
1045+ sourcepackagerelease=build.source_package_release)
1046+ lfa = self.factory.makeLibraryFileAlias()
1047+ lfa_id = lfa.id
1048+ build.updateStatus(BuildStatus.BUILDING)
1049+ issuer = removeSecurityProxy(
1050+ getUtility(IMacaroonIssuer, "binary-package-build"))
1051+ macaroon = issuer.issueMacaroon(build)
1052+ self.assertFalse(issuer.verifyMacaroon(macaroon, lfa_id))