Merge lp:~cjwatson/launchpad/authserver-macaroon into lp:launchpad

Proposed by Colin Watson on 2018-05-10
Status: Merged
Merged at revision: 18725
Proposed branch: lp:~cjwatson/launchpad/authserver-macaroon
Merge into: lp:launchpad
Diff against target: 197 lines (+123/-5)
3 files modified
lib/lp/services/authserver/interfaces.py (+12/-1)
lib/lp/services/authserver/tests/test_authserver.py (+90/-2)
lib/lp/services/authserver/xmlrpc.py (+21/-2)
To merge this branch: bzr merge lp:~cjwatson/launchpad/authserver-macaroon
Reviewer Review Type Date Requested Status
William Grant code 2018-05-10 Approve on 2018-07-12
Review via email: mp+345354@code.launchpad.net

Commit message

Add an authserver endpoint to verify macaroons.

Description of the change

This is intended to support a reworking of https://code.launchpad.net/~cjwatson/launchpad/librarian-accept-macaroon/+merge/345079.

"Returns True or a fault" is a bit of a weird interface, but there isn't really anything else to return, None is annoying in XML-RPC, and I think this is a bit more idiomatic for our XML-RPC endpoints than "returns True or False" would be.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/services/authserver/interfaces.py'
--- lib/lp/services/authserver/interfaces.py 2015-10-14 15:22:01 +0000
+++ lib/lp/services/authserver/interfaces.py 2018-05-10 10:45:27 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 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 for the XML-RPC authentication server."""4"""Interface for the XML-RPC authentication server."""
@@ -27,6 +27,17 @@
27 person with the given name.27 person with the given name.
28 """28 """
2929
30 def verifyMacaroon(macaroon_raw, context):
31 """Verify that `macaroon_raw` grants access to `context`.
32
33 :param macaroon_raw: A serialised macaroon.
34 :param context: The context to check. Note that this is passed over
35 XML-RPC, so it should be plain data (e.g. an ID) rather than a
36 database object.
37 :return: True if the macaroon grants access to `context`, otherwise
38 an `Unauthorized` fault.
39 """
40
3041
31class IAuthServerApplication(ILaunchpadApplication):42class IAuthServerApplication(ILaunchpadApplication):
32 """Launchpad legacy AuthServer application root."""43 """Launchpad legacy AuthServer application root."""
3344
=== modified file 'lib/lp/services/authserver/tests/test_authserver.py'
--- lib/lp/services/authserver/tests/test_authserver.py 2012-06-14 05:18:22 +0000
+++ lib/lp/services/authserver/tests/test_authserver.py 2018-05-10 10:45:27 +0000
@@ -1,19 +1,32 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 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"""Tests for the internal codehosting API."""4"""Tests for the internal codehosting API."""
55
6__metaclass__ = type6__metaclass__ = type
77
8from pymacaroons import (
9 Macaroon,
10 Verifier,
11 )
12from testtools.matchers import Is
8from zope.component import getUtility13from zope.component import getUtility
14from zope.interface import implementer
9from zope.publisher.xmlrpc import TestRequest15from zope.publisher.xmlrpc import TestRequest
1016
11from lp.services.authserver.xmlrpc import AuthServerAPIView17from lp.services.authserver.xmlrpc import AuthServerAPIView
18from lp.services.config import config
19from lp.services.macaroons.interfaces import IMacaroonIssuer
12from lp.testing import (20from lp.testing import (
13 person_logged_in,21 person_logged_in,
22 TestCase,
14 TestCaseWithFactory,23 TestCaseWithFactory,
15 )24 )
16from lp.testing.layers import DatabaseFunctionalLayer25from lp.testing.fixture import ZopeUtilityFixture
26from lp.testing.layers import (
27 DatabaseFunctionalLayer,
28 ZopelessLayer,
29 )
17from lp.xmlrpc import faults30from lp.xmlrpc import faults
18from lp.xmlrpc.interfaces import IPrivateApplication31from lp.xmlrpc.interfaces import IPrivateApplication
1932
@@ -57,3 +70,78 @@
57 dict(id=new_person.id, name=new_person.name,70 dict(id=new_person.id, name=new_person.name,
58 keys=[(key.keytype.title, key.keytext)]),71 keys=[(key.keytype.title, key.keytext)]),
59 self.authserver.getUserAndSSHKeys(new_person.name))72 self.authserver.getUserAndSSHKeys(new_person.name))
73
74
75@implementer(IMacaroonIssuer)
76class DummyMacaroonIssuer:
77
78 _root_secret = 'test'
79
80 def issueMacaroon(self, context):
81 """See `IMacaroonIssuer`."""
82 macaroon = Macaroon(
83 location=config.vhost.mainsite.hostname, identifier='test',
84 key=self._root_secret)
85 macaroon.add_first_party_caveat('test %s' % context)
86 return macaroon
87
88 def checkMacaroonIssuer(self, macaroon):
89 """See `IMacaroonIssuer`."""
90 if macaroon.location != config.vhost.mainsite.hostname:
91 return False
92 try:
93 verifier = Verifier()
94 verifier.satisfy_general(
95 lambda caveat: caveat.startswith('test '))
96 return verifier.verify(macaroon, self._root_secret)
97 except Exception:
98 return False
99
100 def verifyMacaroon(self, macaroon, context):
101 """See `IMacaroonIssuer`."""
102 if not self.checkMacaroonIssuer(macaroon):
103 return False
104 try:
105 verifier = Verifier()
106 verifier.satisfy_exact('test %s' % context)
107 return verifier.verify(macaroon, self._root_secret)
108 except Exception:
109 return False
110
111
112class VerifyMacaroonTests(TestCase):
113
114 layer = ZopelessLayer
115
116 def setUp(self):
117 super(VerifyMacaroonTests, self).setUp()
118 self.issuer = DummyMacaroonIssuer()
119 self.useFixture(ZopeUtilityFixture(
120 self.issuer, IMacaroonIssuer, name='test'))
121 private_root = getUtility(IPrivateApplication)
122 self.authserver = AuthServerAPIView(
123 private_root.authserver, TestRequest())
124
125 def test_nonsense_macaroon(self):
126 self.assertEqual(
127 faults.Unauthorized(),
128 self.authserver.verifyMacaroon('nonsense', 0))
129
130 def test_unknown_issuer(self):
131 macaroon = Macaroon(
132 location=config.vhost.mainsite.hostname,
133 identifier='unknown-issuer', key='test')
134 self.assertEqual(
135 faults.Unauthorized(),
136 self.authserver.verifyMacaroon(macaroon.serialize(), 0))
137
138 def test_wrong_context(self):
139 macaroon = self.issuer.issueMacaroon(0)
140 self.assertEqual(
141 faults.Unauthorized(),
142 self.authserver.verifyMacaroon(macaroon.serialize(), 1))
143
144 def test_success(self):
145 macaroon = self.issuer.issueMacaroon(0)
146 self.assertThat(
147 self.authserver.verifyMacaroon(macaroon.serialize(), 0), Is(True))
60148
=== modified file 'lib/lp/services/authserver/xmlrpc.py'
--- lib/lp/services/authserver/xmlrpc.py 2015-10-14 15:22:01 +0000
+++ lib/lp/services/authserver/xmlrpc.py 2018-05-10 10:45:27 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 Canonical Ltd. This software is licensed under the1# Copyright 2009-2018 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"""Auth-Server XML-RPC API ."""4"""Auth-Server XML-RPC API ."""
@@ -10,7 +10,11 @@
10 'AuthServerAPIView',10 'AuthServerAPIView',
11 ]11 ]
1212
13from zope.component import getUtility13from pymacaroons import Macaroon
14from zope.component import (
15 ComponentLookupError,
16 getUtility,
17 )
14from zope.interface import implementer18from zope.interface import implementer
1519
16from lp.registry.interfaces.person import IPersonSet20from lp.registry.interfaces.person import IPersonSet
@@ -18,6 +22,7 @@
18 IAuthServer,22 IAuthServer,
19 IAuthServerApplication,23 IAuthServerApplication,
20 )24 )
25from lp.services.macaroons.interfaces import IMacaroonIssuer
21from lp.services.webapp import LaunchpadXMLRPCView26from lp.services.webapp import LaunchpadXMLRPCView
22from lp.xmlrpc import faults27from lp.xmlrpc import faults
2328
@@ -38,6 +43,20 @@
38 for key in person.sshkeys],43 for key in person.sshkeys],
39 }44 }
4045
46 def verifyMacaroon(self, macaroon_raw, context):
47 """See `IAuthServer.verifyMacaroon`."""
48 try:
49 macaroon = Macaroon.deserialize(macaroon_raw)
50 except Exception:
51 return faults.Unauthorized()
52 try:
53 issuer = getUtility(IMacaroonIssuer, macaroon.identifier)
54 except ComponentLookupError:
55 return faults.Unauthorized()
56 if not issuer.verifyMacaroon(macaroon, context):
57 return faults.Unauthorized()
58 return True
59
4160
42@implementer(IAuthServerApplication)61@implementer(IAuthServerApplication)
43class AuthServerApplication:62class AuthServerApplication: