Merge lp:~cjwatson/launchpad/snap-build-macaroon into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18942
Proposed branch: lp:~cjwatson/launchpad/snap-build-macaroon
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/librarian-accept-macaroon
Diff against target: 977 lines (+469/-210)
11 files modified
lib/lp/code/model/codeimportjob.py (+30/-42)
lib/lp/code/model/tests/test_codeimportjob.py (+27/-22)
lib/lp/code/xmlrpc/git.py (+2/-2)
lib/lp/services/authserver/tests/test_authserver.py (+23/-38)
lib/lp/services/macaroons/interfaces.py (+19/-10)
lib/lp/services/macaroons/model.py (+132/-0)
lib/lp/snappy/configure.zcml (+8/-0)
lib/lp/snappy/model/snapbuild.py (+58/-1)
lib/lp/snappy/tests/test_snapbuild.py (+121/-1)
lib/lp/soyuz/model/binarypackagebuild.py (+43/-72)
lib/lp/soyuz/tests/test_binarypackagebuild.py (+6/-22)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-build-macaroon
Reviewer Review Type Date Requested Status
William Grant (community) code Approve
Review via email: mp+364333@code.launchpad.net

Commit message

Add a snap-build macaroon issuer.

Description of the change

I factored out some common code into a base class, since there are now three rather similar implementations.

Nothing uses this yet, but I'm planning for SnapBuildBehaviour to do so via an extension to the authserver.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/model/codeimportjob.py'
--- lib/lp/code/model/codeimportjob.py 2018-11-01 18:03:06 +0000
+++ lib/lp/code/model/codeimportjob.py 2019-04-24 16:19:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Database classes for the CodeImportJob table."""4"""Database classes for the CodeImportJob table."""
@@ -12,10 +12,6 @@
1212
13import datetime13import datetime
1414
15from pymacaroons import (
16 Macaroon,
17 Verifier,
18 )
19from sqlobject import (15from sqlobject import (
20 ForeignKey,16 ForeignKey,
21 IntCol,17 IntCol,
@@ -58,7 +54,11 @@
58 SQLBase,54 SQLBase,
59 sqlvalues,55 sqlvalues,
60 )56 )
61from lp.services.macaroons.interfaces import IMacaroonIssuer57from lp.services.macaroons.interfaces import (
58 BadMacaroonContext,
59 IMacaroonIssuer,
60 )
61from lp.services.macaroons.model import MacaroonIssuerBase
6262
6363
64@implementer(ICodeImportJob)64@implementer(ICodeImportJob)
@@ -409,7 +409,9 @@
409409
410410
411@implementer(IMacaroonIssuer)411@implementer(IMacaroonIssuer)
412class CodeImportJobMacaroonIssuer:412class CodeImportJobMacaroonIssuer(MacaroonIssuerBase):
413
414 identifier = "code-import-job"
413415
414 @property416 @property
415 def _root_secret(self):417 def _root_secret(self):
@@ -423,38 +425,24 @@
423 "launchpad.internal_macaroon_secret_key not configured.")425 "launchpad.internal_macaroon_secret_key not configured.")
424 return secret426 return secret
425427
426 def issueMacaroon(self, context):428 def checkIssuingContext(self, context):
427 """See `IMacaroonIssuer`."""429 """See `MacaroonIssuerBase`."""
428 assert context.code_import.git_repository is not None430 if context.code_import.git_repository is None:
429 macaroon = Macaroon(431 raise BadMacaroonContext(
430 location=config.vhost.mainsite.hostname,432 context, "context.code_import.git_repository is None")
431 identifier="code-import-job", key=self._root_secret)433 return context.id
432 macaroon.add_first_party_caveat("lp.code-import-job %s" % context.id)434
433 return macaroon435 def checkVerificationContext(self, context):
434436 """See `MacaroonIssuerBase`."""
435 def checkMacaroonIssuer(self, macaroon):437 if (not ICodeImportJob.providedBy(context) or
436 """See `IMacaroonIssuer`."""438 context.state != CodeImportJobState.RUNNING):
437 if macaroon.location != config.vhost.mainsite.hostname:439 raise BadMacaroonContext(context)
438 return False440 return context
439 try:441
440 verifier = Verifier()442 def verifyPrimaryCaveat(self, caveat_value, context):
441 verifier.satisfy_general(443 """See `MacaroonIssuerBase`."""
442 lambda caveat: caveat.startswith("lp.code-import-job "))444 if context is None:
443 return verifier.verify(macaroon, self._root_secret)445 # We're only verifying that the macaroon could be valid for some
444 except Exception:446 # context.
445 return False447 return True
446448 return caveat_value == str(context.id)
447 def verifyMacaroon(self, macaroon, context):
448 """See `IMacaroonIssuer`."""
449 if not ICodeImportJob.providedBy(context):
450 return False
451 if not self.checkMacaroonIssuer(macaroon):
452 return False
453 try:
454 verifier = Verifier()
455 verifier.satisfy_exact("lp.code-import-job %s" % context.id)
456 return (
457 verifier.verify(macaroon, self._root_secret) and
458 context.state == CodeImportJobState.RUNNING)
459 except Exception:
460 return False
461449
=== modified file 'lib/lp/code/model/tests/test_codeimportjob.py'
--- lib/lp/code/model/tests/test_codeimportjob.py 2018-11-01 18:03:06 +0000
+++ lib/lp/code/model/tests/test_codeimportjob.py 2019-04-24 16:19:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2018 Canonical Ltd. This software is licensed under the1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Unit tests for CodeImportJob and CodeImportJobWorkflow."""4"""Unit tests for CodeImportJob and CodeImportJobWorkflow."""
@@ -59,7 +59,10 @@
59from lp.services.database.sqlbase import get_transaction_timestamp59from lp.services.database.sqlbase import get_transaction_timestamp
60from lp.services.librarian.interfaces import ILibraryFileAliasSet60from lp.services.librarian.interfaces import ILibraryFileAliasSet
61from lp.services.librarian.interfaces.client import ILibrarianClient61from lp.services.librarian.interfaces.client import ILibrarianClient
62from lp.services.macaroons.interfaces import IMacaroonIssuer62from lp.services.macaroons.interfaces import (
63 BadMacaroonContext,
64 IMacaroonIssuer,
65 )
63from lp.services.webapp import canonical_url66from lp.services.webapp import canonical_url
64from lp.testing import (67from lp.testing import (
65 ANONYMOUS,68 ANONYMOUS,
@@ -1285,7 +1288,7 @@
1285 job = self.makeJob(target_rcs_type=TargetRevisionControlSystems.BZR)1288 job = self.makeJob(target_rcs_type=TargetRevisionControlSystems.BZR)
1286 issuer = getUtility(IMacaroonIssuer, "code-import-job")1289 issuer = getUtility(IMacaroonIssuer, "code-import-job")
1287 self.assertRaises(1290 self.assertRaises(
1288 AssertionError, removeSecurityProxy(issuer).issueMacaroon, job)1291 BadMacaroonContext, removeSecurityProxy(issuer).issueMacaroon, job)
12891292
1290 def test_issueMacaroon_good(self):1293 def test_issueMacaroon_good(self):
1291 job = self.makeJob()1294 job = self.makeJob()
@@ -1303,25 +1306,6 @@
1303 self.pushConfig("codeimport", macaroon_secret_key="some-secret")1306 self.pushConfig("codeimport", macaroon_secret_key="some-secret")
1304 self.test_issueMacaroon_good()1307 self.test_issueMacaroon_good()
13051308
1306 def test_checkMacaroonIssuer_good(self):
1307 job = self.makeJob()
1308 issuer = getUtility(IMacaroonIssuer, "code-import-job")
1309 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
1310 self.assertTrue(issuer.checkMacaroonIssuer(macaroon))
1311
1312 def test_checkMacaroonIssuer_wrong_location(self):
1313 issuer = getUtility(IMacaroonIssuer, "code-import-job")
1314 macaroon = Macaroon(
1315 location="another-location",
1316 key=removeSecurityProxy(issuer)._root_secret)
1317 self.assertFalse(issuer.checkMacaroonIssuer(macaroon))
1318
1319 def test_checkMacaroonIssuer_wrong_key(self):
1320 issuer = getUtility(IMacaroonIssuer, "code-import-job")
1321 macaroon = Macaroon(
1322 location=config.vhost.mainsite.hostname, key="another-secret")
1323 self.assertFalse(issuer.checkMacaroonIssuer(macaroon))
1324
1325 def test_verifyMacaroon_good(self):1309 def test_verifyMacaroon_good(self):
1326 machine = self.factory.makeCodeImportMachine(set_online=True)1310 machine = self.factory.makeCodeImportMachine(set_online=True)
1327 job = self.makeJob()1311 job = self.makeJob()
@@ -1330,6 +1314,23 @@
1330 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)1314 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
1331 self.assertTrue(issuer.verifyMacaroon(macaroon, job))1315 self.assertTrue(issuer.verifyMacaroon(macaroon, job))
13321316
1317 def test_verifyMacaroon_good_no_context(self):
1318 machine = self.factory.makeCodeImportMachine(set_online=True)
1319 job = self.makeJob()
1320 issuer = getUtility(IMacaroonIssuer, "code-import-job")
1321 getUtility(ICodeImportJobWorkflow).startJob(job, machine)
1322 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
1323 self.assertTrue(
1324 issuer.verifyMacaroon(macaroon, None, require_context=False))
1325
1326 def test_verifyMacaroon_no_context_but_require_context(self):
1327 machine = self.factory.makeCodeImportMachine(set_online=True)
1328 job = self.makeJob()
1329 issuer = getUtility(IMacaroonIssuer, "code-import-job")
1330 getUtility(ICodeImportJobWorkflow).startJob(job, machine)
1331 macaroon = removeSecurityProxy(issuer).issueMacaroon(job)
1332 self.assertFalse(issuer.verifyMacaroon(macaroon, None))
1333
1333 def test_verifyMacaroon_wrong_location(self):1334 def test_verifyMacaroon_wrong_location(self):
1334 machine = self.factory.makeCodeImportMachine(set_online=True)1335 machine = self.factory.makeCodeImportMachine(set_online=True)
1335 job = self.makeJob()1336 job = self.makeJob()
@@ -1339,6 +1340,8 @@
1339 location="another-location",1340 location="another-location",
1340 key=removeSecurityProxy(issuer)._root_secret)1341 key=removeSecurityProxy(issuer)._root_secret)
1341 self.assertFalse(issuer.verifyMacaroon(macaroon, job))1342 self.assertFalse(issuer.verifyMacaroon(macaroon, job))
1343 self.assertFalse(
1344 issuer.verifyMacaroon(macaroon, None, require_context=False))
13421345
1343 def test_verifyMacaroon_wrong_key(self):1346 def test_verifyMacaroon_wrong_key(self):
1344 machine = self.factory.makeCodeImportMachine(set_online=True)1347 machine = self.factory.makeCodeImportMachine(set_online=True)
@@ -1348,6 +1351,8 @@
1348 macaroon = Macaroon(1351 macaroon = Macaroon(
1349 location=config.vhost.mainsite.hostname, key="another-secret")1352 location=config.vhost.mainsite.hostname, key="another-secret")
1350 self.assertFalse(issuer.verifyMacaroon(macaroon, job))1353 self.assertFalse(issuer.verifyMacaroon(macaroon, job))
1354 self.assertFalse(
1355 issuer.verifyMacaroon(macaroon, None, require_context=False))
13511356
1352 def test_verifyMacaroon_not_running(self):1357 def test_verifyMacaroon_not_running(self):
1353 job = self.makeJob()1358 job = self.makeJob()
13541359
=== modified file 'lib/lp/code/xmlrpc/git.py'
--- lib/lp/code/xmlrpc/git.py 2019-04-23 12:30:16 +0000
+++ lib/lp/code/xmlrpc/git.py 2019-04-24 16:19:42 +0000
@@ -101,9 +101,9 @@
101 job = code_import.import_job101 job = code_import.import_job
102 if job is None:102 if job is None:
103 return False103 return False
104 return issuer.verifyMacaroon(macaroon, job)
105 else:104 else:
106 return issuer.checkMacaroonIssuer(macaroon)105 job = None
106 return issuer.verifyMacaroon(macaroon, job, require_context=False)
107107
108 def _performLookup(self, requester, path, auth_params):108 def _performLookup(self, requester, path, auth_params):
109 repository, extra_path = getUtility(IGitLookup).getByPath(path)109 repository, extra_path = getUtility(IGitLookup).getByPath(path)
110110
=== modified file 'lib/lp/services/authserver/tests/test_authserver.py'
--- lib/lp/services/authserver/tests/test_authserver.py 2019-04-23 13:50:35 +0000
+++ lib/lp/services/authserver/tests/test_authserver.py 2019-04-24 16:19:42 +0000
@@ -5,10 +5,7 @@
55
6__metaclass__ = type6__metaclass__ = type
77
8from pymacaroons import (8from pymacaroons import Macaroon
9 Macaroon,
10 Verifier,
11 )
12from storm.sqlobject import SQLObjectNotFound9from storm.sqlobject import SQLObjectNotFound
13from testtools.matchers import Is10from testtools.matchers import Is
14from zope.component import getUtility11from zope.component import getUtility
@@ -21,7 +18,11 @@
21 ILibraryFileAlias,18 ILibraryFileAlias,
22 ILibraryFileAliasSet,19 ILibraryFileAliasSet,
23 )20 )
24from lp.services.macaroons.interfaces import IMacaroonIssuer21from lp.services.macaroons.interfaces import (
22 BadMacaroonContext,
23 IMacaroonIssuer,
24 )
25from lp.services.macaroons.model import MacaroonIssuerBase
25from lp.testing import (26from lp.testing import (
26 person_logged_in,27 person_logged_in,
27 TestCase,28 TestCase,
@@ -78,42 +79,26 @@
7879
7980
80@implementer(IMacaroonIssuer)81@implementer(IMacaroonIssuer)
81class DummyMacaroonIssuer:82class DummyMacaroonIssuer(MacaroonIssuerBase):
8283
84 identifier = 'test'
83 _root_secret = 'test'85 _root_secret = 'test'
8486
85 def issueMacaroon(self, context):87 def checkIssuingContext(self, context):
86 """See `IMacaroonIssuer`."""88 """See `MacaroonIssuerBase`."""
87 macaroon = Macaroon(89 if not ILibraryFileAlias.providedBy(context):
88 location=config.vhost.mainsite.hostname, identifier='test',90 raise BadMacaroonContext(context)
89 key=self._root_secret)91 return context.id
90 macaroon.add_first_party_caveat('test %s' % context.id)92
91 return macaroon93 def checkVerificationContext(self, context):
9294 """See `IMacaroonIssuerBase`."""
93 def checkMacaroonIssuer(self, macaroon):95 if not ILibraryFileAlias.providedBy(context):
94 """See `IMacaroonIssuer`."""96 raise BadMacaroonContext(context)
95 if macaroon.location != config.vhost.mainsite.hostname:97 return context
96 return False98
97 try:99 def verifyPrimaryCaveat(self, caveat_value, context):
98 verifier = Verifier()100 """See `MacaroonIssuerBase`."""
99 verifier.satisfy_general(101 return caveat_value == str(context.id)
100 lambda caveat: caveat.startswith('test '))
101 return verifier.verify(macaroon, self._root_secret)
102 except Exception:
103 return False
104
105 def verifyMacaroon(self, macaroon, context):
106 """See `IMacaroonIssuer`."""
107 if not ILibraryFileAlias.providedBy(context):
108 return False
109 if not self.checkMacaroonIssuer(macaroon):
110 return False
111 try:
112 verifier = Verifier()
113 verifier.satisfy_exact('test %s' % context.id)
114 return verifier.verify(macaroon, self._root_secret)
115 except Exception:
116 return False
117102
118103
119class VerifyMacaroonTests(TestCase):104class VerifyMacaroonTests(TestCase):
120105
=== modified file 'lib/lp/services/macaroons/interfaces.py'
--- lib/lp/services/macaroons/interfaces.py 2016-10-07 15:50:10 +0000
+++ lib/lp/services/macaroons/interfaces.py 2019-04-24 16:19:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the1# Copyright 2016-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Interface to a policy for issuing and verifying macaroons."""4"""Interface to a policy for issuing and verifying macaroons."""
@@ -7,28 +7,36 @@
77
8__metaclass__ = type8__metaclass__ = type
9__all__ = [9__all__ = [
10 'BadMacaroonContext',
10 'IMacaroonIssuer',11 'IMacaroonIssuer',
11 ]12 ]
1213
13from zope.interface import Interface14from zope.interface import Interface
1415
1516
17class BadMacaroonContext(Exception):
18 """The requested context is unsuitable."""
19
20 def __init__(self, context, message=None):
21 if message is None:
22 message = "Cannot handle context %r." % context
23 super(BadMacaroonContext, self).__init__(message)
24 self.context = context
25
26
16class IMacaroonIssuerPublic(Interface):27class IMacaroonIssuerPublic(Interface):
17 """Public interface to a policy for verifying macaroons."""28 """Public interface to a policy for verifying macaroons."""
1829
19 def checkMacaroonIssuer(macaroon):30 def verifyMacaroon(macaroon, context, require_context=True):
20 """Check that `macaroon` was issued by this issuer.
21
22 This does not verify that the macaroon is valid for a given context,
23 only that it could be valid for some context. Use this in the
24 authentication part of an authentication/authorisation API.
25 """
26
27 def verifyMacaroon(macaroon, context):
28 """Verify that `macaroon` is valid for `context`.31 """Verify that `macaroon` is valid for `context`.
2932
30 :param macaroon: A `Macaroon`.33 :param macaroon: A `Macaroon`.
31 :param context: The context to check.34 :param context: The context to check.
35 :param require_context: If True (the default), fail verification if
36 the context is None. If False and the context is None, only
37 verify that the macaroon could be valid for some context. Use
38 this in the authentication part of an
39 authentication/authorisation API.
32 :return: True if `macaroon` is valid for `context`, otherwise False.40 :return: True if `macaroon` is valid for `context`, otherwise False.
33 """41 """
3442
@@ -41,5 +49,6 @@
4149
42 :param context: The context that the returned macaroon should relate50 :param context: The context that the returned macaroon should relate
43 to.51 to.
52 :raises ValueError: if the context is unsuitable.
44 :return: A macaroon.53 :return: A macaroon.
45 """54 """
4655
=== added file 'lib/lp/services/macaroons/model.py'
--- lib/lp/services/macaroons/model.py 1970-01-01 00:00:00 +0000
+++ lib/lp/services/macaroons/model.py 2019-04-24 16:19:42 +0000
@@ -0,0 +1,132 @@
1# Copyright 2016-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Policies for issuing and verifying macaroons."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9__all__ = [
10 "MacaroonIssuerBase",
11 ]
12
13from pymacaroons import (
14 Macaroon,
15 Verifier,
16 )
17from pymacaroons.exceptions import MacaroonVerificationFailedException
18
19from lp.services.config import config
20from lp.services.macaroons.interfaces import BadMacaroonContext
21
22
23class MacaroonIssuerBase:
24 """See `IMacaroonIssuer`."""
25
26 @property
27 def identifier(self):
28 """An identifying name for this issuer."""
29 raise NotImplementedError
30
31 @property
32 def _primary_caveat_name(self):
33 """The name of the primary context caveat issued by this issuer."""
34 return "lp.%s" % self.identifier
35
36 @property
37 def _root_secret(self):
38 secret = config.launchpad.internal_macaroon_secret_key
39 if not secret:
40 raise RuntimeError(
41 "launchpad.internal_macaroon_secret_key not configured.")
42 return secret
43
44 def checkIssuingContext(self, context):
45 """Check that the issuing context is suitable.
46
47 Concrete implementations may implement this method to check that the
48 context of a macaroon issuance is suitable. The returned context is
49 used to create the primary caveat, and may be the same context that
50 was passed in or an adapted one.
51
52 :param context: The context to check.
53 :raises BadMacaroonContext: if the context is unsuitable.
54 :return: The context to use to create the primary caveat.
55 """
56 return context
57
58 def issueMacaroon(self, context):
59 """See `IMacaroonIssuer`.
60
61 Concrete implementations should normally wrap this with some
62 additional checks of and/or changes to the context.
63 """
64 context = self.checkIssuingContext(context)
65 macaroon = Macaroon(
66 location=config.vhost.mainsite.hostname,
67 identifier=self.identifier, key=self._root_secret)
68 macaroon.add_first_party_caveat(
69 "%s %s" % (self._primary_caveat_name, context))
70 return macaroon
71
72 def checkVerificationContext(self, context):
73 """Check that the verification context is suitable.
74
75 Concrete implementations may implement this method to check that the
76 context of a macaroon verification is suitable. The returned
77 context is passed to individual caveat checkers, and may be the same
78 context that was passed in or an adapted one.
79
80 :param context: The context to check.
81 :raises BadMacaroonContext: if the context is unsuitable.
82 :return: The context to pass to individual caveat checkers.
83 """
84 return context
85
86 def verifyPrimaryCaveat(self, caveat_value, context):
87 """Verify the primary context caveat on one of this issuer's macaroons.
88
89 :param caveat_value: The text of the caveat, with this issuer's
90 prefix removed.
91 :param context: The context to check.
92 :return: True if this caveat is allowed, otherwise False.
93 """
94 raise NotImplementedError
95
96 def verifyMacaroon(self, macaroon, context, require_context=True):
97 """See `IMacaroonIssuer`."""
98 if macaroon.location != config.vhost.mainsite.hostname:
99 return False
100 if require_context and context is None:
101 return False
102 if context is not None:
103 try:
104 context = self.checkVerificationContext(context)
105 except BadMacaroonContext:
106 return False
107
108 def verify(caveat):
109 try:
110 caveat_name, caveat_value = caveat.split(" ", 1)
111 except ValueError:
112 return False
113 if caveat_name == self._primary_caveat_name:
114 checker = self.verifyPrimaryCaveat
115 else:
116 # XXX cjwatson 2019-04-09: For now we just fail closed if
117 # there are any other caveats, which is good enough for
118 # internal use.
119 return False
120 return checker(caveat_value, context)
121
122 try:
123 verifier = Verifier()
124 verifier.satisfy_general(verify)
125 return verifier.verify(macaroon, self._root_secret)
126 # XXX cjwatson 2019-04-24: This can currently raise a number of
127 # other exceptions in the presence of non-well-formed input data,
128 # but most of them are too broad to reasonably catch so we let them
129 # turn into OOPSes for now. Revisit this once
130 # https://github.com/ecordell/pymacaroons/issues/51 is fixed.
131 except MacaroonVerificationFailedException:
132 return False
0133
=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 2019-02-11 13:23:34 +0000
+++ lib/lp/snappy/configure.zcml 2019-04-24 16:19:42 +0000
@@ -83,6 +83,14 @@
83 <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />83 <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />
84 </securedutility>84 </securedutility>
8585
86 <!-- SnapBuildMacaroonIssuer -->
87 <securedutility
88 class="lp.snappy.model.snapbuild.SnapBuildMacaroonIssuer"
89 provides="lp.services.macaroons.interfaces.IMacaroonIssuer"
90 name="snap-build">
91 <allow interface="lp.services.macaroons.interfaces.IMacaroonIssuerPublic"/>
92 </securedutility>
93
86 <!-- SnapBuildBehaviour -->94 <!-- SnapBuildBehaviour -->
87 <adapter95 <adapter
88 for="lp.snappy.interfaces.snapbuild.ISnapBuild"96 for="lp.snappy.interfaces.snapbuild.ISnapBuild"
8997
=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 2018-12-18 18:14:37 +0000
+++ lib/lp/snappy/model/snapbuild.py 2019-04-24 16:19:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015-2018 Canonical Ltd. This software is licensed under the1# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -35,6 +35,7 @@
35from zope.component.interfaces import ObjectEvent35from zope.component.interfaces import ObjectEvent
36from zope.event import notify36from zope.event import notify
37from zope.interface import implementer37from zope.interface import implementer
38from zope.security.proxy import removeSecurityProxy
3839
39from lp.app.errors import NotFoundError40from lp.app.errors import NotFoundError
40from lp.buildmaster.enums import (41from lp.buildmaster.enums import (
@@ -45,6 +46,7 @@
45from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource46from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
46from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin47from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
47from lp.buildmaster.model.packagebuild import PackageBuildMixin48from lp.buildmaster.model.packagebuild import PackageBuildMixin
49from lp.code.interfaces.gitrepository import IGitRepository
48from lp.registry.interfaces.pocket import PackagePublishingPocket50from lp.registry.interfaces.pocket import PackagePublishingPocket
49from lp.registry.model.distribution import Distribution51from lp.registry.model.distribution import Distribution
50from lp.registry.model.distroseries import DistroSeries52from lp.registry.model.distroseries import DistroSeries
@@ -65,6 +67,11 @@
65 LibraryFileAlias,67 LibraryFileAlias,
66 LibraryFileContent,68 LibraryFileContent,
67 )69 )
70from lp.services.macaroons.interfaces import (
71 BadMacaroonContext,
72 IMacaroonIssuer,
73 )
74from lp.services.macaroons.model import MacaroonIssuerBase
68from lp.services.propertycache import (75from lp.services.propertycache import (
69 cachedproperty,76 cachedproperty,
70 get_property_cache,77 get_property_cache,
@@ -584,3 +591,53 @@
584 SnapBuild, SnapBuild.build_farm_job_id.is_in(591 SnapBuild, SnapBuild.build_farm_job_id.is_in(
585 bfj.id for bfj in build_farm_jobs))592 bfj.id for bfj in build_farm_jobs))
586 return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)593 return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)
594
595
596@implementer(IMacaroonIssuer)
597class SnapBuildMacaroonIssuer(MacaroonIssuerBase):
598
599 identifier = "snap-build"
600
601 def checkIssuingContext(self, context):
602 """See `MacaroonIssuerBase`.
603
604 For issuing, the context is an `ISnapBuild` or its ID.
605 """
606 if ISnapBuild.providedBy(context):
607 pass
608 elif isinstance(context, int):
609 context = getUtility(ISnapBuildSet).getByID(context)
610 else:
611 raise BadMacaroonContext(context)
612 if not removeSecurityProxy(context).is_private:
613 raise BadMacaroonContext(
614 context, "Refusing to issue macaroon for public build.")
615 return removeSecurityProxy(context).id
616
617 def checkVerificationContext(self, context):
618 """See `MacaroonIssuerBase`."""
619 if not IGitRepository.providedBy(context):
620 raise BadMacaroonContext(context)
621 return context
622
623 def verifyPrimaryCaveat(self, caveat_value, context):
624 """See `MacaroonIssuerBase`.
625
626 For verification, the context is an `IGitRepository`. We check that
627 the repository is needed to build the `ISnapBuild` that is the
628 context of the macaroon, and that the context build is currently
629 building.
630 """
631 # Circular import.
632 from lp.snappy.model.snap import Snap
633
634 try:
635 build_id = int(caveat_value)
636 except ValueError:
637 return False
638 return not IStore(SnapBuild).find(
639 SnapBuild,
640 SnapBuild.id == build_id,
641 SnapBuild.snap_id == Snap.id,
642 Snap.git_repository == context,
643 SnapBuild.status == BuildStatus.BUILDING).is_empty()
587644
=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py 2018-12-18 18:23:52 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py 2019-04-24 16:19:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015-2018 Canonical Ltd. This software is licensed under the1# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test snap package build features."""4"""Test snap package build features."""
@@ -22,11 +22,13 @@
22 Equals,22 Equals,
23 Is,23 Is,
24 MatchesDict,24 MatchesDict,
25 MatchesListwise,
25 MatchesStructure,26 MatchesStructure,
26 )27 )
27from zope.component import getUtility28from zope.component import getUtility
28from zope.security.proxy import removeSecurityProxy29from zope.security.proxy import removeSecurityProxy
2930
31from lp.app.enums import InformationType
30from lp.app.errors import NotFoundError32from lp.app.errors import NotFoundError
31from lp.app.interfaces.launchpad import ILaunchpadCelebrities33from lp.app.interfaces.launchpad import ILaunchpadCelebrities
32from lp.buildmaster.enums import BuildStatus34from lp.buildmaster.enums import BuildStatus
@@ -38,6 +40,10 @@
38from lp.services.features.testing import FeatureFixture40from lp.services.features.testing import FeatureFixture
39from lp.services.job.interfaces.job import JobStatus41from lp.services.job.interfaces.job import JobStatus
40from lp.services.librarian.browser import ProxiedLibraryFileAlias42from lp.services.librarian.browser import ProxiedLibraryFileAlias
43from lp.services.macaroons.interfaces import (
44 BadMacaroonContext,
45 IMacaroonIssuer,
46 )
41from lp.services.propertycache import clear_property_cache47from lp.services.propertycache import clear_property_cache
42from lp.services.webapp.interfaces import OAuthPermission48from lp.services.webapp.interfaces import OAuthPermission
43from lp.services.webapp.publisher import canonical_url49from lp.services.webapp.publisher import canonical_url
@@ -774,3 +780,117 @@
774 browser = self.getNonRedirectingBrowser(user=self.person)780 browser = self.getNonRedirectingBrowser(user=self.person)
775 for file_url in file_urls:781 for file_url in file_urls:
776 self.assertCanOpenRedirectedUrl(browser, file_url)782 self.assertCanOpenRedirectedUrl(browser, file_url)
783
784
785class TestSnapBuildMacaroonIssuer(TestCaseWithFactory):
786 """Test SnapBuild macaroon issuing and verification."""
787
788 layer = LaunchpadZopelessLayer
789
790 def setUp(self):
791 super(TestSnapBuildMacaroonIssuer, self).setUp()
792 self.useFixture(FeatureFixture(SNAP_TESTING_FLAGS))
793 self.pushConfig(
794 "launchpad", internal_macaroon_secret_key="some-secret")
795
796 def test_issueMacaroon_refuses_public_snap(self):
797 build = self.factory.makeSnapBuild()
798 issuer = getUtility(IMacaroonIssuer, "snap-build")
799 self.assertRaises(
800 BadMacaroonContext, removeSecurityProxy(issuer).issueMacaroon,
801 build)
802
803 def test_issueMacaroon_good(self):
804 build = self.factory.makeSnapBuild(
805 snap=self.factory.makeSnap(private=True))
806 issuer = getUtility(IMacaroonIssuer, "snap-build")
807 macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
808 self.assertThat(macaroon, MatchesStructure(
809 location=Equals("launchpad.dev"),
810 identifier=Equals("snap-build"),
811 caveats=MatchesListwise([
812 MatchesStructure.byEquality(
813 caveat_id="lp.snap-build %s" % build.id),
814 ])))
815
816 def test_verifyMacaroon_good(self):
817 [ref] = self.factory.makeGitRefs(
818 information_type=InformationType.USERDATA)
819 build = self.factory.makeSnapBuild(
820 snap=self.factory.makeSnap(git_ref=ref, private=True))
821 build.updateStatus(BuildStatus.BUILDING)
822 issuer = removeSecurityProxy(
823 getUtility(IMacaroonIssuer, "snap-build"))
824 macaroon = issuer.issueMacaroon(build)
825 self.assertTrue(issuer.verifyMacaroon(macaroon, ref.repository))
826
827 def test_verifyMacaroon_wrong_location(self):
828 [ref] = self.factory.makeGitRefs(
829 information_type=InformationType.USERDATA)
830 build = self.factory.makeSnapBuild(
831 snap=self.factory.makeSnap(git_ref=ref, private=True))
832 build.updateStatus(BuildStatus.BUILDING)
833 issuer = removeSecurityProxy(
834 getUtility(IMacaroonIssuer, "snap-build"))
835 macaroon = Macaroon(
836 location="another-location", key=issuer._root_secret)
837 self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository))
838
839 def test_verifyMacaroon_wrong_key(self):
840 [ref] = self.factory.makeGitRefs(
841 information_type=InformationType.USERDATA)
842 build = self.factory.makeSnapBuild(
843 snap=self.factory.makeSnap(git_ref=ref, private=True))
844 build.updateStatus(BuildStatus.BUILDING)
845 issuer = removeSecurityProxy(
846 getUtility(IMacaroonIssuer, "snap-build"))
847 macaroon = Macaroon(
848 location=config.vhost.mainsite.hostname, key="another-secret")
849 self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository))
850
851 def test_verifyMacaroon_refuses_branch(self):
852 branch = self.factory.makeAnyBranch(
853 information_type=InformationType.USERDATA)
854 build = self.factory.makeSnapBuild(
855 snap=self.factory.makeSnap(branch=branch, private=True))
856 build.updateStatus(BuildStatus.BUILDING)
857 issuer = removeSecurityProxy(
858 getUtility(IMacaroonIssuer, "snap-build"))
859 macaroon = issuer.issueMacaroon(build)
860 self.assertFalse(issuer.verifyMacaroon(macaroon, branch))
861
862 def test_verifyMacaroon_not_building(self):
863 [ref] = self.factory.makeGitRefs(
864 information_type=InformationType.USERDATA)
865 build = self.factory.makeSnapBuild(
866 snap=self.factory.makeSnap(git_ref=ref, private=True))
867 issuer = removeSecurityProxy(
868 getUtility(IMacaroonIssuer, "snap-build"))
869 macaroon = issuer.issueMacaroon(build)
870 self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository))
871
872 def test_verifyMacaroon_wrong_build(self):
873 [ref] = self.factory.makeGitRefs(
874 information_type=InformationType.USERDATA)
875 build = self.factory.makeSnapBuild(
876 snap=self.factory.makeSnap(git_ref=ref, private=True))
877 build.updateStatus(BuildStatus.BUILDING)
878 other_build = self.factory.makeSnapBuild(
879 snap=self.factory.makeSnap(private=True))
880 other_build.updateStatus(BuildStatus.BUILDING)
881 issuer = removeSecurityProxy(
882 getUtility(IMacaroonIssuer, "snap-build"))
883 macaroon = issuer.issueMacaroon(other_build)
884 self.assertFalse(issuer.verifyMacaroon(macaroon, ref.repository))
885
886 def test_verifyMacaroon_wrong_repository(self):
887 [ref] = self.factory.makeGitRefs(
888 information_type=InformationType.USERDATA)
889 build = self.factory.makeSnapBuild(
890 snap=self.factory.makeSnap(git_ref=ref, private=True))
891 other_repository = self.factory.makeGitRepository()
892 build.updateStatus(BuildStatus.BUILDING)
893 issuer = removeSecurityProxy(
894 getUtility(IMacaroonIssuer, "snap-build"))
895 macaroon = issuer.issueMacaroon(build)
896 self.assertFalse(issuer.verifyMacaroon(macaroon, other_repository))
777897
=== modified file 'lib/lp/soyuz/model/binarypackagebuild.py'
--- lib/lp/soyuz/model/binarypackagebuild.py 2019-04-23 14:00:43 +0000
+++ lib/lp/soyuz/model/binarypackagebuild.py 2019-04-24 16:19:42 +0000
@@ -21,10 +21,6 @@
2121
22import apt_pkg22import apt_pkg
23from debian.deb822 import PkgRelation23from debian.deb822 import PkgRelation
24from pymacaroons import (
25 Macaroon,
26 Verifier,
27 )
28import pytz24import pytz
29from sqlobject import SQLObjectNotFound25from sqlobject import SQLObjectNotFound
30from storm.expr import (26from storm.expr import (
@@ -86,7 +82,11 @@
86 LibraryFileAlias,82 LibraryFileAlias,
87 LibraryFileContent,83 LibraryFileContent,
88 )84 )
89from lp.services.macaroons.interfaces import IMacaroonIssuer85from lp.services.macaroons.interfaces import (
86 BadMacaroonContext,
87 IMacaroonIssuer,
88 )
89from lp.services.macaroons.model import MacaroonIssuerBase
90from lp.soyuz.adapters.buildarch import determine_architectures_to_build90from lp.soyuz.adapters.buildarch import determine_architectures_to_build
91from lp.soyuz.enums import (91from lp.soyuz.enums import (
92 ArchivePurpose,92 ArchivePurpose,
@@ -1374,52 +1374,39 @@
13741374
13751375
1376@implementer(IMacaroonIssuer)1376@implementer(IMacaroonIssuer)
1377class BinaryPackageBuildMacaroonIssuer:1377class BinaryPackageBuildMacaroonIssuer(MacaroonIssuerBase):
1378
1379 identifier = "binary-package-build"
13781380
1379 @property1381 @property
1380 def _root_secret(self):1382 def _primary_caveat_name(self):
1381 secret = config.launchpad.internal_macaroon_secret_key1383 """See `MacaroonIssuerBase`."""
1382 if not secret:
1383 raise RuntimeError(
1384 "launchpad.internal_macaroon_secret_key not configured.")
1385 return secret
1386
1387 def issueMacaroon(self, context):
1388 """See `IMacaroonIssuer`.
1389
1390 For issuing, the context is an `IBinaryPackageBuild`.
1391 """
1392 if not removeSecurityProxy(context).archive.private:
1393 raise ValueError("Refusing to issue macaroon for public build.")
1394 macaroon = Macaroon(
1395 location=config.vhost.mainsite.hostname,
1396 identifier="binary-package-build", key=self._root_secret)
1397 # The "lp.principal" prefix indicates that this caveat constrains1384 # The "lp.principal" prefix indicates that this caveat constrains
1398 # the macaroon to access only resources that should be accessible1385 # the macaroon to access only resources that should be accessible
1399 # when acting on behalf of the named build, rather than to access1386 # when acting on behalf of the named build, rather than to access
1400 # the named build directly.1387 # the named build directly.
1401 macaroon.add_first_party_caveat(1388 return "lp.principal.binary-package-build"
1402 "lp.principal.binary-package-build %s" %1389
1403 removeSecurityProxy(context).id)1390 def checkIssuingContext(self, context):
1404 return macaroon1391 """See `MacaroonIssuerBase`.
14051392
1406 def checkMacaroonIssuer(self, macaroon):1393 For issuing, the context is an `IBinaryPackageBuild`.
1407 """See `IMacaroonIssuer`."""1394 """
1408 if macaroon.location != config.vhost.mainsite.hostname:1395 if not removeSecurityProxy(context).archive.private:
1409 return False1396 raise BadMacaroonContext(
1410 try:1397 context, "Refusing to issue macaroon for public build.")
1411 verifier = Verifier()1398 return removeSecurityProxy(context).id
1412 verifier.satisfy_general(1399
1413 lambda caveat: caveat.startswith(1400 def checkVerificationContext(self, context):
1414 "lp.principal.binary-package-build "))1401 """See `MacaroonIssuerBase`."""
1415 return verifier.verify(macaroon, self._root_secret)1402 if not ILibraryFileAlias.providedBy(context):
1416 except Exception:1403 raise BadMacaroonContext(context)
1417 return False1404 return context
14181405
1419 def verifyMacaroon(self, macaroon, context):1406 def verifyPrimaryCaveat(self, caveat_value, context):
1420 """See `IMacaroonIssuer`.1407 """See `MacaroonIssuerBase`.
14211408
1422 For verification, the context is a `LibraryFileAlias`. We check1409 For verification, the context is an `ILibraryFileAlias`. We check
1423 that the file is one of those required to build the1410 that the file is one of those required to build the
1424 `IBinaryPackageBuild` that is the context of the macaroon, and that1411 `IBinaryPackageBuild` that is the context of the macaroon, and that
1425 the context build is currently building.1412 the context build is currently building.
@@ -1427,32 +1414,16 @@
1427 # Circular import.1414 # Circular import.
1428 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease1415 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
14291416
1430 if not ILibraryFileAlias.providedBy(context):
1431 return False
1432 if not self.checkMacaroonIssuer(macaroon):
1433 return False
1434
1435 def verify_build(caveat):
1436 prefix = "lp.principal.binary-package-build "
1437 if not caveat.startswith(prefix):
1438 return False
1439 try:
1440 build_id = int(caveat[len(prefix):])
1441 except ValueError:
1442 return False
1443 return not IStore(BinaryPackageBuild).find(
1444 BinaryPackageBuild,
1445 BinaryPackageBuild.id == build_id,
1446 BinaryPackageBuild.source_package_release_id ==
1447 SourcePackageRelease.id,
1448 SourcePackageReleaseFile.sourcepackagereleaseID ==
1449 SourcePackageRelease.id,
1450 SourcePackageReleaseFile.libraryfile == context,
1451 BinaryPackageBuild.status == BuildStatus.BUILDING).is_empty()
1452
1453 try:1417 try:
1454 verifier = Verifier()1418 build_id = int(caveat_value)
1455 verifier.satisfy_general(verify_build)1419 except ValueError:
1456 return verifier.verify(macaroon, self._root_secret)
1457 except Exception:
1458 return False1420 return False
1421 return not IStore(BinaryPackageBuild).find(
1422 BinaryPackageBuild,
1423 BinaryPackageBuild.id == build_id,
1424 BinaryPackageBuild.source_package_release_id ==
1425 SourcePackageRelease.id,
1426 SourcePackageReleaseFile.sourcepackagereleaseID ==
1427 SourcePackageRelease.id,
1428 SourcePackageReleaseFile.libraryfile == context,
1429 BinaryPackageBuild.status == BuildStatus.BUILDING).is_empty()
14591430
=== modified file 'lib/lp/soyuz/tests/test_binarypackagebuild.py'
--- lib/lp/soyuz/tests/test_binarypackagebuild.py 2019-04-23 14:00:43 +0000
+++ lib/lp/soyuz/tests/test_binarypackagebuild.py 2019-04-24 16:19:42 +0000
@@ -29,7 +29,10 @@
29from lp.registry.interfaces.sourcepackage import SourcePackageUrgency29from lp.registry.interfaces.sourcepackage import SourcePackageUrgency
30from lp.services.config import config30from lp.services.config import config
31from lp.services.log.logger import DevNullLogger31from lp.services.log.logger import DevNullLogger
32from lp.services.macaroons.interfaces import IMacaroonIssuer32from lp.services.macaroons.interfaces import (
33 BadMacaroonContext,
34 IMacaroonIssuer,
35 )
33from lp.services.webapp.interaction import ANONYMOUS36from lp.services.webapp.interaction import ANONYMOUS
34from lp.services.webapp.interfaces import OAuthPermission37from lp.services.webapp.interfaces import OAuthPermission
35from lp.soyuz.enums import (38from lp.soyuz.enums import (
@@ -920,7 +923,8 @@
920 build = self.factory.makeBinaryPackageBuild()923 build = self.factory.makeBinaryPackageBuild()
921 issuer = getUtility(IMacaroonIssuer, "binary-package-build")924 issuer = getUtility(IMacaroonIssuer, "binary-package-build")
922 self.assertRaises(925 self.assertRaises(
923 ValueError, removeSecurityProxy(issuer).issueMacaroon, build)926 BadMacaroonContext, removeSecurityProxy(issuer).issueMacaroon,
927 build)
924928
925 def test_issueMacaroon_good(self):929 def test_issueMacaroon_good(self):
926 build = self.factory.makeBinaryPackageBuild(930 build = self.factory.makeBinaryPackageBuild(
@@ -934,26 +938,6 @@
934 caveat_id="lp.principal.binary-package-build %s" % build.id),938 caveat_id="lp.principal.binary-package-build %s" % build.id),
935 ]))939 ]))
936940
937 def test_checkMacaroonIssuer_good(self):
938 build = self.factory.makeBinaryPackageBuild(
939 archive=self.factory.makeArchive(private=True))
940 issuer = getUtility(IMacaroonIssuer, "binary-package-build")
941 macaroon = removeSecurityProxy(issuer).issueMacaroon(build)
942 self.assertTrue(issuer.checkMacaroonIssuer(macaroon))
943
944 def test_checkMacaroonIssuer_wrong_location(self):
945 issuer = getUtility(IMacaroonIssuer, "binary-package-build")
946 macaroon = Macaroon(
947 location="another-location",
948 key=removeSecurityProxy(issuer)._root_secret)
949 self.assertFalse(issuer.checkMacaroonIssuer(macaroon))
950
951 def test_checkMacaroonIssuer_wrong_key(self):
952 issuer = getUtility(IMacaroonIssuer, "binary-package-build")
953 macaroon = Macaroon(
954 location=config.vhost.mainsite.hostname, key="another-secret")
955 self.assertFalse(issuer.checkMacaroonIssuer(macaroon))
956
957 def test_verifyMacaroon_good(self):941 def test_verifyMacaroon_good(self):
958 build = self.factory.makeBinaryPackageBuild(942 build = self.factory.makeBinaryPackageBuild(
959 archive=self.factory.makeArchive(private=True))943 archive=self.factory.makeArchive(private=True))