From 619cb9d927e2d6955d0b6a97d4d4c5ea9548150a Mon Sep 17 00:00:00 2001 From: Colin Watson Date: Mon, 15 Oct 2018 10:58:25 +0100 Subject: New upstream version 1.2.1 --- macaroonbakery/bakery/_discharge.py | 11 ++++--- macaroonbakery/bakery/_oven.py | 22 ++++++++------ macaroonbakery/httpbakery/_error.py | 17 +++++++---- macaroonbakery/tests/common.py | 2 +- macaroonbakery/tests/test_discharge.py | 51 ++++++++++++++++++++++++++++++--- macaroonbakery/tests/test_httpbakery.py | 32 +++++++++++++++++++-- 6 files changed, 110 insertions(+), 25 deletions(-) (limited to 'macaroonbakery') diff --git a/macaroonbakery/bakery/_discharge.py b/macaroonbakery/bakery/_discharge.py index 1831209..32284b7 100644 --- a/macaroonbakery/bakery/_discharge.py +++ b/macaroonbakery/bakery/_discharge.py @@ -33,9 +33,12 @@ def discharge_all(m, get_discharge, local_key=None): It returns a list of macaroon with m as the first element, followed by all the discharge macaroons. All the discharge macaroons will be bound to the primary macaroon. + The get_discharge function is passed a context (AuthContext), - the caveat(Caveat) to be discharged and encrypted_caveat (bytes)will be + the caveat(pymacaroons.Caveat) to be discharged and encrypted_caveat (bytes) will be passed the external caveat payload found in m, if any. + It should return a bakery.Macaroon object holding the discharge + macaroon for the third party caveat. ''' primary = m.macaroon discharges = [primary] @@ -161,7 +164,7 @@ def discharge(ctx, id, caveat, key, checker, locator): raise VerificationError(exc.args[0]) if cond == checkers.COND_NEED_DECLARED: - cav_info = cav_info._replace(condition=arg.encode('utf-8')) + cav_info = cav_info._replace(condition=arg) caveats = _check_need_declared(ctx, cav_info, checker) else: caveats = checker.check_third_party_caveat(ctx, cav_info) @@ -185,7 +188,7 @@ def discharge(ctx, id, caveat, key, checker, locator): def _check_need_declared(ctx, cav_info, checker): - arg = cav_info.condition.decode('utf-8') + arg = cav_info.condition i = arg.find(' ') if i <= 0: raise VerificationError( @@ -197,7 +200,7 @@ def _check_need_declared(ctx, cav_info, checker): raise VerificationError('need-declared caveat with empty required attribute') if len(need_declared) == 0: raise VerificationError('need-declared caveat with no required attributes') - cav_info = cav_info._replace(condition=arg[i + 1:].encode('utf-8')) + cav_info = cav_info._replace(condition=arg[i + 1:]) caveats = checker.check_third_party_caveat(ctx, cav_info) declared = {} for cav in caveats: diff --git a/macaroonbakery/bakery/_oven.py b/macaroonbakery/bakery/_oven.py index 414a164..d0a2a23 100644 --- a/macaroonbakery/bakery/_oven.py +++ b/macaroonbakery/bakery/_oven.py @@ -28,10 +28,6 @@ from macaroonbakery._utils import ( ) from ._internal import id_pb2 from pymacaroons import MACAROON_V2, Verifier -from pymacaroons.exceptions import ( - MacaroonInvalidSignatureException, - MacaroonUnmetCaveatException, -) class Oven: @@ -183,10 +179,20 @@ class Oven: v.satisfy_general(validator) try: v.verify(macaroons[0], root_key, macaroons[1:]) - except (MacaroonUnmetCaveatException, - MacaroonInvalidSignatureException) as exc: - raise VerificationError( - 'verification failed: {}'.format(exc.args[0])) + except Exception as exc: + # Unfortunately pymacaroons doesn't control + # the set of exceptions that can be raised here. + # Possible candidates are: + # pymacaroons.exceptions.MacaroonUnmetCaveatException + # pymacaroons.exceptions.MacaroonInvalidSignatureException + # ValueError + # nacl.exceptions.CryptoError + # + # There may be others too, so just catch everything. + raise six.raise_from( + VerificationError('verification failed: {}'.format(str(exc))), + exc, + ) if (self.ops_store is not None and len(ops) == 1 diff --git a/macaroonbakery/httpbakery/_error.py b/macaroonbakery/httpbakery/_error.py index ff75f13..0ef7e7b 100644 --- a/macaroonbakery/httpbakery/_error.py +++ b/macaroonbakery/httpbakery/_error.py @@ -20,7 +20,7 @@ class DischargeError(Exception): '''This is thrown by Client when a third party has refused a discharge''' def __init__(self, msg): super(DischargeError, self).__init__( - 'third party refused discharge: {}'.format(msg)) + 'third party refused dischargex: {}'.format(msg)) class InteractionError(Exception): @@ -106,11 +106,16 @@ class Error(namedtuple('Error', 'code, message, version, info')): '''Create an error from a JSON-deserialized object @param serialized the object holding the serialized error {dict} ''' - code = serialized.get('Code') - message = serialized.get('Message') - info = ErrorInfo.from_dict(serialized.get('Info')) - return Error(code=code, message=message, info=info, - version=bakery.LATEST_VERSION) + # Some servers return lower case field names for message and code. + # The Go client is tolerant of this, so be similarly tolerant here. + def field(name): + return serialized.get(name) or serialized.get(name.lower()) + return Error( + code=field('Code'), + message=field('Message'), + info=ErrorInfo.from_dict(field('Info')), + version=bakery.LATEST_VERSION, + ) def interaction_method(self, kind, x): ''' Checks whether the error is an InteractionRequired error diff --git a/macaroonbakery/tests/common.py b/macaroonbakery/tests/common.py index 972b3ad..aacdaf3 100644 --- a/macaroonbakery/tests/common.py +++ b/macaroonbakery/tests/common.py @@ -80,7 +80,7 @@ class ThirdPartyStrcmpChecker(bakery.ThirdPartyCaveatChecker): condition = cav_info.condition.decode('utf-8') if condition != self.str: raise bakery.ThirdPartyCaveatCheckFailed( - '{} doesn\'t match {}'.format(condition, self.str)) + '{} doesn\'t match {}'.format(repr(condition), repr(self.str))) return [] diff --git a/macaroonbakery/tests/test_discharge.py b/macaroonbakery/tests/test_discharge.py index 0802070..5360317 100644 --- a/macaroonbakery/tests/test_discharge.py +++ b/macaroonbakery/tests/test_discharge.py @@ -1,5 +1,6 @@ # Copyright 2017 Canonical Ltd. # Licensed under the LGPLv3, see LICENCE file for details. +import os import unittest import macaroonbakery.bakery as bakery @@ -351,14 +352,14 @@ class TestDischarge(unittest.TestCase): # Since no declarations are added by the discharger, class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker): def check_third_party_caveat(self, ctx, cav_info): - if cav_info.condition == b'x': + if cav_info.condition == 'x': return [checkers.declared_caveat('foo', 'fooval1')] - if cav_info.condition == b'y': + if cav_info.condition == 'y': return [ checkers.declared_caveat('foo', 'fooval2'), checkers.declared_caveat('baz', 'bazval') ] - raise common.ThirdPartyCaveatCheckFailed('not matched') + raise bakery.ThirdPartyCaveatCheckFailed('not matched') def get_discharge(cav, payload): return bakery.discharge( @@ -448,7 +449,7 @@ class TestDischarge(unittest.TestCase): location='as2-loc')] if self._loc == 'as2-loc': return [] - raise common.ThirdPartyCaveatCheckFailed( + raise bakery.ThirdPartyCaveatCheckFailed( 'unknown location {}'.format(self._loc)) def get_discharge(cav, payload): @@ -472,3 +473,45 @@ class TestDischarge(unittest.TestCase): len(cav.caveat_id) > 3): self.fail('caveat id on caveat {} of macaroon {} ' 'is too big ({})'.format(j, i, cav.id)) + + def test_third_party_discharge_macaroon_wrong_root_key_and_third_party_caveat(self): + + root_keys = bakery.MemoryKeyStore() + ts = bakery.Bakery( + key=bakery.generate_key(), + checker=common.test_checker(), + root_key_store=root_keys, + identity_client=common.OneIdentity(), + ) + locator = bakery.ThirdPartyStore() + bs = common.new_bakery('bs-loc', locator) + + # ts creates a macaroon with a third party caveat addressed to bs. + ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION, + common.ages, + None, [bakery.LOGIN_OP]) + ts_macaroon.add_caveat( + checkers.Caveat(location='bs-loc', condition='true'), + ts.oven.key, locator, + ) + + def get_discharge(cav, payload): + return bakery.discharge( + common.test_context, + cav.caveat_id_bytes, + payload, + bs.oven.key, + common.ThirdPartyStrcmpChecker('true'), + bs.oven.locator, + ) + + d = bakery.discharge_all(ts_macaroon, get_discharge) + + # The authorization should succeed at first. + ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) + # Corrupt the root key and try again. + # We should get a DischargeRequiredError because the verification has failed. + root_keys._key = os.urandom(24) + with self.assertRaises(bakery.PermissionDenied) as err: + ts.checker.auth([d]).allow(common.test_context, [bakery.LOGIN_OP]) + self.assertEqual(str(err.exception), 'verification failed: Decryption failed. Ciphertext failed verification') diff --git a/macaroonbakery/tests/test_httpbakery.py b/macaroonbakery/tests/test_httpbakery.py index 4aac850..c372f13 100644 --- a/macaroonbakery/tests/test_httpbakery.py +++ b/macaroonbakery/tests/test_httpbakery.py @@ -1,6 +1,7 @@ from unittest import TestCase -from macaroonbakery.httpbakery import WebBrowserInteractionInfo +import macaroonbakery.httpbakery as httpbakery +import macaroonbakery.bakery as bakery class TestWebBrowserInteractionInfo(TestCase): @@ -9,8 +10,35 @@ class TestWebBrowserInteractionInfo(TestCase): info_dict = { 'VisitURL': 'https://example.com/visit', 'WaitTokenURL': 'https://example.com/wait'} - interaction_info = WebBrowserInteractionInfo.from_dict(info_dict) + interaction_info = httpbakery.WebBrowserInteractionInfo.from_dict(info_dict) self.assertEqual( interaction_info.visit_url, 'https://example.com/visit') self.assertEqual( interaction_info.wait_token_url, 'https://example.com/wait') + + +class TestError(TestCase): + + def test_from_dict_upper_case_fields(self): + err = httpbakery.Error.from_dict({ + 'Message': 'm', + 'Code': 'c', + }) + self.assertEqual(err, httpbakery.Error( + code='c', + message='m', + info=None, + version=bakery.LATEST_VERSION, + )) + + def test_from_dict_lower_case_fields(self): + err = httpbakery.Error.from_dict({ + 'message': 'm', + 'code': 'c', + }) + self.assertEqual(err, httpbakery.Error( + code='c', + message='m', + info=None, + version=bakery.LATEST_VERSION, + )) -- cgit v1.2.3