launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #22500
[Merge] lp:~cjwatson/launchpad/authserver-macaroon into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/authserver-macaroon into lp:launchpad.
Commit message:
Add an authserver endpoint to verify macaroons.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/authserver-macaroon/+merge/345354
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.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/authserver-macaroon into lp:launchpad.
=== 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 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interface for the XML-RPC authentication server."""
@@ -27,6 +27,17 @@
person with the given name.
"""
+ def verifyMacaroon(macaroon_raw, context):
+ """Verify that `macaroon_raw` grants access to `context`.
+
+ :param macaroon_raw: A serialised macaroon.
+ :param context: The context to check. Note that this is passed over
+ XML-RPC, so it should be plain data (e.g. an ID) rather than a
+ database object.
+ :return: True if the macaroon grants access to `context`, otherwise
+ an `Unauthorized` fault.
+ """
+
class IAuthServerApplication(ILaunchpadApplication):
"""Launchpad legacy AuthServer application root."""
=== 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 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for the internal codehosting API."""
__metaclass__ = type
+from pymacaroons import (
+ Macaroon,
+ Verifier,
+ )
+from testtools.matchers import Is
from zope.component import getUtility
+from zope.interface import implementer
from zope.publisher.xmlrpc import TestRequest
from lp.services.authserver.xmlrpc import AuthServerAPIView
+from lp.services.config import config
+from lp.services.macaroons.interfaces import IMacaroonIssuer
from lp.testing import (
person_logged_in,
+ TestCase,
TestCaseWithFactory,
)
-from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.fixture import ZopeUtilityFixture
+from lp.testing.layers import (
+ DatabaseFunctionalLayer,
+ ZopelessLayer,
+ )
from lp.xmlrpc import faults
from lp.xmlrpc.interfaces import IPrivateApplication
@@ -57,3 +70,78 @@
dict(id=new_person.id, name=new_person.name,
keys=[(key.keytype.title, key.keytext)]),
self.authserver.getUserAndSSHKeys(new_person.name))
+
+
+@implementer(IMacaroonIssuer)
+class DummyMacaroonIssuer:
+
+ _root_secret = 'test'
+
+ def issueMacaroon(self, context):
+ """See `IMacaroonIssuer`."""
+ macaroon = Macaroon(
+ location=config.vhost.mainsite.hostname, identifier='test',
+ key=self._root_secret)
+ macaroon.add_first_party_caveat('test %s' % context)
+ return macaroon
+
+ def checkMacaroonIssuer(self, macaroon):
+ """See `IMacaroonIssuer`."""
+ if macaroon.location != config.vhost.mainsite.hostname:
+ return False
+ try:
+ verifier = Verifier()
+ verifier.satisfy_general(
+ lambda caveat: caveat.startswith('test '))
+ return verifier.verify(macaroon, self._root_secret)
+ except Exception:
+ return False
+
+ def verifyMacaroon(self, macaroon, context):
+ """See `IMacaroonIssuer`."""
+ if not self.checkMacaroonIssuer(macaroon):
+ return False
+ try:
+ verifier = Verifier()
+ verifier.satisfy_exact('test %s' % context)
+ return verifier.verify(macaroon, self._root_secret)
+ except Exception:
+ return False
+
+
+class VerifyMacaroonTests(TestCase):
+
+ layer = ZopelessLayer
+
+ def setUp(self):
+ super(VerifyMacaroonTests, self).setUp()
+ self.issuer = DummyMacaroonIssuer()
+ self.useFixture(ZopeUtilityFixture(
+ self.issuer, IMacaroonIssuer, name='test'))
+ private_root = getUtility(IPrivateApplication)
+ self.authserver = AuthServerAPIView(
+ private_root.authserver, TestRequest())
+
+ def test_nonsense_macaroon(self):
+ self.assertEqual(
+ faults.Unauthorized(),
+ self.authserver.verifyMacaroon('nonsense', 0))
+
+ def test_unknown_issuer(self):
+ macaroon = Macaroon(
+ location=config.vhost.mainsite.hostname,
+ identifier='unknown-issuer', key='test')
+ self.assertEqual(
+ faults.Unauthorized(),
+ self.authserver.verifyMacaroon(macaroon.serialize(), 0))
+
+ def test_wrong_context(self):
+ macaroon = self.issuer.issueMacaroon(0)
+ self.assertEqual(
+ faults.Unauthorized(),
+ self.authserver.verifyMacaroon(macaroon.serialize(), 1))
+
+ def test_success(self):
+ macaroon = self.issuer.issueMacaroon(0)
+ self.assertThat(
+ self.authserver.verifyMacaroon(macaroon.serialize(), 0), Is(True))
=== 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 @@
-# Copyright 2009 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2018 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Auth-Server XML-RPC API ."""
@@ -10,7 +10,11 @@
'AuthServerAPIView',
]
-from zope.component import getUtility
+from pymacaroons import Macaroon
+from zope.component import (
+ ComponentLookupError,
+ getUtility,
+ )
from zope.interface import implementer
from lp.registry.interfaces.person import IPersonSet
@@ -18,6 +22,7 @@
IAuthServer,
IAuthServerApplication,
)
+from lp.services.macaroons.interfaces import IMacaroonIssuer
from lp.services.webapp import LaunchpadXMLRPCView
from lp.xmlrpc import faults
@@ -38,6 +43,20 @@
for key in person.sshkeys],
}
+ def verifyMacaroon(self, macaroon_raw, context):
+ """See `IAuthServer.verifyMacaroon`."""
+ try:
+ macaroon = Macaroon.deserialize(macaroon_raw)
+ except Exception:
+ return faults.Unauthorized()
+ try:
+ issuer = getUtility(IMacaroonIssuer, macaroon.identifier)
+ except ComponentLookupError:
+ return faults.Unauthorized()
+ if not issuer.verifyMacaroon(macaroon, context):
+ return faults.Unauthorized()
+ return True
+
@implementer(IAuthServerApplication)
class AuthServerApplication:
Follow ups