summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--debian/.git-dpm14
-rw-r--r--debian/changelog8
-rw-r--r--debian/patches/isolate-from-proxy.patch43
-rw-r--r--debian/patches/lower-protobuf-requests-deps.patch32
-rw-r--r--debian/patches/series2
-rwxr-xr-xdocs/conf.py2
-rw-r--r--macaroonbakery/__init__.py97
-rw-r--r--macaroonbakery/authorizer.py4
-rw-r--r--macaroonbakery/bakery.py175
-rw-r--r--macaroonbakery/checker.py42
-rw-r--r--macaroonbakery/checkers/__init__.py54
-rw-r--r--macaroonbakery/checkers/time.py53
-rw-r--r--macaroonbakery/checkers/utils.py2
-rw-r--r--macaroonbakery/codec.py50
-rw-r--r--macaroonbakery/discharge.py55
-rw-r--r--macaroonbakery/httpbakery/__init__.py48
-rw-r--r--macaroonbakery/httpbakery/agent.py52
-rw-r--r--macaroonbakery/httpbakery/agent/__init__.py17
-rw-r--r--macaroonbakery/httpbakery/agent/agent.py180
-rw-r--r--macaroonbakery/httpbakery/browser.py86
-rw-r--r--macaroonbakery/httpbakery/client.py442
-rw-r--r--macaroonbakery/httpbakery/discharge.py33
-rw-r--r--macaroonbakery/httpbakery/error.py151
-rw-r--r--macaroonbakery/httpbakery/interactor.py73
-rw-r--r--macaroonbakery/httpbakery/keyring.py26
-rw-r--r--macaroonbakery/identity.py10
-rw-r--r--macaroonbakery/macaroon.py81
-rw-r--r--macaroonbakery/oven.py44
-rw-r--r--macaroonbakery/tests/common.py30
-rw-r--r--macaroonbakery/tests/test_agent.py331
-rw-r--r--macaroonbakery/tests/test_authorizer.py72
-rw-r--r--macaroonbakery/tests/test_bakery.py88
-rw-r--r--macaroonbakery/tests/test_checker.py443
-rw-r--r--macaroonbakery/tests/test_client.py395
-rw-r--r--macaroonbakery/tests/test_codec.py115
-rw-r--r--macaroonbakery/tests/test_discharge.py274
-rw-r--r--macaroonbakery/tests/test_discharge_all.py71
-rw-r--r--macaroonbakery/tests/test_keyring.py34
-rw-r--r--macaroonbakery/tests/test_macaroon.py69
-rw-r--r--macaroonbakery/tests/test_namespace.py2
-rw-r--r--macaroonbakery/tests/test_oven.py127
-rw-r--r--macaroonbakery/tests/test_store.py4
-rw-r--r--macaroonbakery/tests/test_time.py129
-rw-r--r--macaroonbakery/third_party.py38
-rw-r--r--macaroonbakery/utils.py106
-rw-r--r--macaroonbakery/versions.py10
-rwxr-xr-xsetup.py2
-rw-r--r--tox.ini7
48 files changed, 2968 insertions, 1255 deletions
diff --git a/debian/.git-dpm b/debian/.git-dpm
index 78ae886..865a7b9 100644
--- a/debian/.git-dpm
+++ b/debian/.git-dpm
@@ -1,11 +1,11 @@
# see git-dpm(1) from git-dpm package
-075e9d186663b19cc3d3a892377e28fcfd7993a7
-075e9d186663b19cc3d3a892377e28fcfd7993a7
-3d9eaeb5dacee168a93da090e2c0d46eedbe51a2
-3d9eaeb5dacee168a93da090e2c0d46eedbe51a2
-py-macaroon-bakery_0.0.4.orig.tar.gz
-0991cc6e4167b4b83740f03baa89123ff6d9a424
-69675
+8051fc0e07186078ae5419ac9de246cf6e57359a
+8051fc0e07186078ae5419ac9de246cf6e57359a
+37d61d0415f6cc96a7a9abe057e1ae0f89fd977e
+37d61d0415f6cc96a7a9abe057e1ae0f89fd977e
+py-macaroon-bakery_0.0.5.orig.tar.gz
+b6a1d6a8ff0cc252cf2dd3464bb881daadb7b056
+80202
debianTag="debian/%e%v"
patchedTag="patched/%e%v"
upstreamTag="upstream/%e%u"
diff --git a/debian/changelog b/debian/changelog
index 618344a..a5ebfcc 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+py-macaroon-bakery (0.0.5-1) UNRELEASED; urgency=medium
+
+ * New upstream release.
+ * Apply https://github.com/go-macaroon-bakery/py-macaroon-bakery/pull/28
+ to isolate client tests from any configured HTTP proxy.
+
+ -- Colin Watson <cjwatson@debian.org> Mon, 06 Nov 2017 10:05:18 +0000
+
py-macaroon-bakery (0.0.4-1) unstable; urgency=medium
* New upstream release.
diff --git a/debian/patches/isolate-from-proxy.patch b/debian/patches/isolate-from-proxy.patch
new file mode 100644
index 0000000..ee5d08a
--- /dev/null
+++ b/debian/patches/isolate-from-proxy.patch
@@ -0,0 +1,43 @@
+From 8051fc0e07186078ae5419ac9de246cf6e57359a Mon Sep 17 00:00:00 2001
+From: Colin Watson <cjwatson@debian.org>
+Date: Mon, 6 Nov 2017 10:27:10 +0000
+Subject: Isolate client tests from any HTTP proxy
+
+Debian's Python packaging tools set http_proxy to a non-existent proxy
+to help flush out packages that try to talk to the network during build,
+but these tests could previously fail in more normal development
+environments too.
+
+Forwarded: https://github.com/go-macaroon-bakery/py-macaroon-bakery/pull/28
+Last-Update: 2017-11-06
+
+Patch-Name: isolate-from-proxy.patch
+---
+ macaroonbakery/tests/test_client.py | 7 +++++++
+ 1 file changed, 7 insertions(+)
+
+diff --git a/macaroonbakery/tests/test_client.py b/macaroonbakery/tests/test_client.py
+index e1a4009..8263f54 100644
+--- a/macaroonbakery/tests/test_client.py
++++ b/macaroonbakery/tests/test_client.py
+@@ -3,6 +3,7 @@
+ import base64
+ import datetime
+ import json
++import os
+ from unittest import TestCase
+ try:
+ from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+@@ -26,6 +27,12 @@ TEST_OP = bakery.Op(entity='test', action='test')
+
+
+ class TestClient(TestCase):
++ def setUp(self):
++ super(TestClient, self).setUp()
++ # http_proxy would cause requests to talk to the proxy, which is
++ # unlikely to know how to talk to the test server.
++ os.environ.pop('http_proxy', None)
++
+ def test_single_service_first_party(self):
+ b = new_bakery('loc', None, None)
+
diff --git a/debian/patches/lower-protobuf-requests-deps.patch b/debian/patches/lower-protobuf-requests-deps.patch
deleted file mode 100644
index 112c9ba..0000000
--- a/debian/patches/lower-protobuf-requests-deps.patch
+++ /dev/null
@@ -1,32 +0,0 @@
-From 075e9d186663b19cc3d3a892377e28fcfd7993a7 Mon Sep 17 00:00:00 2001
-From: Andrea Azzarone <andrea.azzarone@canonical.com>
-Date: Fri, 3 Nov 2017 15:00:01 +0000
-Subject: Lowering the protobuf and requests deps.
-
-Origin: other, https://github.com/go-macaroon-bakery/py-macaroon-bakery/pull/26
-Forwarded: https://github.com/go-macaroon-bakery/py-macaroon-bakery/pull/26
-Patch-Name: lower-protobuf-requests-deps.patch
-
-Last-Update: 2017-11-03
----
- setup.py | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/setup.py b/setup.py
-index 7fbc6d3..54000e9 100755
---- a/setup.py
-+++ b/setup.py
-@@ -24,11 +24,11 @@ with open('README.rst') as readme_file:
- readme = readme_file.read()
-
- requirements = [
-- 'requests>=2.18.4,<3.0',
-+ 'requests>=2.18.1,<3.0',
- 'PyNaCl>=1.1.2,<2.0',
- 'pymacaroons>=0.12.0,<1.0',
- 'six>=1.11.0,<2.0',
-- 'protobuf>=3.4.0,<4.0',
-+ 'protobuf>=3.0.0,<4.0',
- 'pyRFC3339>=1.0,<2.0',
- 'pytz>=2017.2,<2018.0'
- ]
diff --git a/debian/patches/series b/debian/patches/series
index f688483..781b9ec 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1 +1 @@
-lower-protobuf-requests-deps.patch
+isolate-from-proxy.patch
diff --git a/docs/conf.py b/docs/conf.py
index 75593c5..df296f1 100755
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -31,7 +31,7 @@ project_root = os.path.dirname(cwd)
# version is used.
sys.path.insert(0, project_root)
-import macaroonbakery
+import macaroonbakery as bakery
# -- General configuration ---------------------------------------------
diff --git a/macaroonbakery/__init__.py b/macaroonbakery/__init__.py
index dd2e6df..6397a19 100644
--- a/macaroonbakery/__init__.py
+++ b/macaroonbakery/__init__.py
@@ -1,63 +1,96 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from __future__ import unicode_literals
-try:
- import urllib3.contrib.pyopenssl
-except ImportError:
- pass
-else:
- urllib3.contrib.pyopenssl.inject_into_urllib3()
-
from macaroonbakery.versions import (
- LATEST_BAKERY_VERSION, BAKERY_V3, BAKERY_V2, BAKERY_V1, BAKERY_V0
+ VERSION_0,
+ VERSION_1,
+ VERSION_2,
+ VERSION_3,
+ LATEST_VERSION,
)
from macaroonbakery.authorizer import (
- ClosedAuthorizer, EVERYONE, AuthorizerFunc, Authorizer, ACLAuthorizer
+ ACLAuthorizer,
+ Authorizer,
+ AuthorizerFunc,
+ ClosedAuthorizer,
+ EVERYONE,
)
from macaroonbakery.codec import (
- encode_caveat, decode_caveat, encode_uvarint
+ decode_caveat,
+ encode_caveat,
+ encode_uvarint,
)
from macaroonbakery.checker import (
- Op, LOGIN_OP, AuthInfo, AuthChecker, Checker
+ AuthChecker,
+ AuthInfo,
+ Checker,
+ LOGIN_OP,
+ Op,
)
from macaroonbakery.error import (
- ThirdPartyCaveatCheckFailed, CaveatNotRecognizedError, AuthInitError,
- PermissionDenied, IdentityError, DischargeRequiredError, VerificationError,
- ThirdPartyInfoNotFound
+ AuthInitError,
+ CaveatNotRecognizedError,
+ DischargeRequiredError,
+ IdentityError,
+ PermissionDenied,
+ ThirdPartyCaveatCheckFailed,
+ ThirdPartyInfoNotFound,
+ VerificationError,
)
from macaroonbakery.identity import (
- Identity, ACLIdentity, SimpleIdentity, IdentityClient, NoIdentities
+ ACLIdentity,
+ Identity,
+ IdentityClient,
+ NoIdentities,
+ SimpleIdentity,
+)
+from macaroonbakery.keys import (
+ generate_key,
+ PrivateKey,
+ PublicKey,
+)
+from macaroonbakery.store import (
+ MemoryOpsStore,
+ MemoryKeyStore,
)
-from macaroonbakery.keys import generate_key, PrivateKey, PublicKey
-from macaroonbakery.store import MemoryOpsStore, MemoryKeyStore
from macaroonbakery.third_party import (
- ThirdPartyCaveatInfo, ThirdPartyInfo, legacy_namespace
+ ThirdPartyCaveatInfo,
+ ThirdPartyInfo,
+ legacy_namespace,
)
from macaroonbakery.macaroon import (
- Macaroon, MacaroonJSONDecoder, MacaroonJSONEncoder, ThirdPartyStore,
- ThirdPartyLocator, macaroon_version
+ Macaroon,
+ MacaroonJSONDecoder,
+ MacaroonJSONEncoder,
+ ThirdPartyLocator,
+ ThirdPartyStore,
+ macaroon_version,
)
from macaroonbakery.discharge import (
- discharge_all, discharge, local_third_party_caveat, ThirdPartyCaveatChecker
+ ThirdPartyCaveatChecker,
+ discharge,
+ discharge_all,
+ local_third_party_caveat,
+)
+from macaroonbakery.oven import (
+ Oven,
+ canonical_ops,
)
-from macaroonbakery.oven import Oven, canonical_ops
from macaroonbakery.bakery import Bakery
-
+from macaroonbakery.utils import b64decode
__all__ = [
- 'ACLIdentity',
'ACLAuthorizer',
+ 'ACLIdentity',
'AuthChecker',
'AuthInfo',
'AuthInitError',
'Authorizer',
'AuthorizerFunc',
- 'Bakery',
- 'BAKERY_V0',
- 'BAKERY_V1',
- 'BAKERY_V2',
- 'BAKERY_V3',
+ 'VERSION_0',
+ 'VERSION_1',
+ 'VERSION_2',
+ 'VERSION_3',
'Bakery',
'CaveatNotRecognizedError',
'Checker',
@@ -67,7 +100,7 @@ __all__ = [
'Identity',
'IdentityClient',
'IdentityError',
- 'LATEST_BAKERY_VERSION',
+ 'LATEST_VERSION',
'LOGIN_OP',
'Macaroon',
'MacaroonJSONDecoder',
@@ -80,7 +113,6 @@ __all__ = [
'PermissionDenied',
'PrivateKey',
'PublicKey',
- 'NoIdentities',
'SimpleIdentity',
'ThirdPartyCaveatCheckFailed',
'ThirdPartyCaveatChecker',
@@ -91,6 +123,7 @@ __all__ = [
'ThirdPartyStore',
'VERSION',
'VerificationError',
+ 'b64decode',
'canonical_ops',
'decode_caveat',
'discharge',
diff --git a/macaroonbakery/authorizer.py b/macaroonbakery/authorizer.py
index b7128c0..ae84104 100644
--- a/macaroonbakery/authorizer.py
+++ b/macaroonbakery/authorizer.py
@@ -2,7 +2,7 @@
# Licensed under the LGPLv3, see LICENCE file for details.
import abc
-import macaroonbakery
+import macaroonbakery as bakery
# EVERYONE is recognized by ACLAuthorizer as the name of a
@@ -90,7 +90,7 @@ class ACLAuthorizer(Authorizer):
# Anyone is allowed to do nothing.
return [], []
allowed = [False] * len(ops)
- has_allow = isinstance(identity, macaroonbakery.ACLIdentity)
+ has_allow = isinstance(identity, bakery.ACLIdentity)
for i, op in enumerate(ops):
acl = self._get_acl(ctx, op)
if has_allow:
diff --git a/macaroonbakery/bakery.py b/macaroonbakery/bakery.py
index 1e03191..5d9d56a 100644
--- a/macaroonbakery/bakery.py
+++ b/macaroonbakery/bakery.py
@@ -1,163 +1,10 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from collections import namedtuple
-import requests
-from macaroonbakery import utils
-from macaroonbakery.discharge import discharge
from macaroonbakery.checkers import checkers
from macaroonbakery.oven import Oven
from macaroonbakery.checker import Checker
-
-
-ERR_INTERACTION_REQUIRED = 'interaction required'
-ERR_DISCHARGE_REQUIRED = 'macaroon discharge required'
-TIME_OUT = 30
-DEFAULT_PROTOCOL_VERSION = {'Bakery-Protocol-Version': '1'}
-MAX_DISCHARGE_RETRIES = 3
-
-NONCE_LEN = 24
-
-
-# A named tuple composed of the visit_url and wait_url coming from the error
-# response in discharge
-_Info = namedtuple('Info', 'visit_url wait_url')
-
-
-class DischargeException(Exception):
- '''A discharge error occurred.'''
-
-
-def discharge_all(macaroon, visit_page=None, jar=None, key=None):
- '''Gathers discharge macaroons for all the third party caveats in macaroon.
-
- All the discharge macaroons will be bound to the primary macaroon.
- The key parameter may optionally hold the key of the client, in which case
- it will be used to discharge any third party caveats with the special
- location "local". In this case, the caveat itself must be "true". This
- can be used by a server to ask a client to prove ownership of the
- private key.
- @param macaroon The macaroon to be discharged.
- @param visit_page function called when the discharge process requires
- further interaction.
- @param jar the storage for the cookies.
- @param key optional nacl key.
- @return An array with macaroon as the first element, followed by all the
- discharge macaroons.
- '''
- discharges = [macaroon]
- if visit_page is None:
- visit_page = utils.visit_page_with_browser
- if jar is None:
- jar = requests.cookies.RequestsCookieJar()
- client = _Client(visit_page, jar)
- try:
- client.discharge_caveats(macaroon, discharges, macaroon, key)
- except Exception as exc:
- raise DischargeException('unable to discharge the macaroon', exc)
- return discharges
-
-
-class _Client:
- def __init__(self, visit_page, jar):
- self._visit_page = visit_page
- self._jar = jar
-
- def discharge_caveats(self, macaroon, discharges,
- primary_macaroon, key):
- '''Gathers discharge macaroons for all the third party caveats.
-
- @param macaroon the macaroon to discharge.
- @param discharges the list of discharged macaroons.
- @param primary_macaroon used for the signature of the discharge
- macaroon.
- @param key nacl key holds the key to use to decrypt the third party
- caveat information and to encrypt any additional
- third party caveats returned by the caveat checker
- '''
- caveats = macaroon.third_party_caveats()
- for caveat in caveats:
- location = caveat.location
- b_cav_id = caveat.caveat_id
- if key is not None and location == 'local':
- # if tuple is only 2 element otherwise TODO add caveat
- dm = discharge(key, id=b_cav_id)
- else:
- dm = self._get_discharge(location, b_cav_id)
- dm = primary_macaroon.prepare_for_request(dm)
- discharges.append(dm)
- self.discharge_caveats(dm, discharges, primary_macaroon, key)
-
- def _get_discharge(self, third_party_location,
- third_party_caveat_condition):
- '''Get the discharge macaroon from the third party location.
-
- @param third_party_location where to get a discharge from.
- @param third_party_caveat_condition encoded 64 string associated to the
- discharged macaroon.
- @return a discharge macaroon.
- @raise DischargeError when an error occurs during the discharge
- process.
- '''
- headers = DEFAULT_PROTOCOL_VERSION
- payload = {'id': third_party_caveat_condition}
-
- response = requests.post(third_party_location + '/discharge',
- headers=headers,
- data=payload,
- # timeout=TIME_OUT, TODO: add a time out
- cookies=self._jar)
- status_code = response.status_code
- if status_code == 200:
- return _extract_macaroon_from_response(response)
- if (status_code == 401 and
- response.headers.get('WWW-Authenticate') == 'Macaroon'):
- error = response.json()
- if error.get('Code', '') != ERR_INTERACTION_REQUIRED:
- return DischargeException('unable to get code from discharge')
- info = _extract_urls(response)
- self._visit_page(info.visit_url)
- # Wait on the wait url and then get a macaroon if validated.
- return _acquire_macaroon_from_wait(info.wait_url)
-
-
-def _extract_macaroon_from_response(response):
- '''Extract the macaroon from a direct successful discharge.
-
- @param response from direct successful discharge.
- @return a macaroon object.
- @raises DischargeError if any error happens.
- '''
- response_json = response.json()
- return utils.deserialize(response_json['Macaroon'])
-
-
-def _acquire_macaroon_from_wait(wait_url):
- ''' Return the macaroon acquired from the wait endpoint.
-
- Note that will block until the user interaction has completed.
-
- @param wait_url the get url to call to get a macaroon.
- @return a macaroon object
- @raises DischargeError if any error happens.
- '''
- resp = requests.get(wait_url, headers=DEFAULT_PROTOCOL_VERSION)
- response_json = resp.json()
- macaroon = response_json['Macaroon']
- return utils.deserialize(macaroon)
-
-
-def _extract_urls(response):
- '''Return _Info of the visit and wait URL from response.
-
- @param response the response from the discharge endpoint.
- @return a _Info object of the visit and wait URL.
- @raises DischargeError for ant error during the process response.
- '''
- response_json = response.json()
- visit_url = response_json['Info']['VisitURL']
- wait_url = response_json['Info']['WaitURL']
- return _Info(visit_url=visit_url, wait_url=wait_url)
+from macaroonbakery.authorizer import ClosedAuthorizer
class Bakery(object):
@@ -165,36 +12,36 @@ class Bakery(object):
'''
def __init__(self, location=None, locator=None, ops_store=None, key=None,
identity_client=None, checker=None, root_key_store=None,
- authorizer=None):
+ authorizer=ClosedAuthorizer()):
'''Returns a new Bakery instance which combines an Oven with a
Checker for the convenience of callers that wish to use both
together.
- :param: checker holds the checker used to check first party caveats.
+ @param checker holds the checker used to check first party caveats.
If this is None, it will use checkers.Checker(None).
- :param: root_key_store holds the root key store to use.
+ @param root_key_store holds the root key store to use.
If you need to use a different root key store for different operations,
you'll need to pass a root_key_store_for_ops value to Oven directly.
- :param: root_key_store If this is None, it will use MemoryKeyStore().
+ @param root_key_store If this is None, it will use MemoryKeyStore().
Note that that is almost certain insufficient for production services
that are spread across multiple instances or that need
to persist keys across restarts.
- :param: locator is used to find out information on third parties when
+ @param locator is used to find out information on third parties when
adding third party caveats. If this is None, no non-local third
party caveats can be added.
- :param: key holds the private key of the oven. If this is None,
+ @param key holds the private key of the oven. If this is None,
no third party caveats may be added.
- :param: identity_client holds the identity implementation to use for
+ @param identity_client holds the identity implementation to use for
authentication. If this is None, no authentication will be possible.
- :param: authorizer is used to check whether an authenticated user is
+ @param authorizer is used to check whether an authenticated user is
allowed to perform operations. If it is None, it will use
a ClosedAuthorizer.
The identity parameter passed to authorizer.allow will
always have been obtained from a call to
IdentityClient.declared_identity.
- :param: ops_store used to persistently store the association of
+ @param ops_store used to persistently store the association of
multi-op entities with their associated operations
when oven.macaroon is called with multiple operations.
- :param: location holds the location to use when creating new macaroons.
+ @param location holds the location to use when creating new macaroons.
'''
if checker is None:
diff --git a/macaroonbakery/checker.py b/macaroonbakery/checker.py
index b73c92f..568fd7c 100644
--- a/macaroonbakery/checker.py
+++ b/macaroonbakery/checker.py
@@ -6,7 +6,7 @@ from threading import Lock
import pyrfc3339
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
@@ -38,7 +38,7 @@ class Checker(object):
See the Oven type (TODO) for one way of doing that.
'''
def __init__(self, checker=checkers.Checker(),
- authorizer=macaroonbakery.ClosedAuthorizer(),
+ authorizer=bakery.ClosedAuthorizer(),
identity_client=None,
macaroon_opstore=None):
'''
@@ -57,7 +57,7 @@ class Checker(object):
self._first_party_caveat_checker = checker
self._authorizer = authorizer
if identity_client is None:
- identity_client = macaroonbakery.NoIdentities()
+ identity_client = bakery.NoIdentities()
self._identity_client = identity_client
self._macaroon_opstore = macaroon_opstore
@@ -106,16 +106,18 @@ class AuthChecker(object):
self._init_once(ctx)
self._executed = True
if self._init_errors is not None and len(self._init_errors) > 0:
- raise macaroonbakery.AuthInitError(self._init_errors[0])
+ raise bakery.AuthInitError(self._init_errors[0])
def _init_once(self, ctx):
self._auth_indexes = {}
- self._conditions = [None]*len(self._macaroons)
+ self._conditions = [None] * len(self._macaroons)
for i, ms in enumerate(self._macaroons):
try:
ops, conditions = self.parent._macaroon_opstore.macaroon_ops(
ms)
- except macaroonbakery.VerificationError as exc:
+ except bakery.VerificationError:
+ raise
+ except Exception as exc:
self._init_errors.append(exc.args[0])
continue
@@ -155,7 +157,7 @@ class AuthChecker(object):
try:
identity = self.parent._identity_client.declared_identity(
ctx, declared)
- except macaroonbakery.IdentityError as exc:
+ except bakery.IdentityError as exc:
self._init_errors.append(
'cannot decode declared identity: {}'.format(exc.args[0]))
continue
@@ -169,7 +171,7 @@ class AuthChecker(object):
try:
identity, cavs = self.parent.\
_identity_client.identity_from_context(ctx)
- except macaroonbakery.IdentityError:
+ except bakery.IdentityError:
self._init_errors.append('could not determine identity')
if cavs is None:
cavs = []
@@ -195,8 +197,8 @@ class AuthChecker(object):
If an operation was not allowed, an exception will be raised which may
be DischargeRequiredError holding the operations that remain to
be authorized in order to allow authorization to proceed.
- :param: ctx AuthContext
- :param: ops an array of Op
+ @param ctx AuthContext
+ @param ops an array of Op
:return: an AuthInfo object.
'''
auth_info, _ = self.allow_any(ctx, ops)
@@ -217,8 +219,8 @@ class AuthChecker(object):
The LOGIN_OP operation is treated specially - it is always required if
present in ops.
- :param: ctx AuthContext
- :param: ops an array of Op
+ @param ctx AuthContext
+ @param ops an array of Op
:return: an AuthInfo object and the auth used as an array of int.
'''
authed, used = self._allow_any(ctx, ops)
@@ -233,8 +235,8 @@ class AuthChecker(object):
def _allow_any(self, ctx, ops):
self._init(ctx)
- used = [False]*len(self._macaroons)
- authed = [False]*len(ops)
+ used = [False] * len(self._macaroons)
+ authed = [False] * len(ops)
num_authed = 0
errors = []
for i, op in enumerate(ops):
@@ -269,7 +271,7 @@ class AuthChecker(object):
return authed, used
# There are some unauthorized operations.
need = []
- need_index = [0]*(len(ops)-num_authed)
+ need_index = [0] * (len(ops) - num_authed)
for i, ok in enumerate(authed):
if not ok:
need_index[len(need)] = i
@@ -290,7 +292,7 @@ class AuthChecker(object):
# no caveats to be discharged.
return authed, used
if self._identity is None and len(self._identity_caveats) > 0:
- raise macaroonbakery.DischargeRequiredError(
+ raise bakery.DischargeRequiredError(
msg='authentication required',
ops=[LOGIN_OP],
cavs=self._identity_caveats)
@@ -301,8 +303,8 @@ class AuthChecker(object):
err = ''
if len(all_errors) > 0:
err = all_errors[0]
- raise macaroonbakery.PermissionDenied(err)
- raise macaroonbakery.DischargeRequiredError(
+ raise bakery.PermissionDenied(err)
+ raise bakery.DischargeRequiredError(
msg='some operations have extra caveats', ops=ops, cavs=caveats)
def allow_capability(self, ctx, ops):
@@ -352,11 +354,11 @@ class AuthChecker(object):
class AuthInfo(namedtuple('AuthInfo', 'identity macaroons')):
'''AuthInfo information about an authorization decision.
- :param: identity: holds information on the authenticated user as
+ @param identity: holds information on the authenticated user as
returned identity_client. It may be None after a successful
authorization if LOGIN_OP access was not required.
- :param: macaroons: holds all the macaroons that were used for the
+ @param macaroons: holds all the macaroons that were used for the
authorization. Macaroons that were invalid or unnecessary are
not included.
'''
diff --git a/macaroonbakery/checkers/__init__.py b/macaroonbakery/checkers/__init__.py
index 9f0b022..25c6b7d 100644
--- a/macaroonbakery/checkers/__init__.py
+++ b/macaroonbakery/checkers/__init__.py
@@ -1,24 +1,53 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
from macaroonbakery.checkers.conditions import (
- STD_NAMESPACE, COND_DECLARED, COND_TIME_BEFORE, COND_ERROR, COND_ALLOW,
- COND_DENY, COND_NEED_DECLARED
+ STD_NAMESPACE,
+ COND_DECLARED,
+ COND_TIME_BEFORE,
+ COND_ERROR,
+ COND_ALLOW,
+ COND_DENY,
+ COND_NEED_DECLARED,
)
from macaroonbakery.checkers.caveat import (
- allow_caveat, deny_caveat, declared_caveat, parse_caveat,
- time_before_caveat, Caveat
+ allow_caveat,
+ deny_caveat,
+ declared_caveat,
+ parse_caveat,
+ time_before_caveat,
+ Caveat,
)
from macaroonbakery.checkers.declared import (
- context_with_declared, infer_declared, infer_declared_from_conditions,
- need_declared_caveat
+ context_with_declared,
+ infer_declared,
+ infer_declared_from_conditions,
+ need_declared_caveat,
+)
+from macaroonbakery.checkers.operation import (
+ context_with_operations,
+)
+from macaroonbakery.checkers.namespace import (
+ Namespace,
+ deserialize_namespace
+)
+from macaroonbakery.checkers.time import (
+ context_with_clock,
+ expiry_time,
+ macaroons_expiry_time,
)
-from macaroonbakery.checkers.operation import context_with_operations
-from macaroonbakery.checkers.namespace import Namespace, deserialize_namespace
-from macaroonbakery.checkers.time import context_with_clock
from macaroonbakery.checkers.checkers import (
- Checker, CheckerInfo, RegisterError
+ Checker,
+ CheckerInfo,
+ RegisterError,
+)
+from macaroonbakery.checkers.auth_context import (
+ AuthContext,
+ ContextKey,
+)
+
+from macaroonbakery.checkers.utils import (
+ condition_with_prefix,
)
-from macaroonbakery.checkers.auth_context import AuthContext, ContextKey
__all__ = [
'AuthContext',
@@ -36,14 +65,17 @@ __all__ = [
'Namespace',
'RegisterError',
'allow_caveat',
+ 'condition_with_prefix',
'context_with_declared',
'context_with_operations',
'context_with_clock',
'declared_caveat',
'deny_caveat',
'deserialize_namespace',
+ 'expiry_time',
'infer_declared',
'infer_declared_from_conditions',
+ 'macaroons_expiry_time',
'need_declared_caveat',
'parse_caveat',
'time_before_caveat',
diff --git a/macaroonbakery/checkers/time.py b/macaroonbakery/checkers/time.py
index 052d983..0b52131 100644
--- a/macaroonbakery/checkers/time.py
+++ b/macaroonbakery/checkers/time.py
@@ -1,14 +1,20 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
+
+import pyrfc3339
+
from macaroonbakery.checkers.auth_context import ContextKey
+from macaroonbakery.checkers.conditions import COND_TIME_BEFORE, STD_NAMESPACE
+from macaroonbakery.checkers.utils import condition_with_prefix
+from macaroonbakery.checkers.caveat import parse_caveat
TIME_KEY = ContextKey('time-key')
def context_with_clock(ctx, clock):
- ''' Returns a copy of ctx with a key added that associates it with the given
- clock implementation, which will be used by the time-before checker
+ ''' Returns a copy of ctx with a key added that associates it with the
+ given clock implementation, which will be used by the time-before checker
to determine the current time.
The clock should have a utcnow method that returns the current time
as a datetime value in UTC.
@@ -16,3 +22,46 @@ def context_with_clock(ctx, clock):
if clock is None:
return ctx
return ctx.with_value(TIME_KEY, clock)
+
+
+def macaroons_expiry_time(ns, ms):
+ ''' Returns the minimum time of any time-before caveats found in the given
+ macaroons or None if no such caveats were found.
+ :param ns: a Namespace, used to resolve caveats.
+ :param ms: a list of pymacaroons.Macaroon
+ :return: datetime.DateTime or None.
+ '''
+ t = None
+ for m in ms:
+ et = expiry_time(ns, m.caveats)
+ if et is not None and (t is None or et < t):
+ t = et
+ return t
+
+
+def expiry_time(ns, cavs):
+ ''' Returns the minimum time of any time-before caveats found
+ in the given list or None if no such caveats were found.
+
+ The ns parameter is
+ :param ns: used to determine the standard namespace prefix - if
+ the standard namespace is not found, the empty prefix is assumed.
+ :param cavs: a list of pymacaroons.Caveat
+ :return: datetime.DateTime or None.
+ '''
+ prefix = ns.resolve(STD_NAMESPACE)
+ time_before_cond = condition_with_prefix(
+ prefix, COND_TIME_BEFORE)
+ t = None
+ for cav in cavs:
+ cav = cav.caveat_id_bytes.decode('utf-8')
+ name, rest = parse_caveat(cav)
+ if name != time_before_cond:
+ continue
+ try:
+ et = pyrfc3339.parse(rest)
+ if t is None or et < t:
+ t = et
+ except ValueError:
+ continue
+ return t
diff --git a/macaroonbakery/checkers/utils.py b/macaroonbakery/checkers/utils.py
index f2e51b1..925e8c7 100644
--- a/macaroonbakery/checkers/utils.py
+++ b/macaroonbakery/checkers/utils.py
@@ -7,7 +7,7 @@ def condition_with_prefix(prefix, condition):
If the prefix is non-empty, a colon is used to separate them.
'''
- if prefix == '':
+ if prefix == '' or prefix is None:
return condition
return prefix + ':' + condition
diff --git a/macaroonbakery/codec.py b/macaroonbakery/codec.py
index d9340b7..2946da9 100644
--- a/macaroonbakery/codec.py
+++ b/macaroonbakery/codec.py
@@ -6,7 +6,7 @@ import json
import six
import nacl.public
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
_PUBLIC_KEY_PREFIX_LEN = 4
@@ -33,11 +33,11 @@ def encode_caveat(condition, root_key, third_party_info, key, ns):
@param ns not used yet
@return bytes
'''
- if third_party_info.version == macaroonbakery.BAKERY_V1:
+ if third_party_info.version == bakery.VERSION_1:
return _encode_caveat_v1(condition, root_key,
third_party_info.public_key, key)
- if (third_party_info.version == macaroonbakery.BAKERY_V2 or
- third_party_info.version == macaroonbakery.BAKERY_V3):
+ if (third_party_info.version == bakery.VERSION_2 or
+ third_party_info.version == bakery.VERSION_3):
return _encode_caveat_v2_v3(third_party_info.version, condition,
root_key, third_party_info.public_key,
key, ns)
@@ -99,7 +99,7 @@ def _encode_caveat_v2_v3(version, condition, root_key, third_party_pub_key,
condition [rest of encrypted part]
'''
ns_data = bytearray()
- if version >= macaroonbakery.BAKERY_V3:
+ if version >= bakery.VERSION_3:
ns_data = ns.serialize_text()
data = bytearray()
data.append(version)
@@ -131,7 +131,7 @@ def _encode_secret_part_v2_v3(version, condition, root_key, ns):
data.append(version)
encode_uvarint(len(root_key), data)
data.extend(root_key)
- if version >= macaroonbakery.BAKERY_V3:
+ if version >= bakery.VERSION_3:
encode_uvarint(len(ns), data)
data.extend(ns)
data.extend(condition.encode('utf-8'))
@@ -146,7 +146,7 @@ def decode_caveat(key, caveat):
@return ThirdPartyCaveatInfo
'''
if len(caveat) == 0:
- raise macaroonbakery.VerificationError('empty third party caveat')
+ raise bakery.VerificationError('empty third party caveat')
first = caveat[:1]
if first == b'e':
@@ -154,17 +154,17 @@ def decode_caveat(key, caveat):
# encoded JSON object.
return _decode_caveat_v1(key, caveat)
first_as_int = six.byte2int(first)
- if (first_as_int == macaroonbakery.BAKERY_V2 or
- first_as_int == macaroonbakery.BAKERY_V3):
+ if (first_as_int == bakery.VERSION_2 or
+ first_as_int == bakery.VERSION_3):
if (len(caveat) < _VERSION3_CAVEAT_MIN_LEN
- and first_as_int == macaroonbakery.BAKERY_V3):
+ and first_as_int == bakery.VERSION_3):
# If it has the version 3 caveat tag and it's too short, it's
# almost certainly an id, not an encrypted payload.
- raise macaroonbakery.VerificationError(
+ raise bakery.VerificationError(
'caveat id payload not provided for caveat id {}'.format(
caveat))
return _decode_caveat_v2_v3(first_as_int, key, caveat)
- raise macaroonbakery.VerificationError('unknown version for caveat')
+ raise bakery.VerificationError('unknown version for caveat')
def _decode_caveat_v1(key, caveat):
@@ -196,14 +196,15 @@ def _decode_caveat_v1(key, caveat):
record = json.loads(c.decode('utf-8'))
fp_key = nacl.public.PublicKey(
base64.b64decode(wrapper.get('FirstPartyPublicKey')))
- return macaroonbakery.ThirdPartyCaveatInfo(
+ return bakery.ThirdPartyCaveatInfo(
condition=record.get('Condition'),
- first_party_public_key=macaroonbakery.PublicKey(fp_key),
+ first_party_public_key=bakery.PublicKey(fp_key),
third_party_key_pair=key,
root_key=base64.b64decode(record.get('RootKey')),
caveat=caveat,
- version=macaroonbakery.BAKERY_V1,
- namespace=macaroonbakery.legacy_namespace()
+ id=None,
+ version=bakery.VERSION_1,
+ namespace=bakery.legacy_namespace()
)
@@ -212,14 +213,14 @@ def _decode_caveat_v2_v3(version, key, caveat):
'''
if (len(caveat) < 1 + _PUBLIC_KEY_PREFIX_LEN +
_KEY_LEN + nacl.public.Box.NONCE_SIZE + 16):
- raise macaroonbakery.VerificationError('caveat id too short')
+ raise bakery.VerificationError('caveat id too short')
original_caveat = caveat
caveat = caveat[1:] # skip version (already checked)
pk_prefix = caveat[:_PUBLIC_KEY_PREFIX_LEN]
caveat = caveat[_PUBLIC_KEY_PREFIX_LEN:]
if key.public_key.encode(raw=True)[:_PUBLIC_KEY_PREFIX_LEN] != pk_prefix:
- raise macaroonbakery.VerificationError('public key mismatch')
+ raise bakery.VerificationError('public key mismatch')
first_party_pub = caveat[:_KEY_LEN]
caveat = caveat[_KEY_LEN:]
@@ -229,38 +230,39 @@ def _decode_caveat_v2_v3(version, key, caveat):
box = nacl.public.Box(key.key, fp_public_key)
data = box.decrypt(caveat, nonce)
root_key, condition, ns = _decode_secret_part_v2_v3(version, data)
- return macaroonbakery.ThirdPartyCaveatInfo(
+ return bakery.ThirdPartyCaveatInfo(
condition=condition.decode('utf-8'),
- first_party_public_key=macaroonbakery.PublicKey(fp_public_key),
+ first_party_public_key=bakery.PublicKey(fp_public_key),
third_party_key_pair=key,
root_key=root_key,
caveat=original_caveat,
version=version,
+ id=None,
namespace=ns
)
def _decode_secret_part_v2_v3(version, data):
if len(data) < 1:
- raise macaroonbakery.VerificationError('secret part too short')
+ raise bakery.VerificationError('secret part too short')
got_version = six.byte2int(data[:1])
data = data[1:]
if version != got_version:
- raise macaroonbakery.VerificationError(
+ raise bakery.VerificationError(
'unexpected secret part version, got {} want {}'.format(
got_version, version))
root_key_length, read = decode_uvarint(data)
data = data[read:]
root_key = data[:root_key_length]
data = data[root_key_length:]
- if version >= macaroonbakery.BAKERY_V3:
+ if version >= bakery.VERSION_3:
namespace_length, read = decode_uvarint(data)
data = data[read:]
ns_data = data[:namespace_length]
data = data[namespace_length:]
ns = checkers.deserialize_namespace(ns_data)
else:
- ns = macaroonbakery.legacy_namespace()
+ ns = bakery.legacy_namespace()
return root_key, data, ns
diff --git a/macaroonbakery/discharge.py b/macaroonbakery/discharge.py
index d4c0e5a..f54fc97 100644
--- a/macaroonbakery/discharge.py
+++ b/macaroonbakery/discharge.py
@@ -3,11 +3,13 @@
import abc
from collections import namedtuple
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
+emptyContext = checkers.AuthContext()
-def discharge_all(ctx, m, get_discharge, local_key=None):
+
+def discharge_all(m, get_discharge, local_key=None):
'''Gathers discharge macaroons for all the third party caveats in m
(and any subsequent caveats required by those) using get_discharge to
acquire each discharge macaroon.
@@ -46,13 +48,14 @@ def discharge_all(ctx, m, get_discharge, local_key=None):
need = need[1:]
if local_key is not None and cav.cav.location == 'local':
# TODO use a small caveat id.
- dm = discharge(ctx=ctx, key=local_key,
+ dm = discharge(ctx=emptyContext,
+ key=local_key,
checker=_LocalDischargeChecker(),
caveat=cav.encrypted_caveat,
id=cav.cav.caveat_id_bytes,
locator=_EmptyLocator())
else:
- dm = get_discharge(ctx, cav.cav, cav.encrypted_caveat)
+ dm = get_discharge(cav.cav, cav.encrypted_caveat)
# It doesn't matter that we're invalidating dm here because we're
# about to throw it away.
discharge_m = dm.macaroon
@@ -87,7 +90,7 @@ class ThirdPartyCaveatChecker(object):
class _LocalDischargeChecker(ThirdPartyCaveatChecker):
def check_third_party_caveat(self, ctx, info):
if info.condition != 'true':
- raise macaroonbakery.CaveatNotRecognizedError()
+ raise bakery.CaveatNotRecognizedError()
return []
@@ -122,15 +125,24 @@ def discharge(ctx, id, caveat, key, checker, locator):
# caveats are added, use that id as the prefix
# for any more ids.
caveat_id_prefix = id
- cav_info = macaroonbakery.decode_caveat(key, caveat)
-
+ cav_info = bakery.decode_caveat(key, caveat)
+ cav_info = bakery.ThirdPartyCaveatInfo(
+ condition=cav_info.condition,
+ first_party_public_key=cav_info.first_party_public_key,
+ third_party_key_pair=cav_info.third_party_key_pair,
+ root_key=cav_info.root_key,
+ caveat=cav_info.caveat,
+ version=cav_info.version,
+ id=id,
+ namespace=cav_info.namespace
+ )
# Note that we don't check the error - we allow the
# third party checker to see even caveats that we can't
# understand.
try:
cond, arg = checkers.parse_caveat(cav_info.condition)
except ValueError as exc:
- raise macaroonbakery.VerificationError(exc.args[0])
+ raise bakery.VerificationError(exc.args[0])
if cond == checkers.COND_NEED_DECLARED:
cav_info = cav_info._replace(condition=arg.encode('utf-8'))
@@ -142,8 +154,13 @@ def discharge(ctx, id, caveat, key, checker, locator):
# be stored persistently. Indeed, it would be a problem if
# we did, because then the macaroon could potentially be used
# for normal authorization with the third party.
- m = macaroonbakery.Macaroon(cav_info.root_key, id, '', cav_info.version,
- cav_info.namespace)
+ m = bakery.Macaroon(
+ cav_info.root_key,
+ id,
+ '',
+ cav_info.version,
+ cav_info.namespace,
+ )
m._caveat_id_prefix = caveat_id_prefix
if caveats is not None:
for cav in caveats:
@@ -155,16 +172,15 @@ def _check_need_declared(ctx, cav_info, checker):
arg = cav_info.condition.decode('utf-8')
i = arg.find(' ')
if i <= 0:
- raise macaroonbakery.VerificationError(
- 'need-declared caveat requires an argument, got %q'.format(arg))
+ raise bakery.VerificationError(
+ 'need-declared caveat requires an argument, got %q'.format(arg),
+ )
need_declared = arg[0:i].split(',')
for d in need_declared:
if d == '':
- raise macaroonbakery.VerificationError('need-declared caveat with '
- 'empty required attribute')
+ raise bakery.VerificationError('need-declared caveat with empty required attribute')
if len(need_declared) == 0:
- raise macaroonbakery.VerificationError('need-declared caveat with no '
- 'required attributes')
+ raise bakery.VerificationError('need-declared caveat with no required attributes')
cav_info = cav_info._replace(condition=arg[i + 1:].encode('utf-8'))
caveats = checker.check_third_party_caveat(ctx, cav_info)
declared = {}
@@ -181,8 +197,7 @@ def _check_need_declared(ctx, cav_info, checker):
continue
parts = arg.split()
if len(parts) != 2:
- raise macaroonbakery.VerificationError('declared caveat has no '
- 'value')
+ raise bakery.VerificationError('declared caveat has no value')
declared[parts[0]] = True
# Add empty declarations for everything mentioned in need-declared
# that was not actually declared.
@@ -192,7 +207,7 @@ def _check_need_declared(ctx, cav_info, checker):
return caveats
-class _EmptyLocator(macaroonbakery.ThirdPartyLocator):
+class _EmptyLocator(bakery.ThirdPartyLocator):
def third_party_info(self, loc):
return None
@@ -205,6 +220,6 @@ def local_third_party_caveat(key, version):
'''
encoded_key = key.encode().decode('utf-8')
loc = 'local {}'.format(encoded_key)
- if version >= macaroonbakery.BAKERY_V2:
+ if version >= bakery.VERSION_2:
loc = 'local {} {}'.format(version, encoded_key)
return checkers.Caveat(location=loc, condition='')
diff --git a/macaroonbakery/httpbakery/__init__.py b/macaroonbakery/httpbakery/__init__.py
index 3b40dc2..3f183c5 100644
--- a/macaroonbakery/httpbakery/__init__.py
+++ b/macaroonbakery/httpbakery/__init__.py
@@ -1,17 +1,55 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from macaroonbakery.httpbakery.client import BakeryAuth, extract_macaroons
+from macaroonbakery.httpbakery.client import (
+ BakeryException,
+ Client,
+ extract_macaroons,
+)
from macaroonbakery.httpbakery.error import (
- BAKERY_PROTOCOL_HEADER, discharged_required_response, request_version
+ BAKERY_PROTOCOL_HEADER,
+ DischargeError,
+ ERR_DISCHARGE_REQUIRED,
+ ERR_INTERACTION_REQUIRED,
+ Error,
+ ErrorInfo,
+ InteractionError,
+ InteractionMethodNotFound,
+ discharge_required_response,
+ request_version,
)
from macaroonbakery.httpbakery.keyring import ThirdPartyLocator
-
+from macaroonbakery.httpbakery.interactor import (
+ DischargeToken,
+ Interactor,
+ LegacyInteractor,
+ WEB_BROWSER_INTERACTION_KIND,
+)
+from macaroonbakery.httpbakery.browser import (
+ WebBrowserInteractionInfo,
+ WebBrowserInteractor,
+)
+from macaroonbakery.httpbakery.discharge import discharge
__all__ = [
'BAKERY_PROTOCOL_HEADER',
- 'BakeryAuth',
+ 'BakeryException',
+ 'Client',
+ 'DischargeError',
+ 'DischargeToken',
+ 'ERR_DISCHARGE_REQUIRED',
+ 'ERR_INTERACTION_REQUIRED',
+ 'Error',
+ 'ErrorInfo',
+ 'InteractionError',
+ 'InteractionMethodNotFound',
+ 'Interactor',
+ 'LegacyInteractor',
'ThirdPartyLocator',
- 'discharged_required_response',
+ 'WEB_BROWSER_INTERACTION_KIND',
+ 'WebBrowserInteractionInfo',
+ 'WebBrowserInteractor',
+ 'discharge',
+ 'discharge_required_response',
'extract_macaroons',
'request_version',
]
diff --git a/macaroonbakery/httpbakery/agent.py b/macaroonbakery/httpbakery/agent.py
deleted file mode 100644
index e5a09e4..0000000
--- a/macaroonbakery/httpbakery/agent.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# Copyright 2017 Canonical Ltd.
-# Licensed under the LGPLv3, see LICENCE file for details.
-import base64
-import json
-
-import nacl.public
-import nacl.encoding
-import requests.cookies
-import six
-from six.moves.urllib.parse import urlparse
-
-
-class AgentFileFormatError(Exception):
- ''' AgentFileFormatError is the exception raised when an agent file has a bad
- structure.
- '''
- pass
-
-
-def load_agent_file(filename, cookies=None):
- ''' Loads agent information from the specified file.
-
- The agent cookies are added to cookies, or a newly created cookie jar
- if cookies is not specified. The updated cookies is returned along
- with the private key associated with the agent. These can be passed
- directly as the cookies and key parameter to BakeryAuth.
- '''
-
- with open(filename) as f:
- data = json.load(f)
- try:
- key = nacl.public.PrivateKey(data['key']['private'],
- nacl.encoding.Base64Encoder)
- if cookies is None:
- cookies = requests.cookies.RequestsCookieJar()
- for agent in data['agents']:
- u = urlparse(agent['url'])
- value = {'username': agent['username'],
- 'public_key': data['key']['public']}
- jv = json.dumps(value)
- if six.PY3:
- jv = jv.encode('utf-8')
- v = base64.b64encode(jv)
- if six.PY3:
- v = v.decode('utf-8')
- cookie = requests.cookies.create_cookie('agent-login', v,
- domain=u.netloc,
- path=u.path)
- cookies.set_cookie(cookie)
- return cookies, key
- except (KeyError, ValueError) as e:
- raise AgentFileFormatError('invalid agent file', e)
diff --git a/macaroonbakery/httpbakery/agent/__init__.py b/macaroonbakery/httpbakery/agent/__init__.py
new file mode 100644
index 0000000..db252de
--- /dev/null
+++ b/macaroonbakery/httpbakery/agent/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+
+from macaroonbakery.httpbakery.agent.agent import (
+ load_agent_file,
+ Agent,
+ AgentInteractor,
+ AgentFileFormatError,
+ AuthInfo,
+)
+__all__ = [
+ 'Agent',
+ 'AgentFileFormatError',
+ 'AgentInteractor',
+ 'AuthInfo',
+ 'load_agent_file',
+]
diff --git a/macaroonbakery/httpbakery/agent/agent.py b/macaroonbakery/httpbakery/agent/agent.py
new file mode 100644
index 0000000..862f00e
--- /dev/null
+++ b/macaroonbakery/httpbakery/agent/agent.py
@@ -0,0 +1,180 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import base64
+from collections import namedtuple
+import json
+
+import nacl.public
+import nacl.encoding
+import requests.cookies
+import six
+from six.moves.urllib.parse import urlparse
+from six.moves.urllib.parse import urljoin
+
+import macaroonbakery as bakery
+import macaroonbakery.utils as utils
+import macaroonbakery.httpbakery as httpbakery
+
+
+class AgentFileFormatError(Exception):
+ ''' AgentFileFormatError is the exception raised when an agent file has a
+ bad structure.
+ '''
+ pass
+
+
+def load_agent_file(filename, cookies=None):
+ ''' Loads agent information from the specified file.
+
+ The agent cookies are added to cookies, or a newly created cookie jar
+ if cookies is not specified. The updated cookies is returned along
+ with the private key associated with the agent. These can be passed
+ directly as the cookies and key parameter to BakeryAuth.
+ '''
+
+ with open(filename) as f:
+ data = json.load(f)
+ try:
+ key = nacl.public.PrivateKey(data['key']['private'],
+ nacl.encoding.Base64Encoder)
+ if cookies is None:
+ cookies = requests.cookies.RequestsCookieJar()
+ for agent in data['agents']:
+ u = urlparse(agent['url'])
+ value = {'username': agent['username'],
+ 'public_key': data['key']['public']}
+ jv = json.dumps(value)
+ if six.PY3:
+ jv = jv.encode('utf-8')
+ v = base64.b64encode(jv)
+ if six.PY3:
+ v = v.decode('utf-8')
+ cookie = requests.cookies.create_cookie('agent-login', v,
+ domain=u.netloc,
+ path=u.path)
+ cookies.set_cookie(cookie)
+ return cookies, key
+ except (KeyError, ValueError) as e:
+ raise AgentFileFormatError('invalid agent file', e)
+
+
+class InteractionInfo(object):
+ '''Holds the information expected in the agent interaction entry in an
+ interaction-required error.
+ '''
+ def __init__(self, login_url):
+ self._login_url = login_url
+
+ @property
+ def login_url(self):
+ ''' Return the URL from which to acquire a macaroon that can be used
+ to complete the agent login. To acquire the macaroon, make a POST
+ request to the URL with user and public-key parameters.
+ :return string
+ '''
+ return self._login_url
+
+ @classmethod
+ def from_dict(cls, json_dict):
+ '''Return an InteractionInfo obtained from the given dictionary as
+ deserialized from JSON.
+ @param json_dict The deserialized JSON object.
+ '''
+ return InteractionInfo(json_dict.get('login-url'))
+
+
+class AgentInteractor(httpbakery.Interactor, httpbakery.LegacyInteractor):
+ ''' Interactor that performs interaction using the agent login protocol.
+ '''
+ def __init__(self, auth_info):
+ self._auth_info = auth_info
+
+ def kind(self):
+ '''Implement Interactor.kind by returning the agent kind'''
+ return 'agent'
+
+ def interact(self, client, location, interaction_required_err):
+ '''Implement Interactor.interact by obtaining obtaining
+ a macaroon from the discharger, discharging it with the
+ local private key using the discharged macaroon as
+ a discharge token'''
+ p = interaction_required_err.interaction_method('agent',
+ InteractionInfo)
+ if p.login_url is None or p.login_url == '':
+ raise httpbakery.InteractionError(
+ 'no login-url field found in agent interaction method')
+ agent = self._find_agent(location)
+ if not location.endswith('/'):
+ location += '/'
+ login_url = urljoin(location, p.login_url)
+ resp = requests.get(login_url, json={
+ 'Username': agent.username,
+ 'PublicKey': self._auth_info.key.encode().decode('utf-8'),
+ })
+ if resp.status_code != 200:
+ raise httpbakery.InteractionError(
+ 'cannot acquire agent macaroon: {}'.format(resp.status_code)
+ )
+ m = resp.json().get('macaroon')
+ if m is None:
+ raise httpbakery.InteractionError('no macaroon in response')
+ m = bakery.Macaroon.from_dict(m)
+ ms = bakery.discharge_all(m, None, self._auth_info.key)
+ b = bytearray()
+ for m in ms:
+ b.extend(utils.b64decode(m.serialize()))
+ return httpbakery.DischargeToken(kind='agent', value=bytes(b))
+
+ def _find_agent(self, location):
+ ''' Finds an appropriate agent entry for the given location.
+ :return Agent
+ '''
+ for a in self._auth_info.agents:
+ # Don't worry about trailing slashes
+ if a.url.rstrip('/') == location.rstrip('/'):
+ return a
+ raise httpbakery.InteractionMethodNotFound(
+ 'cannot find username for discharge location {}'.format(location))
+
+ def legacy_interact(self, client, location, visit_url):
+ '''Implement LegacyInteractor.legacy_interact by obtaining
+ the discharge macaroon using the client's private key
+ '''
+ agent = self._find_agent(location)
+ pk_encoded = self._auth_info.key.public_key.encode().decode('utf-8')
+ value = {
+ 'username': agent.username,
+ 'public_key': pk_encoded,
+ }
+ # TODO(rogpeppe) use client passed into interact method.
+ client = httpbakery.Client(key=self._auth_info.key)
+ client.cookies.set_cookie(utils.cookie(
+ url=visit_url,
+ name='agent-login',
+ value=base64.urlsafe_b64encode(
+ json.dumps(value).encode('utf-8')).decode('utf-8'),
+ ))
+ resp = requests.get(url=visit_url, cookies=client.cookies, auth=client.auth())
+ if resp.status_code != 200:
+ raise httpbakery.InteractionError(
+ 'cannot acquire agent macaroon: {}'.format(resp.status_code))
+ if not resp.json().get('agent-login', False):
+ raise httpbakery.InteractionError('agent login failed')
+
+
+class Agent(namedtuple('Agent', 'url, username')):
+ ''' Represents an agent that can be used for agent authentication.
+ @param url holds the URL of the discharger that knows about the agent (string).
+ @param username holds the username agent (string).
+ '''
+
+
+class AuthInfo(namedtuple('AuthInfo', 'key, agents')):
+ ''' Holds the agent information required to set up agent authentication
+ information.
+
+ It holds the agent's private key and information about the username
+ associated with each known agent-authentication server.
+ @param key the agent's private key (bakery.PrivateKey).
+ @param agents information about the known agents (list of Agent).
+ '''
diff --git a/macaroonbakery/httpbakery/browser.py b/macaroonbakery/httpbakery/browser.py
new file mode 100644
index 0000000..e3ce538
--- /dev/null
+++ b/macaroonbakery/httpbakery/browser.py
@@ -0,0 +1,86 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import base64
+from collections import namedtuple
+import requests
+from six.moves.urllib.parse import urljoin
+
+from macaroonbakery.utils import visit_page_with_browser
+from macaroonbakery.httpbakery.interactor import (
+ Interactor, LegacyInteractor, WEB_BROWSER_INTERACTION_KIND,
+ DischargeToken
+)
+from macaroonbakery.httpbakery.error import InteractionError
+
+
+class WebBrowserInteractor(Interactor, LegacyInteractor):
+ ''' Handles web-browser-based interaction-required errors by opening a
+ web browser to allow the user to prove their credentials interactively.
+ '''
+ def __init__(self, open=visit_page_with_browser):
+ '''Create a WebBrowserInteractor that uses the given function
+ to open a browser window. The open function is expected to take
+ a single argument of string type, the URL to open.
+ '''
+ self._open_web_browser = open
+
+ def kind(self):
+ return WEB_BROWSER_INTERACTION_KIND
+
+ def legacy_interact(self, ctx, location, visit_url):
+ '''Implement LegacyInteractor.legacy_interact by opening the
+ web browser window'''
+ self._open_web_browser(visit_url)
+
+ def interact(self, ctx, location, ir_err):
+ '''Implement Interactor.interact by opening the browser window
+ and waiting for the discharge token'''
+ p = ir_err.interaction_method(self.kind(), WebBrowserInteractionInfo)
+ if not location.endswith('/'):
+ location += '/'
+ visit_url = urljoin(location, p.visit_url)
+ wait_token_url = urljoin(location, p.wait_token_url)
+ self._open_web_browser(visit_url)
+ return self._wait_for_token(ctx, wait_token_url)
+
+ def _wait_for_token(self, ctx, wait_token_url):
+ ''' Returns a token from a the wait token URL
+ @param wait_token_url URL to wait for (string)
+ :return DischargeToken
+ '''
+ resp = requests.get(wait_token_url)
+ if resp.status_code != 200:
+ raise InteractionError('cannot get {}'.format(wait_token_url))
+ json_resp = resp.json()
+ kind = json_resp.get('kind')
+ if kind is None:
+ raise InteractionError(
+ 'cannot get kind token from {}'.format(wait_token_url))
+ token_val = json_resp.get('token')
+ if token_val is None:
+ token_val = json_resp.get('token64')
+ if token_val is None:
+ raise InteractionError(
+ 'cannot get token from {}'.format(wait_token_url))
+ token_val = base64.b64decode(token_val)
+ return DischargeToken(kind=kind, value=token_val)
+
+
+class WebBrowserInteractionInfo(namedtuple('WebBrowserInteractionInfo',
+ 'visit_url, wait_token_url')):
+ ''' holds the information expected in the browser-window interaction
+ entry in an interaction-required error.
+
+ :param visit_url holds the URL to be visited in a web browser.
+ :param wait_token_url holds a URL that will block on GET until the browser
+ interaction has completed.
+ '''
+ @classmethod
+ def from_dict(cls, info_dict):
+ '''Create a new instance of WebBrowserInteractionInfo, as expected
+ by the Error.interaction_method method.
+ @param info_dict The deserialized JSON object
+ @return a new WebBrowserInteractionInfo object.
+ '''
+ return WebBrowserInteractionInfo(visit_url=info_dict.get('VisitURL'),
+ wait_token_url=info_dict('WaitURL'))
diff --git a/macaroonbakery/httpbakery/client.py b/macaroonbakery/httpbakery/client.py
index b62c61d..b3036a1 100644
--- a/macaroonbakery/httpbakery/client.py
+++ b/macaroonbakery/httpbakery/client.py
@@ -4,65 +4,246 @@ import base64
import json
import requests
from six.moves.http_cookies import SimpleCookie
-from six.moves.http_cookiejar import Cookie
from six.moves.urllib.parse import urljoin
-from six.moves.urllib.parse import urlparse
-from pymacaroons import Macaroon
-from pymacaroons.serializers.json_serializer import JsonSerializer
-
-from macaroonbakery.bakery import discharge_all
+import macaroonbakery as bakery
+import macaroonbakery.checkers as checkers
from macaroonbakery import utils
+from macaroonbakery.httpbakery.interactor import (
+ LegacyInteractor,
+ WEB_BROWSER_INTERACTION_KIND,
+)
+from macaroonbakery.httpbakery.error import (
+ DischargeError,
+ ERR_DISCHARGE_REQUIRED,
+ ERR_INTERACTION_REQUIRED,
+ Error,
+ InteractionError,
+ InteractionMethodNotFound,
+)
+from macaroonbakery.httpbakery.error import BAKERY_PROTOCOL_HEADER
+from macaroonbakery.httpbakery.browser import WebBrowserInteractor
-ERR_INTERACTION_REQUIRED = 'interaction required'
-ERR_DISCHARGE_REQUIRED = 'macaroon discharge required'
TIME_OUT = 30
MAX_DISCHARGE_RETRIES = 3
-class BakeryAuth:
- ''' BakeryAuth holds the context for making HTTP requests with macaroons.
+class BakeryException(requests.RequestException):
+ '''Raised when some errors happen using the httpbakery
+ authorizer'''
+
- This will automatically acquire and discharge macaroons around the
- requests framework.
- Usage:
- from macaroonbakery import httpbakery
- jar = requests.cookies.RequestsCookieJar()
- resp = requests.get('some protected url',
- cookies=jar,
- auth=httpbakery.BakeryAuth(cookies=jar))
- resp.raise_for_status()
+class Client:
+ '''Client holds the context for making HTTP requests with macaroons.
+ To make a request, use the auth method to obtain
+ an HTTP authorizer suitable for passing as the auth parameter
+ to a requests method. Note that the same cookie jar
+ should be passed to requests as is used to initialize
+ the client.
+ For example:
+ import macaroonbakery.httpbakery
+ client = httpbakery.Client()
+ resp = requests.get('some protected url',
+ cookies=client.cookies,
+ auth=client.auth())
+ @param interaction_methods A list of Interactor implementations.
+ @param key The private key of the client {bakery.PrivateKey}
+ @param cookies storage for the cookies {CookieJar}. It should be the
+ same as in the requests cookies. If not provided, one
+ will be created.
'''
- def __init__(self, visit_page=None, key=None,
- cookies=requests.cookies.RequestsCookieJar()):
+ def __init__(self, interaction_methods=None, key=None, cookies=None):
+ if interaction_methods is None:
+ interaction_methods = [WebBrowserInteractor()]
+ if cookies is None:
+ cookies = requests.cookies.RequestsCookieJar()
+ self._interaction_methods = interaction_methods
+ self._key = key
+ self.cookies = cookies
+
+ def auth(self):
+ '''Return an authorizer object suitable for passing
+ to requests methods that accept one.
+ If a request returns a discharge-required error,
+ the authorizer will acquire discharge macaroons
+ and retry the request.
'''
+ return _BakeryAuth(self)
- @param visit_page function called when the discharge process requires
- further interaction taking a visit_url string as parameter.
- @param key holds the client's private nacl key. If set, the client
- will try to discharge third party caveats with the special location
- "local" by using this key.
- @param cookies storage for the cookies {CookieJar}. It should be the
- same than in the requests cookies
+ def request(self, method, url, **kwargs):
+ '''Use the requests library to make a request.
+ Using this method is like doing:
+
+ requests.request(method, url, auth=client.auth())
'''
- if visit_page is None:
- visit_page = utils.visit_page_with_browser
- if 'agent-login' in cookies.keys():
- self._visit_page = _visit_page_for_agent(cookies, key)
+ kwargs.setdefault('auth', self.auth())
+ return requests.request(method=method, url=url, **kwargs)
+
+ def handle_error(self, error, url):
+ '''Try to resolve the given error, which should be a response
+ to the given URL, by discharging any macaroon contained in
+ it. That is, if error.code is ERR_DISCHARGE_REQUIRED
+ then it will try to discharge err.info.macaroon. If the discharge
+ succeeds, the discharged macaroon will be saved to the client's cookie jar,
+ otherwise an exception will be raised.
+ '''
+ if error.info is None or error.info.macaroon is None:
+ raise BakeryException('unable to read info in discharge error response')
+
+ discharges = bakery.discharge_all(
+ error.info.macaroon,
+ self.acquire_discharge,
+ self._key,
+ )
+ macaroons = '[' + ','.join(map(utils.macaroon_to_json_string, discharges)) + ']'
+ all_macaroons = base64.urlsafe_b64encode(utils.to_bytes(macaroons))
+
+ full_path = relative_url(url, error.info.macaroon_path)
+ if error.info.cookie_name_suffix is not None:
+ name = 'macaroon-' + error.info.cookie_name_suffix
else:
- self._visit_page = visit_page
- self._jar = cookies
- self._key = key
+ name = 'macaroon-auth'
+ expires = checkers.macaroons_expiry_time(checkers.Namespace(), discharges)
+ expires = None # TODO(rogpeppe) remove this line after fixing the tests.
+ self.cookies.set_cookie(utils.cookie(
+ name=name,
+ value=all_macaroons.decode('ascii'),
+ url=full_path,
+ expires=expires,
+ ))
+
+ def acquire_discharge(self, cav, payload):
+ ''' Request a discharge macaroon from the caveat location
+ as an HTTP URL.
+ @param cav Third party {pymacaroons.Caveat} to be discharged.
+ @param payload External caveat data {bytes}.
+ @return The acquired macaroon {macaroonbakery.Macaroon}
+ '''
+ resp = self._acquire_discharge_with_token(cav, payload, None)
+ # TODO Fabrice what is the other http response possible ??
+ if resp.status_code == 200:
+ return bakery.Macaroon.from_dict(resp.json().get('Macaroon'))
+ cause = Error.from_dict(resp.json())
+ if cause.code != ERR_INTERACTION_REQUIRED:
+ raise DischargeError(cause.message)
+ if cause.info is None:
+ raise DischargeError(
+ 'interaction-required response with no info: {}'.format(resp.json())
+ )
+ loc = cav.location
+ if not loc.endswith('/'):
+ loc = loc + '/'
+ token, m = self._interact(loc, cause, payload)
+ if m is not None:
+ # We've acquired the macaroon directly via legacy interaction.
+ return m
+ # Try to acquire the discharge again, but this time with
+ # the token acquired by the interaction method.
+ resp = self._acquire_discharge_with_token(cav, payload, token)
+ if resp.status_code == 200:
+ return bakery.Macaroon.deserialize_json(
+ resp.json().get('Macaroon'))
+ else:
+ raise DischargeError()
+
+ def _acquire_discharge_with_token(self, cav, payload, token):
+ req = {}
+ _add_json_binary_field(cav.caveat_id_bytes, req, 'id')
+ if token is not None:
+ _add_json_binary_field(token.value, req, 'token')
+ req['token-kind'] = token.kind
+ if payload is not None:
+ req['caveat64'] = base64.urlsafe_b64encode(payload).rstrip(
+ b'=').decode('utf-8')
+ target = relative_url(cav.location, 'discharge')
+ headers = {
+ BAKERY_PROTOCOL_HEADER: str(bakery.LATEST_VERSION)
+ }
+ return self.request('POST', target, data=req, headers=headers)
+
+ def _interact(self, location, error_info, payload):
+ '''Gathers a macaroon by directing the user to interact with a
+ web page. The error_info argument holds the interaction-required
+ error response.
+ @return DischargeToken, bakery.Macaroon
+ '''
+ if self._interaction_methods is None or len(self._interaction_methods) == 0:
+ raise InteractionError('interaction required but not possible')
+ # TODO(rogpeppe) make the robust against a wider range of error info.
+ if error_info.info.interaction_methods is None and \
+ error_info.info.visit_url is not None:
+ # It's an old-style error; deal with it differently.
+ return None, self._legacy_interact(location, error_info)
+
+ for interactor in self._interaction_methods:
+ found = error_info.info.interaction_methods.get(interactor.kind())
+ if found is None:
+ continue
+ try:
+ token = interactor.interact(self, location, error_info)
+ except InteractionMethodNotFound:
+ continue
+ if token is None:
+ raise InteractionError('interaction method returned an empty token')
+ return token, None
+
+ raise InteractionError('no supported interaction method')
+
+ def _legacy_interact(self, location, error_info):
+ visit_url = relative_url(location, error_info.info.visit_url)
+ wait_url = relative_url(location, error_info.info.wait_url)
+ method_urls = {
+ "interactive": visit_url
+ }
+ if len(self._interaction_methods) > 1 or \
+ self._interaction_methods[0].kind() != WEB_BROWSER_INTERACTION_KIND:
+ # We have several possible methods or we only support a non-window
+ # method, so we need to fetch the possible methods supported by
+ # the discharger.
+ method_urls = _legacy_get_interaction_methods(visit_url)
+
+ for interactor in self._interaction_methods:
+ kind = interactor.kind()
+ if kind == WEB_BROWSER_INTERACTION_KIND:
+ # This is the old name for browser-window interaction.
+ kind = "interactive"
+
+ if not isinstance(interactor, LegacyInteractor):
+ # Legacy interaction mode isn't supported.
+ continue
+
+ visit_url = method_urls.get(kind)
+ if visit_url is None:
+ continue
+
+ visit_url = relative_url(location, visit_url)
+ interactor.legacy_interact(self, location, visit_url)
+ return _wait_for_macaroon(wait_url)
+
+ raise InteractionError('no methods supported')
+
+
+class _BakeryAuth:
+ '''_BakeryAuth implements an authorizer as required
+ by the requests HTTP client.
+ '''
+ def __init__(self, client):
+ '''
+ @param interaction_methods A list of Interactor implementations.
+ @param key The private key of the client (macaroonbakery.PrivateKey)
+ @param cookies storage for the cookies {CookieJar}. It should be the
+ same as in the requests cookies.
+ '''
+ self._client = client
def __call__(self, req):
- req.headers['Bakery-Protocol-Version'] = '1'
- hook = _prepare_discharge_hook(req.copy(), self._key, self._jar,
- self._visit_page)
+ req.headers[BAKERY_PROTOCOL_HEADER] = str(bakery.LATEST_VERSION)
+ hook = _prepare_discharge_hook(req.copy(), self._client)
req.register_hook(event='response', hook=hook)
return req
-def _prepare_discharge_hook(req, key, jar, visit_page):
+def _prepare_discharge_hook(req, client):
''' Return the hook function (called when the response is received.)
This allows us to intercept the response and do any necessary
@@ -76,106 +257,131 @@ def _prepare_discharge_hook(req, key, jar, visit_page):
def hook(response, *args, **kwargs):
''' Requests hooks system, this is the hook for the response.
'''
- status_401 = (response.status_code == 401
- and response.headers.get('WWW-Authenticate') ==
- 'Macaroon')
- if not status_401 and response.status_code != 407:
+ status_code = response.status_code
+
+ if status_code != 407 and status_code != 401:
return response
- if response.headers.get('Content-Type') != 'application/json':
+ if (status_code == 401 and response.headers.get('WWW-Authenticate') !=
+ 'Macaroon'):
return response
- try:
- error = response.json()
- except:
- raise BakeryException(
- 'unable to read discharge error response')
- if error.get('Code') != ERR_DISCHARGE_REQUIRED:
+ if response.headers.get('Content-Type') != 'application/json':
return response
+ errorJSON = response.json()
+ if errorJSON.get('Code') != ERR_DISCHARGE_REQUIRED:
+ return response
+ error = Error.from_dict(errorJSON)
Retry.count += 1
- if Retry.count > MAX_DISCHARGE_RETRIES:
- raise BakeryException('too many discharges')
- info = error.get('Info')
- if not isinstance(info, dict):
- raise BakeryException(
- 'unable to read info in discharge error response')
- serialized_macaroon = info.get('Macaroon')
- if not isinstance(serialized_macaroon, dict):
- raise BakeryException(
- 'unable to read macaroon in discharge error response')
-
- macaroon = utils.deserialize(serialized_macaroon)
- discharges = discharge_all(macaroon, visit_page, jar, key)
- encoded_discharges = map(utils.serialize_macaroon_string, discharges)
-
- macaroons = '[' + ','.join(encoded_discharges) + ']'
- all_macaroons = base64.urlsafe_b64encode(
- macaroons.encode('utf-8')).decode('ascii')
-
- full_path = urljoin(response.url,
- info['MacaroonPath'])
- parsed_url = urlparse(full_path)
- if info and info.get('CookieNameSuffix'):
- name = 'macaroon-' + info['CookieNameSuffix']
- else:
- name = 'macaroon-' + discharges[0].signature
- cookie = Cookie(
- version=0,
- name=name,
- value=all_macaroons,
- port=None,
- port_specified=False,
- domain=parsed_url[1],
- domain_specified=True,
- domain_initial_dot=False,
- path=parsed_url[2],
- path_specified=True,
- secure=False,
- expires=None,
- discard=False,
- comment=None,
- comment_url=None,
- rest=None,
- rfc2109=False)
- jar.set_cookie(cookie)
+ if Retry.count >= MAX_DISCHARGE_RETRIES:
+ raise BakeryException('too many ({}) discharge requests'.format(
+ Retry.count)
+ )
+ client.handle_error(error, req.url)
# Replace the private _cookies from req as it is a copy of
# the original cookie jar passed into the requests method and we need
# to set the cookie for this request.
- req._cookies = jar
+ req._cookies = client.cookies
req.headers.pop('Cookie', None)
req.prepare_cookies(req._cookies)
- req.headers['Bakery-Protocol-Version'] = '1'
+ req.headers[BAKERY_PROTOCOL_HEADER] = \
+ str(bakery.LATEST_VERSION)
with requests.Session() as s:
return s.send(req)
return hook
-class BakeryException(requests.RequestException):
- ''' Bakery exception '''
-
-
-def _visit_page_for_agent(cookies, key):
- def visit_page_for_agent(visit_url):
- resp = requests.get(visit_url, cookies=cookies,
- auth=BakeryAuth(cookies=cookies, key=key))
- resp.raise_for_status()
- return visit_page_for_agent
-
-
def extract_macaroons(headers):
''' Returns an array of any macaroons found in the given slice of cookies.
@param headers: dict of headers
@return: An array of array of mpy macaroons
'''
- cookie_string = "\n".join(headers.get_all('Cookie', failobj=[]))
- cs = SimpleCookie()
- cs.load(cookie_string)
mss = []
- for c in cs:
- if not c.startswith('macaroon-'):
- continue
- data = base64.b64decode(cs[c].value)
+
+ def add_macaroon(data):
+ data = utils.b64decode(data)
data_as_objs = json.loads(data.decode('utf-8'))
- ms = [Macaroon.deserialize(json.dumps(x), serializer=JsonSerializer())
- for x in data_as_objs]
+ ms = [utils.macaroon_from_dict(x) for x in data_as_objs]
mss.append(ms)
+
+ cookieHeader = headers.get('Cookie')
+ if cookieHeader is not None:
+ cs = SimpleCookie()
+ # The cookie might be a unicode object, so convert it
+ # to ASCII. This may cause an exception under Python 2.
+ # TODO is that a problem?
+ cs.load(str(cookieHeader))
+ for c in cs:
+ if c.startswith('macaroon-'):
+ add_macaroon(cs[c].value)
+ # Python doesn't make it easy to have multiple values for a
+ # key, so split the header instead, which is necessary
+ # for HTTP1.1 compatibility anyway.
+ macaroonHeader = headers.get('Macaroons')
+ if macaroonHeader is not None:
+ for h in macaroonHeader.split(','):
+ add_macaroon(h)
return mss
+
+
+def _add_json_binary_field(b, serialized, field):
+ '''' Set the given field to the given val (bytes) in the serialized
+ dictionary.
+ If the value isn't valid utf-8, we base64 encode it and use field+"64"
+ as the field name.
+ '''
+ try:
+ val = b.decode('utf-8')
+ serialized[field] = val
+ except UnicodeDecodeError:
+ val = base64.b64encode(b).decode('utf-8')
+ serialized[field + '64'] = val
+
+
+def _wait_for_macaroon(wait_url):
+ ''' Returns a macaroon from a legacy wait endpoint.
+ '''
+ headers = {
+ BAKERY_PROTOCOL_HEADER: str(bakery.LATEST_VERSION)
+ }
+ resp = requests.get(url=wait_url, headers=headers)
+ if resp.status_code != 200:
+ return InteractionError('cannot get {}'.format(wait_url))
+
+ return bakery.Macaroon.from_dict(resp.json().get('Macaroon'))
+
+
+def relative_url(base, new):
+ ''' Returns new path relative to an original URL.
+ '''
+ if new == '':
+ return base
+ if not base.endswith('/'):
+ base += '/'
+ return urljoin(base, new)
+
+
+def _legacy_get_interaction_methods(u):
+ ''' Queries a URL as found in an ErrInteractionRequired VisitURL field to
+ find available interaction methods.
+ It does this by sending a GET request to the URL with the Accept
+ header set to "application/json" and parsing the resulting
+ response as a dict.
+ '''
+ headers = {
+ BAKERY_PROTOCOL_HEADER: str(bakery.LATEST_VERSION),
+ 'Accept': 'application/json'
+ }
+ resp = requests.get(url=u, headers=headers)
+ method_urls = {}
+ if resp.status_code == 200:
+ json_resp = resp.json()
+ for m in json_resp:
+ relative_url(u, json_resp[m])
+ method_urls[m] = relative_url(u, json_resp[m])
+
+ if method_urls.get('interactive') is None:
+ # There's no "interactive" method returned, but we know
+ # the server does actually support it, because all dischargers
+ # are required to, so fill it in with the original URL.
+ method_urls['interactive'] = u
+ return method_urls
diff --git a/macaroonbakery/httpbakery/discharge.py b/macaroonbakery/httpbakery/discharge.py
new file mode 100644
index 0000000..ef3481a
--- /dev/null
+++ b/macaroonbakery/httpbakery/discharge.py
@@ -0,0 +1,33 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import macaroonbakery.utils as utils
+import macaroonbakery as bakery
+
+
+def discharge(ctx, content, key, locator, checker):
+ '''Handles a discharge request as received by the /discharge
+ endpoint.
+ @param ctx The context passed to the checker {checkers.AuthContext}
+ @param content URL and form parameters {dict}
+ @param locator Locator used to add third party caveats returned by
+ the checker {macaroonbakery.ThirdPartyLocator}
+ @param checker Used to check third party caveats {macaroonbakery.ThirdPartyCaveatChecker}
+ @return The discharge macaroon {macaroonbakery.Macaroon}
+ '''
+ id = content.get('id')
+ if id is None:
+ id = content.get('id64')
+ if id is not None:
+ id = utils.b64decode(id)
+ caveat = content.get('caveat64')
+ if caveat is not None:
+ caveat = utils.b64decode(caveat)
+
+ return bakery.discharge(
+ ctx,
+ id=id,
+ caveat=caveat,
+ key=key,
+ checker=checker,
+ locator=locator,
+ )
diff --git a/macaroonbakery/httpbakery/error.py b/macaroonbakery/httpbakery/error.py
index e138c66..422b346 100644
--- a/macaroonbakery/httpbakery/error.py
+++ b/macaroonbakery/httpbakery/error.py
@@ -1,11 +1,37 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
+from collections import namedtuple
import json
-import macaroonbakery
+import macaroonbakery as bakery
+ERR_INTERACTION_REQUIRED = 'interaction required'
+ERR_DISCHARGE_REQUIRED = 'macaroon discharge required'
-def discharged_required_response(macaroon, path, cookie_suffix_name):
+
+class InteractionMethodNotFound(Exception):
+ '''This is thrown by client-side interaction methods when
+ they find that a given interaction isn't supported by the
+ client for a location'''
+ pass
+
+
+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))
+
+
+class InteractionError(Exception):
+ '''This is thrown by Client when it fails to deal with an
+ interaction-required error
+ '''
+ def __init__(self, msg):
+ super(InteractionError, self).__init__('cannot start interactive session: {}'.format(msg))
+
+
+def discharge_required_response(macaroon, path, cookie_suffix_name,
+ message=None):
''' Get response content and headers from a discharge macaroons error.
@param macaroon may hold a macaroon that, when discharged, may
@@ -18,17 +44,19 @@ def discharged_required_response(macaroon, path, cookie_suffix_name):
older clients will always use ("macaroon-" + macaroon.signature() in hex)
@return content(bytes) and the headers to set on the response(dict).
'''
+ if message is None:
+ message = 'discharge required'
content = json.dumps(
{
'Code': 'macaroon discharge required',
- 'Message': 'discharge required',
+ 'Message': message,
'Info': {
'Macaroon': macaroon.to_dict(),
'MacaroonPath': path,
'CookieNameSuffix': cookie_suffix_name
},
}
- )
+ ).encode('utf-8')
return content, {
'WWW-Authenticate': 'Macaroon',
'Content-Type': 'application/json'
@@ -49,19 +77,124 @@ def request_version(req_headers):
version is used, which is OK because versions are backwardly compatible.
@param req_headers: the request headers as a dict.
- @return: bakery protocol version (for example macaroonbakery.BAKERY_V1)
+ @return: bakery protocol version (for example macaroonbakery.VERSION_1)
'''
vs = req_headers.get(BAKERY_PROTOCOL_HEADER)
if vs is None:
# No header - use backward compatibility mode.
- return macaroonbakery.BAKERY_V1
+ return bakery.VERSION_1
try:
x = int(vs)
except ValueError:
# Badly formed header - use backward compatibility mode.
- return macaroonbakery.BAKERY_V1
- if x > macaroonbakery.LATEST_BAKERY_VERSION:
+ return bakery.VERSION_1
+ if x > bakery.LATEST_VERSION:
# Later version than we know about - use the
# latest version that we can.
- return macaroonbakery.LATEST_BAKERY_VERSION
+ return bakery.LATEST_VERSION
return x
+
+
+class Error(namedtuple('Error', 'code, message, version, info')):
+ '''This class defines an error value as returned from
+ an httpbakery API.
+ '''
+ @classmethod
+ def from_dict(cls, serialized):
+ '''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)
+
+ def interaction_method(self, kind, x):
+ ''' Checks whether the error is an InteractionRequired error
+ that implements the method with the given name, and JSON-unmarshals the
+ method-specific data into x by calling its from_dict method
+ with the deserialized JSON object.
+ @param kind The interaction method kind (string).
+ @param x A class with a class method from_dict that returns a new
+ instance of the interaction info for the given kind.
+ @return The result of x.from_dict.
+ '''
+ if self.info is None or self.code != ERR_INTERACTION_REQUIRED:
+ raise InteractionError(
+ 'not an interaction-required error (code {})'.format(
+ self.code)
+ )
+ entry = self.info.interaction_methods.get(kind)
+ if entry is None:
+ raise InteractionMethodNotFound(
+ 'interaction method {} not found'.format(kind)
+ )
+ return x.from_dict(entry)
+
+
+class ErrorInfo(
+ namedtuple('ErrorInfo', 'macaroon, macaroon_path, cookie_name_suffix, '
+ 'interaction_methods, visit_url, wait_url')):
+ ''' Holds additional information provided
+ by an error.
+
+ @param macaroon may hold a macaroon that, when
+ discharged, may allow access to a service.
+ This field is associated with the ERR_DISCHARGE_REQUIRED
+ error code.
+
+ @param macaroon_path holds the URL path to be associated
+ with the macaroon. The macaroon is potentially
+ valid for all URLs under the given path.
+ If it is empty, the macaroon will be associated with
+ the original URL from which the error was returned.
+
+ @param cookie_name_suffix holds the desired cookie name suffix to be
+ associated with the macaroon. The actual name used will be
+ ("macaroon-" + cookie_name_suffix). Clients may ignore this field -
+ older clients will always use ("macaroon-" +
+ macaroon.signature() in hex).
+
+ @param visit_url holds a URL that the client should visit
+ in a web browser to authenticate themselves.
+
+ @param wait_url holds a URL that the client should visit
+ to acquire the discharge macaroon. A GET on
+ this URL will block until the client has authenticated,
+ and then it will return the discharge macaroon.
+ '''
+
+ __slots__ = ()
+
+ @classmethod
+ def from_dict(cls, serialized):
+ '''Create a new ErrorInfo object from a JSON deserialized
+ dictionary
+ @param serialized The JSON object {dict}
+ @return ErrorInfo object
+ '''
+ if serialized is None:
+ return None
+ macaroon = serialized.get('Macaroon')
+ if macaroon is not None:
+ macaroon = bakery.Macaroon.from_dict(macaroon)
+ path = serialized.get('MacaroonPath')
+ cookie_name_suffix = serialized.get('CookieNameSuffix')
+ visit_url = serialized.get('VisitURL')
+ wait_url = serialized.get('WaitURL')
+ interaction_methods = serialized.get('InteractionMethods')
+ return ErrorInfo(macaroon=macaroon, macaroon_path=path,
+ cookie_name_suffix=cookie_name_suffix,
+ visit_url=visit_url, wait_url=wait_url,
+ interaction_methods=interaction_methods)
+
+ def __new__(cls, macaroon=None, macaroon_path=None,
+ cookie_name_suffix=None, interaction_methods=None,
+ visit_url=None, wait_url=None):
+ '''Override the __new__ method so that we can
+ have optional arguments, which namedtuple doesn't
+ allow'''
+ return super(ErrorInfo, cls).__new__(
+ cls, macaroon, macaroon_path, cookie_name_suffix,
+ interaction_methods, visit_url, wait_url)
diff --git a/macaroonbakery/httpbakery/interactor.py b/macaroonbakery/httpbakery/interactor.py
new file mode 100644
index 0000000..0c15338
--- /dev/null
+++ b/macaroonbakery/httpbakery/interactor.py
@@ -0,0 +1,73 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import abc
+from collections import namedtuple
+
+WEB_BROWSER_INTERACTION_KIND = 'browser-window'
+
+
+class Interactor(object):
+ ''' Represents a way of persuading a discharger that it should grant a
+ discharge macaroon.
+ '''
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def kind(self):
+ '''Returns the interaction method name. This corresponds to the key in
+ the Error.interaction_methods type.
+ @return {str}
+ '''
+ raise NotImplementedError('kind method must be defined in '
+ 'subclass')
+
+ def interact(self, client, location, interaction_required_err):
+ ''' Performs the interaction, and returns a token that can be
+ used to acquire the discharge macaroon. The location provides
+ the third party caveat location to make it possible to use
+ relative URLs. The client holds the client being used to do the current
+ request.
+
+ If the given interaction isn't supported by the client for
+ the given location, it may raise an InteractionMethodNotFound
+ which will cause the interactor to be ignored that time.
+ @param client The client being used for the current request {Client}
+ @param location Third party caveat location {str}
+ @param interaction_required_err The error causing the interaction to
+ take place {Error}
+ @return {DischargeToken} The discharge token.
+ '''
+ raise NotImplementedError('interact method must be defined in '
+ 'subclass')
+
+
+class LegacyInteractor(object):
+ ''' May optionally be implemented by Interactor implementations that
+ implement the legacy interaction-required error protocols.
+ '''
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def legacy_interact(self, client, location, visit_url):
+ ''' Implements the "visit" half of a legacy discharge
+ interaction. The "wait" half will be implemented by httpbakery.
+ The location is the location specified by the third party
+ caveat. The client holds the client being used to do the current
+ request.
+ @param client The client being used for the current request {Client}
+ @param location Third party caveat location {str}
+ @param visit_url The visit_url field from the error {str}
+ @return None
+ '''
+ raise NotImplementedError('legacy_interact method must be defined in '
+ 'subclass')
+
+
+class DischargeToken(namedtuple('DischargeToken', 'kind, value')):
+ ''' Holds a token that is intended to persuade a discharger to discharge
+ a third party caveat.
+ @param kind holds the kind of the token. By convention this
+ matches the name of the interaction method used to
+ obtain the token, but that's not required {str}
+ @param value holds the token data. {bytes}
+ '''
diff --git a/macaroonbakery/httpbakery/keyring.py b/macaroonbakery/httpbakery/keyring.py
index f4e93f7..01a4349 100644
--- a/macaroonbakery/httpbakery/keyring.py
+++ b/macaroonbakery/httpbakery/keyring.py
@@ -3,10 +3,11 @@
from six.moves.urllib.parse import urlparse
import requests
-import macaroonbakery
+import macaroonbakery as bakery
+from macaroonbakery.httpbakery.error import BAKERY_PROTOCOL_HEADER
-class ThirdPartyLocator(macaroonbakery.ThirdPartyLocator):
+class ThirdPartyLocator(bakery.ThirdPartyLocator):
''' Implements macaroonbakery.ThirdPartyLocator by first looking in the
backing cache and, if that fails, making an HTTP request to find the
information associated with the given discharge location.
@@ -23,33 +24,36 @@ class ThirdPartyLocator(macaroonbakery.ThirdPartyLocator):
def third_party_info(self, loc):
u = urlparse(loc)
if u.scheme != 'https' and not self._allow_insecure:
- raise macaroonbakery.ThirdPartyInfoNotFound(
+ raise bakery.ThirdPartyInfoNotFound(
'untrusted discharge URL {}'.format(loc))
loc = loc.rstrip('/')
info = self._cache.get(loc)
if info is not None:
return info
url_endpoint = '/discharge/info'
- resp = requests.get(loc + url_endpoint)
+ headers = {
+ BAKERY_PROTOCOL_HEADER: str(bakery.LATEST_VERSION)
+ }
+ resp = requests.get(url=loc + url_endpoint, headers=headers)
status_code = resp.status_code
if status_code == 404:
url_endpoint = '/publickey'
- resp = requests.get(loc + url_endpoint)
+ resp = requests.get(url=loc + url_endpoint, headers=headers)
status_code = resp.status_code
if status_code != 200:
- raise macaroonbakery.ThirdPartyInfoNotFound(
+ raise bakery.ThirdPartyInfoNotFound(
'unable to get info from {}'.format(url_endpoint))
json_resp = resp.json()
if json_resp is None:
- raise macaroonbakery.ThirdPartyInfoNotFound(
+ raise bakery.ThirdPartyInfoNotFound(
'no response from /discharge/info')
pk = json_resp.get('PublicKey')
if pk is None:
- raise macaroonbakery.ThirdPartyInfoNotFound(
+ raise bakery.ThirdPartyInfoNotFound(
'no public key found in /discharge/info')
- idm_pk = macaroonbakery.PublicKey.deserialize(pk)
- version = json_resp.get('Version', macaroonbakery.BAKERY_V1)
- self._cache[loc] = macaroonbakery.ThirdPartyInfo(
+ idm_pk = bakery.PublicKey.deserialize(pk)
+ version = json_resp.get('Version', bakery.VERSION_1)
+ self._cache[loc] = bakery.ThirdPartyInfo(
version=version,
public_key=idm_pk
)
diff --git a/macaroonbakery/identity.py b/macaroonbakery/identity.py
index 23e2e4b..1579bba 100644
--- a/macaroonbakery/identity.py
+++ b/macaroonbakery/identity.py
@@ -2,7 +2,7 @@
# Licensed under the LGPLv3, see LICENCE file for details.
import abc
-import macaroonbakery
+import macaroonbakery as bakery
class Identity(object):
@@ -96,7 +96,7 @@ class IdentityClient(object):
(for example because of a database access error) - it's
OK to return all zero values when there's
no identity found and no third party to address caveats to.
- :param: ctx an AuthContext
+ @param ctx an AuthContext
:return: an Identity and array of caveats
'''
raise NotImplementedError('identity_from_context method must be '
@@ -107,8 +107,8 @@ class IdentityClient(object):
'''Parses the identity declaration from the given declared attributes.
TODO take the set of first party caveat conditions instead?
- :param: ctx (AuthContext)
- :param: declared (dict of string/string)
+ @param ctx (AuthContext)
+ @param declared (dict of string/string)
:return: an Identity
'''
raise NotImplementedError('declared_identity method must be '
@@ -123,4 +123,4 @@ class NoIdentities(IdentityClient):
return None, None
def declared_identity(self, ctx, declared):
- raise macaroonbakery.IdentityError('no identity declared or possible')
+ raise bakery.IdentityError('no identity declared or possible')
diff --git a/macaroonbakery/macaroon.py b/macaroonbakery/macaroon.py
index 6f6039e..b745282 100644
--- a/macaroonbakery/macaroon.py
+++ b/macaroonbakery/macaroon.py
@@ -9,7 +9,7 @@ import os
import pymacaroons
from pymacaroons.serializers import json_serializer
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
from macaroonbakery import utils
@@ -24,7 +24,7 @@ class Macaroon(object):
'''
def __init__(self, root_key, id, location=None,
- version=macaroonbakery.LATEST_BAKERY_VERSION, namespace=None):
+ version=bakery.LATEST_VERSION, namespace=None):
'''Creates a new macaroon with the given root key, id and location.
If the version is more than the latest known version,
@@ -36,11 +36,11 @@ class Macaroon(object):
@param version the bakery version.
@param namespace is that of the service creating it
'''
- if version > macaroonbakery.LATEST_BAKERY_VERSION:
+ if version > bakery.LATEST_VERSION:
log.info('use last known version:{} instead of: {}'.format(
- macaroonbakery.LATEST_BAKERY_VERSION, version
+ bakery.LATEST_VERSION, version
))
- version = macaroonbakery.LATEST_BAKERY_VERSION
+ version = bakery.LATEST_VERSION
# m holds the underlying macaroon.
self._macaroon = pymacaroons.Macaroon(
location=location, key=root_key, identifier=id,
@@ -115,12 +115,14 @@ class Macaroon(object):
# Use the least supported version to encode the caveat.
if self._version < info.version:
- info = macaroonbakery.ThirdPartyInfo(version=self._version,
- public_key=info.public_key)
+ info = bakery.ThirdPartyInfo(
+ version=self._version,
+ public_key=info.public_key,
+ )
- caveat_info = macaroonbakery.encode_caveat(
+ caveat_info = bakery.encode_caveat(
cav.condition, root_key, info, key, self._namespace)
- if info.version < macaroonbakery.BAKERY_V3:
+ if info.version < bakery.VERSION_3:
# We're encoding for an earlier client or third party which does
# not understand bundled caveat info, so use the encoded
# caveat information as the caveat id.
@@ -155,7 +157,7 @@ class Macaroon(object):
'''Return a dict representation of the macaroon data in JSON format.
@return a dict
'''
- if self.version < macaroonbakery.BAKERY_V3:
+ if self.version < bakery.VERSION_3:
if len(self._caveat_data) > 0:
raise ValueError('cannot serialize pre-version3 macaroon with '
'external caveat data')
@@ -178,37 +180,40 @@ class Macaroon(object):
return serialized
@classmethod
- def deserialize_json(cls, serialized_json):
- serialized = json.loads(serialized_json)
- json_macaroon = serialized.get('m')
+ def from_dict(cls, json_dict):
+ '''Return a macaroon obtained from the given dictionary as
+ deserialized from JSON.
+ @param json_dict The deserialized JSON object.
+ '''
+ json_macaroon = json_dict.get('m')
if json_macaroon is None:
- # Try the v1 format if we don't have a macaroon filed
+ # Try the v1 format if we don't have a macaroon field.
m = pymacaroons.Macaroon.deserialize(
- serialized_json, json_serializer.JsonSerializer())
+ json.dumps(json_dict), json_serializer.JsonSerializer())
macaroon = Macaroon(root_key=None, id=None,
- namespace=macaroonbakery.legacy_namespace(),
+ namespace=bakery.legacy_namespace(),
version=_bakery_version(m.version))
macaroon._macaroon = m
return macaroon
- version = serialized.get('v', None)
+ version = json_dict.get('v', None)
if version is None:
raise ValueError('no version specified')
- if (version < macaroonbakery.BAKERY_V3 or
- version > macaroonbakery.LATEST_BAKERY_VERSION):
- raise ValueError('unknow bakery version {}'.format(version))
+ if (version < bakery.VERSION_3 or
+ version > bakery.LATEST_VERSION):
+ raise ValueError('unknown bakery version {}'.format(version))
m = pymacaroons.Macaroon.deserialize(json.dumps(json_macaroon),
json_serializer.JsonSerializer())
if m.version != macaroon_version(version):
raise ValueError(
'underlying macaroon has inconsistent version; '
'got {} want {}'.format(m.version, macaroon_version(version)))
- namespace = checkers.deserialize_namespace(serialized.get('ns'))
- cdata = serialized.get('cdata', {})
+ namespace = checkers.deserialize_namespace(json_dict.get('ns'))
+ cdata = json_dict.get('cdata', {})
caveat_data = {}
for id64 in cdata:
- id = utils.raw_b64decode(id64)
- data = utils.raw_b64decode(cdata[id64])
+ id = utils.b64decode(id64)
+ data = utils.b64decode(cdata[id64])
caveat_data[id] = data
macaroon = Macaroon(root_key=None, id=None,
namespace=namespace,
@@ -217,6 +222,15 @@ class Macaroon(object):
macaroon._macaroon = m
return macaroon
+ @classmethod
+ def deserialize_json(cls, serialized_json):
+ '''Return a macaroon deserialized from a string
+ @param serialized_json The string to decode {str}
+ @return {Macaroon}
+ '''
+ serialized = json.loads(serialized_json)
+ return Macaroon.from_dict(serialized)
+
def _new_caveat_id(self, base):
'''Return a third party caveat id
@@ -237,7 +251,7 @@ class Macaroon(object):
# payload, having this version gives a strong indication
# that the payload has been omitted so we can produce
# a better error for the user.
- id.append(macaroonbakery.BAKERY_V3)
+ id.append(bakery.VERSION_3)
# Iterate through integers looking for one that isn't already used,
# starting from n so that if everyone is using this same algorithm,
@@ -251,7 +265,7 @@ class Macaroon(object):
# end up with a duplicate third party caveat id and thus create
# a macaroon that cannot be discharged.
temp = id[:]
- macaroonbakery.encode_uvarint(i, temp)
+ bakery.encode_uvarint(i, temp)
found = False
for cav in caveats:
if (cav.verification_key_id is not None
@@ -296,7 +310,7 @@ def macaroon_version(bakery_version):
@param bakery_version the bakery version
@return macaroon_version the derived macaroon version
'''
- if bakery_version in [macaroonbakery.BAKERY_V0, macaroonbakery.BAKERY_V1]:
+ if bakery_version in [bakery.VERSION_0, bakery.VERSION_1]:
return pymacaroons.MACAROON_V1
return pymacaroons.MACAROON_V2
@@ -326,7 +340,7 @@ class ThirdPartyStore(ThirdPartyLocator):
def third_party_info(self, loc):
info = self._store.get(loc.rstrip('/'))
if info is None:
- raise macaroonbakery.ThirdPartyInfoNotFound(
+ raise bakery.ThirdPartyInfoNotFound(
'cannot retrieve the info for location {}'.format(loc))
return info
@@ -355,7 +369,7 @@ def _parse_local_location(loc):
'''
if not (loc.startswith('local ')):
return None
- v = macaroonbakery.BAKERY_V1
+ v = bakery.VERSION_1
fields = loc.split()
fields = fields[1:] # Skip 'local'
if len(fields) == 2:
@@ -365,9 +379,8 @@ def _parse_local_location(loc):
return None
fields = fields[1:]
if len(fields) == 1:
- key = macaroonbakery.PublicKey.deserialize(fields[0])
- return macaroonbakery.ThirdPartyInfo(public_key=key,
- version=v)
+ key = bakery.PublicKey.deserialize(fields[0])
+ return bakery.ThirdPartyInfo(public_key=key, version=v)
return None
@@ -381,11 +394,11 @@ def _bakery_version(v):
if v == pymacaroons.MACAROON_V1:
# Use version 1 because we don't know of any existing
# version 0 clients.
- return macaroonbakery.BAKERY_V1
+ return bakery.VERSION_1
elif v == pymacaroons.MACAROON_V2:
# Note that this could also correspond to Version 3, but
# this logic is explicitly for legacy versions.
- return macaroonbakery.BAKERY_V2
+ return bakery.VERSION_2
else:
raise ValueError('unknown macaroon version when deserializing legacy '
'bakery macaroon; got {}'.format(v))
diff --git a/macaroonbakery/oven.py b/macaroonbakery/oven.py
index 69a89cb..bf4bd27 100644
--- a/macaroonbakery/oven.py
+++ b/macaroonbakery/oven.py
@@ -8,9 +8,12 @@ import os
import google
from pymacaroons import MACAROON_V2, Verifier
+from pymacaroons.exceptions import (
+ MacaroonUnmetCaveatException, MacaroonInvalidSignatureException
+)
import six
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
from macaroonbakery import utils
from macaroonbakery.internal import id_pb2
@@ -61,7 +64,7 @@ class Oven:
self.ops_store = ops_store
self.root_keystore_for_ops = root_keystore_for_ops
if root_keystore_for_ops is None:
- my_store = macaroonbakery.MemoryKeyStore()
+ my_store = bakery.MemoryKeyStore()
self.root_keystore_for_ops = lambda x: my_store
def macaroon(self, version, expiry, caveats, ops):
@@ -82,16 +85,21 @@ class Oven:
id = self._new_macaroon_id(storage_id, expiry, ops)
- id_bytes = six.int2byte(macaroonbakery.LATEST_BAKERY_VERSION) + \
+ id_bytes = six.int2byte(bakery.LATEST_VERSION) + \
id.SerializeToString()
- if macaroonbakery.macaroon_version(version) < MACAROON_V2:
+ if bakery.macaroon_version(version) < MACAROON_V2:
# The old macaroon format required valid text for the macaroon id,
# so base64-encode it.
id_bytes = utils.raw_urlsafe_b64encode(id_bytes)
- m = macaroonbakery.Macaroon(root_key, id_bytes, self.location, version,
- self.namespace)
+ m = bakery.Macaroon(
+ root_key,
+ id_bytes,
+ self.location,
+ version,
+ self.namespace,
+ )
m.add_caveat(checkers.time_before_caveat(expiry), self.key,
self.locator)
m.add_caveats(caveats, self.key, self.locator)
@@ -147,7 +155,7 @@ class Oven:
storage_id, ops = _decode_macaroon_id(macaroons[0].identifier_bytes)
root_key = self.root_keystore_for_ops(ops).get(storage_id)
if root_key is None:
- raise macaroonbakery.VerificationError(
+ raise bakery.VerificationError(
'macaroon key not found in storage')
v = Verifier()
conditions = []
@@ -158,7 +166,13 @@ class Oven:
conditions.append(condition)
return True
v.satisfy_general(validator)
- v.verify(macaroons[0], root_key, macaroons[1:])
+ try:
+ v.verify(macaroons[0], root_key, macaroons[1:])
+ except (MacaroonUnmetCaveatException,
+ MacaroonInvalidSignatureException) as exc:
+ raise bakery.VerificationError(
+ 'verification failed: {}'.format(exc.args[0]))
+
if (self.ops_store is not None
and len(ops) == 1
and ops[0].entity.startswith('multi-')):
@@ -182,7 +196,7 @@ def _decode_macaroon_id(id):
# Note that old-style ids always start with an ASCII character >= 4
# (> 32 in fact) so this logic won't be triggered for those.
try:
- dec = utils.raw_b64decode(id.decode('utf-8'))
+ dec = utils.b64decode(id.decode('utf-8'))
# Set the id only on success.
id = dec
base64_decoded = True
@@ -195,23 +209,23 @@ def _decode_macaroon_id(id):
# creating macaroons to make all macaroons unique even if
# they're using the same root key.
first = six.byte2int(id[:1])
- if first == macaroonbakery.BAKERY_V2:
+ if first == bakery.VERSION_2:
# Skip the UUID at the start of the id.
storage_id = id[1 + 16:]
- if first == macaroonbakery.BAKERY_V3:
+ if first == bakery.VERSION_3:
try:
id1 = id_pb2.MacaroonId.FromString(id[1:])
except google.protobuf.message.DecodeError:
- raise macaroonbakery.VerificationError(
+ raise bakery.VerificationError(
'no operations found in macaroon')
if len(id1.ops) == 0 or len(id1.ops[0].actions) == 0:
- raise macaroonbakery.VerificationError(
+ raise bakery.VerificationError(
'no operations found in macaroon')
ops = []
for op in id1.ops:
for action in op.actions:
- ops.append(macaroonbakery.Op(op.entity, action))
+ ops.append(bakery.Op(op.entity, action))
return id1.storageId, ops
if not base64_decoded and _is_lower_case_hex_char(first):
@@ -220,7 +234,7 @@ def _decode_macaroon_id(id):
last = id.rfind(b'-')
if last >= 0:
storage_id = id[0:last]
- return storage_id, [macaroonbakery.LOGIN_OP]
+ return storage_id, [bakery.LOGIN_OP]
def _is_lower_case_hex_char(b):
diff --git a/macaroonbakery/tests/common.py b/macaroonbakery/tests/common.py
index 2619127..f238dfd 100644
--- a/macaroonbakery/tests/common.py
+++ b/macaroonbakery/tests/common.py
@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
import pytz
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
@@ -51,7 +51,7 @@ def true_check(ctx, cond, args):
return None
-class OneIdentity(macaroonbakery.IdentityClient):
+class OneIdentity(bakery.IdentityClient):
'''An IdentityClient implementation that always returns a single identity
from declared_identity, allowing allow(LOGIN_OP) to work even when there
are no declaration caveats (this is mostly to support the legacy tests
@@ -73,7 +73,7 @@ class _NoOne(object):
return ''
-class ThirdPartyStrcmpChecker(macaroonbakery.ThirdPartyCaveatChecker):
+class ThirdPartyStrcmpChecker(bakery.ThirdPartyCaveatChecker):
def __init__(self, str):
self.str = str
@@ -82,12 +82,12 @@ class ThirdPartyStrcmpChecker(macaroonbakery.ThirdPartyCaveatChecker):
if isinstance(cav_info.condition, bytes):
condition = cav_info.condition.decode('utf-8')
if condition != self.str:
- raise macaroonbakery.ThirdPartyCaveatCheckFailed(
+ raise bakery.ThirdPartyCaveatCheckFailed(
'{} doesn\'t match {}'.format(condition, self.str))
return []
-class ThirdPartyCheckerWithCaveats(macaroonbakery.ThirdPartyCaveatChecker):
+class ThirdPartyCheckerWithCaveats(bakery.ThirdPartyCaveatChecker):
def __init__(self, cavs=None):
if cavs is None:
cavs = []
@@ -97,7 +97,7 @@ class ThirdPartyCheckerWithCaveats(macaroonbakery.ThirdPartyCaveatChecker):
return self.cavs
-class ThirdPartyCaveatCheckerEmpty(macaroonbakery.ThirdPartyCaveatChecker):
+class ThirdPartyCaveatCheckerEmpty(bakery.ThirdPartyCaveatChecker):
def check_third_party_caveat(self, ctx, cav_info):
return []
@@ -107,14 +107,16 @@ def new_bakery(location, locator=None):
# key pair, and registers the key with the given locator if provided.
#
# It uses test_checker to check first party caveats.
- key = macaroonbakery.generate_key()
+ key = bakery.generate_key()
if locator is not None:
locator.add_info(location,
- macaroonbakery.ThirdPartyInfo(
+ bakery.ThirdPartyInfo(
public_key=key.public_key,
- version=macaroonbakery.LATEST_BAKERY_VERSION))
- return macaroonbakery.Bakery(key=key,
- checker=test_checker(),
- location=location,
- identity_client=OneIdentity(),
- locator=locator)
+ version=bakery.LATEST_VERSION))
+ return bakery.Bakery(
+ key=key,
+ checker=test_checker(),
+ location=location,
+ identity_client=OneIdentity(),
+ locator=locator,
+ )
diff --git a/macaroonbakery/tests/test_agent.py b/macaroonbakery/tests/test_agent.py
index 49134f5..67f5b84 100644
--- a/macaroonbakery/tests/test_agent.py
+++ b/macaroonbakery/tests/test_agent.py
@@ -1,6 +1,7 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
import base64
+from datetime import datetime, timedelta
import json
import os
import tempfile
@@ -9,7 +10,17 @@ from unittest import TestCase
import nacl.encoding
import requests.cookies
import six
+from six.moves.urllib.parse import parse_qs
+from six.moves.http_cookies import SimpleCookie
+from httmock import (
+ HTTMock,
+ urlmatch,
+ response
+)
+import macaroonbakery as bakery
+import macaroonbakery.httpbakery as httpbakery
+import macaroonbakery.checkers as checkers
import macaroonbakery.httpbakery.agent as agent
@@ -62,13 +73,19 @@ class TestAgents(TestCase):
def test_load_agents_into_cookies(self):
cookies = requests.cookies.RequestsCookieJar()
- c1, key = agent.load_agent_file(self.agent_filename, cookies=cookies)
+ c1, key = agent.load_agent_file(
+ self.agent_filename,
+ cookies=cookies,
+ )
self.assertEqual(c1, cookies)
- self.assertEqual(key.encode(nacl.encoding.Base64Encoder),
- b'CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=')
+ self.assertEqual(
+ key.encode(nacl.encoding.Base64Encoder),
+ b'CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=',
+ )
self.assertEqual(
key.public_key.encode(nacl.encoding.Base64Encoder),
- b'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
+ b'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=',
+ )
value = cookies.get('agent-login', domain='1.example.com')
jv = base64.b64decode(value)
@@ -76,8 +93,7 @@ class TestAgents(TestCase):
jv = jv.decode('utf-8')
data = json.loads(jv)
self.assertEqual(data['username'], 'user-1')
- self.assertEqual(data['public_key'],
- 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
+ self.assertEqual(data['public_key'], 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
value = cookies.get('agent-login', domain='2.example.com',
path='/discharger')
@@ -86,8 +102,7 @@ class TestAgents(TestCase):
jv = jv.decode('utf-8')
data = json.loads(jv)
self.assertEqual(data['username'], 'user-2')
- self.assertEqual(data['public_key'],
- 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
+ self.assertEqual(data['public_key'], 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
def test_load_agents_with_bad_key(self):
with self.assertRaises(agent.AgentFileFormatError):
@@ -97,6 +112,295 @@ class TestAgents(TestCase):
with self.assertRaises(agent.AgentFileFormatError):
agent.load_agent_file(self.no_username_agent_filename)
+ def test_agent_login(self):
+ discharge_key = bakery.generate_key()
+
+ class _DischargerLocator(bakery.ThirdPartyLocator):
+ def third_party_info(self, loc):
+ if loc == 'http://0.3.2.1':
+ return bakery.ThirdPartyInfo(
+ public_key=discharge_key.public_key,
+ version=bakery.LATEST_VERSION,
+ )
+ d = _DischargerLocator()
+ server_key = bakery.generate_key()
+ server_bakery = bakery.Bakery(key=server_key, locator=d)
+
+ @urlmatch(path='.*/here')
+ def server_get(url, request):
+ ctx = checkers.AuthContext()
+ test_ops = [bakery.Op(entity='test-op', action='read')]
+ auth_checker = server_bakery.checker.auth(
+ httpbakery.extract_macaroons(request.headers))
+ try:
+ auth_checker.allow(ctx, test_ops)
+ resp = response(status_code=200,
+ content='done')
+ except bakery.PermissionDenied:
+ caveats = [
+ checkers.Caveat(location='http://0.3.2.1', condition='is-ok')
+ ]
+ m = server_bakery.oven.macaroon(
+ version=bakery.LATEST_VERSION,
+ expiry=datetime.utcnow() + timedelta(days=1),
+ caveats=caveats, ops=test_ops)
+ content, headers = httpbakery.discharge_required_response(
+ m, '/',
+ 'test',
+ 'message')
+ resp = response(status_code=401,
+ content=content,
+ headers=headers)
+ return request.hooks['response'][0](resp)
+
+ @urlmatch(path='.*/discharge')
+ def discharge(url, request):
+ qs = parse_qs(request.body)
+ if qs.get('token64') is None:
+ return response(
+ status_code=401,
+ content={
+ 'Code': httpbakery.ERR_INTERACTION_REQUIRED,
+ 'Message': 'interaction required',
+ 'Info': {
+ 'InteractionMethods': {
+ 'agent': {'login-url': '/login'},
+ },
+ },
+ },
+ headers={'Content-Type': 'application/json'})
+ else:
+ qs = parse_qs(request.body)
+ content = {q: qs[q][0] for q in qs}
+ m = httpbakery.discharge(checkers.AuthContext(), content,
+ discharge_key, None, alwaysOK3rd)
+ return {
+ 'status_code': 200,
+ 'content': {
+ 'Macaroon': m.serialize_json()
+ }
+ }
+
+ key = bakery.generate_key()
+
+ @urlmatch(path='.*/login')
+ def login(url, request):
+ b = bakery.Bakery(key=discharge_key)
+ m = b.oven.macaroon(
+ version=bakery.LATEST_VERSION,
+ expiry=datetime.utcnow() + timedelta(days=1),
+ caveats=[bakery.local_third_party_caveat(
+ key.public_key,
+ version=httpbakery.request_version(request.headers))],
+ ops=[bakery.Op(entity='agent', action='login')])
+ return {
+ 'status_code': 200,
+ 'content': {
+ 'macaroon': m.to_dict()
+ }
+ }
+
+ with HTTMock(server_get), \
+ HTTMock(discharge), \
+ HTTMock(login):
+ client = httpbakery.Client(interaction_methods=[
+ agent.AgentInteractor(
+ agent.AuthInfo(
+ key=key,
+ agents=[
+ agent.Agent(
+ username='test-user',
+ url=u'http://0.3.2.1'
+ )
+ ],
+ ),
+ ),
+ ])
+ resp = requests.get(
+ 'http://0.1.2.3/here',
+ cookies=client.cookies,
+ auth=client.auth())
+ self.assertEquals(resp.content, b'done')
+
+ def test_agent_legacy(self):
+ discharge_key = bakery.generate_key()
+
+ class _DischargerLocator(bakery.ThirdPartyLocator):
+ def third_party_info(self, loc):
+ if loc == 'http://0.3.2.1':
+ return bakery.ThirdPartyInfo(
+ public_key=discharge_key.public_key,
+ version=bakery.LATEST_VERSION,
+ )
+ d = _DischargerLocator()
+ server_key = bakery.generate_key()
+ server_bakery = bakery.Bakery(key=server_key, locator=d)
+
+ @urlmatch(path='.*/here')
+ def server_get(url, request):
+ ctx = checkers.AuthContext()
+ test_ops = [bakery.Op(entity='test-op', action='read')]
+ auth_checker = server_bakery.checker.auth(
+ httpbakery.extract_macaroons(request.headers))
+ try:
+ auth_checker.allow(ctx, test_ops)
+ resp = response(status_code=200,
+ content='done')
+ except bakery.PermissionDenied:
+ caveats = [
+ checkers.Caveat(location='http://0.3.2.1',
+ condition='is-ok')
+ ]
+ m = server_bakery.oven.macaroon(
+ version=bakery.LATEST_VERSION,
+ expiry=datetime.utcnow() + timedelta(days=1),
+ caveats=caveats, ops=test_ops)
+ content, headers = httpbakery.discharge_required_response(
+ m, '/',
+ 'test',
+ 'message')
+ resp = response(
+ status_code=401,
+ content=content,
+ headers=headers,
+ )
+ return request.hooks['response'][0](resp)
+
+ class InfoStorage:
+ info = None
+
+ @urlmatch(path='.*/discharge')
+ def discharge(url, request):
+ qs = parse_qs(request.body)
+ if qs.get('caveat64') is not None:
+ content = {q: qs[q][0] for q in qs}
+
+ class InteractionRequiredError(Exception):
+ def __init__(self, error):
+ self.error = error
+
+ class CheckerInError(bakery.ThirdPartyCaveatChecker):
+ def check_third_party_caveat(self, ctx, info):
+ InfoStorage.info = info
+ raise InteractionRequiredError(
+ httpbakery.Error(
+ code=httpbakery.ERR_INTERACTION_REQUIRED,
+ version=httpbakery.request_version(
+ request.headers),
+ message='interaction required',
+ info=httpbakery.ErrorInfo(
+ wait_url='http://0.3.2.1/wait?'
+ 'dischargeid=1',
+ visit_url='http://0.3.2.1/visit?'
+ 'dischargeid=1'
+ ),
+ ),
+ )
+ try:
+ httpbakery.discharge(
+ checkers.AuthContext(), content,
+ discharge_key, None, CheckerInError())
+ except InteractionRequiredError as exc:
+ return response(
+ status_code=401,
+ content={
+ 'Code': exc.error.code,
+ 'Message': exc.error.message,
+ 'Info': {
+ 'WaitURL': exc.error.info.wait_url,
+ 'VisitURL': exc.error.info.visit_url,
+ },
+ },
+ headers={'Content-Type': 'application/json'})
+
+ key = bakery.generate_key()
+
+ @urlmatch(path='.*/visit?$')
+ def visit(url, request):
+ if request.headers.get('Accept') == 'application/json':
+ return {
+ 'status_code': 200,
+ 'content': {
+ 'agent': request.url
+ }
+ }
+ cs = SimpleCookie()
+ cookies = request.headers.get('Cookie')
+ if cookies is not None:
+ cs.load(str(cookies))
+ public_key = None
+ for c in cs:
+ if c == 'agent-login':
+ json_cookie = json.loads(
+ base64.b64decode(cs[c].value).decode('utf-8'))
+ public_key = bakery.PublicKey.deserialize(json_cookie.get('public_key'))
+ ms = httpbakery.extract_macaroons(request.headers)
+ if len(ms) == 0:
+ b = bakery.Bakery(key=discharge_key)
+ m = b.oven.macaroon(
+ version=bakery.LATEST_VERSION,
+ expiry=datetime.utcnow() + timedelta(days=1),
+ caveats=[bakery.local_third_party_caveat(
+ public_key,
+ version=httpbakery.request_version(request.headers))],
+ ops=[bakery.Op(entity='agent', action='login')])
+ content, headers = httpbakery.discharge_required_response(
+ m, '/',
+ 'test',
+ 'message')
+ resp = response(status_code=401,
+ content=content,
+ headers=headers)
+ return request.hooks['response'][0](resp)
+
+ return {
+ 'status_code': 200,
+ 'content': {
+ 'agent-login': True
+ }
+ }
+
+ @urlmatch(path='.*/wait?$')
+ def wait(url, request):
+ class EmptyChecker(bakery.ThirdPartyCaveatChecker):
+ def check_third_party_caveat(self, ctx, info):
+ return []
+ if InfoStorage.info is None:
+ self.fail('visit url has not been visited')
+ m = bakery.discharge(
+ checkers.AuthContext(),
+ InfoStorage.info.id,
+ InfoStorage.info.caveat,
+ discharge_key,
+ EmptyChecker(),
+ _DischargerLocator(),
+ )
+ return {
+ 'status_code': 200,
+ 'content': {
+ 'Macaroon': m.to_dict()
+ }
+ }
+
+ with HTTMock(server_get), \
+ HTTMock(discharge), \
+ HTTMock(visit), \
+ HTTMock(wait):
+ client = httpbakery.Client(interaction_methods=[
+ agent.AgentInteractor(
+ agent.AuthInfo(
+ key=key,
+ agents=[agent.Agent(username='test-user', url=u'http://0.3.2.1')],
+ ),
+ ),
+ ])
+ resp = requests.get(
+ 'http://0.1.2.3/here',
+ cookies=client.cookies,
+ auth=client.auth(),
+ )
+ self.assertEquals(resp.content, b'done')
+
agent_file = '''
{
@@ -146,3 +450,14 @@ no_username_agent_file = '''
}]
}
'''
+
+
+class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker):
+ def __init__(self, check):
+ self._check = check
+
+ def check_third_party_caveat(self, ctx, info):
+ cond, arg = checkers.parse_caveat(info.condition)
+ return self._check(cond, arg)
+
+alwaysOK3rd = ThirdPartyCaveatCheckerF(lambda cond, arg: [])
diff --git a/macaroonbakery/tests/test_authorizer.py b/macaroonbakery/tests/test_authorizer.py
index da01974..f90d2b5 100644
--- a/macaroonbakery/tests/test_authorizer.py
+++ b/macaroonbakery/tests/test_authorizer.py
@@ -2,7 +2,7 @@
# Licensed under the LGPLv3, see LICENCE file for details.
from unittest import TestCase
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
@@ -23,11 +23,11 @@ class TestAuthorizer(TestCase):
else:
self.fail('unexpected entity: ' + op.Entity)
- ops = [macaroonbakery.Op('a', 'x'), macaroonbakery.Op('b', 'x'),
- macaroonbakery.Op('c', 'x'), macaroonbakery.Op('d', 'x')]
- allowed, caveats = macaroonbakery.AuthorizerFunc(f).authorize(
+ ops = [bakery.Op('a', 'x'), bakery.Op('b', 'x'),
+ bakery.Op('c', 'x'), bakery.Op('d', 'x')]
+ allowed, caveats = bakery.AuthorizerFunc(f).authorize(
checkers.AuthContext(),
- macaroonbakery.SimpleIdentity('bob'),
+ bakery.SimpleIdentity('bob'),
ops
)
self.assertEqual(allowed, [False, True, True, True])
@@ -40,42 +40,45 @@ class TestAuthorizer(TestCase):
ctx = checkers.AuthContext()
tests = [
('no ops, no problem',
- macaroonbakery.ACLAuthorizer(allow_public=True,
- get_acl=lambda x, y: []), None, [],
+ bakery.ACLAuthorizer(allow_public=True, get_acl=lambda x, y: []),
+ None,
+ [],
[]),
('identity that does not implement ACLIdentity; '
'user should be denied except for everyone group',
- macaroonbakery.ACLAuthorizer(allow_public=True,
- get_acl=lambda ctx, op: [
- macaroonbakery.EVERYONE]
- if op.entity == 'a' else ['alice']),
+ bakery.ACLAuthorizer(
+ allow_public=True,
+ get_acl=lambda ctx, op: [bakery.EVERYONE] if op.entity == 'a' else ['alice'],
+ ),
SimplestIdentity('bob'),
- [macaroonbakery.Op(entity='a', action='a'),
- macaroonbakery.Op(entity='b', action='b')],
+ [bakery.Op(entity='a', action='a'),
+ bakery.Op(entity='b', action='b')],
[True, False]),
('identity that does not implement ACLIdentity with user == Id; '
'user should be denied except for everyone group',
- macaroonbakery.ACLAuthorizer(allow_public=True,
- get_acl=lambda ctx, op: [
- macaroonbakery.EVERYONE] if
- op.entity == 'a' else ['bob']),
+ bakery.ACLAuthorizer(
+ allow_public=True,
+ get_acl=lambda ctx, op: [bakery.EVERYONE] if op.entity == 'a' else ['bob'],
+ ),
SimplestIdentity('bob'),
- [macaroonbakery.Op(entity='a', action='a'),
- macaroonbakery.Op(entity='b', action='b')],
+ [bakery.Op(entity='a', action='a'),
+ bakery.Op(entity='b', action='b')],
[True, False]),
('permission denied for everyone without AllowPublic',
- macaroonbakery.ACLAuthorizer(allow_public=False,
- get_acl=lambda x, y: [
- macaroonbakery.EVERYONE]),
+ bakery.ACLAuthorizer(
+ allow_public=False,
+ get_acl=lambda x, y: [bakery.EVERYONE],
+ ),
SimplestIdentity('bob'),
- [macaroonbakery.Op(entity='a', action='a')],
+ [bakery.Op(entity='a', action='a')],
[False]),
('permission granted to anyone with no identity with AllowPublic',
- macaroonbakery.ACLAuthorizer(allow_public=True,
- get_acl=lambda x, y: [
- macaroonbakery.EVERYONE]),
+ bakery.ACLAuthorizer(
+ allow_public=True,
+ get_acl=lambda x, y: [bakery.EVERYONE],
+ ),
None,
- [macaroonbakery.Op(entity='a', action='a')],
+ [bakery.Op(entity='a', action='a')],
[True])
]
for test in tests:
@@ -96,12 +99,12 @@ class TestAuthorizer(TestCase):
Visited.in_f = True
return False, None
- macaroonbakery.AuthorizerFunc(f).authorize(
- ctx, macaroonbakery.SimpleIdentity('bob'), ['op1']
+ bakery.AuthorizerFunc(f).authorize(
+ ctx, bakery.SimpleIdentity('bob'), ['op1']
)
self.assertTrue(Visited.in_f)
- class TestIdentity(SimplestIdentity, macaroonbakery.ACLIdentity):
+ class TestIdentity(SimplestIdentity, bakery.ACLIdentity):
def allow(other, ctx, acls):
self.assertEqual(ctx.get('a'), 'aval')
Visited.in_allow = True
@@ -112,14 +115,15 @@ class TestAuthorizer(TestCase):
Visited.in_get_acl = True
return []
- macaroonbakery.ACLAuthorizer(allow_public=False,
- get_acl=get_acl).authorize(
- ctx, TestIdentity('bob'), ['op1'])
+ bakery.ACLAuthorizer(
+ allow_public=False,
+ get_acl=get_acl,
+ ).authorize(ctx, TestIdentity('bob'), ['op1'])
self.assertTrue(Visited.in_get_acl)
self.assertTrue(Visited.in_allow)
-class SimplestIdentity(macaroonbakery.Identity):
+class SimplestIdentity(bakery.Identity):
# SimplestIdentity implements Identity for a string. Unlike
# SimpleIdentity, it does not implement ACLIdentity.
def __init__(self, user):
diff --git a/macaroonbakery/tests/test_bakery.py b/macaroonbakery/tests/test_bakery.py
index 724b264..5a13cff 100644
--- a/macaroonbakery/tests/test_bakery.py
+++ b/macaroonbakery/tests/test_bakery.py
@@ -14,7 +14,7 @@ from httmock import (
response
)
-from macaroonbakery import httpbakery
+import macaroonbakery.httpbakery as httpbakery
ID_PATH = 'http://example.com/someprotecteurl'
@@ -90,6 +90,32 @@ def first_407_then_200(url, request):
return request.hooks['response'][0](resp)
+@urlmatch(netloc='example.com:8000', path='.*/someprotecteurl')
+def first_407_then_200_with_port(url, request):
+ if request.headers.get('cookie', '').startswith('macaroon-'):
+ return {
+ 'status_code': 200,
+ 'content': {
+ 'Value': 'some value'
+ }
+ }
+ else:
+ resp = response(status_code=407,
+ content={
+ 'Info': {
+ 'Macaroon': json_macaroon,
+ 'MacaroonPath': '/',
+ 'CookieNameSuffix': 'test'
+ },
+ 'Message': 'verification failed: no macaroon '
+ 'cookies in request',
+ 'Code': 'macaroon discharge required'
+ },
+ headers={'Content-Type': 'application/json'},
+ request=request)
+ return request.hooks['response'][0](resp)
+
+
@urlmatch(path='.*/someprotecteurl')
def valid_200(url, request):
return {
@@ -142,25 +168,55 @@ def wait_after_401(url, request):
class TestBakery(TestCase):
+
+ def assert_cookie_security(self, cookies, name, secure):
+ for cookie in cookies:
+ if cookie.name == name:
+ assert cookie.secure == secure
+ break
+ else:
+ assert False, 'no cookie named {} found in jar'.format(name)
+
def test_discharge(self):
- jar = requests.cookies.RequestsCookieJar()
- with HTTMock(first_407_then_200):
- with HTTMock(discharge_200):
+ client = httpbakery.Client()
+ with HTTMock(first_407_then_200), HTTMock(discharge_200):
resp = requests.get(ID_PATH,
- cookies=jar,
- auth=httpbakery.BakeryAuth(cookies=jar))
+ cookies=client.cookies,
+ auth=client.auth())
resp.raise_for_status()
- assert 'macaroon-test' in jar.keys()
+ assert 'macaroon-test' in client.cookies.keys()
+ self.assert_cookie_security(client.cookies, 'macaroon-test', secure=False)
@patch('webbrowser.open')
def test_407_then_401_on_discharge(self, mock_open):
- jar = requests.cookies.RequestsCookieJar()
- with HTTMock(first_407_then_200):
- with HTTMock(discharge_401):
- with HTTMock(wait_after_401):
- resp = requests.get(ID_PATH,
- auth=httpbakery.BakeryAuth(
- cookies=jar))
- resp.raise_for_status()
+ client = httpbakery.Client()
+ with HTTMock(first_407_then_200), HTTMock(discharge_401), HTTMock(wait_after_401):
+ resp = requests.get(
+ ID_PATH,
+ cookies=client.cookies,
+ auth=client.auth(),
+ )
+ resp.raise_for_status()
mock_open.assert_called_once_with(u'http://example.com/visit', new=1)
- assert 'macaroon-test' in jar.keys()
+ assert 'macaroon-test' in client.cookies.keys()
+
+ def test_cookie_with_port(self):
+ client = httpbakery.Client()
+ with HTTMock(first_407_then_200_with_port):
+ with HTTMock(discharge_200):
+ resp = requests.get('http://example.com:8000/someprotecteurl',
+ cookies=client.cookies,
+ auth=client.auth())
+ resp.raise_for_status()
+ assert 'macaroon-test' in client.cookies.keys()
+
+ def test_secure_cookie_for_https(self):
+ client = httpbakery.Client()
+ with HTTMock(first_407_then_200_with_port), HTTMock(discharge_200):
+ resp = requests.get(
+ 'https://example.com:8000/someprotecteurl',
+ cookies=client.cookies,
+ auth=client.auth())
+ resp.raise_for_status()
+ assert 'macaroon-test' in client.cookies.keys()
+ self.assert_cookie_security(client.cookies, 'macaroon-test', secure=True)
diff --git a/macaroonbakery/tests/test_checker.py b/macaroonbakery/tests/test_checker.py
index 06bf008..643c756 100644
--- a/macaroonbakery/tests/test_checker.py
+++ b/macaroonbakery/tests/test_checker.py
@@ -9,7 +9,7 @@ from datetime import timedelta
from pymacaroons.verifier import Verifier, FirstPartyCaveatVerifierDelegate
import pymacaroons
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
from macaroonbakery.tests.common import test_context, epoch, test_checker
@@ -22,13 +22,13 @@ class TestChecker(TestCase):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
auth = _OpAuthorizer(
- {macaroonbakery.Op(entity='something', action='read'):
- {macaroonbakery.EVERYONE}})
+ {bakery.Op(entity='something', action='read'):
+ {bakery.EVERYONE}})
ts = _Service('myservice', auth, ids, locator)
client = _Client(locator)
- auth_info = client.do(test_context, ts,
- [macaroonbakery.Op(entity='something',
- action='read')])
+ auth_info = client.do(test_context, ts, [
+ bakery.Op(entity='something', action='read'),
+ ])
self.assertEqual(len(self._discharges), 0)
self.assertIsNotNone(auth_info)
self.assertIsNone(auth_info.identity)
@@ -37,25 +37,23 @@ class TestChecker(TestCase):
def test_authorization_denied(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = macaroonbakery.ClosedAuthorizer()
+ auth = bakery.ClosedAuthorizer()
ts = _Service('myservice', auth, ids, locator)
client = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
- with self.assertRaises(macaroonbakery.PermissionDenied):
- client.do(ctx, ts, [macaroonbakery.Op(entity='something',
- action='read')])
+ with self.assertRaises(bakery.PermissionDenied):
+ client.do(ctx, ts, [bakery.Op(entity='something', action='read')])
def test_authorize_with_authentication_required(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
auth = _OpAuthorizer(
- {macaroonbakery.Op(entity='something', action='read'): {'bob'}})
+ {bakery.Op(entity='something', action='read'): {'bob'}})
ts = _Service('myservice', auth, ids, locator)
client = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
- auth_info = client.do(ctx, ts, [macaroonbakery.Op(entity='something',
- action='read')])
+ auth_info = client.do(ctx, ts, [bakery.Op(entity='something', action='read')])
self.assertEqual(self._discharges,
[_DischargeRecord(location='ids', user='bob')])
self.assertIsNotNone(auth_info)
@@ -67,16 +65,16 @@ class TestChecker(TestCase):
ids = _IdService('ids', locator, self)
auth = _OpAuthorizer(
{
- macaroonbakery.Op(entity='something', action='read'): {'bob'},
- macaroonbakery.Op(entity='otherthing', action='read'): {'bob'}
+ bakery.Op(entity='something', action='read'): {'bob'},
+ bakery.Op(entity='otherthing', action='read'): {'bob'}
}
)
ts = _Service('myservice', auth, ids, locator)
client = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
client.do(ctx, ts, [
- macaroonbakery.Op(entity='something', action='read'),
- macaroonbakery.Op(entity='otherthing', action='read')
+ bakery.Op(entity='something', action='read'),
+ bakery.Op(entity='otherthing', action='read')
])
self.assertEqual(self._discharges,
[_DischargeRecord(location='ids', user='bob')])
@@ -85,159 +83,150 @@ class TestChecker(TestCase):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
auth = _OpAuthorizer(
- {macaroonbakery.Op(entity='something', action='read'): {'bob'}})
+ {bakery.Op(entity='something', action='read'): {'bob'}})
ts = _Service('myservice', auth, ids, locator)
client = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
m = client.discharged_capability(
- ctx, ts, [macaroonbakery.Op(entity='something', action='read')])
+ ctx, ts, [bakery.Op(entity='something', action='read')])
# Check that we can exercise the capability directly on the service
# with no discharging required.
- auth_info = ts.do(test_context, [m],
- [macaroonbakery.Op(entity='something',
- action='read')])
+ auth_info = ts.do(test_context, [m], [
+ bakery.Op(entity='something', action='read'),
+ ])
self.assertIsNotNone(auth_info)
self.assertIsNone(auth_info.identity)
self.assertEqual(len(auth_info.macaroons), 1)
- self.assertEqual(auth_info.macaroons[0][0].identifier_bytes,
- m[0].identifier_bytes)
+ self.assertEqual(auth_info.macaroons[0][0].identifier_bytes, m[0].identifier_bytes)
def test_capability_multiple_entities(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = _OpAuthorizer(
- {
- macaroonbakery.Op(entity='e1', action='read'): {'bob'},
- macaroonbakery.Op(entity='e2', action='read'): {'bob'},
- macaroonbakery.Op(entity='e3', action='read'): {'bob'},
- })
+ auth = _OpAuthorizer({
+ bakery.Op(entity='e1', action='read'): {'bob'},
+ bakery.Op(entity='e2', action='read'): {'bob'},
+ bakery.Op(entity='e3', action='read'): {'bob'},
+ })
ts = _Service('myservice', auth, ids, locator)
client = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
m = client.discharged_capability(ctx, ts, [
- macaroonbakery.Op(entity='e1',
- action='read'),
- macaroonbakery.Op(entity='e2',
- action='read'),
- macaroonbakery.Op(entity='e3',
- action='read')])
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e2', action='read'),
+ bakery.Op(entity='e3', action='read'),
+ ])
self.assertEqual(self._discharges,
[_DischargeRecord(location='ids', user='bob')])
# Check that we can exercise the capability directly on the service
# with no discharging required.
ts.do(test_context, [m], [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e2', action='read'),
- macaroonbakery.Op(entity='e3', action='read')])
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e2', action='read'),
+ bakery.Op(entity='e3', action='read'),
+ ])
# Check that we can exercise the capability to act on a subset of
# the operations.
ts.do(test_context, [m], [
- macaroonbakery.Op(entity='e2', action='read'),
- macaroonbakery.Op(entity='e3', action='read')]
- )
+ bakery.Op(entity='e2', action='read'),
+ bakery.Op(entity='e3', action='read'),
+ ])
ts.do(test_context, [m],
- [macaroonbakery.Op(entity='e3', action='read')])
+ [bakery.Op(entity='e3', action='read')])
def test_multiple_capabilities(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = _OpAuthorizer(
- {
- macaroonbakery.Op(entity='e1', action='read'): {'alice'},
- macaroonbakery.Op(entity='e2', action='read'): {'bob'},
- })
+ auth = _OpAuthorizer({
+ bakery.Op(entity='e1', action='read'): {'alice'},
+ bakery.Op(entity='e2', action='read'): {'bob'},
+ })
ts = _Service('myservice', auth, ids, locator)
# Acquire two capabilities as different users and check
# that we can combine them together to do both operations
# at once.
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice')
- m1 = _Client(locator).discharged_capability(ctx, ts,
- [macaroonbakery.Op(
- entity='e1',
- action='read')])
+ m1 = _Client(locator).discharged_capability(ctx, ts, [
+ bakery.Op(entity='e1', action='read'),
+ ])
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
m2 = _Client(locator).discharged_capability(ctx, ts,
- [macaroonbakery.Op(
+ [bakery.Op(
entity='e2',
action='read')])
- self.assertEqual(self._discharges,
- [
- _DischargeRecord(location='ids', user='alice'),
- _DischargeRecord(location='ids', user='bob'),
- ])
+ self.assertEqual(self._discharges, [
+ _DischargeRecord(location='ids', user='alice'),
+ _DischargeRecord(location='ids', user='bob'),
+ ])
auth_info = ts.do(test_context, [m1, m2], [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e2', action='read')])
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e2', action='read'),
+ ])
self.assertIsNotNone(auth_info)
self.assertIsNone(auth_info.identity)
self.assertEqual(len(auth_info.macaroons), 2)
- self.assertEqual(auth_info.macaroons[0][0].identifier_bytes,
- m1[0].identifier_bytes)
- self.assertEqual(auth_info.macaroons[1][0].identifier_bytes,
- m2[0].identifier_bytes)
+ self.assertEqual(auth_info.macaroons[0][0].identifier_bytes, m1[0].identifier_bytes)
+ self.assertEqual(auth_info.macaroons[1][0].identifier_bytes, m2[0].identifier_bytes)
def test_combine_capabilities(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = _OpAuthorizer(
- {
- macaroonbakery.Op(entity='e1', action='read'): {'alice'},
- macaroonbakery.Op(entity='e2', action='read'): {'bob'},
- macaroonbakery.Op(entity='e3', action='read'): {'bob',
- 'alice'},
- })
+ auth = _OpAuthorizer({
+ bakery.Op(entity='e1', action='read'): {'alice'},
+ bakery.Op(entity='e2', action='read'): {'bob'},
+ bakery.Op(entity='e3', action='read'): {'bob', 'alice'},
+ })
ts = _Service('myservice', auth, ids, locator)
# Acquire two capabilities as different users and check
# that we can combine them together into a single capability
# capable of both operations.
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice')
- m1 = _Client(locator).discharged_capability(
- ctx, ts, [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e3', action='read')])
+ m1 = _Client(locator).discharged_capability(ctx, ts, [
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e3', action='read'),
+ ])
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
m2 = _Client(locator).discharged_capability(
- ctx, ts, [macaroonbakery.Op(entity='e2', action='read')])
+ ctx, ts, [bakery.Op(entity='e2', action='read')])
m = ts.capability(test_context, [m1, m2], [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e2', action='read'),
- macaroonbakery.Op(entity='e3', action='read')])
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e2', action='read'),
+ bakery.Op(entity='e3', action='read'),
+ ])
ts.do(test_context, [[m.macaroon]], [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e2', action='read'),
- macaroonbakery.Op(entity='e3', action='read')])
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e2', action='read'),
+ bakery.Op(entity='e3', action='read'),
+ ])
def test_partially_authorized_request(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = _OpAuthorizer(
- {
- macaroonbakery.Op(entity='e1', action='read'): {'alice'},
- macaroonbakery.Op(entity='e2', action='read'): {'bob'},
- })
+ auth = _OpAuthorizer({
+ bakery.Op(entity='e1', action='read'): {'alice'},
+ bakery.Op(entity='e2', action='read'): {'bob'},
+ })
ts = _Service('myservice', auth, ids, locator)
# Acquire a capability for e1 but rely on authentication to
# authorize e2.
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice')
- m = _Client(locator).discharged_capability(ctx, ts,
- [macaroonbakery.Op(
- entity='e1',
- action='read')])
+ m = _Client(locator).discharged_capability(ctx, ts, [
+ bakery.Op(entity='e1', action='read'),
+ ])
client = _Client(locator)
client.add_macaroon(ts, 'authz', m)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
- client.discharged_capability(
- ctx, ts, [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e2', action='read')])
+ client.discharged_capability(ctx, ts, [
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e2', action='read'),
+ ])
def test_auth_with_third_party_caveats(self):
locator = _DischargerLocator()
@@ -247,16 +236,15 @@ class TestChecker(TestCase):
# when authorizing.
def authorize_with_tp_discharge(ctx, id, op):
if (id is not None and id.id() == 'bob' and
- op == macaroonbakery.Op(entity='something',
- action='read')):
+ op == bakery.Op(entity='something', action='read')):
return True, [checkers.Caveat(condition='question',
location='other third party')]
return False, None
- auth = macaroonbakery.AuthorizerFunc(authorize_with_tp_discharge)
+ auth = bakery.AuthorizerFunc(authorize_with_tp_discharge)
ts = _Service('myservice', auth, ids, locator)
- class _LocalDischargeChecker(macaroonbakery.ThirdPartyCaveatChecker):
+ class _LocalDischargeChecker(bakery.ThirdPartyCaveatChecker):
def check_third_party_caveat(_, ctx, info):
if info.condition != 'question':
raise ValueError('third party condition not recognized')
@@ -267,29 +255,25 @@ class TestChecker(TestCase):
return []
locator['other third party'] = _Discharger(
- key=macaroonbakery.generate_key(),
+ key=bakery.generate_key(),
checker=_LocalDischargeChecker(),
locator=locator,
)
client = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
- client.do(ctx, ts, [macaroonbakery.Op(entity='something',
- action='read')])
+ client.do(ctx, ts, [bakery.Op(entity='something', action='read')])
self.assertEqual(self._discharges, [
_DischargeRecord(location='ids', user='bob'),
- _DischargeRecord(location='other third party',
- user='bob')
+ _DischargeRecord(location='other third party', user='bob')
])
def test_capability_combines_first_party_caveats(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = _OpAuthorizer(
- {
- macaroonbakery.Op(entity='e1', action='read'): {'alice'},
- macaroonbakery.Op(entity='e2', action='read'): {'bob'}
- }
- )
+ auth = _OpAuthorizer({
+ bakery.Op(entity='e1', action='read'): {'alice'},
+ bakery.Op(entity='e2', action='read'): {'bob'},
+ })
ts = _Service('myservice', auth, ids, locator)
# Acquire two capabilities as different users, add some first party
@@ -297,12 +281,12 @@ class TestChecker(TestCase):
# capable of both operations.
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice')
m1 = _Client(locator).capability(
- ctx, ts, [macaroonbakery.Op(entity='e1', action='read')])
+ ctx, ts, [bakery.Op(entity='e1', action='read')])
m1.macaroon.add_first_party_caveat('true 1')
m1.macaroon.add_first_party_caveat('true 2')
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
m2 = _Client(locator).capability(
- ctx, ts, [macaroonbakery.Op(entity='e2', action='read')])
+ ctx, ts, [bakery.Op(entity='e2', action='read')])
m2.macaroon.add_first_party_caveat('true 3')
m2.macaroon.add_first_party_caveat('true 4')
@@ -311,8 +295,9 @@ class TestChecker(TestCase):
client.add_macaroon(ts, 'authz2', [m2.macaroon])
m = client.capability(test_context, ts, [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e2', action='read')])
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e2', action='read'),
+ ])
self.assertEqual(_macaroon_conditions(m.macaroon.caveats, False), [
'true 1',
'true 2',
@@ -323,11 +308,10 @@ class TestChecker(TestCase):
def test_first_party_caveat_squashing(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = _OpAuthorizer(
- {
- macaroonbakery.Op(entity='e1', action='read'): {'alice'},
- macaroonbakery.Op(entity='e2', action='read'): {'alice'},
- })
+ auth = _OpAuthorizer({
+ bakery.Op(entity='e1', action='read'): {'alice'},
+ bakery.Op(entity='e2', action='read'): {'alice'},
+ })
ts = _Service('myservice', auth, ids, locator)
tests = [
('duplicates removed', [
@@ -366,13 +350,13 @@ class TestChecker(TestCase):
# Make a first macaroon with all the required first party caveats.
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice')
m1 = _Client(locator).capability(
- ctx, ts, [macaroonbakery.Op(entity='e1', action='read')])
+ ctx, ts, [bakery.Op(entity='e1', action='read')])
m1.add_caveats(test[1], None, None)
# Make a second macaroon that's not used to check that it's
# caveats are not added.
m2 = _Client(locator).capability(
- ctx, ts, [macaroonbakery.Op(entity='e1', action='read')])
+ ctx, ts, [bakery.Op(entity='e1', action='read')])
m2.add_caveat(checkers.Caveat(
condition='true notused', namespace='testns'), None, None)
client = _Client(locator)
@@ -380,8 +364,7 @@ class TestChecker(TestCase):
client.add_macaroon(ts, 'authz2', [m2.macaroon])
m3 = client.capability(
- test_context, ts, [macaroonbakery.Op(entity='e1',
- action='read')])
+ test_context, ts, [bakery.Op(entity='e1', action='read')])
self.assertEqual(
_macaroon_conditions(m3.macaroon.caveats, False),
_resolve_caveats(m3.namespace, test[2]))
@@ -389,11 +372,11 @@ class TestChecker(TestCase):
def test_login_only(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = macaroonbakery.ClosedAuthorizer()
+ auth = bakery.ClosedAuthorizer()
ts = _Service('myservice', auth, ids, locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
- auth_info = _Client(locator).do(ctx, ts, [macaroonbakery.LOGIN_OP])
+ auth_info = _Client(locator).do(ctx, ts, [bakery.LOGIN_OP])
self.assertIsNotNone(auth_info)
self.assertEqual(auth_info.identity.id(), 'bob')
@@ -402,18 +385,17 @@ class TestChecker(TestCase):
ids = _IdService('ids', locator, self)
auth = _OpAuthorizer(
{
- macaroonbakery.Op(entity='e1', action='read'): {'alice'},
- macaroonbakery.Op(entity='e2', action='read'): {'bob'},
+ bakery.Op(entity='e1', action='read'): {'alice'},
+ bakery.Op(entity='e2', action='read'): {'bob'},
})
ts = _Service('myservice', auth, ids, locator)
# Acquire a capability for e1 but rely on authentication to
# authorize e2.
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice')
- m = _Client(locator).discharged_capability(ctx, ts,
- [macaroonbakery.Op(
- entity='e1',
- action='read')])
+ m = _Client(locator).discharged_capability(ctx, ts, [
+ bakery.Op(entity='e1', action='read'),
+ ])
client = _Client(locator)
client.add_macaroon(ts, 'authz', m)
@@ -423,24 +405,22 @@ class TestChecker(TestCase):
with self.assertRaises(_DischargeRequiredError):
client.do_any(
ctx, ts, [
- macaroonbakery.LOGIN_OP,
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e1', action='read')
+ bakery.LOGIN_OP,
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e1', action='read')
]
)
self.assertEqual(len(self._discharges), 0)
# Log in as bob.
- _, err = client.do(ctx, ts, [macaroonbakery.LOGIN_OP])
+ _, err = client.do(ctx, ts, [bakery.LOGIN_OP])
# All the previous actions should now be allowed.
- auth_info, allowed = client.do_any(
- ctx, ts, [
- macaroonbakery.LOGIN_OP,
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e1', action='read')
- ]
- )
+ auth_info, allowed = client.do_any(ctx, ts, [
+ bakery.LOGIN_OP,
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e1', action='read'),
+ ])
self.assertEqual(auth_info.identity.id(), 'bob')
self.assertEqual(len(auth_info.macaroons), 2)
self.assertEqual(allowed, [True, True, True])
@@ -448,123 +428,121 @@ class TestChecker(TestCase):
def test_auth_with_identity_from_context(self):
locator = _DischargerLocator()
ids = _BasicAuthIdService()
- auth = _OpAuthorizer(
- {
- macaroonbakery.Op(entity='e1', action='read'): {'sherlock'},
- macaroonbakery.Op(entity='e2', action='read'): {'bob'},
- })
+ auth = _OpAuthorizer({
+ bakery.Op(entity='e1', action='read'): {'sherlock'},
+ bakery.Op(entity='e2', action='read'): {'bob'},
+ })
ts = _Service('myservice', auth, ids, locator)
# Check that we can perform the ops with basic auth in the
# context.
ctx = _context_with_basic_auth(test_context, 'sherlock', 'holmes')
auth_info = _Client(locator).do(
- ctx, ts, [macaroonbakery.Op(entity='e1', action='read')])
+ ctx, ts, [bakery.Op(entity='e1', action='read')])
self.assertEqual(auth_info.identity.id(), 'sherlock')
self.assertEqual(len(auth_info.macaroons), 0)
def test_auth_login_op_with_identity_from_context(self):
locator = _DischargerLocator()
ids = _BasicAuthIdService()
- ts = _Service('myservice', macaroonbakery.ClosedAuthorizer(),
- ids, locator)
+ ts = _Service('myservice', bakery.ClosedAuthorizer(), ids, locator)
# Check that we can use LoginOp
# when auth isn't granted through macaroons.
ctx = _context_with_basic_auth(test_context, 'sherlock', 'holmes')
- auth_info = _Client(locator).do(ctx, ts, [macaroonbakery.LOGIN_OP])
+ auth_info = _Client(locator).do(ctx, ts, [bakery.LOGIN_OP])
self.assertEqual(auth_info.identity.id(), 'sherlock')
self.assertEqual(len(auth_info.macaroons), 0)
def test_operation_allow_caveat(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = _OpAuthorizer(
- {
- macaroonbakery.Op(entity='e1', action='read'): {'bob'},
- macaroonbakery.Op(entity='e1', action='write'): {'bob'},
- macaroonbakery.Op(entity='e2', action='read'): {'bob'},
- })
+ auth = _OpAuthorizer({
+ bakery.Op(entity='e1', action='read'): {'bob'},
+ bakery.Op(entity='e1', action='write'): {'bob'},
+ bakery.Op(entity='e2', action='read'): {'bob'},
+ })
ts = _Service('myservice', auth, ids, locator)
client = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
- m = client.capability(
- ctx, ts, [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e1', action='write'),
- macaroonbakery.Op(entity='e2', action='read')])
+ m = client.capability(ctx, ts, [
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e1', action='write'),
+ bakery.Op(entity='e2', action='read'),
+ ])
# Sanity check that we can do a write.
ts.do(test_context, [[m.macaroon]],
- [macaroonbakery.Op(entity='e1', action='write')])
+ [bakery.Op(entity='e1', action='write')])
m.add_caveat(checkers.allow_caveat(['read']), None, None)
# A read operation should work.
ts.do(test_context, [[m.macaroon]], [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e2', action='read')])
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e2', action='read'),
+ ])
# A write operation should fail
# even though the original macaroon allowed it.
with self.assertRaises(_DischargeRequiredError):
ts.do(test_context, [[m.macaroon]], [
- macaroonbakery.Op(entity='e1', action='write')])
+ bakery.Op(entity='e1', action='write'),
+ ])
def test_operation_deny_caveat(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = _OpAuthorizer(
- {
- macaroonbakery.Op(entity='e1', action='read'): {'bob'},
- macaroonbakery.Op(entity='e1', action='write'): {'bob'},
- macaroonbakery.Op(entity='e2', action='read'): {'bob'},
- })
+ auth = _OpAuthorizer({
+ bakery.Op(entity='e1', action='read'): {'bob'},
+ bakery.Op(entity='e1', action='write'): {'bob'},
+ bakery.Op(entity='e2', action='read'): {'bob'},
+ })
ts = _Service('myservice', auth, ids, locator)
client = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
- m = client.capability(
- ctx, ts, [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e1', action='write'),
- macaroonbakery.Op(entity='e2', action='read')])
+ m = client.capability(ctx, ts, [
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e1', action='write'),
+ bakery.Op(entity='e2', action='read'),
+ ])
# Sanity check that we can do a write.
ts.do(test_context, [[m.macaroon]], [
- macaroonbakery.Op(entity='e1', action='write')])
+ bakery.Op(entity='e1', action='write')])
m.add_caveat(checkers.deny_caveat(['write']), None, None)
# A read operation should work.
- ts.do(
- test_context, [[m.macaroon]], [
- macaroonbakery.Op(entity='e1', action='read'),
- macaroonbakery.Op(entity='e2', action='read')])
+ ts.do(test_context, [[m.macaroon]], [
+ bakery.Op(entity='e1', action='read'),
+ bakery.Op(entity='e2', action='read'),
+ ])
# A write operation should fail
# even though the original macaroon allowed it.
with self.assertRaises(_DischargeRequiredError):
ts.do(test_context, [[m.macaroon]], [
- macaroonbakery.Op(entity='e1', action='write')])
+ bakery.Op(entity='e1', action='write')])
def test_duplicate_login_macaroons(self):
locator = _DischargerLocator()
ids = _IdService('ids', locator, self)
- auth = macaroonbakery.ClosedAuthorizer()
+ auth = bakery.ClosedAuthorizer()
ts = _Service('myservice', auth, ids, locator)
# Acquire a login macaroon for bob.
client1 = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
- auth_info = client1.do(ctx, ts, [macaroonbakery.LOGIN_OP])
+ auth_info = client1.do(ctx, ts, [bakery.LOGIN_OP])
self.assertEqual(auth_info.identity.id(), 'bob')
# Acquire a login macaroon for alice.
client2 = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'alice')
- auth_info = client2.do(ctx, ts, [macaroonbakery.LOGIN_OP])
+ auth_info = client2.do(ctx, ts, [bakery.LOGIN_OP])
self.assertEqual(auth_info.identity.id(), 'alice')
# Combine the two login macaroons into one client.
@@ -576,32 +554,30 @@ class TestChecker(TestCase):
# We should authenticate as bob (because macaroons are presented
# ordered by "cookie" name)
- auth_info = client3.do(test_context, ts, [macaroonbakery.LOGIN_OP])
+ auth_info = client3.do(test_context, ts, [bakery.LOGIN_OP])
self.assertEqual(auth_info.identity.id(), 'bob')
self.assertEqual(len(auth_info.macaroons), 1)
# Try them the other way around and we should authenticate as alice.
client3 = _Client(locator)
- client3.add_macaroon(ts, '1.alice',
- client2._macaroons[ts.name()]['authn'])
- client3.add_macaroon(ts, '2.bob',
- client1._macaroons[ts.name()]['authn'])
+ client3.add_macaroon(ts, '1.alice', client2._macaroons[ts.name()]['authn'])
+ client3.add_macaroon(ts, '2.bob', client1._macaroons[ts.name()]['authn'])
- auth_info = client3.do(test_context, ts, [macaroonbakery.LOGIN_OP])
+ auth_info = client3.do(test_context, ts, [bakery.LOGIN_OP])
self.assertEqual(auth_info.identity.id(), 'alice')
self.assertEqual(len(auth_info.macaroons), 1)
def test_macaroon_ops_fatal_error(self):
# When we get a non-VerificationError error from the
# opstore, we don't do any more verification.
- checker = macaroonbakery.Checker(
+ checker = bakery.Checker(
macaroon_opstore=_MacaroonStoreWithError())
m = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V2)
- with self.assertRaises(ValueError):
- checker.auth([m]).allow(test_context, [macaroonbakery.LOGIN_OP])
+ with self.assertRaises(bakery.AuthInitError):
+ checker.auth([m]).allow(test_context, [bakery.LOGIN_OP])
-class _DischargerLocator(object):
+class _DischargerLocator(bakery.ThirdPartyLocator):
def __init__(self, dischargers=None):
if dischargers is None:
dischargers = {}
@@ -611,9 +587,9 @@ class _DischargerLocator(object):
d = self._dischargers.get(loc)
if d is None:
return None
- return macaroonbakery.ThirdPartyInfo(
+ return bakery.ThirdPartyInfo(
public_key=d._key.public_key,
- version=macaroonbakery.LATEST_BAKERY_VERSION,
+ version=bakery.LATEST_VERSION,
)
def __setitem__(self, key, item):
@@ -626,25 +602,23 @@ class _DischargerLocator(object):
return self._dischargers.get(key)
-class _IdService(macaroonbakery.IdentityClient,
- macaroonbakery.ThirdPartyCaveatChecker):
+class _IdService(bakery.IdentityClient,
+ bakery.ThirdPartyCaveatChecker):
def __init__(self, location, locator, test_class):
self._location = location
self._test = test_class
- key = macaroonbakery.generate_key()
+ key = bakery.generate_key()
self._discharger = _Discharger(key=key, checker=self, locator=locator)
locator[location] = self._discharger
def check_third_party_caveat(self, ctx, info):
if info.condition != 'is-authenticated-user':
- raise macaroonbakery.CaveatNotRecognizedError(
- 'third party condition not '
- 'recognized')
+ raise bakery.CaveatNotRecognizedError(
+ 'third party condition not recognized')
username = ctx.get(_DISCHARGE_USER_KEY, '')
if username == '':
- return macaroonbakery.ThirdPartyCaveatCheckFailed(
- 'no current user')
+ raise bakery.ThirdPartyCaveatCheckFailed('no current user')
self._test._discharges.append(
_DischargeRecord(location=self._location, user=username))
return [checkers.declared_caveat('username', username)]
@@ -656,8 +630,8 @@ class _IdService(macaroonbakery.IdentityClient,
def declared_identity(self, ctx, declared):
user = declared.get('username')
if user is None:
- raise macaroonbakery.IdentityError('no username declared')
- return macaroonbakery.SimpleIdentity(user)
+ raise bakery.IdentityError('no username declared')
+ return bakery.SimpleIdentity(user)
_DISCHARGE_USER_KEY = checkers.ContextKey('user-key')
@@ -676,13 +650,17 @@ class _Discharger(object):
self._checker = checker
def discharge(self, ctx, cav, payload):
- return macaroonbakery.discharge(ctx, key=self._key, id=cav.caveat_id,
- caveat=payload,
- checker=self._checker,
- locator=self._locator)
+ return bakery.discharge(
+ ctx,
+ key=self._key,
+ id=cav.caveat_id,
+ caveat=payload,
+ checker=self._checker,
+ locator=self._locator,
+ )
-class _OpAuthorizer(macaroonbakery.Authorizer):
+class _OpAuthorizer(bakery.Authorizer):
'''Implements bakery.Authorizer by looking the operation
up in the given map. If the username is in the associated list
or the list contains "everyone", authorization is granted.
@@ -694,7 +672,7 @@ class _OpAuthorizer(macaroonbakery.Authorizer):
self._auth = auth
def authorize(self, ctx, id, ops):
- return macaroonbakery.ACLAuthorizer(
+ return bakery.ACLAuthorizer(
allow_public=True,
get_acl=lambda ctx, op: self._auth.get(op, [])).authorize(
ctx, id, ops)
@@ -705,7 +683,7 @@ class _MacaroonStore(object):
'''
def __init__(self, key, locator):
- self._root_key_store = macaroonbakery.MemoryKeyStore()
+ self._root_key_store = bakery.MemoryKeyStore()
self._key = key
self._locator = locator
@@ -713,9 +691,9 @@ class _MacaroonStore(object):
root_key, id = self._root_key_store.root_key()
m_id = {'id': base64.urlsafe_b64encode(id).decode('utf-8'), 'ops': ops}
data = json.dumps(m_id)
- m = macaroonbakery.Macaroon(
+ m = bakery.Macaroon(
root_key=root_key, id=data, location='',
- version=macaroonbakery.LATEST_BAKERY_VERSION,
+ version=bakery.LATEST_VERSION,
namespace=namespace)
m.add_caveats(caveats, self._key, self._locator)
return m
@@ -739,7 +717,7 @@ class _MacaroonStore(object):
ok = v.verify(macaroon=ms[0], key=root_key,
discharge_macaroons=ms[1:])
if not ok:
- raise macaroonbakery.VerificationError('invalid signature')
+ raise bakery.VerificationError('invalid signature')
conditions = []
for m in ms:
cavs = m.first_party_caveats()
@@ -747,7 +725,7 @@ class _MacaroonStore(object):
conditions.append(cav.caveat_id_bytes.decode('utf-8'))
ops = []
for op in m_id['ops']:
- ops.append(macaroonbakery.Op(entity=op[0], action=op[1]))
+ ops.append(bakery.Op(entity=op[0], action=op[1]))
return ops, conditions
@@ -761,8 +739,8 @@ class _Service(object):
def __init__(self, name, auth, idm, locator):
self._name = name
- self._store = _MacaroonStore(macaroonbakery.generate_key(), locator)
- self._checker = macaroonbakery.Checker(
+ self._store = _MacaroonStore(bakery.generate_key(), locator)
+ self._checker = bakery.Checker(
checker=test_checker(),
authorizer=auth,
identity_client=idm,
@@ -774,7 +752,7 @@ class _Service(object):
def do(self, ctx, ms, ops):
try:
authInfo = self._checker.auth(ms).allow(ctx, ops)
- except macaroonbakery.DischargeRequiredError as exc:
+ except bakery.DischargeRequiredError as exc:
self._discharge_required_error(exc)
return authInfo
@@ -784,13 +762,13 @@ class _Service(object):
try:
authInfo, allowed = self._checker.auth(ms).allow_any(ctx, ops)
return authInfo, allowed
- except macaroonbakery.DischargeRequiredError as exc:
+ except bakery.DischargeRequiredError as exc:
self._discharge_required_error(exc)
def capability(self, ctx, ms, ops):
try:
conds = self._checker.auth(ms).allow_capability(ctx, ops)
- except macaroonbakery.DischargeRequiredError as exc:
+ except bakery.DischargeRequiredError as exc:
self._discharge_required_error(exc)
m = self._store.new_macaroon(None, self._checker.namespace(), ops)
@@ -802,7 +780,7 @@ class _Service(object):
m = self._store.new_macaroon(err.cavs(), self._checker.namespace(),
err.ops())
name = 'authz'
- if len(err.ops()) == 1 and err.ops()[0] == macaroonbakery.LOGIN_OP:
+ if len(err.ops()) == 1 and err.ops()[0] == bakery.LOGIN_OP:
name = 'authn'
raise _DischargeRequiredError(name=name, m=m)
@@ -824,7 +802,7 @@ class _Client(object):
max_retries = 3
def __init__(self, dischargers):
- self._key = macaroonbakery.generate_key()
+ self._key = bakery.generate_key()
self._macaroons = {}
self._dischargers = dischargers
@@ -894,26 +872,25 @@ class _Client(object):
return ms
def _discharge_all(self, ctx, m):
- def get_discharge(ctx, cav, pay_load):
+ def get_discharge(cav, payload):
d = self._dischargers.get(cav.location)
if d is None:
raise ValueError('third party discharger '
'{} not found'.format(cav.location))
- return d.discharge(ctx, cav, pay_load)
+ return d.discharge(ctx, cav, payload)
- return macaroonbakery.discharge_all(ctx, m, get_discharge)
+ return bakery.discharge_all(m, get_discharge)
-class _BasicAuthIdService(macaroonbakery.IdentityClient):
+class _BasicAuthIdService(bakery.IdentityClient):
def identity_from_context(self, ctx):
user, pwd = _basic_auth_from_context(ctx)
if user != 'sherlock' or pwd != 'holmes':
return None, None
- return macaroonbakery.SimpleIdentity(user), None
+ return bakery.SimpleIdentity(user), None
def declared_identity(self, ctx, declared):
- raise macaroonbakery.IdentityError('no identity declarations in basic '
- 'auth id service')
+ raise bakery.IdentityError('no identity declarations in basic auth id service')
_BASIC_AUTH_KEY = checkers.ContextKey('user-key')
diff --git a/macaroonbakery/tests/test_client.py b/macaroonbakery/tests/test_client.py
new file mode 100644
index 0000000..8263f54
--- /dev/null
+++ b/macaroonbakery/tests/test_client.py
@@ -0,0 +1,395 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import base64
+import datetime
+import json
+import os
+from unittest import TestCase
+try:
+ from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+except ImportError:
+ from http.server import HTTPServer, BaseHTTPRequestHandler
+import threading
+
+from httmock import (
+ HTTMock,
+ urlmatch
+)
+import requests
+from six.moves.urllib.parse import parse_qs
+
+import macaroonbakery as bakery
+import macaroonbakery.httpbakery as httpbakery
+import macaroonbakery.checkers as checkers
+
+AGES = datetime.datetime.utcnow() + datetime.timedelta(days=1)
+TEST_OP = bakery.Op(entity='test', action='test')
+
+
+class TestClient(TestCase):
+ def setUp(self):
+ super(TestClient, self).setUp()
+ # http_proxy would cause requests to talk to the proxy, which is
+ # unlikely to know how to talk to the test server.
+ os.environ.pop('http_proxy', None)
+
+ def test_single_service_first_party(self):
+ b = new_bakery('loc', None, None)
+
+ def handler(*args):
+ GetHandler(b, None, None, None, None, *args)
+ try:
+ httpd = HTTPServer(('', 0), handler)
+ thread = threading.Thread(target=httpd.serve_forever)
+ thread.start()
+ srv_macaroon = b.oven.macaroon(
+ version=bakery.LATEST_VERSION, expiry=AGES,
+ caveats=None, ops=[TEST_OP])
+ self.assertEquals(srv_macaroon.macaroon.location, 'loc')
+ client = httpbakery.Client()
+ client.cookies.set_cookie(requests.cookies.create_cookie(
+ 'macaroon-test', base64.b64encode(json.dumps([
+ srv_macaroon.to_dict().get('m')
+ ]).encode('utf-8')).decode('utf-8')
+ ))
+ resp = requests.get(
+ url='http://' + httpd.server_address[0] + ':' +
+ str(httpd.server_address[1]),
+ cookies=client.cookies, auth=client.auth())
+ resp.raise_for_status()
+ self.assertEquals(resp.text, 'done')
+ finally:
+ httpd.shutdown()
+
+ def test_single_party_with_header(self):
+ b = new_bakery('loc', None, None)
+
+ def handler(*args):
+ GetHandler(b, None, None, None, None, *args)
+ try:
+ httpd = HTTPServer(('', 0), handler)
+ thread = threading.Thread(target=httpd.serve_forever)
+ thread.start()
+ srv_macaroon = b.oven.macaroon(
+ version=bakery.LATEST_VERSION,
+ expiry=AGES, caveats=None, ops=[TEST_OP])
+ self.assertEquals(srv_macaroon.macaroon.location, 'loc')
+ headers = {
+ 'Macaroons': base64.b64encode(json.dumps([
+ srv_macaroon.to_dict().get('m')
+ ]).encode('utf-8'))
+ }
+ resp = requests.get(
+ url='http://' + httpd.server_address[0] + ':' +
+ str(httpd.server_address[1]),
+ headers=headers)
+ resp.raise_for_status()
+ self.assertEquals(resp.text, 'done')
+ finally:
+ httpd.shutdown()
+
+ def test_repeated_request_with_body(self):
+ class _DischargerLocator(bakery.ThirdPartyLocator):
+ def __init__(self):
+ self.key = bakery.generate_key()
+
+ def third_party_info(self, loc):
+ if loc == 'http://1.2.3.4':
+ return bakery.ThirdPartyInfo(
+ public_key=self.key.public_key,
+ version=bakery.LATEST_VERSION,
+ )
+
+ d = _DischargerLocator()
+ b = new_bakery('loc', d, None)
+
+ @urlmatch(path='.*/discharge')
+ def discharge(url, request):
+ qs = parse_qs(request.body)
+ content = {q: qs[q][0] for q in qs}
+ m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d, alwaysOK3rd)
+ return {
+ 'status_code': 200,
+ 'content': {
+ 'Macaroon': m.to_dict()
+ }
+ }
+
+ def handler(*args):
+ GetHandler(b, 'http://1.2.3.4', None, None, None, *args)
+ try:
+ httpd = HTTPServer(('', 0), handler)
+ thread = threading.Thread(target=httpd.serve_forever)
+ thread.start()
+ client = httpbakery.Client()
+ with HTTMock(discharge):
+ resp = requests.get(
+ url='http://' + httpd.server_address[0] + ':' +
+ str(httpd.server_address[1]),
+ cookies=client.cookies,
+ auth=client.auth())
+ resp.raise_for_status()
+ self.assertEquals(resp.text, 'done')
+ finally:
+ httpd.shutdown()
+
+ def test_too_many_discharge(self):
+ class _DischargerLocator(bakery.ThirdPartyLocator):
+ def __init__(self):
+ self.key = bakery.generate_key()
+
+ def third_party_info(self, loc):
+ if loc == 'http://1.2.3.4':
+ return bakery.ThirdPartyInfo(
+ public_key=self.key.public_key,
+ version=bakery.LATEST_VERSION,
+ )
+
+ d = _DischargerLocator()
+ b = new_bakery('loc', d, None)
+
+ @urlmatch(path='.*/discharge')
+ def discharge(url, request):
+ wrong_macaroon = bakery.Macaroon(
+ root_key=b'some key', id=b'xxx',
+ location='some other location',
+ version=bakery.VERSION_0)
+ return {
+ 'status_code': 200,
+ 'content': {
+ 'Macaroon': wrong_macaroon.to_dict()
+ }
+ }
+
+ def handler(*args):
+ GetHandler(b, 'http://1.2.3.4', None, None, None, *args)
+ try:
+ httpd = HTTPServer(('', 0), handler)
+ thread = threading.Thread(target=httpd.serve_forever)
+ thread.start()
+ client = httpbakery.Client()
+ with HTTMock(discharge):
+ with self.assertRaises(httpbakery.BakeryException) as ctx:
+ requests.get(
+ url='http://' + httpd.server_address[0] + ':' +
+ str(httpd.server_address[1]),
+ cookies=client.cookies,
+ auth=client.auth())
+ self.assertEqual(ctx.exception.args[0],
+ 'too many (3) discharge requests')
+ finally:
+ httpd.shutdown()
+
+ def test_third_party_discharge_refused(self):
+ class _DischargerLocator(bakery.ThirdPartyLocator):
+ def __init__(self):
+ self.key = bakery.generate_key()
+
+ def third_party_info(self, loc):
+ if loc == 'http://1.2.3.4':
+ return bakery.ThirdPartyInfo(
+ public_key=self.key.public_key,
+ version=bakery.LATEST_VERSION,
+ )
+
+ def check(cond, arg):
+ raise bakery.ThirdPartyCaveatCheckFailed('boo! cond' + cond)
+
+ d = _DischargerLocator()
+ b = new_bakery('loc', d, None)
+
+ @urlmatch(path='.*/discharge')
+ def discharge(url, request):
+ qs = parse_qs(request.body)
+ content = {q: qs[q][0] for q in qs}
+ httpbakery.discharge(checkers.AuthContext(), content, d.key, d,
+ ThirdPartyCaveatCheckerF(check))
+
+ def handler(*args):
+ GetHandler(b, 'http://1.2.3.4', None, None, None, *args)
+ try:
+ httpd = HTTPServer(('', 0), handler)
+ thread = threading.Thread(target=httpd.serve_forever)
+ thread.start()
+ client = httpbakery.Client()
+ with HTTMock(discharge):
+ with self.assertRaises(bakery.ThirdPartyCaveatCheckFailed):
+ requests.get(
+ url='http://' + httpd.server_address[0] + ':' +
+ str(httpd.server_address[1]),
+ cookies=client.cookies,
+ auth=client.auth())
+ finally:
+ httpd.shutdown()
+
+ def test_discharge_with_interaction_required_error(self):
+ class _DischargerLocator(bakery.ThirdPartyLocator):
+ def __init__(self):
+ self.key = bakery.generate_key()
+
+ def third_party_info(self, loc):
+ if loc == 'http://1.2.3.4':
+ return bakery.ThirdPartyInfo(
+ public_key=self.key.public_key,
+ version=bakery.LATEST_VERSION,
+ )
+ d = _DischargerLocator()
+ b = new_bakery('loc', d, None)
+
+ @urlmatch(path='.*/discharge')
+ def discharge(url, request):
+ return {
+ 'status_code': 401,
+ 'content': {
+ 'Code': httpbakery.ERR_INTERACTION_REQUIRED,
+ 'Message': 'interaction required',
+ 'Info': {
+ 'WaitURL': 'http://0.1.2.3/',
+ 'VisitURL': 'http://0.1.2.3/',
+ },
+ }
+ }
+
+ def handler(*args):
+ GetHandler(b, 'http://1.2.3.4', None, None, None, *args)
+
+ try:
+ httpd = HTTPServer(('', 0), handler)
+ thread = threading.Thread(target=httpd.serve_forever)
+ thread.start()
+
+ class MyInteractor(httpbakery.LegacyInteractor):
+ def legacy_interact(self, ctx, location, visit_url):
+ raise httpbakery.InteractionError('cannot visit')
+
+ def interact(self, ctx, location, interaction_required_err):
+ pass
+
+ def kind(self):
+ return httpbakery.WEB_BROWSER_INTERACTION_KIND
+
+ client = httpbakery.Client(interaction_methods=[MyInteractor()])
+
+ with HTTMock(discharge):
+ with self.assertRaises(httpbakery.InteractionError):
+ requests.get(
+ 'http://' + httpd.server_address[0] + ':' + str(
+ httpd.server_address[1]),
+ cookies=client.cookies,
+ auth=client.auth())
+ finally:
+ httpd.shutdown()
+
+
+class GetHandler(BaseHTTPRequestHandler):
+ '''A mock HTTP server that serves a GET request'''
+ def __init__(self, bakery, auth_location, mutate_error,
+ caveats, version, *args):
+ '''
+ @param bakery used to check incoming requests and macaroons
+ for discharge-required errors.
+ @param auth_location holds the location of any 3rd party
+ authorizer. If this is not None, a 3rd party caveat will be
+ added addressed to this location.
+ @param mutate_error if non None, will be called with any
+ discharge-required error before responding to the client.
+ @param caveats called to get caveats to add to the returned
+ macaroon.
+ @param holds the version of the bakery that the
+ server will purport to serve.
+ '''
+ self._bakery = bakery
+ self._auth_location = auth_location
+ self._mutate_error = mutate_error
+ self._caveats = caveats
+ self._server_version = version
+ BaseHTTPRequestHandler.__init__(self, *args)
+
+ def do_GET(self):
+ '''do_GET implements a handler for the HTTP GET method'''
+ ctx = checkers.AuthContext()
+ auth_checker = self._bakery.checker.auth(
+ httpbakery.extract_macaroons(self.headers))
+ try:
+ auth_checker.allow(ctx, [TEST_OP])
+ except (bakery.PermissionDenied,
+ bakery.VerificationError) as exc:
+ return self._write_discharge_error(exc)
+ self.send_response(200)
+ self.end_headers()
+ content_len = int(self.headers.get('content-length', 0))
+ content = 'done'
+ if self.path != '/no-body'and content_len > 0:
+ body = self.rfile.read(content_len)
+ content = content + ' ' + body
+ self.wfile.write(content.encode('utf-8'))
+ return
+
+ def _write_discharge_error(self, exc):
+ version = httpbakery.request_version(self.headers)
+ if version < bakery.LATEST_VERSION:
+ self._server_version = version
+
+ caveats = []
+ if self._auth_location != '':
+ caveats = [
+ checkers.Caveat(location=self._auth_location,
+ condition='is-ok')
+ ]
+ if self._caveats is not None:
+ caveats.extend(self._caveats)
+
+ m = self._bakery.oven.macaroon(
+ version=bakery.LATEST_VERSION, expiry=AGES,
+ caveats=caveats, ops=[TEST_OP])
+
+ content, headers = httpbakery.discharge_required_response(
+ m, '/', 'test', exc.args[0])
+ self.send_response(401)
+ for h in headers:
+ self.send_header(h, headers[h])
+ self.send_header('Connection', 'close')
+ self.end_headers()
+ self.wfile.write(content)
+
+
+def new_bakery(location, locator, checker):
+ '''Return a new bakery instance.
+ @param location Location of the bakery {str}.
+ @param locator Locator for third parties {ThirdPartyLocator or None}
+ @param checker Caveat checker {FirstPartyCaveatChecker or None}
+ @return {Bakery}
+ '''
+ if checker is None:
+ c = checkers.Checker()
+ c.namespace().register('testns', '')
+ c.register('is', 'testns', check_is_something)
+ checker = c
+ key = bakery.generate_key()
+ return bakery.Bakery(
+ location=location,
+ locator=locator,
+ key=key,
+ checker=checker,
+ )
+
+
+def is_something_caveat():
+ return checkers.Caveat(condition='is something', namespace='testns')
+
+
+def check_is_something(ctx, cond, arg):
+ if arg != 'something':
+ return '{} doesn\'t match "something"'.format(arg)
+ return None
+
+
+class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker):
+ def __init__(self, check):
+ self._check = check
+
+ def check_third_party_caveat(self, ctx, info):
+ cond, arg = checkers.parse_caveat(info.condition)
+ return self._check(cond, arg)
+
+alwaysOK3rd = ThirdPartyCaveatCheckerF(lambda cond, arg: [])
diff --git a/macaroonbakery/tests/test_codec.py b/macaroonbakery/tests/test_codec.py
index 6573266..d4fbc57 100644
--- a/macaroonbakery/tests/test_codec.py
+++ b/macaroonbakery/tests/test_codec.py
@@ -6,92 +6,94 @@ from unittest import TestCase
import nacl.public
import six
-import macaroonbakery
-from macaroonbakery import utils
+import macaroonbakery as bakery
from macaroonbakery import codec
import macaroonbakery.checkers as checkers
class TestCodec(TestCase):
def setUp(self):
- self.fp_key = macaroonbakery.generate_key()
- self.tp_key = macaroonbakery.generate_key()
+ self.fp_key = bakery.generate_key()
+ self.tp_key = bakery.generate_key()
def test_v1_round_trip(self):
- tp_info = macaroonbakery.ThirdPartyInfo(
- version=macaroonbakery.BAKERY_V1,
+ tp_info = bakery.ThirdPartyInfo(
+ version=bakery.VERSION_1,
public_key=self.tp_key.public_key)
- cid = macaroonbakery.encode_caveat(
+ cid = bakery.encode_caveat(
'is-authenticated-user',
b'a random string',
tp_info,
self.fp_key,
None)
- res = macaroonbakery.decode_caveat(self.tp_key, cid)
- self.assertEquals(res, macaroonbakery.ThirdPartyCaveatInfo(
+ res = bakery.decode_caveat(self.tp_key, cid)
+ self.assertEquals(res, bakery.ThirdPartyCaveatInfo(
first_party_public_key=self.fp_key.public_key,
root_key=b'a random string',
condition='is-authenticated-user',
caveat=cid,
third_party_key_pair=self.tp_key,
- version=macaroonbakery.BAKERY_V1,
- namespace=macaroonbakery.legacy_namespace()
+ version=bakery.VERSION_1,
+ id=None,
+ namespace=bakery.legacy_namespace()
))
def test_v2_round_trip(self):
- tp_info = macaroonbakery.ThirdPartyInfo(
- version=macaroonbakery.BAKERY_V2,
+ tp_info = bakery.ThirdPartyInfo(
+ version=bakery.VERSION_2,
public_key=self.tp_key.public_key)
- cid = macaroonbakery.encode_caveat(
+ cid = bakery.encode_caveat(
'is-authenticated-user',
b'a random string',
tp_info,
self.fp_key,
None)
- res = macaroonbakery.decode_caveat(self.tp_key, cid)
- self.assertEquals(res, macaroonbakery.ThirdPartyCaveatInfo(
+ res = bakery.decode_caveat(self.tp_key, cid)
+ self.assertEquals(res, bakery.ThirdPartyCaveatInfo(
first_party_public_key=self.fp_key.public_key,
root_key=b'a random string',
condition='is-authenticated-user',
caveat=cid,
third_party_key_pair=self.tp_key,
- version=macaroonbakery.BAKERY_V2,
- namespace=macaroonbakery.legacy_namespace()
+ version=bakery.VERSION_2,
+ id=None,
+ namespace=bakery.legacy_namespace()
))
def test_v3_round_trip(self):
- tp_info = macaroonbakery.ThirdPartyInfo(
- version=macaroonbakery.BAKERY_V3,
+ tp_info = bakery.ThirdPartyInfo(
+ version=bakery.VERSION_3,
public_key=self.tp_key.public_key)
ns = checkers.Namespace()
ns.register('testns', 'x')
- cid = macaroonbakery.encode_caveat(
+ cid = bakery.encode_caveat(
'is-authenticated-user',
b'a random string',
tp_info,
self.fp_key,
ns)
- res = macaroonbakery.decode_caveat(self.tp_key, cid)
- self.assertEquals(res, macaroonbakery.ThirdPartyCaveatInfo(
+ res = bakery.decode_caveat(self.tp_key, cid)
+ self.assertEquals(res, bakery.ThirdPartyCaveatInfo(
first_party_public_key=self.fp_key.public_key,
root_key=b'a random string',
condition='is-authenticated-user',
caveat=cid,
third_party_key_pair=self.tp_key,
- version=macaroonbakery.BAKERY_V3,
+ version=bakery.VERSION_3,
+ id=None,
namespace=ns
))
def test_empty_caveat_id(self):
- with self.assertRaises(macaroonbakery.VerificationError) as context:
- macaroonbakery.decode_caveat(self.tp_key, b'')
+ with self.assertRaises(bakery.VerificationError) as context:
+ bakery.decode_caveat(self.tp_key, b'')
self.assertTrue('empty third party caveat' in str(context.exception))
def test_decode_caveat_v1_from_go(self):
- tp_key = macaroonbakery.PrivateKey(
+ tp_key = bakery.PrivateKey(
nacl.public.PrivateKey(base64.b64decode(
'TSpvLpQkRj+T3JXnsW2n43n5zP/0X4zn0RvDiWC3IJ0=')))
- fp_key = macaroonbakery.PrivateKey(
+ fp_key = bakery.PrivateKey(
nacl.public.PrivateKey(base64.b64decode(
'KXpsoJ9ujZYi/O2Cca6kaWh65MSawzy79LWkrjOfzcs=')))
root_key = base64.b64decode('vDxEmWZEkgiNEFlJ+8ruXe3qDSLf1H+o')
@@ -108,67 +110,70 @@ class TestCodec(TestCase):
'BORldUUExGdjVla1dWUjA4Uk1sbGJhc3c4VGdFbkhzM0laeVo'
'0V2lEOHhRUWdjU3ljOHY4eUt4dEhxejVEczJOYmh1ZDJhUFdt'
'UTVMcVlNWitmZ2FNaTAxdE9DIn0=')
- cav = macaroonbakery.decode_caveat(tp_key, encrypted_cav)
- self.assertEquals(cav, macaroonbakery.ThirdPartyCaveatInfo(
+ cav = bakery.decode_caveat(tp_key, encrypted_cav)
+ self.assertEquals(cav, bakery.ThirdPartyCaveatInfo(
condition='caveat condition',
first_party_public_key=fp_key.public_key,
third_party_key_pair=tp_key,
root_key=root_key,
caveat=encrypted_cav,
- version=macaroonbakery.BAKERY_V1,
- namespace=macaroonbakery.legacy_namespace()
+ version=bakery.VERSION_1,
+ id=None,
+ namespace=bakery.legacy_namespace()
))
def test_decode_caveat_v2_from_go(self):
- tp_key = macaroonbakery.PrivateKey(nacl.public.PrivateKey(
+ tp_key = bakery.PrivateKey(nacl.public.PrivateKey(
base64.b64decode(
'TSpvLpQkRj+T3JXnsW2n43n5zP/0X4zn0RvDiWC3IJ0=')))
- fp_key = macaroonbakery.PrivateKey(
+ fp_key = bakery.PrivateKey(
nacl.public.PrivateKey(base64.b64decode(
'KXpsoJ9ujZYi/O2Cca6kaWh65MSawzy79LWkrjOfzcs=')))
root_key = base64.b64decode('wh0HSM65wWHOIxoGjgJJOFvQKn2jJFhC')
# This caveat has been generated from the go code
# to check the compatibilty
- encrypted_cav = base64.urlsafe_b64decode(
- utils.add_base64_padding(six.b(
- 'AvD-xlUf2MdGMgtu7OKRQnCP1OQJk6PKeFWRK26WIBA6DNwKGIHq9xGcHS9IZ'
- 'Lh0cL6D9qpeKI0mXmCPfnwRQDuVYC8y5gVWd-oCGZaj5TGtk3byp2Vnw6ojmt'
- 'sULDhY59YA_J_Y0ATkERO5T9ajoRWBxU2OXBoX6bImXA')))
- cav = macaroonbakery.decode_caveat(tp_key, encrypted_cav)
- self.assertEqual(cav, macaroonbakery.ThirdPartyCaveatInfo(
+ encrypted_cav = bakery.b64decode(
+ 'AvD-xlUf2MdGMgtu7OKRQnCP1OQJk6PKeFWRK26WIBA6DNwKGIHq9xGcHS9IZ'
+ 'Lh0cL6D9qpeKI0mXmCPfnwRQDuVYC8y5gVWd-oCGZaj5TGtk3byp2Vnw6ojmt'
+ 'sULDhY59YA_J_Y0ATkERO5T9ajoRWBxU2OXBoX6bImXA',
+ )
+ cav = bakery.decode_caveat(tp_key, encrypted_cav)
+ self.assertEqual(cav, bakery.ThirdPartyCaveatInfo(
condition='third party condition',
first_party_public_key=fp_key.public_key,
third_party_key_pair=tp_key,
root_key=root_key,
caveat=encrypted_cav,
- version=macaroonbakery.BAKERY_V2,
- namespace=macaroonbakery.legacy_namespace()
+ version=bakery.VERSION_2,
+ id=None,
+ namespace=bakery.legacy_namespace()
))
def test_decode_caveat_v3_from_go(self):
- tp_key = macaroonbakery.PrivateKey(
+ tp_key = bakery.PrivateKey(
nacl.public.PrivateKey(base64.b64decode(
'TSpvLpQkRj+T3JXnsW2n43n5zP/0X4zn0RvDiWC3IJ0=')))
- fp_key = macaroonbakery.PrivateKey(nacl.public.PrivateKey(
+ fp_key = bakery.PrivateKey(nacl.public.PrivateKey(
base64.b64decode(
'KXpsoJ9ujZYi/O2Cca6kaWh65MSawzy79LWkrjOfzcs=')))
root_key = base64.b64decode(b'oqOXI3/Mz/pKjCuFOt2eYxb7ndLq66GY')
# This caveat has been generated from the go code
# to check the compatibilty
- encrypted_cav = base64.urlsafe_b64decode(
- utils.add_base64_padding(six.b(
- 'A_D-xlUf2MdGMgtu7OKRQnCP1OQJk6PKeFWRK26WIBA6DNwKGNLeFSkD2M-8A'
- 'EYvmgVH95GWu7T7caKxKhhOQFcEKgnXKJvYXxz1zin4cZc4Q6C7gVqA-J4_j3'
- '1LX4VKxymqG62UGPo78wOv0_fKjr3OI6PPJOYOQgBMclemlRF2')))
- cav = macaroonbakery.decode_caveat(tp_key, encrypted_cav)
- self.assertEquals(cav, macaroonbakery.ThirdPartyCaveatInfo(
+ encrypted_cav = bakery.b64decode(
+ 'A_D-xlUf2MdGMgtu7OKRQnCP1OQJk6PKeFWRK26WIBA6DNwKGNLeFSkD2M-8A'
+ 'EYvmgVH95GWu7T7caKxKhhOQFcEKgnXKJvYXxz1zin4cZc4Q6C7gVqA-J4_j3'
+ '1LX4VKxymqG62UGPo78wOv0_fKjr3OI6PPJOYOQgBMclemlRF2',
+ )
+ cav = bakery.decode_caveat(tp_key, encrypted_cav)
+ self.assertEquals(cav, bakery.ThirdPartyCaveatInfo(
condition='third party condition',
first_party_public_key=fp_key.public_key,
third_party_key_pair=tp_key,
root_key=root_key,
caveat=encrypted_cav,
- version=macaroonbakery.BAKERY_V3,
- namespace=macaroonbakery.legacy_namespace()
+ version=bakery.VERSION_3,
+ id=None,
+ namespace=bakery.legacy_namespace()
))
def test_encode_decode_varint(self):
@@ -183,7 +188,7 @@ class TestCodec(TestCase):
for test in tests:
data = bytearray()
expected = bytearray()
- macaroonbakery.encode_uvarint(test[0], data)
+ bakery.encode_uvarint(test[0], data)
for v in test[1]:
expected.append(v)
self.assertEquals(data, expected)
diff --git a/macaroonbakery/tests/test_discharge.py b/macaroonbakery/tests/test_discharge.py
index 6e2df6a..433483a 100644
--- a/macaroonbakery/tests/test_discharge.py
+++ b/macaroonbakery/tests/test_discharge.py
@@ -3,11 +3,8 @@
import unittest
from pymacaroons import MACAROON_V1, Macaroon
-from pymacaroons.exceptions import (
- MacaroonInvalidSignatureException, MacaroonUnmetCaveatException
-)
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
from macaroonbakery.tests import common
@@ -20,15 +17,15 @@ class TestDischarge(unittest.TestCase):
can verify this macaroon as valid.
'''
oc = common.new_bakery('bakerytest')
- primary = oc.oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION,
+ primary = oc.oven.macaroon(bakery.LATEST_VERSION,
common.ages, None,
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
self.assertEqual(primary.macaroon.location, 'bakerytest')
primary.add_caveat(checkers.Caveat(condition='str something',
namespace='testns'),
oc.oven.key, oc.oven.locator)
oc.checker.auth([[primary.macaroon]]).allow(
- common.str_context('something'), [macaroonbakery.LOGIN_OP])
+ common.str_context('something'), [bakery.LOGIN_OP])
def test_macaroon_paper_fig6(self):
''' Implements an example flow as described in the macaroons paper:
@@ -45,15 +42,15 @@ class TestDischarge(unittest.TestCase):
The target service verifies the original macaroon it delegated to fs
No direct contact between bs and ts is required
'''
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
bs = common.new_bakery('bs-loc', locator)
ts = common.new_bakery('ts-loc', locator)
fs = common.new_bakery('fs-loc', locator)
# ts creates a macaroon.
- ts_macaroon = ts.oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION,
+ ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION,
common.ages,
- None, [macaroonbakery.LOGIN_OP])
+ None, [bakery.LOGIN_OP])
# ts somehow sends the macaroon to fs which adds a third party caveat
# to be discharged by bs.
@@ -62,51 +59,55 @@ class TestDischarge(unittest.TestCase):
fs.oven.key, fs.oven.locator)
# client asks for a discharge macaroon for each third party caveat
- def get_discharge(ctx, cav, payload):
+ def get_discharge(cav, payload):
self.assertEqual(cav.location, 'bs-loc')
- return macaroonbakery.discharge(ctx, cav.caveat_id_bytes, payload,
- bs.oven.key,
- common.ThirdPartyStrcmpChecker(
- 'user==bob'),
- bs.oven.locator)
+ return bakery.discharge(
+ common.test_context,
+ cav.caveat_id_bytes,
+ payload,
+ bs.oven.key,
+ common.ThirdPartyStrcmpChecker('user==bob'),
+ bs.oven.locator,
+ )
- d = macaroonbakery.discharge_all(common.test_context, ts_macaroon,
- get_discharge)
+ d = bakery.discharge_all(ts_macaroon, get_discharge)
ts.checker.auth([d]).allow(common.test_context,
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
def test_discharge_with_version1_macaroon(self):
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
bs = common.new_bakery('bs-loc', locator)
ts = common.new_bakery('ts-loc', locator)
# ts creates a old-version macaroon.
- ts_macaroon = ts.oven.macaroon(macaroonbakery.BAKERY_V1, common.ages,
- None, [macaroonbakery.LOGIN_OP])
+ ts_macaroon = ts.oven.macaroon(bakery.VERSION_1, common.ages,
+ None, [bakery.LOGIN_OP])
ts_macaroon.add_caveat(checkers.Caveat(condition='something',
location='bs-loc'),
ts.oven.key, ts.oven.locator)
# client asks for a discharge macaroon for each third party caveat
- def get_discharge(ctx, cav, payload):
+ def get_discharge(cav, payload):
# Make sure that the caveat id really is old-style.
try:
cav.caveat_id_bytes.decode('utf-8')
except UnicodeDecodeError:
self.fail('caveat id is not utf-8')
- return macaroonbakery.discharge(ctx, cav.caveat_id_bytes, payload,
- bs.oven.key,
- common.ThirdPartyStrcmpChecker(
- 'something'),
- bs.oven.locator)
+ return bakery.discharge(
+ common.test_context,
+ cav.caveat_id_bytes,
+ payload,
+ bs.oven.key,
+ common.ThirdPartyStrcmpChecker('something'),
+ bs.oven.locator,
+ )
- d = macaroonbakery.discharge_all(common.test_context, ts_macaroon,
- get_discharge)
+ d = bakery.discharge_all(ts_macaroon, get_discharge)
ts.checker.auth([d]).allow(common.test_context,
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
for m in d:
self.assertEqual(m.version, MACAROON_V1)
@@ -114,29 +115,31 @@ class TestDischarge(unittest.TestCase):
def test_version1_macaroon_id(self):
# In the version 1 bakery, macaroon ids were hex-encoded with a
# hyphenated UUID suffix.
- root_key_store = macaroonbakery.MemoryKeyStore()
- b = macaroonbakery.Bakery(root_key_store=root_key_store,
- identity_client=common.OneIdentity())
+ root_key_store = bakery.MemoryKeyStore()
+ b = bakery.Bakery(
+ root_key_store=root_key_store,
+ identity_client=common.OneIdentity(),
+ )
key, id = root_key_store.root_key()
root_key_store.get(id)
m = Macaroon(key=key, version=MACAROON_V1, location='',
identifier=id + b'-deadl00f')
b.checker.auth([[m]]).allow(common.test_context,
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
def test_macaroon_paper_fig6_fails_without_discharges(self):
''' Runs a similar test as test_macaroon_paper_fig6 without the client
discharging the third party caveats.
'''
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
ts = common.new_bakery('ts-loc', locator)
fs = common.new_bakery('fs-loc', locator)
common.new_bakery('as-loc', locator)
# ts creates a macaroon.
- ts_macaroon = ts.oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION,
+ ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION,
common.ages, None,
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
# ts somehow sends the macaroon to fs which adds a third party
# caveat to be discharged by as.
@@ -148,24 +151,24 @@ class TestDischarge(unittest.TestCase):
try:
ts.checker.auth([[ts_macaroon.macaroon]]).allow(
common.test_context,
- macaroonbakery.LOGIN_OP
+ bakery.LOGIN_OP
)
self.fail('macaroon unmet should be raised')
- except MacaroonUnmetCaveatException:
+ except bakery.VerificationError:
pass
def test_macaroon_paper_fig6_fails_with_binding_on_tampered_sig(self):
''' Runs a similar test as test_macaroon_paper_fig6 with the discharge
macaroon binding being done on a tampered signature.
'''
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
bs = common.new_bakery('bs-loc', locator)
ts = common.new_bakery('ts-loc', locator)
# ts creates a macaroon.
- ts_macaroon = ts.oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION,
+ ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION,
common.ages, None,
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
# ts somehow sends the macaroon to fs which adds a third party caveat
# to be discharged by as.
ts_macaroon.add_caveat(checkers.Caveat(condition='user==bob',
@@ -173,16 +176,18 @@ class TestDischarge(unittest.TestCase):
ts.oven.key, ts.oven.locator)
# client asks for a discharge macaroon for each third party caveat
- def get_discharge(ctx, cav, payload):
+ def get_discharge(cav, payload):
self.assertEqual(cav.location, 'bs-loc')
- return macaroonbakery.discharge(ctx, cav.caveat_id_bytes, payload,
- bs.oven.key,
- common.ThirdPartyStrcmpChecker(
- 'user==bob'),
- bs.oven.locator)
-
- d = macaroonbakery.discharge_all(common.test_context, ts_macaroon,
- get_discharge)
+ return bakery.discharge(
+ common.test_context,
+ cav.caveat_id_bytes,
+ payload,
+ bs.oven.key,
+ common.ThirdPartyStrcmpChecker('user==bob'),
+ bs.oven.locator,
+ )
+
+ d = bakery.discharge_all(ts_macaroon, get_discharge)
# client has all the discharge macaroons. For each discharge macaroon
# bind it to our ts_macaroon and add it to our request.
tampered_macaroon = Macaroon()
@@ -190,35 +195,39 @@ class TestDischarge(unittest.TestCase):
d[i + 1] = tampered_macaroon.prepare_for_request(dm)
# client makes request to ts.
- with self.assertRaises(MacaroonInvalidSignatureException) as exc:
+ with self.assertRaises(bakery.VerificationError) as exc:
ts.checker.auth([d]).allow(common.test_context,
- macaroonbakery.LOGIN_OP)
- self.assertEqual('Signatures do not match', exc.exception.args[0])
+ bakery.LOGIN_OP)
+ self.assertEqual('verification failed: Signatures do not match',
+ exc.exception.args[0])
def test_need_declared(self):
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
first_party = common.new_bakery('first', locator)
third_party = common.new_bakery('third', locator)
# firstParty mints a macaroon with a third-party caveat addressed
# to thirdParty with a need-declared caveat.
m = first_party.oven.macaroon(
- macaroonbakery.LATEST_BAKERY_VERSION, common.ages, [
+ bakery.LATEST_VERSION, common.ages, [
checkers.need_declared_caveat(
checkers.Caveat(location='third', condition='something'),
['foo', 'bar']
)
- ], [macaroonbakery.LOGIN_OP])
+ ], [bakery.LOGIN_OP])
# The client asks for a discharge macaroon for each third party caveat.
- def get_discharge(ctx, cav, payload):
- return macaroonbakery.discharge(ctx, cav.caveat_id_bytes, payload,
- third_party.oven.key,
- common.ThirdPartyStrcmpChecker(
- 'something'),
- third_party.oven.locator)
+ def get_discharge(cav, payload):
+ return bakery.discharge(
+ common.test_context,
+ cav.caveat_id_bytes,
+ payload,
+ third_party.oven.key,
+ common.ThirdPartyStrcmpChecker('something'),
+ third_party.oven.locator,
+ )
- d = macaroonbakery.discharge_all(common.test_context, m, get_discharge)
+ d = bakery.discharge_all(m, get_discharge)
# The required declared attributes should have been added
# to the discharge macaroons.
@@ -231,22 +240,26 @@ class TestDischarge(unittest.TestCase):
# Make sure the macaroons actually check out correctly
# when provided with the declared checker.
ctx = checkers.context_with_declared(common.test_context, declared)
- first_party.checker.auth([d]).allow(ctx, [macaroonbakery.LOGIN_OP])
+ first_party.checker.auth([d]).allow(ctx, [bakery.LOGIN_OP])
# Try again when the third party does add a required declaration.
# The client asks for a discharge macaroon for each third party caveat.
- def get_discharge(ctx, cav, payload):
+ def get_discharge(cav, payload):
checker = common.ThirdPartyCheckerWithCaveats([
checkers.declared_caveat('foo', 'a'),
checkers.declared_caveat('arble', 'b')
])
- return macaroonbakery.discharge(ctx, cav.caveat_id_bytes, payload,
- third_party.oven.key,
- checker,
- third_party.oven.locator)
+ return bakery.discharge(
+ common.test_context,
+ cav.caveat_id_bytes,
+ payload,
+ third_party.oven.key,
+ checker,
+ third_party.oven.locator,
+ )
- d = macaroonbakery.discharge_all(common.test_context, m, get_discharge)
+ d = bakery.discharge_all(m, get_discharge)
# One attribute should have been added, the other was already there.
declared = checkers.infer_declared(d, first_party.checker.namespace())
@@ -257,25 +270,28 @@ class TestDischarge(unittest.TestCase):
})
ctx = checkers.context_with_declared(common.test_context, declared)
- first_party.checker.auth([d]).allow(ctx, [macaroonbakery.LOGIN_OP])
+ first_party.checker.auth([d]).allow(ctx, [bakery.LOGIN_OP])
# Try again, but this time pretend a client is sneakily trying
# to add another 'declared' attribute to alter the declarations.
- def get_discharge(ctx, cav, payload):
+ def get_discharge(cav, payload):
checker = common.ThirdPartyCheckerWithCaveats([
checkers.declared_caveat('foo', 'a'),
checkers.declared_caveat('arble', 'b'),
])
# Sneaky client adds a first party caveat.
- m = macaroonbakery.discharge(ctx, cav.caveat_id_bytes, payload,
- third_party.oven.key, checker,
- third_party.oven.locator)
+ m = bakery.discharge(
+ common.test_context, cav.caveat_id_bytes,
+ payload,
+ third_party.oven.key, checker,
+ third_party.oven.locator,
+ )
m.add_caveat(checkers.declared_caveat('foo', 'c'), None, None)
return m
- d = macaroonbakery.discharge_all(common.test_context, m, get_discharge)
+ d = bakery.discharge_all(m, get_discharge)
declared = checkers.infer_declared(d, first_party.checker.namespace())
self.assertEqual(declared, {
@@ -283,22 +299,22 @@ class TestDischarge(unittest.TestCase):
'arble': 'b',
})
- with self.assertRaises(macaroonbakery.AuthInitError) as exc:
+ with self.assertRaises(bakery.AuthInitError) as exc:
first_party.checker.auth([d]).allow(common.test_context,
- macaroonbakery.LOGIN_OP)
+ bakery.LOGIN_OP)
self.assertEqual('cannot authorize login macaroon: caveat '
'"declared foo a" not satisfied: got foo=null, '
'expected "a"', exc.exception.args[0])
def test_discharge_two_need_declared(self):
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
first_party = common.new_bakery('first', locator)
third_party = common.new_bakery('third', locator)
# first_party mints a macaroon with two third party caveats
# with overlapping attributes.
m = first_party.oven.macaroon(
- macaroonbakery.LATEST_BAKERY_VERSION,
+ bakery.LATEST_VERSION,
common.ages, [
checkers.need_declared_caveat(
checkers.Caveat(location='third', condition='x'),
@@ -306,18 +322,22 @@ class TestDischarge(unittest.TestCase):
checkers.need_declared_caveat(
checkers.Caveat(location='third', condition='y'),
['bar', 'baz']),
- ], [macaroonbakery.LOGIN_OP])
+ ], [bakery.LOGIN_OP])
# The client asks for a discharge macaroon for each third party caveat.
# Since no declarations are added by the discharger,
- def get_discharge(ctx, cav, payload):
- return macaroonbakery.discharge(
- ctx, cav.caveat_id_bytes, payload, third_party.oven.key,
+ def get_discharge(cav, payload):
+ return bakery.discharge(
+ common.test_context,
+ cav.caveat_id_bytes,
+ payload,
+ third_party.oven.key,
common.ThirdPartyCaveatCheckerEmpty(),
- third_party.oven.locator)
+ third_party.oven.locator,
+ )
- d = macaroonbakery.discharge_all(common.test_context, m, get_discharge)
+ d = bakery.discharge_all(m, get_discharge)
declared = checkers.infer_declared(d, first_party.checker.namespace())
self.assertEqual(declared, {
'foo': '',
@@ -325,12 +345,12 @@ class TestDischarge(unittest.TestCase):
'baz': '',
})
ctx = checkers.context_with_declared(common.test_context, declared)
- first_party.checker.auth([d]).allow(ctx, [macaroonbakery.LOGIN_OP])
+ first_party.checker.auth([d]).allow(ctx, [bakery.LOGIN_OP])
# If they return conflicting values, the discharge fails.
# The client asks for a discharge macaroon for each third party caveat.
# Since no declarations are added by the discharger,
- class ThirdPartyCaveatCheckerF(macaroonbakery.ThirdPartyCaveatChecker):
+ class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker):
def check_third_party_caveat(self, ctx, cav_info):
if cav_info.condition == b'x':
return [checkers.declared_caveat('foo', 'fooval1')]
@@ -341,62 +361,70 @@ class TestDischarge(unittest.TestCase):
]
raise common.ThirdPartyCaveatCheckFailed('not matched')
- def get_discharge(ctx, cav, payload):
- return macaroonbakery.discharge(ctx, cav.caveat_id_bytes, payload,
- third_party.oven.key,
- ThirdPartyCaveatCheckerF(),
- third_party.oven.locator)
+ def get_discharge(cav, payload):
+ return bakery.discharge(
+ common.test_context,
+ cav.caveat_id_bytes,
+ payload,
+ third_party.oven.key,
+ ThirdPartyCaveatCheckerF(),
+ third_party.oven.locator,
+ )
- d = macaroonbakery.discharge_all(common.test_context, m, get_discharge)
+ d = bakery.discharge_all(m, get_discharge)
declared = checkers.infer_declared(d, first_party.checker.namespace())
self.assertEqual(declared, {
'bar': '',
'baz': 'bazval',
})
- with self.assertRaises(macaroonbakery.AuthInitError) as exc:
+ with self.assertRaises(bakery.AuthInitError) as exc:
first_party.checker.auth([d]).allow(common.test_context,
- macaroonbakery.LOGIN_OP)
+ bakery.LOGIN_OP)
self.assertEqual('cannot authorize login macaroon: caveat "declared '
'foo fooval1" not satisfied: got foo=null, expected '
'"fooval1"', exc.exception.args[0])
def test_discharge_macaroon_cannot_be_used_as_normal_macaroon(self):
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
first_party = common.new_bakery('first', locator)
third_party = common.new_bakery('third', locator)
# First party mints a macaroon with a 3rd party caveat.
- m = first_party.oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION,
+ m = first_party.oven.macaroon(bakery.LATEST_VERSION,
common.ages, [
checkers.Caveat(location='third',
condition='true')],
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
# Acquire the discharge macaroon, but don't bind it to the original.
class M:
unbound = None
- def get_discharge(ctx, cav, payload):
- m = macaroonbakery.discharge(
- ctx, cav.caveat_id_bytes, payload, third_party.oven.key,
+ def get_discharge(cav, payload):
+ m = bakery.discharge(
+ common.test_context,
+ cav.caveat_id_bytes,
+ payload,
+ third_party.oven.key,
common.ThirdPartyStrcmpChecker('true'),
- third_party.oven.locator)
+ third_party.oven.locator,
+ )
M.unbound = m.macaroon.copy()
return m
- macaroonbakery.discharge_all(common.test_context, m, get_discharge)
+ bakery.discharge_all(m, get_discharge)
self.assertIsNotNone(M.unbound)
# Make sure it cannot be used as a normal macaroon in the third party.
- with self.assertRaises(macaroonbakery.AuthInitError) as exc:
+ with self.assertRaises(bakery.VerificationError) as exc:
third_party.checker.auth([[M.unbound]]).allow(
- common.test_context, [macaroonbakery.LOGIN_OP])
+ common.test_context, [bakery.LOGIN_OP])
self.assertEqual('no operations found in macaroon',
exc.exception.args[0])
def test_third_party_discharge_macaroon_ids_are_small(self):
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
bakeries = {
'ts-loc': common.new_bakery('ts-loc', locator),
'as1-loc': common.new_bakery('as1-loc', locator),
@@ -404,14 +432,14 @@ class TestDischarge(unittest.TestCase):
}
ts = bakeries['ts-loc']
- ts_macaroon = ts.oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION,
+ ts_macaroon = ts.oven.macaroon(bakery.LATEST_VERSION,
common.ages,
- None, [macaroonbakery.LOGIN_OP])
+ None, [bakery.LOGIN_OP])
ts_macaroon.add_caveat(checkers.Caveat(condition='something',
location='as1-loc'),
ts.oven.key, ts.oven.locator)
- class ThirdPartyCaveatCheckerF(macaroonbakery.ThirdPartyCaveatChecker):
+ class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker):
def __init__(self, loc):
self._loc = loc
@@ -424,18 +452,20 @@ class TestDischarge(unittest.TestCase):
raise common.ThirdPartyCaveatCheckFailed(
'unknown location {}'.format(self._loc))
- def get_discharge(ctx, cav, payload):
+ def get_discharge(cav, payload):
oven = bakeries[cav.location].oven
- return macaroonbakery.discharge(ctx, cav.caveat_id_bytes, payload,
- oven.key,
- ThirdPartyCaveatCheckerF(
- cav.location),
- oven.locator)
-
- d = macaroonbakery.discharge_all(common.test_context, ts_macaroon,
- get_discharge)
+ return bakery.discharge(
+ common.test_context,
+ cav.caveat_id_bytes,
+ payload,
+ oven.key,
+ ThirdPartyCaveatCheckerF(cav.location),
+ oven.locator,
+ )
+
+ d = bakery.discharge_all(ts_macaroon, get_discharge)
ts.checker.auth([d]).allow(common.test_context,
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
for i, m in enumerate(d):
for j, cav in enumerate(m.caveats):
diff --git a/macaroonbakery/tests/test_discharge_all.py b/macaroonbakery/tests/test_discharge_all.py
index 8da8823..7999f5f 100644
--- a/macaroonbakery/tests/test_discharge_all.py
+++ b/macaroonbakery/tests/test_discharge_all.py
@@ -4,7 +4,7 @@ import unittest
from pymacaroons.verifier import Verifier
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
from macaroonbakery.tests import common
@@ -16,12 +16,11 @@ def always_ok(predicate):
class TestDischargeAll(unittest.TestCase):
def test_discharge_all_no_discharges(self):
root_key = b'root key'
- m = macaroonbakery.Macaroon(
+ m = bakery.Macaroon(
root_key=root_key, id=b'id0', location='loc0',
- version=macaroonbakery.LATEST_BAKERY_VERSION,
+ version=bakery.LATEST_VERSION,
namespace=common.test_checker().namespace())
- ms = macaroonbakery.discharge_all(
- common.test_context, m, no_discharge(self))
+ ms = bakery.discharge_all(m, no_discharge(self))
self.assertEqual(len(ms), 1)
self.assertEqual(ms[0], m.macaroon)
v = Verifier()
@@ -30,9 +29,9 @@ class TestDischargeAll(unittest.TestCase):
def test_discharge_all_many_discharges(self):
root_key = b'root key'
- m0 = macaroonbakery.Macaroon(
+ m0 = bakery.Macaroon(
root_key=root_key, id=b'id0', location='loc0',
- version=macaroonbakery.LATEST_BAKERY_VERSION)
+ version=bakery.LATEST_VERSION)
class State(object):
total_required = 40
@@ -52,19 +51,18 @@ class TestDischargeAll(unittest.TestCase):
add_caveats(m0)
- def get_discharge(_, cav, payload):
+ def get_discharge(cav, payload):
self.assertEqual(payload, None)
- m = macaroonbakery.Macaroon(
+ m = bakery.Macaroon(
root_key='root key {}'.format(
cav.caveat_id.decode('utf-8')).encode('utf-8'),
id=cav.caveat_id, location='',
- version=macaroonbakery.LATEST_BAKERY_VERSION)
+ version=bakery.LATEST_VERSION)
add_caveats(m)
return m
- ms = macaroonbakery.discharge_all(
- common.test_context, m0, get_discharge)
+ ms = bakery.discharge_all(m0, get_discharge)
self.assertEqual(len(ms), 41)
@@ -77,7 +75,7 @@ class TestDischargeAll(unittest.TestCase):
# we're using actual third party caveats as added by
# Macaroon.add_caveat and we use a larger number of caveats
# so that caveat ids will need to get larger.
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
bakeries = {}
total_discharges_required = 40
@@ -106,9 +104,9 @@ class TestDischargeAll(unittest.TestCase):
return caveats
root_key = b'root key'
- m0 = macaroonbakery.Macaroon(
+ m0 = bakery.Macaroon(
root_key=root_key, id=b'id0', location='ts-loc',
- version=macaroonbakery.LATEST_BAKERY_VERSION)
+ version=bakery.LATEST_VERSION)
m0.add_caveat(checkers. Caveat(location=add_bakery(),
condition='something'),
@@ -117,18 +115,17 @@ class TestDischargeAll(unittest.TestCase):
# We've added a caveat (the first) so one less caveat is required.
M.still_required -= 1
- class ThirdPartyCaveatCheckerF(macaroonbakery.ThirdPartyCaveatChecker):
+ class ThirdPartyCaveatCheckerF(bakery.ThirdPartyCaveatChecker):
def check_third_party_caveat(self, ctx, info):
return checker(ctx, info)
- def get_discharge(ctx, cav, payload):
- return macaroonbakery.discharge(
- ctx, cav.caveat_id, payload,
+ def get_discharge(cav, payload):
+ return bakery.discharge(
+ common.test_context, cav.caveat_id, payload,
bakeries[cav.location].oven.key,
ThirdPartyCaveatCheckerF(), locator)
- ms = macaroonbakery.discharge_all(common.test_context, m0,
- get_discharge)
+ ms = bakery.discharge_all(m0, get_discharge)
self.assertEqual(len(ms), total_discharges_required + 1)
@@ -138,33 +135,31 @@ class TestDischargeAll(unittest.TestCase):
def test_discharge_all_local_discharge(self):
oc = common.new_bakery('ts', None)
- client_key = macaroonbakery.generate_key()
- m = oc.oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION, common.ages,
+ client_key = bakery.generate_key()
+ m = oc.oven.macaroon(bakery.LATEST_VERSION, common.ages,
[
- macaroonbakery.local_third_party_caveat(
+ bakery.local_third_party_caveat(
client_key.public_key,
- macaroonbakery.LATEST_BAKERY_VERSION)
- ], [macaroonbakery.LOGIN_OP])
- ms = macaroonbakery.discharge_all(
- common.test_context, m, no_discharge(self), client_key)
+ bakery.LATEST_VERSION)
+ ], [bakery.LOGIN_OP])
+ ms = bakery.discharge_all(m, no_discharge(self), client_key)
oc.checker.auth([ms]).allow(common.test_context,
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
def test_discharge_all_local_discharge_version1(self):
oc = common.new_bakery('ts', None)
- client_key = macaroonbakery.generate_key()
- m = oc.oven.macaroon(macaroonbakery.BAKERY_V1, common.ages, [
- macaroonbakery.local_third_party_caveat(
- client_key.public_key, macaroonbakery.BAKERY_V1)
- ], [macaroonbakery.LOGIN_OP])
- ms = macaroonbakery.discharge_all(
- common.test_context, m, no_discharge(self), client_key)
+ client_key = bakery.generate_key()
+ m = oc.oven.macaroon(bakery.VERSION_1, common.ages, [
+ bakery.local_third_party_caveat(
+ client_key.public_key, bakery.VERSION_1)
+ ], [bakery.LOGIN_OP])
+ ms = bakery.discharge_all(m, no_discharge(self), client_key)
oc.checker.auth([ms]).allow(common.test_context,
- [macaroonbakery.LOGIN_OP])
+ [bakery.LOGIN_OP])
def no_discharge(test):
- def get_discharge(ctx, cav, payload):
+ def get_discharge(cav, payload):
test.fail("get_discharge called unexpectedly")
return get_discharge
diff --git a/macaroonbakery/tests/test_keyring.py b/macaroonbakery/tests/test_keyring.py
index 351b144..438ab1b 100644
--- a/macaroonbakery/tests/test_keyring.py
+++ b/macaroonbakery/tests/test_keyring.py
@@ -4,28 +4,28 @@ import unittest
from httmock import urlmatch, HTTMock
-import macaroonbakery
-from macaroonbakery import httpbakery
+import macaroonbakery as bakery
+import macaroonbakery.httpbakery as httpbakery
class TestKeyRing(unittest.TestCase):
def test_cache_fetch(self):
- key = macaroonbakery.generate_key()
+ key = bakery.generate_key()
@urlmatch(path='.*/discharge/info')
def discharge_info(url, request):
return {
'status_code': 200,
'content': {
- 'Version': macaroonbakery.LATEST_BAKERY_VERSION,
+ 'Version': bakery.LATEST_VERSION,
'PublicKey': key.public_key.encode().decode('utf-8')
}
}
- expectInfo = macaroonbakery.ThirdPartyInfo(
+ expectInfo = bakery.ThirdPartyInfo(
public_key=key.public_key,
- version=macaroonbakery.LATEST_BAKERY_VERSION
+ version=bakery.LATEST_VERSION
)
kr = httpbakery.ThirdPartyLocator(allow_insecure=True)
with HTTMock(discharge_info):
@@ -33,21 +33,21 @@ class TestKeyRing(unittest.TestCase):
self.assertEqual(info, expectInfo)
def test_cache_norefetch(self):
- key = macaroonbakery.generate_key()
+ key = bakery.generate_key()
@urlmatch(path='.*/discharge/info')
def discharge_info(url, request):
return {
'status_code': 200,
'content': {
- 'Version': macaroonbakery.LATEST_BAKERY_VERSION,
+ 'Version': bakery.LATEST_VERSION,
'PublicKey': key.public_key.encode().decode('utf-8')
}
}
- expectInfo = macaroonbakery.ThirdPartyInfo(
+ expectInfo = bakery.ThirdPartyInfo(
public_key=key.public_key,
- version=macaroonbakery.LATEST_BAKERY_VERSION
+ version=bakery.LATEST_VERSION
)
kr = httpbakery.ThirdPartyLocator(allow_insecure=True)
with HTTMock(discharge_info):
@@ -57,7 +57,7 @@ class TestKeyRing(unittest.TestCase):
self.assertEqual(info, expectInfo)
def test_cache_fetch_no_version(self):
- key = macaroonbakery.generate_key()
+ key = bakery.generate_key()
@urlmatch(path='.*/discharge/info')
def discharge_info(url, request):
@@ -68,9 +68,9 @@ class TestKeyRing(unittest.TestCase):
}
}
- expectInfo = macaroonbakery.ThirdPartyInfo(
+ expectInfo = bakery.ThirdPartyInfo(
public_key=key.public_key,
- version=macaroonbakery.BAKERY_V1
+ version=bakery.VERSION_1
)
kr = httpbakery.ThirdPartyLocator(allow_insecure=True)
with HTTMock(discharge_info):
@@ -79,11 +79,11 @@ class TestKeyRing(unittest.TestCase):
def test_allow_insecure(self):
kr = httpbakery.ThirdPartyLocator()
- with self.assertRaises(macaroonbakery.error.ThirdPartyInfoNotFound):
+ with self.assertRaises(bakery.error.ThirdPartyInfoNotFound):
kr.third_party_info('http://0.1.2.3/')
def test_fallback(self):
- key = macaroonbakery.generate_key()
+ key = bakery.generate_key()
@urlmatch(path='.*/discharge/info')
def discharge_info(url, request):
@@ -100,9 +100,9 @@ class TestKeyRing(unittest.TestCase):
}
}
- expectInfo = macaroonbakery.ThirdPartyInfo(
+ expectInfo = bakery.ThirdPartyInfo(
public_key=key.public_key,
- version=macaroonbakery.BAKERY_V1
+ version=bakery.VERSION_1
)
kr = httpbakery.ThirdPartyLocator(allow_insecure=True)
with HTTMock(discharge_info):
diff --git a/macaroonbakery/tests/test_macaroon.py b/macaroonbakery/tests/test_macaroon.py
index 7e77e2b..93bbbb8 100644
--- a/macaroonbakery/tests/test_macaroon.py
+++ b/macaroonbakery/tests/test_macaroon.py
@@ -7,36 +7,35 @@ import six
import pymacaroons
from pymacaroons import serializers
-import macaroonbakery
+import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
from macaroonbakery.tests import common
class TestMacaroon(TestCase):
def test_new_macaroon(self):
- m = macaroonbakery.Macaroon(
+ m = bakery.Macaroon(
b'rootkey',
b'some id',
'here',
- macaroonbakery.LATEST_BAKERY_VERSION)
+ bakery.LATEST_VERSION)
self.assertIsNotNone(m)
self.assertEquals(m._macaroon.identifier, b'some id')
self.assertEquals(m._macaroon.location, 'here')
- self.assertEquals(m.version, macaroonbakery.LATEST_BAKERY_VERSION)
+ self.assertEquals(m.version, bakery.LATEST_VERSION)
def test_add_first_party_caveat(self):
- m = macaroonbakery.Macaroon('rootkey', 'some id', 'here',
- macaroonbakery.LATEST_BAKERY_VERSION)
+ m = bakery.Macaroon('rootkey', 'some id', 'here', bakery.LATEST_VERSION)
m.add_caveat(checkers.Caveat('test_condition'))
caveats = m.first_party_caveats()
self.assertEquals(len(caveats), 1)
self.assertEquals(caveats[0].caveat_id, b'test_condition')
def test_add_third_party_caveat(self):
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
bs = common.new_bakery('bs-loc', locator)
- lbv = six.int2byte(macaroonbakery.LATEST_BAKERY_VERSION)
+ lbv = six.int2byte(bakery.LATEST_VERSION)
tests = [
('no existing id', b'', [], lbv + six.int2byte(0)),
('several existing ids', b'', [
@@ -53,10 +52,10 @@ class TestMacaroon(TestCase):
for test in tests:
print('test ', test[0])
- m = macaroonbakery.Macaroon(
+ m = bakery.Macaroon(
root_key=b'root key', id=b'id',
location='location',
- version=macaroonbakery.LATEST_BAKERY_VERSION)
+ version=bakery.LATEST_VERSION)
for id in test[2]:
m.macaroon.add_third_party_caveat(key=None, key_id=id,
location='')
@@ -68,21 +67,21 @@ class TestMacaroon(TestCase):
test[3])
def test_marshal_json_latest_version(self):
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
bs = common.new_bakery('bs-loc', locator)
ns = checkers.Namespace({
'testns': 'x',
'otherns': 'y',
})
- m = macaroonbakery.Macaroon(
+ m = bakery.Macaroon(
root_key=b'root key', id=b'id',
location='location',
- version=macaroonbakery.LATEST_BAKERY_VERSION,
+ version=bakery.LATEST_VERSION,
namespace=ns)
m.add_caveat(checkers.Caveat(location='bs-loc', condition='something'),
bs.oven.key, locator)
data = m.serialize_json()
- m1 = macaroonbakery.Macaroon.deserialize_json(data)
+ m1 = bakery.Macaroon.deserialize_json(data)
# Just check the signature and version - we're not interested in fully
# checking the macaroon marshaling here.
self.assertEqual(m1.macaroon.signature, m.macaroon.signature)
@@ -92,8 +91,8 @@ class TestMacaroon(TestCase):
self.assertEqual(m1._caveat_data, m._caveat_data)
# test with the encoder, decoder
- data = json.dumps(m, cls=macaroonbakery.MacaroonJSONEncoder)
- m1 = json.loads(data, cls=macaroonbakery.MacaroonJSONDecoder)
+ data = json.dumps(m, cls=bakery.MacaroonJSONEncoder)
+ m1 = json.loads(data, cls=bakery.MacaroonJSONDecoder)
self.assertEqual(m1.macaroon.signature, m.macaroon.signature)
self.assertEqual(m1.macaroon.version, m.macaroon.version)
self.assertEqual(len(m1.macaroon.caveats), 1)
@@ -101,20 +100,20 @@ class TestMacaroon(TestCase):
self.assertEqual(m1._caveat_data, m._caveat_data)
def test_json_version1(self):
- self._test_json_with_version(macaroonbakery.BAKERY_V1)
+ self._test_json_with_version(bakery.VERSION_1)
def test_json_version2(self):
- self._test_json_with_version(macaroonbakery.BAKERY_V2)
+ self._test_json_with_version(bakery.VERSION_2)
def _test_json_with_version(self, version):
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
bs = common.new_bakery('bs-loc', locator)
ns = checkers.Namespace({
'testns': 'x',
})
- m = macaroonbakery.Macaroon(
+ m = bakery.Macaroon(
root_key=b'root key', id=b'id',
location='location', version=version,
namespace=ns)
@@ -124,18 +123,18 @@ class TestMacaroon(TestCase):
# Sanity check that no external caveat data has been added.
self.assertEqual(len(m._caveat_data), 0)
- data = json.dumps(m, cls=macaroonbakery.MacaroonJSONEncoder)
- m1 = json.loads(data, cls=macaroonbakery.MacaroonJSONDecoder)
+ data = json.dumps(m, cls=bakery.MacaroonJSONEncoder)
+ m1 = json.loads(data, cls=bakery.MacaroonJSONDecoder)
# Just check the signature and version - we're not interested in fully
# checking the macaroon marshaling here.
self.assertEqual(m1.macaroon.signature, m.macaroon.signature)
self.assertEqual(m1.macaroon.version,
- macaroonbakery.macaroon_version(version))
+ bakery.macaroon_version(version))
self.assertEqual(len(m1.macaroon.caveats), 1)
# Namespace information has been thrown away.
- self.assertEqual(m1.namespace, macaroonbakery.legacy_namespace())
+ self.assertEqual(m1.namespace, bakery.legacy_namespace())
self.assertEqual(len(m1._caveat_data), 0)
@@ -144,9 +143,9 @@ class TestMacaroon(TestCase):
with self.assertRaises(ValueError) as exc:
json.loads(json.dumps({
'm': m.serialize(serializer=serializers.JsonSerializer()),
- 'v': macaroonbakery.LATEST_BAKERY_VERSION + 1
- }), cls=macaroonbakery.MacaroonJSONDecoder)
- self.assertEqual('unknow bakery version 4', exc.exception.args[0])
+ 'v': bakery.LATEST_VERSION + 1
+ }), cls=bakery.MacaroonJSONDecoder)
+ self.assertEqual('unknown bakery version 4', exc.exception.args[0])
def test_json_inconsistent_version(self):
m = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V1)
@@ -154,21 +153,21 @@ class TestMacaroon(TestCase):
json.loads(json.dumps({
'm': json.loads(m.serialize(
serializer=serializers.JsonSerializer())),
- 'v': macaroonbakery.LATEST_BAKERY_VERSION
- }), cls=macaroonbakery.MacaroonJSONDecoder)
+ 'v': bakery.LATEST_VERSION
+ }), cls=bakery.MacaroonJSONDecoder)
self.assertEqual('underlying macaroon has inconsistent version; '
'got 1 want 2', exc.exception.args[0])
def test_clone(self):
- locator = macaroonbakery.ThirdPartyStore()
+ locator = bakery.ThirdPartyStore()
bs = common.new_bakery("bs-loc", locator)
ns = checkers.Namespace({
"testns": "x",
})
- m = macaroonbakery.Macaroon(
+ m = bakery.Macaroon(
root_key=b'root key', id=b'id',
location='location',
- version=macaroonbakery.LATEST_BAKERY_VERSION,
+ version=bakery.LATEST_VERSION,
namespace=ns)
m.add_caveat(checkers.Caveat(location='bs-loc', condition='something'),
bs.oven.key, locator)
@@ -185,15 +184,15 @@ class TestMacaroon(TestCase):
def test_json_deserialize_from_go(self):
ns = checkers.Namespace()
ns.register("someuri", "x")
- m = macaroonbakery.Macaroon(
+ m = bakery.Macaroon(
root_key=b'rootkey', id=b'some id', location='here',
- version=macaroonbakery.LATEST_BAKERY_VERSION, namespace=ns)
+ version=bakery.LATEST_VERSION, namespace=ns)
m.add_caveat(checkers.Caveat(condition='something',
namespace='someuri'))
data = '{"m":{"c":[{"i":"x:something"}],"l":"here","i":"some id",' \
'"s64":"c8edRIupArSrY-WZfa62pgZFD8VjDgqho9U2PlADe-E"},"v":3,' \
'"ns":"someuri:x"}'
- m_go = macaroonbakery.Macaroon.deserialize_json(data)
+ m_go = bakery.Macaroon.deserialize_json(data)
self.assertEqual(m.macaroon.signature_bytes,
m_go.macaroon.signature_bytes)
diff --git a/macaroonbakery/tests/test_namespace.py b/macaroonbakery/tests/test_namespace.py
index 2f04bb3..8a821e5 100644
--- a/macaroonbakery/tests/test_namespace.py
+++ b/macaroonbakery/tests/test_namespace.py
@@ -31,6 +31,8 @@ class TestNamespace(TestCase):
ns1 = checkers.deserialize_namespace(data)
self.assertEquals(ns1, ns)
+ # TODO(rogpeppe) add resolve tests
+
def test_register(self):
ns = checkers.Namespace(None)
ns.register('testns', 't')
diff --git a/macaroonbakery/tests/test_oven.py b/macaroonbakery/tests/test_oven.py
index 2976e94..ae235de 100644
--- a/macaroonbakery/tests/test_oven.py
+++ b/macaroonbakery/tests/test_oven.py
@@ -5,7 +5,7 @@ from unittest import TestCase
import copy
from datetime import datetime, timedelta
-import macaroonbakery
+import macaroonbakery as bakery
EPOCH = datetime(1900, 11, 17, 19, 00, 13, 0, None)
AGES = EPOCH + timedelta(days=10)
@@ -15,111 +15,110 @@ class TestOven(TestCase):
def test_canonical_ops(self):
canonical_ops_tests = (
('empty array', [], []),
- ('one element', [macaroonbakery.Op('a', 'a')],
- [macaroonbakery.Op('a', 'a')]),
+ ('one element', [bakery.Op('a', 'a')],
+ [bakery.Op('a', 'a')]),
('all in order',
- [macaroonbakery.Op('a', 'a'), macaroonbakery.Op('a', 'b'),
- macaroonbakery.Op('c', 'c')],
- [macaroonbakery.Op('a', 'a'), macaroonbakery.Op('a', 'b'),
- macaroonbakery.Op('c', 'c')]),
+ [bakery.Op('a', 'a'), bakery.Op('a', 'b'),
+ bakery.Op('c', 'c')],
+ [bakery.Op('a', 'a'), bakery.Op('a', 'b'),
+ bakery.Op('c', 'c')]),
('out of order',
- [macaroonbakery.Op('c', 'c'), macaroonbakery.Op('a', 'b'),
- macaroonbakery.Op('a', 'a')],
- [macaroonbakery.Op('a', 'a'), macaroonbakery.Op('a', 'b'),
- macaroonbakery.Op('c', 'c')]),
+ [bakery.Op('c', 'c'), bakery.Op('a', 'b'),
+ bakery.Op('a', 'a')],
+ [bakery.Op('a', 'a'), bakery.Op('a', 'b'),
+ bakery.Op('c', 'c')]),
('with duplicates',
- [macaroonbakery.Op('c', 'c'), macaroonbakery.Op('a', 'b'),
- macaroonbakery.Op('a', 'a'), macaroonbakery.Op('c', 'a'),
- macaroonbakery.Op('c', 'b'), macaroonbakery.Op('c', 'c'),
- macaroonbakery.Op('a', 'a')],
- [macaroonbakery.Op('a', 'a'), macaroonbakery.Op('a', 'b'),
- macaroonbakery.Op('c', 'a'), macaroonbakery.Op('c', 'b'),
- macaroonbakery.Op('c', 'c')]),
+ [bakery.Op('c', 'c'), bakery.Op('a', 'b'),
+ bakery.Op('a', 'a'), bakery.Op('c', 'a'),
+ bakery.Op('c', 'b'), bakery.Op('c', 'c'),
+ bakery.Op('a', 'a')],
+ [bakery.Op('a', 'a'), bakery.Op('a', 'b'),
+ bakery.Op('c', 'a'), bakery.Op('c', 'b'),
+ bakery.Op('c', 'c')]),
('make sure we\'ve got the fields right',
- [macaroonbakery.Op(entity='read', action='two'),
- macaroonbakery.Op(entity='read', action='one'),
- macaroonbakery.Op(entity='write', action='one')],
- [macaroonbakery.Op(entity='read', action='one'),
- macaroonbakery.Op(entity='read', action='two'),
- macaroonbakery.Op(entity='write', action='one')])
+ [bakery.Op(entity='read', action='two'),
+ bakery.Op(entity='read', action='one'),
+ bakery.Op(entity='write', action='one')],
+ [bakery.Op(entity='read', action='one'),
+ bakery.Op(entity='read', action='two'),
+ bakery.Op(entity='write', action='one')])
)
for about, ops, expected in canonical_ops_tests:
new_ops = copy.copy(ops)
- canonical_ops = macaroonbakery.canonical_ops(new_ops)
+ canonical_ops = bakery.canonical_ops(new_ops)
self.assertEquals(canonical_ops, expected)
# Verify that the original array isn't changed.
self.assertEquals(new_ops, ops)
def test_multiple_ops(self):
- test_oven = macaroonbakery.Oven(
- ops_store=macaroonbakery.MemoryOpsStore())
- ops = [macaroonbakery.Op('one', 'read'),
- macaroonbakery.Op('one', 'write'),
- macaroonbakery.Op('two', 'read')]
- m = test_oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION, AGES,
+ test_oven = bakery.Oven(
+ ops_store=bakery.MemoryOpsStore())
+ ops = [bakery.Op('one', 'read'),
+ bakery.Op('one', 'write'),
+ bakery.Op('two', 'read')]
+ m = test_oven.macaroon(bakery.LATEST_VERSION, AGES,
None, ops)
got_ops, conds = test_oven.macaroon_ops([m.macaroon])
self.assertEquals(len(conds), 1) # time-before caveat.
- self.assertEquals(macaroonbakery.canonical_ops(got_ops), ops)
+ self.assertEquals(bakery.canonical_ops(got_ops), ops)
def test_multiple_ops_in_id(self):
- test_oven = macaroonbakery.Oven()
- ops = [macaroonbakery.Op('one', 'read'),
- macaroonbakery.Op('one', 'write'),
- macaroonbakery.Op('two', 'read')]
- m = test_oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION, AGES,
+ test_oven = bakery.Oven()
+ ops = [bakery.Op('one', 'read'),
+ bakery.Op('one', 'write'),
+ bakery.Op('two', 'read')]
+ m = test_oven.macaroon(bakery.LATEST_VERSION, AGES,
None, ops)
got_ops, conds = test_oven.macaroon_ops([m.macaroon])
self.assertEquals(len(conds), 1) # time-before caveat.
- self.assertEquals(macaroonbakery.canonical_ops(got_ops), ops)
+ self.assertEquals(bakery.canonical_ops(got_ops), ops)
def test_multiple_ops_in_id_with_version1(self):
- test_oven = macaroonbakery.Oven()
- ops = [macaroonbakery.Op('one', 'read'),
- macaroonbakery.Op('one', 'write'),
- macaroonbakery.Op('two', 'read')]
- m = test_oven.macaroon(macaroonbakery.BAKERY_V1, AGES, None, ops)
+ test_oven = bakery.Oven()
+ ops = [bakery.Op('one', 'read'),
+ bakery.Op('one', 'write'),
+ bakery.Op('two', 'read')]
+ m = test_oven.macaroon(bakery.VERSION_1, AGES, None, ops)
got_ops, conds = test_oven.macaroon_ops([m.macaroon])
self.assertEquals(len(conds), 1) # time-before caveat.
- self.assertEquals(macaroonbakery.canonical_ops(got_ops), ops)
+ self.assertEquals(bakery.canonical_ops(got_ops), ops)
def test_huge_number_of_ops_gives_small_macaroon(self):
- test_oven = macaroonbakery.Oven(
- ops_store=macaroonbakery.MemoryOpsStore())
+ test_oven = bakery.Oven(
+ ops_store=bakery.MemoryOpsStore())
ops = []
for i in range(30000):
- ops.append(macaroonbakery.Op(entity='entity{}'.format(i),
- action='action{}'.format(i)))
+ ops.append(bakery.Op(entity='entity' + str(i), action='action' + str(i)))
- m = test_oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION, AGES,
+ m = test_oven.macaroon(bakery.LATEST_VERSION, AGES,
None, ops)
got_ops, conds = test_oven.macaroon_ops([m.macaroon])
self.assertEquals(len(conds), 1) # time-before caveat.
- self.assertEquals(macaroonbakery.canonical_ops(got_ops),
- macaroonbakery.canonical_ops(ops))
+ self.assertEquals(bakery.canonical_ops(got_ops),
+ bakery.canonical_ops(ops))
data = m.serialize_json()
self.assertLess(len(data), 300)
def test_ops_stored_only_once(self):
- st = macaroonbakery.MemoryOpsStore()
- test_oven = macaroonbakery.Oven(ops_store=st)
+ st = bakery.MemoryOpsStore()
+ test_oven = bakery.Oven(ops_store=st)
- ops = [macaroonbakery.Op('one', 'read'),
- macaroonbakery.Op('one', 'write'),
- macaroonbakery.Op('two', 'read')]
+ ops = [bakery.Op('one', 'read'),
+ bakery.Op('one', 'write'),
+ bakery.Op('two', 'read')]
- m = test_oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION, AGES,
+ m = test_oven.macaroon(bakery.LATEST_VERSION, AGES,
None, ops)
got_ops, conds = test_oven.macaroon_ops([m.macaroon])
- self.assertEquals(macaroonbakery.canonical_ops(got_ops),
- macaroonbakery.canonical_ops(ops))
+ self.assertEquals(bakery.canonical_ops(got_ops),
+ bakery.canonical_ops(ops))
# Make another macaroon containing the same ops in a different order.
- ops = [macaroonbakery.Op('one', 'write'),
- macaroonbakery.Op('one', 'read'),
- macaroonbakery.Op('one', 'read'),
- macaroonbakery.Op('two', 'read')]
- test_oven.macaroon(macaroonbakery.LATEST_BAKERY_VERSION, AGES, None,
+ ops = [bakery.Op('one', 'write'),
+ bakery.Op('one', 'read'),
+ bakery.Op('one', 'read'),
+ bakery.Op('two', 'read')]
+ test_oven.macaroon(bakery.LATEST_VERSION, AGES, None,
ops)
self.assertEquals(len(st._store), 1)
diff --git a/macaroonbakery/tests/test_store.py b/macaroonbakery/tests/test_store.py
index 7bcc4c2..5afa7be 100644
--- a/macaroonbakery/tests/test_store.py
+++ b/macaroonbakery/tests/test_store.py
@@ -2,12 +2,12 @@
# Licensed under the LGPLv3, see LICENCE file for details.
from unittest import TestCase
-import macaroonbakery
+import macaroonbakery as bakery
class TestOven(TestCase):
def test_mem_store(self):
- st = macaroonbakery.MemoryKeyStore()
+ st = bakery.MemoryKeyStore()
key, id = st.root_key()
self.assertEqual(len(key), 24)
diff --git a/macaroonbakery/tests/test_time.py b/macaroonbakery/tests/test_time.py
new file mode 100644
index 0000000..38826e1
--- /dev/null
+++ b/macaroonbakery/tests/test_time.py
@@ -0,0 +1,129 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+from datetime import timedelta
+from unittest import TestCase
+from collections import namedtuple
+
+import pyrfc3339
+import pymacaroons
+from pymacaroons import Macaroon
+
+import macaroonbakery.checkers as checkers
+
+t1 = pyrfc3339.parse('2017-10-26T16:19:47.441402074Z')
+t2 = t1 + timedelta(hours=1)
+t3 = t2 + timedelta(hours=1)
+
+
+def fpcaveat(s):
+ return pymacaroons.Caveat(caveat_id=s.encode('utf-8'))
+
+
+class TestExpireTime(TestCase):
+ def test_expire_time(self):
+ ExpireTest = namedtuple('ExpireTest', 'about caveats expectTime')
+ tests = [
+ ExpireTest(
+ about='no caveats',
+ caveats=[],
+ expectTime=None,
+ ),
+ ExpireTest(
+ about='single time-before caveat',
+ caveats=[
+ fpcaveat(checkers.time_before_caveat(t1).condition),
+ ],
+ expectTime=t1,
+ ),
+ ExpireTest(
+ about='multiple time-before caveat',
+ caveats=[
+ fpcaveat(checkers.time_before_caveat(t2).condition),
+ fpcaveat(checkers.time_before_caveat(t1).condition),
+ ],
+ expectTime=t1,
+ ),
+ ExpireTest(
+ about='mixed caveats',
+ caveats=[
+ fpcaveat(checkers.time_before_caveat(t1).condition),
+ fpcaveat('allow bar'),
+ fpcaveat(checkers.time_before_caveat(t2).condition),
+ fpcaveat('deny foo'),
+ ],
+ expectTime=t1,
+ ),
+ ExpireTest(
+ about='mixed caveats',
+ caveats=[
+ fpcaveat(checkers.COND_TIME_BEFORE + ' tomorrow'),
+ ],
+ expectTime=None,
+ ),
+ ]
+ for test in tests:
+ print('test ', test.about)
+ t = checkers.expiry_time(checkers.Namespace(), test.caveats)
+ self.assertEqual(t, test.expectTime)
+
+ def test_macaroons_expire_time(self):
+ ExpireTest = namedtuple('ExpireTest', 'about macaroons expectTime')
+ tests = [
+ ExpireTest(
+ about='no macaroons',
+ macaroons=[newMacaroon()],
+ expectTime=None,
+ ),
+ ExpireTest(
+ about='single macaroon without caveats',
+ macaroons=[newMacaroon()],
+ expectTime=None,
+ ),
+ ExpireTest(
+ about='multiple macaroon without caveats',
+ macaroons=[newMacaroon()],
+ expectTime=None,
+ ),
+ ExpireTest(
+ about='single macaroon with time-before caveat',
+ macaroons=[
+ newMacaroon([checkers.time_before_caveat(t1).condition]),
+ ],
+ expectTime=t1,
+ ),
+ ExpireTest(
+ about='single macaroon with multiple time-before caveats',
+ macaroons=[
+ newMacaroon([
+ checkers.time_before_caveat(t2).condition,
+ checkers.time_before_caveat(t1).condition,
+ ]),
+ ],
+ expectTime=t1,
+ ),
+ ExpireTest(
+ about='multiple macaroons with multiple time-before caveats',
+ macaroons=[
+ newMacaroon([
+ checkers.time_before_caveat(t3).condition,
+ checkers.time_before_caveat(t1).condition,
+ ]),
+ newMacaroon([
+ checkers.time_before_caveat(t3).condition,
+ checkers.time_before_caveat(t1).condition,
+ ]),
+ ],
+ expectTime=t1,
+ ),
+ ]
+ for test in tests:
+ print('test ', test.about)
+ t = checkers.macaroons_expiry_time(checkers.Namespace(), test.macaroons)
+ self.assertEqual(t, test.expectTime)
+
+
+def newMacaroon(conds=[]):
+ m = Macaroon(key='key', version=2)
+ for cond in conds:
+ m.add_first_party_caveat(cond)
+ return m
diff --git a/macaroonbakery/third_party.py b/macaroonbakery/third_party.py
index d43b8ad..91eacaf 100644
--- a/macaroonbakery/third_party.py
+++ b/macaroonbakery/third_party.py
@@ -16,38 +16,42 @@ def legacy_namespace():
class ThirdPartyCaveatInfo(namedtuple(
'ThirdPartyCaveatInfo',
'condition, first_party_public_key, third_party_key_pair, root_key, '
- 'caveat, version, namespace')):
+ 'caveat, version, id, namespace')):
'''ThirdPartyCaveatInfo holds the information decoded from
a third party caveat id.
- :param: condition holds the third party condition to be discharged.
+ @param condition holds the third party condition to be discharged.
This is the only field that most third party dischargers will
- need to consider.
+ need to consider. {str}
- :param: first_party_public_key holds the nacl public key of the party
- that created the third party caveat.
+ @param first_party_public_key holds the public key of the party
+ that created the third party caveat. {PublicKey}
- :param: third_party_key_pair holds the nacl private used to decrypt
- the caveat - the key pair of the discharging service.
+ @param third_party_key_pair holds the nacl private used to decrypt
+ the caveat - the key pair of the discharging service. {PrivateKey}
- :param: root_key bytes holds the secret root key encoded by the caveat.
+ @param root_key holds the secret root key encoded by the caveat. {bytes}
- :param: caveat holds the full encoded base64 string caveat id from
- which all the other fields are derived.
+ @param caveat holds the full caveat id from
+ which all the other fields are derived. {bytes}
- :param: version holds the version that was used to encode
- the caveat id.
+ @param version holds the version that was used to encode
+ the caveat id. {number}
- :param: namespace object that holds the namespace of the first party
+ @param id holds the id of the third party caveat (the id that the
+ discharge macaroon should be given). This will differ from Caveat
+ when the caveat information is encoded separately. {bytes}
+
+ @param namespace object that holds the namespace of the first party
that created the macaroon, as encoded by the party that added the
- third party caveat.
+ third party caveat. {checkers.Namespace}
'''
class ThirdPartyInfo(namedtuple('ThirdPartyInfo', 'version, public_key')):
''' ThirdPartyInfo holds information on a given third party
discharge service.
- version holds latest the bakery protocol version supported
- by the discharger.
- public_key holds the public nacl key of the third party.
+ @param version The latest bakery protocol version supported
+ by the discharger {number}
+ @param public_key Public key of the third party {PublicKey}
'''
diff --git a/macaroonbakery/utils.py b/macaroonbakery/utils.py
index 3b5550b..43b0bf2 100644
--- a/macaroonbakery/utils.py
+++ b/macaroonbakery/utils.py
@@ -3,23 +3,39 @@
import base64
import json
import webbrowser
+import six
+import six.moves.http_cookiejar as http_cookiejar
+from six.moves.urllib.parse import urlparse
from pymacaroons import Macaroon
from pymacaroons.serializers import json_serializer
-def deserialize(json_macaroon):
- '''Deserialize a JSON macaroon into a macaroon object from pymacaroons.
+def to_bytes(s):
+ '''Return s as a bytes type, using utf-8 encoding if necessary.
+ @param s string or bytes
+ @return bytes
+ '''
+ if isinstance(s, six.binary_type):
+ return s
+ if isinstance(s, six.string_types):
+ return s.encode('utf-8')
+ raise TypeError('want string or bytes, got {}', type(s))
+
+
+def macaroon_from_dict(json_macaroon):
+ '''Return a pymacaroons.Macaroon object from the given
+ JSON-deserialized dict.
- @param the JSON macaroon to deserialize as a dict.
+ @param JSON-encoded macaroon as dict
@return the deserialized macaroon object.
'''
return Macaroon.deserialize(json.dumps(json_macaroon),
json_serializer.JsonSerializer())
-def serialize_macaroon_string(macaroon):
- '''Serialize macaroon object to string.
+def macaroon_to_json_string(macaroon):
+ '''Serialize macaroon object to a JSON-encoded string.
@param macaroon object to be serialized.
@return a string serialization form of the macaroon.
@@ -27,7 +43,7 @@ def serialize_macaroon_string(macaroon):
return macaroon.serialize(json_serializer.JsonSerializer())
-def add_base64_padding(b):
+def _add_base64_padding(b):
'''Add padding to base64 encoded bytes.
pymacaroons does not give padded base64 bytes from serialization.
@@ -38,7 +54,7 @@ def add_base64_padding(b):
return b + b'=' * (-len(b) % 4)
-def remove_base64_padding(b):
+def _remove_base64_padding(b):
'''Remove padding from base64 encoded bytes.
pymacaroons does not give padded base64 bytes from serialization.
@@ -46,39 +62,36 @@ def remove_base64_padding(b):
@param bytes b to be padded.
@return a padded bytes.
'''
-
return b.rstrip(b'=')
-def raw_b64decode(s):
- '''Base64 decode with added padding with urlsafe or not.
+def b64decode(s):
+ '''Base64 decodes a base64-encoded string in URL-safe
+ or normal format, with or without padding.
+ The argument may be string or bytes.
- @param s string decode
+ @param s bytes decode
@return bytes decoded
'''
+ # add padding if necessary.
+ s = to_bytes(s)
+ s = s + b'=' * (-len(s) % 4)
if '_' or '-' in s:
- return raw_urlsafe_b64decode(s)
+ return base64.urlsafe_b64decode(s)
else:
- return base64.b64decode(add_base64_padding(s))
-
-
-def raw_urlsafe_b64decode(s):
- '''Base64 decode with added padding and convertion to bytes.
-
- @param s string decode
- @return bytes decoded
- '''
- return base64.urlsafe_b64decode(add_base64_padding(
- s.encode('ascii')))
+ return base64.b64decode(s)
def raw_urlsafe_b64encode(b):
- '''Base64 encode with padding removed.
+ '''Base64 encode using URL-safe encoding with padding removed.
- @param s string decode
+ @param b bytes to decode
@return bytes decoded
'''
- return remove_base64_padding(base64.urlsafe_b64encode(b))
+ b = to_bytes(b)
+ b = base64.urlsafe_b64encode(b)
+ b = b.rstrip(b'=') # strip padding
+ return b
def visit_page_with_browser(visit_url):
@@ -87,3 +100,44 @@ def visit_page_with_browser(visit_url):
@param visit_url: where to prove your identity.
'''
webbrowser.open(visit_url, new=1)
+ print('Opening an authorization web page in your browser.')
+ print('If it does not open, please open this URL:\n', visit_url, '\n')
+
+
+def cookie(
+ url,
+ name,
+ value,
+ expires=None):
+ '''Return a new Cookie using a slightly more
+ friendly API than that provided by six.moves.http_cookiejar
+ @param name The cookie name {str}
+ @param value The cookie value {str}
+ @param url The URL path of the cookie {str}
+ @param expires The expiry time of the cookie {datetime}
+ '''
+ u = urlparse(url)
+ domain = u.hostname or u.netloc
+ port = str(u.port) if u.port is not None else None
+ secure = u.scheme == 'https'
+ if expires is not None:
+ expires = expires.strftime("%s")
+ return http_cookiejar.Cookie(
+ version=0,
+ name=name,
+ value=value,
+ port=port,
+ port_specified=port is not None,
+ domain=domain,
+ domain_specified=True,
+ domain_initial_dot=False,
+ path=u.path,
+ path_specified=True,
+ secure=secure,
+ expires=expires,
+ discard=False,
+ comment=None,
+ comment_url=None,
+ rest=None,
+ rfc2109=False,
+ )
diff --git a/macaroonbakery/versions.py b/macaroonbakery/versions.py
index 5287c4c..7446d31 100644
--- a/macaroonbakery/versions.py
+++ b/macaroonbakery/versions.py
@@ -2,8 +2,8 @@
# Licensed under the LGPLv3, see LICENCE file for details.
-BAKERY_V0 = 0
-BAKERY_V1 = 1
-BAKERY_V2 = 2
-BAKERY_V3 = 3
-LATEST_BAKERY_VERSION = BAKERY_V3
+VERSION_0 = 0
+VERSION_1 = 1
+VERSION_2 = 2
+VERSION_3 = 3
+LATEST_VERSION = VERSION_3
diff --git a/setup.py b/setup.py
index 54000e9..340bf3a 100755
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ from setuptools import (
PROJECT_NAME = 'macaroonbakery'
-VERSION = (0, 0, 4)
+VERSION = (0, 0, 5)
def get_version():
diff --git a/tox.ini b/tox.ini
index 42d4d59..b55483b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,11 +2,14 @@
# Licensed under the LGPLv3, see LICENCE file for details.
[tox]
-envlist = py27, py35, style, docs
+envlist = lint, py27, py35, docs
+# envlist = py27
[testenv]
setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/macaroonbakery
+# drop into debugger with: nosetests --pdb
+# coverage with --with-coverage --cover-inclusive --cover-html
commands =
nosetests
deps =
@@ -19,7 +22,7 @@ commands =
[testenv:lint]
usedevelop = True
-commands = flake8 --show-source macaroonbakery --exclude macaroonbakery/internal/id_pb2.py
+commands = flake8 --ignore E501 --show-source macaroonbakery --exclude macaroonbakery/internal/id_pb2.py
[testenv:docs]
changedir = docs