summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorColin Watson <cjwatson@debian.org>2017-12-12 15:20:49 +0000
committerColin Watson <cjwatson@debian.org>2017-12-12 15:20:49 +0000
commit9e4403035a9953c99117083e6373ae3c441a76b5 (patch)
treed91b137df6767bfb8cb72de6b9fd21efb0c3dee4
parent949b7072cabce0daed6c94993ad44c8ea8648dbd (diff)
Import py-macaroon-bakery_1.1.0.orig.tar.gz
-rw-r--r--Makefile15
-rwxr-xr-xdocs/conf.py2
-rw-r--r--macaroonbakery/__init__.py137
-rw-r--r--macaroonbakery/_utils/__init__.py (renamed from macaroonbakery/utils.py)39
-rw-r--r--macaroonbakery/bakery/__init__.py141
-rw-r--r--macaroonbakery/bakery/_authorizer.py (renamed from macaroonbakery/authorizer.py)5
-rw-r--r--macaroonbakery/bakery/_bakery.py (renamed from macaroonbakery/bakery.py)8
-rw-r--r--macaroonbakery/bakery/_checker.py (renamed from macaroonbakery/checker.py)36
-rw-r--r--macaroonbakery/bakery/_codec.py (renamed from macaroonbakery/codec.py)66
-rw-r--r--macaroonbakery/bakery/_discharge.py (renamed from macaroonbakery/discharge.py)48
-rw-r--r--macaroonbakery/bakery/_error.py (renamed from macaroonbakery/error.py)0
-rw-r--r--macaroonbakery/bakery/_identity.py (renamed from macaroonbakery/identity.py)4
-rw-r--r--macaroonbakery/bakery/_internal/__init__.py (renamed from macaroonbakery/internal/__init__.py)0
-rw-r--r--macaroonbakery/bakery/_internal/id.proto (renamed from macaroonbakery/internal/id.proto)0
-rw-r--r--macaroonbakery/bakery/_internal/id_pb2.py (renamed from macaroonbakery/internal/id_pb2.py)0
-rw-r--r--macaroonbakery/bakery/_keys.py (renamed from macaroonbakery/keys.py)20
-rw-r--r--macaroonbakery/bakery/_macaroon.py (renamed from macaroonbakery/macaroon.py)70
-rw-r--r--macaroonbakery/bakery/_oven.py (renamed from macaroonbakery/oven.py)57
-rw-r--r--macaroonbakery/bakery/_store.py (renamed from macaroonbakery/store.py)0
-rw-r--r--macaroonbakery/bakery/_third_party.py (renamed from macaroonbakery/third_party.py)0
-rw-r--r--macaroonbakery/bakery/_versions.py (renamed from macaroonbakery/versions.py)0
-rw-r--r--macaroonbakery/checkers/__init__.py18
-rw-r--r--macaroonbakery/checkers/_auth_context.py (renamed from macaroonbakery/checkers/auth_context.py)0
-rw-r--r--macaroonbakery/checkers/_caveat.py (renamed from macaroonbakery/checkers/caveat.py)11
-rw-r--r--macaroonbakery/checkers/_checkers.py (renamed from macaroonbakery/checkers/checkers.py)23
-rw-r--r--macaroonbakery/checkers/_conditions.py (renamed from macaroonbakery/checkers/conditions.py)0
-rw-r--r--macaroonbakery/checkers/_declared.py (renamed from macaroonbakery/checkers/declared.py)12
-rw-r--r--macaroonbakery/checkers/_namespace.py (renamed from macaroonbakery/checkers/namespace.py)4
-rw-r--r--macaroonbakery/checkers/_operation.py (renamed from macaroonbakery/checkers/operation.py)2
-rw-r--r--macaroonbakery/checkers/_time.py (renamed from macaroonbakery/checkers/time.py)14
-rw-r--r--macaroonbakery/checkers/_utils.py (renamed from macaroonbakery/checkers/utils.py)0
-rw-r--r--macaroonbakery/httpbakery/__init__.py12
-rw-r--r--macaroonbakery/httpbakery/_browser.py (renamed from macaroonbakery/httpbakery/browser.py)17
-rw-r--r--macaroonbakery/httpbakery/_client.py (renamed from macaroonbakery/httpbakery/client.py)111
-rw-r--r--macaroonbakery/httpbakery/_discharge.py (renamed from macaroonbakery/httpbakery/discharge.py)7
-rw-r--r--macaroonbakery/httpbakery/_error.py (renamed from macaroonbakery/httpbakery/error.py)10
-rw-r--r--macaroonbakery/httpbakery/_interactor.py (renamed from macaroonbakery/httpbakery/interactor.py)9
-rw-r--r--macaroonbakery/httpbakery/_keyring.py (renamed from macaroonbakery/httpbakery/keyring.py)6
-rw-r--r--macaroonbakery/httpbakery/agent/__init__.py8
-rw-r--r--macaroonbakery/httpbakery/agent/_agent.py (renamed from macaroonbakery/httpbakery/agent/agent.py)115
-rw-r--r--macaroonbakery/tests/common.py5
-rw-r--r--macaroonbakery/tests/test_agent.py171
-rw-r--r--macaroonbakery/tests/test_authorizer.py2
-rw-r--r--macaroonbakery/tests/test_bakery.py87
-rw-r--r--macaroonbakery/tests/test_checker.py34
-rw-r--r--macaroonbakery/tests/test_checkers.py7
-rw-r--r--macaroonbakery/tests/test_client.py130
-rw-r--r--macaroonbakery/tests/test_codec.py7
-rw-r--r--macaroonbakery/tests/test_discharge.py5
-rw-r--r--macaroonbakery/tests/test_discharge_all.py5
-rw-r--r--macaroonbakery/tests/test_keyring.py16
-rw-r--r--macaroonbakery/tests/test_macaroon.py12
-rw-r--r--macaroonbakery/tests/test_oven.py8
-rw-r--r--macaroonbakery/tests/test_store.py2
-rw-r--r--macaroonbakery/tests/test_time.py19
-rw-r--r--macaroonbakery/tests/test_utils.py74
-rwxr-xr-xsetup.py2
-rw-r--r--tox.ini2
58 files changed, 955 insertions, 660 deletions
diff --git a/Makefile b/Makefile
index 75d0e27..8466ce9 100644
--- a/Makefile
+++ b/Makefile
@@ -33,8 +33,7 @@ endif
.PHONY: check
-check: setup
- @tox -e lint
+check: setup lint
@tox
.PHONY: clean
@@ -54,7 +53,7 @@ clean:
.PHONY: docs
docs: setup
- tox -e docs
+ @tox -e docs
.PHONY: help
help:
@@ -76,7 +75,7 @@ help:
.PHONY: lint
lint: setup
- @$(DEVENV)/bin/flake8 --show-source macaroonbakery --exclude macaroonbakery/internal/id_pb2.py
+ @tox -e lint
.PHONY: release
release: check
@@ -97,3 +96,11 @@ test: setup
@$(DEVENV)/bin/nosetests \
--verbosity 2 --with-coverage --cover-erase \
--cover-package macaroonbakery
+
+.PHONY: isort
+isort:
+ isort \
+ --trailing-comma \
+ --recursive \
+ --multi-line 3 \
+ `find macaroonbakery -name '*.py' | grep -v 'internal/id_pb2\.py'`
diff --git a/docs/conf.py b/docs/conf.py
index a64ec3a..ff5c82e 100755
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -63,7 +63,7 @@ copyright = u'2017, Juju UI Team'
# the built documents.
#
# The short X.Y version and the full version.
-version = release = '0.0.6'
+version = release = '1.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
diff --git a/macaroonbakery/__init__.py b/macaroonbakery/__init__.py
index 6397a19..e69de29 100644
--- a/macaroonbakery/__init__.py
+++ b/macaroonbakery/__init__.py
@@ -1,137 +0,0 @@
-# Copyright 2017 Canonical Ltd.
-# Licensed under the LGPLv3, see LICENCE file for details.
-
-from macaroonbakery.versions import (
- VERSION_0,
- VERSION_1,
- VERSION_2,
- VERSION_3,
- LATEST_VERSION,
-)
-from macaroonbakery.authorizer import (
- ACLAuthorizer,
- Authorizer,
- AuthorizerFunc,
- ClosedAuthorizer,
- EVERYONE,
-)
-from macaroonbakery.codec import (
- decode_caveat,
- encode_caveat,
- encode_uvarint,
-)
-from macaroonbakery.checker import (
- AuthChecker,
- AuthInfo,
- Checker,
- LOGIN_OP,
- Op,
-)
-from macaroonbakery.error import (
- AuthInitError,
- CaveatNotRecognizedError,
- DischargeRequiredError,
- IdentityError,
- PermissionDenied,
- ThirdPartyCaveatCheckFailed,
- ThirdPartyInfoNotFound,
- VerificationError,
-)
-from macaroonbakery.identity import (
- ACLIdentity,
- Identity,
- IdentityClient,
- NoIdentities,
- SimpleIdentity,
-)
-from macaroonbakery.keys import (
- generate_key,
- PrivateKey,
- PublicKey,
-)
-from macaroonbakery.store import (
- MemoryOpsStore,
- MemoryKeyStore,
-)
-from macaroonbakery.third_party import (
- ThirdPartyCaveatInfo,
- ThirdPartyInfo,
- legacy_namespace,
-)
-from macaroonbakery.macaroon import (
- Macaroon,
- MacaroonJSONDecoder,
- MacaroonJSONEncoder,
- ThirdPartyLocator,
- ThirdPartyStore,
- macaroon_version,
-)
-from macaroonbakery.discharge import (
- ThirdPartyCaveatChecker,
- discharge,
- discharge_all,
- local_third_party_caveat,
-)
-from macaroonbakery.oven import (
- Oven,
- canonical_ops,
-)
-from macaroonbakery.bakery import Bakery
-from macaroonbakery.utils import b64decode
-
-__all__ = [
- 'ACLAuthorizer',
- 'ACLIdentity',
- 'AuthChecker',
- 'AuthInfo',
- 'AuthInitError',
- 'Authorizer',
- 'AuthorizerFunc',
- 'VERSION_0',
- 'VERSION_1',
- 'VERSION_2',
- 'VERSION_3',
- 'Bakery',
- 'CaveatNotRecognizedError',
- 'Checker',
- 'ClosedAuthorizer',
- 'DischargeRequiredError',
- 'EVERYONE',
- 'Identity',
- 'IdentityClient',
- 'IdentityError',
- 'LATEST_VERSION',
- 'LOGIN_OP',
- 'Macaroon',
- 'MacaroonJSONDecoder',
- 'MacaroonJSONEncoder',
- 'MemoryKeyStore',
- 'MemoryOpsStore',
- 'NoIdentities',
- 'Op',
- 'Oven',
- 'PermissionDenied',
- 'PrivateKey',
- 'PublicKey',
- 'SimpleIdentity',
- 'ThirdPartyCaveatCheckFailed',
- 'ThirdPartyCaveatChecker',
- 'ThirdPartyCaveatInfo',
- 'ThirdPartyInfo',
- 'ThirdPartyInfoNotFound',
- 'ThirdPartyLocator',
- 'ThirdPartyStore',
- 'VERSION',
- 'VerificationError',
- 'b64decode',
- 'canonical_ops',
- 'decode_caveat',
- 'discharge',
- 'discharge_all',
- 'encode_caveat',
- 'encode_uvarint',
- 'generate_key',
- 'legacy_namespace',
- 'local_third_party_caveat',
- 'macaroon_version',
-]
diff --git a/macaroonbakery/utils.py b/macaroonbakery/_utils/__init__.py
index 43b0bf2..f2779e0 100644
--- a/macaroonbakery/utils.py
+++ b/macaroonbakery/_utils/__init__.py
@@ -1,15 +1,18 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
import base64
+import binascii
import json
import webbrowser
-import six
-import six.moves.http_cookiejar as http_cookiejar
-from six.moves.urllib.parse import urlparse
+from datetime import datetime
+import six
from pymacaroons import Macaroon
from pymacaroons.serializers import json_serializer
+import six.moves.http_cookiejar as http_cookiejar
+from six.moves.urllib.parse import urlparse
+
def to_bytes(s):
'''Return s as a bytes type, using utf-8 encoding if necessary.
@@ -34,6 +37,13 @@ def macaroon_from_dict(json_macaroon):
json_serializer.JsonSerializer())
+def macaroon_to_dict(macaroon):
+ '''Turn macaroon into JSON-serializable dict object
+ @param pymacaroons.Macaroon.
+ '''
+ return json.loads(macaroon.serialize(json_serializer.JsonSerializer()))
+
+
def macaroon_to_json_string(macaroon):
'''Serialize macaroon object to a JSON-encoded string.
@@ -72,14 +82,19 @@ def b64decode(s):
@param s bytes decode
@return bytes decoded
+ @raises ValueError on failure
'''
# add padding if necessary.
s = to_bytes(s)
- s = s + b'=' * (-len(s) % 4)
- if '_' or '-' in s:
- return base64.urlsafe_b64decode(s)
- else:
- return base64.b64decode(s)
+ if not s.endswith(b'='):
+ s = s + b'=' * (-len(s) % 4)
+ try:
+ if '_' or '-' in s:
+ return base64.urlsafe_b64decode(s)
+ else:
+ return base64.b64decode(s)
+ except (TypeError, binascii.Error) as e:
+ raise ValueError(str(e))
def raw_urlsafe_b64encode(b):
@@ -111,17 +126,21 @@ def cookie(
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}
+ @param expires The expiry time of the cookie {datetime}. If provided,
+ it must be a naive timestamp in UTC.
'''
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")
+ if expires.tzinfo is not None:
+ raise ValueError('Cookie expiration must be a naive datetime')
+ expires = (expires - datetime(1970, 1, 1)).total_seconds()
return http_cookiejar.Cookie(
version=0,
name=name,
diff --git a/macaroonbakery/bakery/__init__.py b/macaroonbakery/bakery/__init__.py
new file mode 100644
index 0000000..4b973e9
--- /dev/null
+++ b/macaroonbakery/bakery/__init__.py
@@ -0,0 +1,141 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+
+from ._versions import (
+ VERSION_0,
+ VERSION_1,
+ VERSION_2,
+ VERSION_3,
+ LATEST_VERSION,
+)
+from ._authorizer import (
+ ACLAuthorizer,
+ Authorizer,
+ AuthorizerFunc,
+ ClosedAuthorizer,
+ EVERYONE,
+)
+from ._codec import (
+ decode_caveat,
+ encode_caveat,
+ encode_uvarint,
+)
+from ._checker import (
+ AuthChecker,
+ AuthInfo,
+ Checker,
+ LOGIN_OP,
+ Op,
+)
+from ._error import (
+ AuthInitError,
+ CaveatNotRecognizedError,
+ DischargeRequiredError,
+ IdentityError,
+ PermissionDenied,
+ ThirdPartyCaveatCheckFailed,
+ ThirdPartyInfoNotFound,
+ VerificationError,
+)
+from ._identity import (
+ ACLIdentity,
+ Identity,
+ IdentityClient,
+ NoIdentities,
+ SimpleIdentity,
+)
+from ._keys import (
+ generate_key,
+ PrivateKey,
+ PublicKey,
+)
+from ._store import (
+ MemoryOpsStore,
+ MemoryKeyStore,
+)
+from ._third_party import (
+ ThirdPartyCaveatInfo,
+ ThirdPartyInfo,
+ legacy_namespace,
+)
+from ._macaroon import (
+ Macaroon,
+ MacaroonJSONDecoder,
+ MacaroonJSONEncoder,
+ ThirdPartyLocator,
+ ThirdPartyStore,
+ macaroon_version,
+)
+from ._discharge import (
+ ThirdPartyCaveatChecker,
+ discharge,
+ discharge_all,
+ local_third_party_caveat,
+)
+from ._oven import (
+ Oven,
+ canonical_ops,
+)
+from ._bakery import Bakery
+from macaroonbakery._utils import (
+ b64decode,
+ macaroon_to_dict,
+)
+
+__all__ = [
+ 'ACLAuthorizer',
+ 'ACLIdentity',
+ 'AuthChecker',
+ 'AuthInfo',
+ 'AuthInitError',
+ 'Authorizer',
+ 'AuthorizerFunc',
+ 'VERSION_0',
+ 'VERSION_1',
+ 'VERSION_2',
+ 'VERSION_3',
+ 'Bakery',
+ 'CaveatNotRecognizedError',
+ 'Checker',
+ 'ClosedAuthorizer',
+ 'DischargeRequiredError',
+ 'EVERYONE',
+ 'Identity',
+ 'IdentityClient',
+ 'IdentityError',
+ 'LATEST_VERSION',
+ 'LOGIN_OP',
+ 'Macaroon',
+ 'MacaroonJSONDecoder',
+ 'MacaroonJSONEncoder',
+ 'MemoryKeyStore',
+ 'MemoryOpsStore',
+ 'NoIdentities',
+ 'Op',
+ 'Oven',
+ 'PermissionDenied',
+ 'PrivateKey',
+ 'PublicKey',
+ 'SimpleIdentity',
+ 'ThirdPartyCaveatCheckFailed',
+ 'ThirdPartyCaveatChecker',
+ 'ThirdPartyCaveatInfo',
+ 'ThirdPartyInfo',
+ 'ThirdPartyInfoNotFound',
+ 'ThirdPartyLocator',
+ 'ThirdPartyStore',
+ 'VERSION',
+ 'VerificationError',
+ 'b64decode',
+ 'canonical_ops',
+ 'decode_caveat',
+ 'discharge',
+ 'discharge_all',
+ 'encode_caveat',
+ 'encode_uvarint',
+ 'generate_key',
+ 'legacy_namespace',
+ 'local_third_party_caveat',
+ 'macaroon_to_dict',
+ 'macaroon_version',
+]
diff --git a/macaroonbakery/authorizer.py b/macaroonbakery/bakery/_authorizer.py
index ae84104..f900430 100644
--- a/macaroonbakery/authorizer.py
+++ b/macaroonbakery/bakery/_authorizer.py
@@ -2,8 +2,7 @@
# Licensed under the LGPLv3, see LICENCE file for details.
import abc
-import macaroonbakery as bakery
-
+from ._identity import ACLIdentity
# EVERYONE is recognized by ACLAuthorizer as the name of a
# group that has everyone in it.
@@ -90,7 +89,7 @@ class ACLAuthorizer(Authorizer):
# Anyone is allowed to do nothing.
return [], []
allowed = [False] * len(ops)
- has_allow = isinstance(identity, bakery.ACLIdentity)
+ has_allow = isinstance(identity, 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/_bakery.py
index 5d9d56a..8fac9ce 100644
--- a/macaroonbakery/bakery.py
+++ b/macaroonbakery/bakery/_bakery.py
@@ -1,10 +1,10 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from macaroonbakery.checkers import checkers
-from macaroonbakery.oven import Oven
-from macaroonbakery.checker import Checker
-from macaroonbakery.authorizer import ClosedAuthorizer
+from ._authorizer import ClosedAuthorizer
+from ._checker import Checker
+import macaroonbakery.checkers as checkers
+from ._oven import Oven
class Bakery(object):
diff --git a/macaroonbakery/checker.py b/macaroonbakery/bakery/_checker.py
index 568fd7c..b796502 100644
--- a/macaroonbakery/checker.py
+++ b/macaroonbakery/bakery/_checker.py
@@ -3,11 +3,17 @@
from collections import namedtuple
from threading import Lock
-
-import pyrfc3339
-
-import macaroonbakery as bakery
+from ._authorizer import ClosedAuthorizer
+from ._identity import NoIdentities
+from ._error import (
+ AuthInitError,
+ VerificationError,
+ IdentityError,
+ DischargeRequiredError,
+ PermissionDenied,
+)
import macaroonbakery.checkers as checkers
+import pyrfc3339
class Op(namedtuple('Op', 'entity, action')):
@@ -38,7 +44,7 @@ class Checker(object):
See the Oven type (TODO) for one way of doing that.
'''
def __init__(self, checker=checkers.Checker(),
- authorizer=bakery.ClosedAuthorizer(),
+ authorizer=ClosedAuthorizer(),
identity_client=None,
macaroon_opstore=None):
'''
@@ -57,7 +63,7 @@ class Checker(object):
self._first_party_caveat_checker = checker
self._authorizer = authorizer
if identity_client is None:
- identity_client = bakery.NoIdentities()
+ identity_client = NoIdentities()
self._identity_client = identity_client
self._macaroon_opstore = macaroon_opstore
@@ -105,8 +111,8 @@ class AuthChecker(object):
if not self._executed:
self._init_once(ctx)
self._executed = True
- if self._init_errors is not None and len(self._init_errors) > 0:
- raise bakery.AuthInitError(self._init_errors[0])
+ if self._init_errors:
+ raise AuthInitError(self._init_errors[0])
def _init_once(self, ctx):
self._auth_indexes = {}
@@ -115,7 +121,7 @@ class AuthChecker(object):
try:
ops, conditions = self.parent._macaroon_opstore.macaroon_ops(
ms)
- except bakery.VerificationError:
+ except VerificationError:
raise
except Exception as exc:
self._init_errors.append(exc.args[0])
@@ -157,7 +163,7 @@ class AuthChecker(object):
try:
identity = self.parent._identity_client.declared_identity(
ctx, declared)
- except bakery.IdentityError as exc:
+ except IdentityError as exc:
self._init_errors.append(
'cannot decode declared identity: {}'.format(exc.args[0]))
continue
@@ -171,7 +177,7 @@ class AuthChecker(object):
try:
identity, cavs = self.parent.\
_identity_client.identity_from_context(ctx)
- except bakery.IdentityError:
+ except IdentityError:
self._init_errors.append('could not determine identity')
if cavs is None:
cavs = []
@@ -292,7 +298,7 @@ class AuthChecker(object):
# no caveats to be discharged.
return authed, used
if self._identity is None and len(self._identity_caveats) > 0:
- raise bakery.DischargeRequiredError(
+ raise DischargeRequiredError(
msg='authentication required',
ops=[LOGIN_OP],
cavs=self._identity_caveats)
@@ -303,8 +309,8 @@ class AuthChecker(object):
err = ''
if len(all_errors) > 0:
err = all_errors[0]
- raise bakery.PermissionDenied(err)
- raise bakery.DischargeRequiredError(
+ raise PermissionDenied(err)
+ raise DischargeRequiredError(
msg='some operations have extra caveats', ops=ops, cavs=caveats)
def allow_capability(self, ctx, ops):
@@ -391,7 +397,7 @@ class _CaveatSquasher(object):
if cond == checkers.COND_TIME_BEFORE:
try:
- et = pyrfc3339.parse(args)
+ et = pyrfc3339.parse(args, utc=True).replace(tzinfo=None)
except ValueError:
# Again, if it doesn't seem valid, leave it alone.
return True
diff --git a/macaroonbakery/codec.py b/macaroonbakery/bakery/_codec.py
index 2946da9..903e604 100644
--- a/macaroonbakery/codec.py
+++ b/macaroonbakery/bakery/_codec.py
@@ -3,11 +3,13 @@
import base64
import json
-import six
-import nacl.public
-
-import macaroonbakery as bakery
+from ._versions import (VERSION_1, VERSION_2, VERSION_3)
+from ._third_party import legacy_namespace, ThirdPartyCaveatInfo
+from ._keys import PublicKey
+from ._error import VerificationError
import macaroonbakery.checkers as checkers
+import nacl.public
+import six
_PUBLIC_KEY_PREFIX_LEN = 4
_KEY_LEN = 32
@@ -33,11 +35,11 @@ def encode_caveat(condition, root_key, third_party_info, key, ns):
@param ns not used yet
@return bytes
'''
- if third_party_info.version == bakery.VERSION_1:
+ if third_party_info.version == VERSION_1:
return _encode_caveat_v1(condition, root_key,
third_party_info.public_key, key)
- if (third_party_info.version == bakery.VERSION_2 or
- third_party_info.version == bakery.VERSION_3):
+ if (third_party_info.version == VERSION_2 or
+ third_party_info.version == VERSION_3):
return _encode_caveat_v2_v3(third_party_info.version, condition,
root_key, third_party_info.public_key,
key, ns)
@@ -67,8 +69,8 @@ def _encode_caveat_v1(condition, root_key, third_party_pub_key, key):
nonce = encrypted[0:nacl.public.Box.NONCE_SIZE]
encrypted = encrypted[nacl.public.Box.NONCE_SIZE:]
return base64.b64encode(six.b(json.dumps({
- 'ThirdPartyPublicKey': third_party_pub_key.encode().decode('ascii'),
- 'FirstPartyPublicKey': key.public_key.encode().decode('ascii'),
+ 'ThirdPartyPublicKey': str(third_party_pub_key),
+ 'FirstPartyPublicKey': str(key.public_key),
'Nonce': base64.b64encode(nonce).decode('ascii'),
'Id': base64.b64encode(encrypted).decode('ascii')
})))
@@ -99,12 +101,12 @@ def _encode_caveat_v2_v3(version, condition, root_key, third_party_pub_key,
condition [rest of encrypted part]
'''
ns_data = bytearray()
- if version >= bakery.VERSION_3:
+ if version >= VERSION_3:
ns_data = ns.serialize_text()
data = bytearray()
data.append(version)
- data.extend(third_party_pub_key.encode(raw=True)[:_PUBLIC_KEY_PREFIX_LEN])
- data.extend(key.public_key.encode(raw=True)[:])
+ data.extend(third_party_pub_key.serialize(raw=True)[:_PUBLIC_KEY_PREFIX_LEN])
+ data.extend(key.public_key.serialize(raw=True)[:])
secret = _encode_secret_part_v2_v3(version, condition, root_key, ns_data)
box = nacl.public.Box(key.key, third_party_pub_key.key)
encrypted = box.encrypt(secret)
@@ -131,7 +133,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 >= bakery.VERSION_3:
+ if version >= VERSION_3:
encode_uvarint(len(ns), data)
data.extend(ns)
data.extend(condition.encode('utf-8'))
@@ -146,7 +148,7 @@ def decode_caveat(key, caveat):
@return ThirdPartyCaveatInfo
'''
if len(caveat) == 0:
- raise bakery.VerificationError('empty third party caveat')
+ raise VerificationError('empty third party caveat')
first = caveat[:1]
if first == b'e':
@@ -154,17 +156,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 == bakery.VERSION_2 or
- first_as_int == bakery.VERSION_3):
+ if (first_as_int == VERSION_2 or
+ first_as_int == VERSION_3):
if (len(caveat) < _VERSION3_CAVEAT_MIN_LEN
- and first_as_int == bakery.VERSION_3):
+ and first_as_int == 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 bakery.VerificationError(
+ raise VerificationError(
'caveat id payload not provided for caveat id {}'.format(
caveat))
return _decode_caveat_v2_v3(first_as_int, key, caveat)
- raise bakery.VerificationError('unknown version for caveat')
+ raise VerificationError('unknown version for caveat')
def _decode_caveat_v1(key, caveat):
@@ -196,15 +198,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 bakery.ThirdPartyCaveatInfo(
+ return ThirdPartyCaveatInfo(
condition=record.get('Condition'),
- first_party_public_key=bakery.PublicKey(fp_key),
+ first_party_public_key=PublicKey(fp_key),
third_party_key_pair=key,
root_key=base64.b64decode(record.get('RootKey')),
caveat=caveat,
id=None,
- version=bakery.VERSION_1,
- namespace=bakery.legacy_namespace()
+ version=VERSION_1,
+ namespace=legacy_namespace()
)
@@ -213,14 +215,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 bakery.VerificationError('caveat id too short')
+ raise 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 bakery.VerificationError('public key mismatch')
+ if key.public_key.serialize(raw=True)[:_PUBLIC_KEY_PREFIX_LEN] != pk_prefix:
+ raise VerificationError('public key mismatch')
first_party_pub = caveat[:_KEY_LEN]
caveat = caveat[_KEY_LEN:]
@@ -230,9 +232,9 @@ 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 bakery.ThirdPartyCaveatInfo(
+ return ThirdPartyCaveatInfo(
condition=condition.decode('utf-8'),
- first_party_public_key=bakery.PublicKey(fp_public_key),
+ first_party_public_key=PublicKey(fp_public_key),
third_party_key_pair=key,
root_key=root_key,
caveat=original_caveat,
@@ -244,25 +246,25 @@ def _decode_caveat_v2_v3(version, key, caveat):
def _decode_secret_part_v2_v3(version, data):
if len(data) < 1:
- raise bakery.VerificationError('secret part too short')
+ raise VerificationError('secret part too short')
got_version = six.byte2int(data[:1])
data = data[1:]
if version != got_version:
- raise bakery.VerificationError(
+ raise 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 >= bakery.VERSION_3:
+ if version >= 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 = bakery.legacy_namespace()
+ ns = legacy_namespace()
return root_key, data, ns
diff --git a/macaroonbakery/discharge.py b/macaroonbakery/bakery/_discharge.py
index f54fc97..1831209 100644
--- a/macaroonbakery/discharge.py
+++ b/macaroonbakery/bakery/_discharge.py
@@ -3,7 +3,19 @@
import abc
from collections import namedtuple
-import macaroonbakery as bakery
+from ._error import (
+ ThirdPartyCaveatCheckFailed,
+ CaveatNotRecognizedError,
+ VerificationError,
+)
+from ._codec import decode_caveat
+from ._macaroon import (
+ Macaroon,
+ ThirdPartyLocator,
+)
+from ._versions import VERSION_2
+from ._third_party import ThirdPartyCaveatInfo
+
import macaroonbakery.checkers as checkers
emptyContext = checkers.AuthContext()
@@ -46,7 +58,11 @@ def discharge_all(m, get_discharge, local_key=None):
while len(need) > 0:
cav = need[0]
need = need[1:]
- if local_key is not None and cav.cav.location == 'local':
+ if cav.cav.location == 'local':
+ if local_key is None:
+ raise ThirdPartyCaveatCheckFailed(
+ 'found local third party caveat but no private key provided',
+ )
# TODO use a small caveat id.
dm = discharge(ctx=emptyContext,
key=local_key,
@@ -90,7 +106,7 @@ class ThirdPartyCaveatChecker(object):
class _LocalDischargeChecker(ThirdPartyCaveatChecker):
def check_third_party_caveat(self, ctx, info):
if info.condition != 'true':
- raise bakery.CaveatNotRecognizedError()
+ raise CaveatNotRecognizedError()
return []
@@ -125,8 +141,8 @@ 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 = bakery.decode_caveat(key, caveat)
- cav_info = bakery.ThirdPartyCaveatInfo(
+ cav_info = decode_caveat(key, caveat)
+ cav_info = 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,
@@ -142,7 +158,7 @@ def discharge(ctx, id, caveat, key, checker, locator):
try:
cond, arg = checkers.parse_caveat(cav_info.condition)
except ValueError as exc:
- raise bakery.VerificationError(exc.args[0])
+ raise VerificationError(exc.args[0])
if cond == checkers.COND_NEED_DECLARED:
cav_info = cav_info._replace(condition=arg.encode('utf-8'))
@@ -154,7 +170,7 @@ 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 = bakery.Macaroon(
+ m = Macaroon(
cav_info.root_key,
id,
'',
@@ -172,15 +188,15 @@ def _check_need_declared(ctx, cav_info, checker):
arg = cav_info.condition.decode('utf-8')
i = arg.find(' ')
if i <= 0:
- raise bakery.VerificationError(
+ raise 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 bakery.VerificationError('need-declared caveat with empty required attribute')
+ raise VerificationError('need-declared caveat with empty required attribute')
if len(need_declared) == 0:
- raise bakery.VerificationError('need-declared caveat with no required attributes')
+ raise 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 = {}
@@ -197,7 +213,7 @@ def _check_need_declared(ctx, cav_info, checker):
continue
parts = arg.split()
if len(parts) != 2:
- raise bakery.VerificationError('declared caveat has no value')
+ raise VerificationError('declared caveat has no value')
declared[parts[0]] = True
# Add empty declarations for everything mentioned in need-declared
# that was not actually declared.
@@ -207,7 +223,7 @@ def _check_need_declared(ctx, cav_info, checker):
return caveats
-class _EmptyLocator(bakery.ThirdPartyLocator):
+class _EmptyLocator(ThirdPartyLocator):
def third_party_info(self, loc):
return None
@@ -218,8 +234,8 @@ def local_third_party_caveat(key, version):
the given PublicKey.
This can be automatically discharged by discharge_all passing a local key.
'''
- encoded_key = key.encode().decode('utf-8')
- loc = 'local {}'.format(encoded_key)
- if version >= bakery.VERSION_2:
- loc = 'local {} {}'.format(version, encoded_key)
+ if version >= VERSION_2:
+ loc = 'local {} {}'.format(version, key)
+ else:
+ loc = 'local {}'.format(key)
return checkers.Caveat(location=loc, condition='')
diff --git a/macaroonbakery/error.py b/macaroonbakery/bakery/_error.py
index b403569..b403569 100644
--- a/macaroonbakery/error.py
+++ b/macaroonbakery/bakery/_error.py
diff --git a/macaroonbakery/identity.py b/macaroonbakery/bakery/_identity.py
index 1579bba..4389cd9 100644
--- a/macaroonbakery/identity.py
+++ b/macaroonbakery/bakery/_identity.py
@@ -2,7 +2,7 @@
# Licensed under the LGPLv3, see LICENCE file for details.
import abc
-import macaroonbakery as bakery
+from ._error import IdentityError
class Identity(object):
@@ -123,4 +123,4 @@ class NoIdentities(IdentityClient):
return None, None
def declared_identity(self, ctx, declared):
- raise bakery.IdentityError('no identity declared or possible')
+ raise IdentityError('no identity declared or possible')
diff --git a/macaroonbakery/internal/__init__.py b/macaroonbakery/bakery/_internal/__init__.py
index e69de29..e69de29 100644
--- a/macaroonbakery/internal/__init__.py
+++ b/macaroonbakery/bakery/_internal/__init__.py
diff --git a/macaroonbakery/internal/id.proto b/macaroonbakery/bakery/_internal/id.proto
index eb3d614..eb3d614 100644
--- a/macaroonbakery/internal/id.proto
+++ b/macaroonbakery/bakery/_internal/id.proto
diff --git a/macaroonbakery/internal/id_pb2.py b/macaroonbakery/bakery/_internal/id_pb2.py
index 0fd54c0..0fd54c0 100644
--- a/macaroonbakery/internal/id_pb2.py
+++ b/macaroonbakery/bakery/_internal/id_pb2.py
diff --git a/macaroonbakery/keys.py b/macaroonbakery/bakery/_keys.py
index 5cf61c5..1da5f05 100644
--- a/macaroonbakery/keys.py
+++ b/macaroonbakery/bakery/_keys.py
@@ -34,15 +34,19 @@ class PrivateKey(object):
nacl.public.PrivateKey(serialized,
encoder=nacl.encoding.Base64Encoder))
- def encode(self, raw=False):
- ''' Encode the key in a base64 format by default but when raw is True
- it will return a an hex encoded bytes.
+ def serialize(self, raw=False):
+ '''Encode the private part of the key in a base64 format by default,
+ but when raw is True it will return hex encoded bytes.
@return: bytes
'''
if raw:
return self._key.encode()
return self._key.encode(nacl.encoding.Base64Encoder)
+ def __str__(self):
+ '''Return the private part of the key key as a base64-encoded string'''
+ return self.serialize().decode('utf-8')
+
def __eq__(self, other):
return self.key == other.key
@@ -63,15 +67,19 @@ class PublicKey(object):
'''
return self._key
- def encode(self, raw=False):
- ''' Encode the key in a base64 format by default but when raw is True
- it will return a an hex encoded bytes.
+ def serialize(self, raw=False):
+ '''Encode the private part of the key in a base64 format by default,
+ but when raw is True it will return hex encoded bytes.
@return: bytes
'''
if raw:
return self._key.encode()
return self._key.encode(nacl.encoding.Base64Encoder)
+ def __str__(self):
+ '''Return the key as a base64-encoded string'''
+ return self.serialize().decode('utf-8')
+
@classmethod
def deserialize(cls, serialized):
''' Create a PublicKey from a base64 encoded bytes.
diff --git a/macaroonbakery/macaroon.py b/macaroonbakery/bakery/_macaroon.py
index b745282..63091f6 100644
--- a/macaroonbakery/macaroon.py
+++ b/macaroonbakery/bakery/_macaroon.py
@@ -6,13 +6,29 @@ import json
import logging
import os
+import macaroonbakery.checkers as checkers
import pymacaroons
+from macaroonbakery._utils import b64decode
from pymacaroons.serializers import json_serializer
-
-import macaroonbakery as bakery
-import macaroonbakery.checkers as checkers
-from macaroonbakery import utils
-
+from ._versions import (
+ LATEST_VERSION,
+ VERSION_0,
+ VERSION_1,
+ VERSION_2,
+ VERSION_3,
+)
+from ._error import (
+ ThirdPartyInfoNotFound,
+)
+from ._codec import (
+ encode_uvarint,
+ encode_caveat,
+)
+from ._keys import PublicKey
+from ._third_party import (
+ legacy_namespace,
+ ThirdPartyInfo,
+)
log = logging.getLogger(__name__)
@@ -24,7 +40,7 @@ class Macaroon(object):
'''
def __init__(self, root_key, id, location=None,
- version=bakery.LATEST_VERSION, namespace=None):
+ version=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 +52,11 @@ class Macaroon(object):
@param version the bakery version.
@param namespace is that of the service creating it
'''
- if version > bakery.LATEST_VERSION:
+ if version > LATEST_VERSION:
log.info('use last known version:{} instead of: {}'.format(
- bakery.LATEST_VERSION, version
+ LATEST_VERSION, version
))
- version = bakery.LATEST_VERSION
+ version = LATEST_VERSION
# m holds the underlying macaroon.
self._macaroon = pymacaroons.Macaroon(
location=location, key=root_key, identifier=id,
@@ -115,14 +131,14 @@ class Macaroon(object):
# Use the least supported version to encode the caveat.
if self._version < info.version:
- info = bakery.ThirdPartyInfo(
+ info = ThirdPartyInfo(
version=self._version,
public_key=info.public_key,
)
- caveat_info = bakery.encode_caveat(
+ caveat_info = encode_caveat(
cav.condition, root_key, info, key, self._namespace)
- if info.version < bakery.VERSION_3:
+ if info.version < 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.
@@ -157,7 +173,7 @@ class Macaroon(object):
'''Return a dict representation of the macaroon data in JSON format.
@return a dict
'''
- if self.version < bakery.VERSION_3:
+ if self.version < VERSION_3:
if len(self._caveat_data) > 0:
raise ValueError('cannot serialize pre-version3 macaroon with '
'external caveat data')
@@ -191,7 +207,7 @@ class Macaroon(object):
m = pymacaroons.Macaroon.deserialize(
json.dumps(json_dict), json_serializer.JsonSerializer())
macaroon = Macaroon(root_key=None, id=None,
- namespace=bakery.legacy_namespace(),
+ namespace=legacy_namespace(),
version=_bakery_version(m.version))
macaroon._macaroon = m
return macaroon
@@ -199,8 +215,8 @@ class Macaroon(object):
version = json_dict.get('v', None)
if version is None:
raise ValueError('no version specified')
- if (version < bakery.VERSION_3 or
- version > bakery.LATEST_VERSION):
+ if (version < VERSION_3 or
+ version > LATEST_VERSION):
raise ValueError('unknown bakery version {}'.format(version))
m = pymacaroons.Macaroon.deserialize(json.dumps(json_macaroon),
json_serializer.JsonSerializer())
@@ -212,8 +228,8 @@ class Macaroon(object):
cdata = json_dict.get('cdata', {})
caveat_data = {}
for id64 in cdata:
- id = utils.b64decode(id64)
- data = utils.b64decode(cdata[id64])
+ id = b64decode(id64)
+ data = b64decode(cdata[id64])
caveat_data[id] = data
macaroon = Macaroon(root_key=None, id=None,
namespace=namespace,
@@ -251,7 +267,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(bakery.VERSION_3)
+ id.append(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,
@@ -265,7 +281,7 @@ class Macaroon(object):
# end up with a duplicate third party caveat id and thus create
# a macaroon that cannot be discharged.
temp = id[:]
- bakery.encode_uvarint(i, temp)
+ encode_uvarint(i, temp)
found = False
for cav in caveats:
if (cav.verification_key_id is not None
@@ -310,7 +326,7 @@ def macaroon_version(bakery_version):
@param bakery_version the bakery version
@return macaroon_version the derived macaroon version
'''
- if bakery_version in [bakery.VERSION_0, bakery.VERSION_1]:
+ if bakery_version in [VERSION_0, VERSION_1]:
return pymacaroons.MACAROON_V1
return pymacaroons.MACAROON_V2
@@ -340,7 +356,7 @@ class ThirdPartyStore(ThirdPartyLocator):
def third_party_info(self, loc):
info = self._store.get(loc.rstrip('/'))
if info is None:
- raise bakery.ThirdPartyInfoNotFound(
+ raise ThirdPartyInfoNotFound(
'cannot retrieve the info for location {}'.format(loc))
return info
@@ -369,7 +385,7 @@ def _parse_local_location(loc):
'''
if not (loc.startswith('local ')):
return None
- v = bakery.VERSION_1
+ v = VERSION_1
fields = loc.split()
fields = fields[1:] # Skip 'local'
if len(fields) == 2:
@@ -379,8 +395,8 @@ def _parse_local_location(loc):
return None
fields = fields[1:]
if len(fields) == 1:
- key = bakery.PublicKey.deserialize(fields[0])
- return bakery.ThirdPartyInfo(public_key=key, version=v)
+ key = PublicKey.deserialize(fields[0])
+ return ThirdPartyInfo(public_key=key, version=v)
return None
@@ -394,11 +410,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 bakery.VERSION_1
+ return 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 bakery.VERSION_2
+ return VERSION_2
else:
raise ValueError('unknown macaroon version when deserializing legacy '
'bakery macaroon; got {}'.format(v))
diff --git a/macaroonbakery/oven.py b/macaroonbakery/bakery/_oven.py
index bf4bd27..414a164 100644
--- a/macaroonbakery/oven.py
+++ b/macaroonbakery/bakery/_oven.py
@@ -7,16 +7,31 @@ import itertools
import os
import google
-from pymacaroons import MACAROON_V2, Verifier
-from pymacaroons.exceptions import (
- MacaroonUnmetCaveatException, MacaroonInvalidSignatureException
+from ._checker import (Op, LOGIN_OP)
+from ._store import MemoryKeyStore
+from ._error import VerificationError
+from ._versions import (
+ VERSION_2,
+ VERSION_3,
+ LATEST_VERSION,
+)
+from ._macaroon import (
+ Macaroon,
+ macaroon_version,
)
-import six
-import macaroonbakery as bakery
import macaroonbakery.checkers as checkers
-from macaroonbakery import utils
-from macaroonbakery.internal import id_pb2
+import six
+from macaroonbakery._utils import (
+ raw_urlsafe_b64encode,
+ b64decode,
+)
+from ._internal import id_pb2
+from pymacaroons import MACAROON_V2, Verifier
+from pymacaroons.exceptions import (
+ MacaroonInvalidSignatureException,
+ MacaroonUnmetCaveatException,
+)
class Oven:
@@ -64,7 +79,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 = bakery.MemoryKeyStore()
+ my_store = MemoryKeyStore()
self.root_keystore_for_ops = lambda x: my_store
def macaroon(self, version, expiry, caveats, ops):
@@ -85,15 +100,15 @@ class Oven:
id = self._new_macaroon_id(storage_id, expiry, ops)
- id_bytes = six.int2byte(bakery.LATEST_VERSION) + \
+ id_bytes = six.int2byte(LATEST_VERSION) + \
id.SerializeToString()
- if bakery.macaroon_version(version) < MACAROON_V2:
+ if 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)
+ id_bytes = raw_urlsafe_b64encode(id_bytes)
- m = bakery.Macaroon(
+ m = Macaroon(
root_key,
id_bytes,
self.location,
@@ -155,7 +170,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 bakery.VerificationError(
+ raise VerificationError(
'macaroon key not found in storage')
v = Verifier()
conditions = []
@@ -170,7 +185,7 @@ class Oven:
v.verify(macaroons[0], root_key, macaroons[1:])
except (MacaroonUnmetCaveatException,
MacaroonInvalidSignatureException) as exc:
- raise bakery.VerificationError(
+ raise VerificationError(
'verification failed: {}'.format(exc.args[0]))
if (self.ops_store is not None
@@ -196,7 +211,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.b64decode(id.decode('utf-8'))
+ dec = b64decode(id.decode('utf-8'))
# Set the id only on success.
id = dec
base64_decoded = True
@@ -209,23 +224,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 == bakery.VERSION_2:
+ if first == VERSION_2:
# Skip the UUID at the start of the id.
storage_id = id[1 + 16:]
- if first == bakery.VERSION_3:
+ if first == VERSION_3:
try:
id1 = id_pb2.MacaroonId.FromString(id[1:])
except google.protobuf.message.DecodeError:
- raise bakery.VerificationError(
+ raise VerificationError(
'no operations found in macaroon')
if len(id1.ops) == 0 or len(id1.ops[0].actions) == 0:
- raise bakery.VerificationError(
+ raise VerificationError(
'no operations found in macaroon')
ops = []
for op in id1.ops:
for action in op.actions:
- ops.append(bakery.Op(op.entity, action))
+ ops.append(Op(op.entity, action))
return id1.storageId, ops
if not base64_decoded and _is_lower_case_hex_char(first):
@@ -234,7 +249,7 @@ def _decode_macaroon_id(id):
last = id.rfind(b'-')
if last >= 0:
storage_id = id[0:last]
- return storage_id, [bakery.LOGIN_OP]
+ return storage_id, [LOGIN_OP]
def _is_lower_case_hex_char(b):
diff --git a/macaroonbakery/store.py b/macaroonbakery/bakery/_store.py
index ae5f7a7..ae5f7a7 100644
--- a/macaroonbakery/store.py
+++ b/macaroonbakery/bakery/_store.py
diff --git a/macaroonbakery/third_party.py b/macaroonbakery/bakery/_third_party.py
index 91eacaf..91eacaf 100644
--- a/macaroonbakery/third_party.py
+++ b/macaroonbakery/bakery/_third_party.py
diff --git a/macaroonbakery/versions.py b/macaroonbakery/bakery/_versions.py
index 7446d31..7446d31 100644
--- a/macaroonbakery/versions.py
+++ b/macaroonbakery/bakery/_versions.py
diff --git a/macaroonbakery/checkers/__init__.py b/macaroonbakery/checkers/__init__.py
index 25c6b7d..b3ea466 100644
--- a/macaroonbakery/checkers/__init__.py
+++ b/macaroonbakery/checkers/__init__.py
@@ -1,6 +1,6 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from macaroonbakery.checkers.conditions import (
+from ._conditions import (
STD_NAMESPACE,
COND_DECLARED,
COND_TIME_BEFORE,
@@ -9,7 +9,7 @@ from macaroonbakery.checkers.conditions import (
COND_DENY,
COND_NEED_DECLARED,
)
-from macaroonbakery.checkers.caveat import (
+from ._caveat import (
allow_caveat,
deny_caveat,
declared_caveat,
@@ -17,35 +17,35 @@ from macaroonbakery.checkers.caveat import (
time_before_caveat,
Caveat,
)
-from macaroonbakery.checkers.declared import (
+from ._declared import (
context_with_declared,
infer_declared,
infer_declared_from_conditions,
need_declared_caveat,
)
-from macaroonbakery.checkers.operation import (
+from ._operation import (
context_with_operations,
)
-from macaroonbakery.checkers.namespace import (
+from ._namespace import (
Namespace,
deserialize_namespace
)
-from macaroonbakery.checkers.time import (
+from ._time import (
context_with_clock,
expiry_time,
macaroons_expiry_time,
)
-from macaroonbakery.checkers.checkers import (
+from ._checkers import (
Checker,
CheckerInfo,
RegisterError,
)
-from macaroonbakery.checkers.auth_context import (
+from ._auth_context import (
AuthContext,
ContextKey,
)
-from macaroonbakery.checkers.utils import (
+from ._utils import (
condition_with_prefix,
)
diff --git a/macaroonbakery/checkers/auth_context.py b/macaroonbakery/checkers/_auth_context.py
index dceb015..dceb015 100644
--- a/macaroonbakery/checkers/auth_context.py
+++ b/macaroonbakery/checkers/_auth_context.py
diff --git a/macaroonbakery/checkers/caveat.py b/macaroonbakery/checkers/_caveat.py
index a1e564e..5732f43 100644
--- a/macaroonbakery/checkers/caveat.py
+++ b/macaroonbakery/checkers/_caveat.py
@@ -3,10 +3,13 @@
import collections
import pyrfc3339
-
-from macaroonbakery.checkers.conditions import (
- STD_NAMESPACE, COND_TIME_BEFORE, COND_ERROR, COND_DENY, COND_ALLOW,
- COND_DECLARED
+from ._conditions import (
+ COND_ALLOW,
+ COND_DECLARED,
+ COND_DENY,
+ COND_ERROR,
+ COND_TIME_BEFORE,
+ STD_NAMESPACE,
)
diff --git a/macaroonbakery/checkers/checkers.py b/macaroonbakery/checkers/_checkers.py
index 776b50b..71cb56f 100644
--- a/macaroonbakery/checkers/checkers.py
+++ b/macaroonbakery/checkers/_checkers.py
@@ -6,17 +6,20 @@ from datetime import datetime
import pyrfc3339
import pytz
-
-from macaroonbakery.checkers.declared import DECLARED_KEY
-from macaroonbakery.checkers.time import TIME_KEY
-from macaroonbakery.checkers.operation import OP_KEY
-from macaroonbakery.checkers.namespace import Namespace
-from macaroonbakery.checkers.caveat import parse_caveat
-from macaroonbakery.checkers.conditions import (
- STD_NAMESPACE, COND_DECLARED, COND_ALLOW, COND_DENY, COND_ERROR,
- COND_TIME_BEFORE
+from ._caveat import parse_caveat
+from ._conditions import (
+ COND_ALLOW,
+ COND_DECLARED,
+ COND_DENY,
+ COND_ERROR,
+ COND_TIME_BEFORE,
+ STD_NAMESPACE,
)
-from macaroonbakery.checkers.utils import condition_with_prefix
+from ._declared import DECLARED_KEY
+from ._namespace import Namespace
+from ._operation import OP_KEY
+from ._time import TIME_KEY
+from ._utils import condition_with_prefix
class RegisterError(Exception):
diff --git a/macaroonbakery/checkers/conditions.py b/macaroonbakery/checkers/_conditions.py
index 74e863e..74e863e 100644
--- a/macaroonbakery/checkers/conditions.py
+++ b/macaroonbakery/checkers/_conditions.py
diff --git a/macaroonbakery/checkers/declared.py b/macaroonbakery/checkers/_declared.py
index 78a6181..ae4f95b 100644
--- a/macaroonbakery/checkers/declared.py
+++ b/macaroonbakery/checkers/_declared.py
@@ -1,11 +1,13 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from macaroonbakery.checkers.namespace import Namespace
-from macaroonbakery.checkers.caveat import parse_caveat, Caveat, error_caveat
-from macaroonbakery.checkers.conditions import (
- COND_DECLARED, COND_NEED_DECLARED, STD_NAMESPACE
+from ._auth_context import ContextKey
+from ._caveat import Caveat, error_caveat, parse_caveat
+from ._conditions import (
+ COND_DECLARED,
+ COND_NEED_DECLARED,
+ STD_NAMESPACE,
)
-from macaroonbakery.checkers.auth_context import ContextKey
+from ._namespace import Namespace
DECLARED_KEY = ContextKey('declared-key')
diff --git a/macaroonbakery/checkers/namespace.py b/macaroonbakery/checkers/_namespace.py
index 31e8801..6c3b1e3 100644
--- a/macaroonbakery/checkers/namespace.py
+++ b/macaroonbakery/checkers/_namespace.py
@@ -2,8 +2,8 @@
# Licensed under the LGPLv3, see LICENCE file for details.
import collections
-from macaroonbakery.checkers.utils import condition_with_prefix
-from macaroonbakery.checkers.caveat import error_caveat
+from ._caveat import error_caveat
+from ._utils import condition_with_prefix
class Namespace:
diff --git a/macaroonbakery/checkers/operation.py b/macaroonbakery/checkers/_operation.py
index a3b3805..56b267a 100644
--- a/macaroonbakery/checkers/operation.py
+++ b/macaroonbakery/checkers/_operation.py
@@ -1,6 +1,6 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from macaroonbakery.checkers.auth_context import ContextKey
+from ._auth_context import ContextKey
OP_KEY = ContextKey('op-key')
diff --git a/macaroonbakery/checkers/time.py b/macaroonbakery/checkers/_time.py
index 0b52131..2ae1d89 100644
--- a/macaroonbakery/checkers/time.py
+++ b/macaroonbakery/checkers/_time.py
@@ -2,12 +2,10 @@
# 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
-
+from ._auth_context import ContextKey
+from ._caveat import parse_caveat
+from ._conditions import COND_TIME_BEFORE, STD_NAMESPACE
+from ._utils import condition_with_prefix
TIME_KEY = ContextKey('time-key')
@@ -54,12 +52,14 @@ def expiry_time(ns, cavs):
prefix, COND_TIME_BEFORE)
t = None
for cav in cavs:
+ if not cav.first_party():
+ continue
cav = cav.caveat_id_bytes.decode('utf-8')
name, rest = parse_caveat(cav)
if name != time_before_cond:
continue
try:
- et = pyrfc3339.parse(rest)
+ et = pyrfc3339.parse(rest, utc=True).replace(tzinfo=None)
if t is None or et < t:
t = et
except ValueError:
diff --git a/macaroonbakery/checkers/utils.py b/macaroonbakery/checkers/_utils.py
index 925e8c7..925e8c7 100644
--- a/macaroonbakery/checkers/utils.py
+++ b/macaroonbakery/checkers/_utils.py
diff --git a/macaroonbakery/httpbakery/__init__.py b/macaroonbakery/httpbakery/__init__.py
index 3f183c5..07a805b 100644
--- a/macaroonbakery/httpbakery/__init__.py
+++ b/macaroonbakery/httpbakery/__init__.py
@@ -1,11 +1,11 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from macaroonbakery.httpbakery.client import (
+from ._client import (
BakeryException,
Client,
extract_macaroons,
)
-from macaroonbakery.httpbakery.error import (
+from ._error import (
BAKERY_PROTOCOL_HEADER,
DischargeError,
ERR_DISCHARGE_REQUIRED,
@@ -17,18 +17,18 @@ from macaroonbakery.httpbakery.error import (
discharge_required_response,
request_version,
)
-from macaroonbakery.httpbakery.keyring import ThirdPartyLocator
-from macaroonbakery.httpbakery.interactor import (
+from ._keyring import ThirdPartyLocator
+from ._interactor import (
DischargeToken,
Interactor,
LegacyInteractor,
WEB_BROWSER_INTERACTION_KIND,
)
-from macaroonbakery.httpbakery.browser import (
+from ._browser import (
WebBrowserInteractionInfo,
WebBrowserInteractor,
)
-from macaroonbakery.httpbakery.discharge import discharge
+from ._discharge import discharge
__all__ = [
'BAKERY_PROTOCOL_HEADER',
diff --git a/macaroonbakery/httpbakery/browser.py b/macaroonbakery/httpbakery/_browser.py
index e3ce538..a1ccbb0 100644
--- a/macaroonbakery/httpbakery/browser.py
+++ b/macaroonbakery/httpbakery/_browser.py
@@ -2,15 +2,18 @@
# 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
+import requests
+from ._error import InteractionError
+from ._interactor import (
+ WEB_BROWSER_INTERACTION_KIND,
+ DischargeToken,
+ Interactor,
+ LegacyInteractor,
)
-from macaroonbakery.httpbakery.error import InteractionError
+from macaroonbakery._utils import visit_page_with_browser
+
+from six.moves.urllib.parse import urljoin
class WebBrowserInteractor(Interactor, LegacyInteractor):
diff --git a/macaroonbakery/httpbakery/client.py b/macaroonbakery/httpbakery/_client.py
index b3036a1..d877140 100644
--- a/macaroonbakery/httpbakery/client.py
+++ b/macaroonbakery/httpbakery/_client.py
@@ -2,31 +2,35 @@
# Licensed under the LGPLv3, see LICENCE file for details.
import base64
import json
-import requests
-from six.moves.http_cookies import SimpleCookie
-from six.moves.urllib.parse import urljoin
+import logging
-import macaroonbakery as bakery
+import macaroonbakery.bakery 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,
+import macaroonbakery._utils as utils
+from ._browser import WebBrowserInteractor
+from ._error import (
+ BAKERY_PROTOCOL_HEADER,
ERR_DISCHARGE_REQUIRED,
ERR_INTERACTION_REQUIRED,
+ DischargeError,
Error,
InteractionError,
InteractionMethodNotFound,
)
-from macaroonbakery.httpbakery.error import BAKERY_PROTOCOL_HEADER
-from macaroonbakery.httpbakery.browser import WebBrowserInteractor
+from ._interactor import (
+ WEB_BROWSER_INTERACTION_KIND,
+ LegacyInteractor,
+)
+
+import requests
+from six.moves.http_cookies import SimpleCookie
+from six.moves.urllib.parse import urljoin
TIME_OUT = 30
MAX_DISCHARGE_RETRIES = 3
+log = logging.getLogger('httpbakery')
+
class BakeryException(requests.RequestException):
'''Raised when some errors happen using the httpbakery
@@ -58,7 +62,7 @@ class Client:
if cookies is None:
cookies = requests.cookies.RequestsCookieJar()
self._interaction_methods = interaction_methods
- self._key = key
+ self.key = key
self.cookies = cookies
def auth(self):
@@ -76,7 +80,10 @@ class Client:
requests.request(method, url, auth=client.auth())
'''
- kwargs.setdefault('auth', self.auth())
+ # TODO should we raise an exception if auth or cookies are explicitly
+ # mentioned in kwargs?
+ kwargs['auth'] = self.auth()
+ kwargs['cookies'] = self.cookies
return requests.request(method=method, url=url, **kwargs)
def handle_error(self, error, url):
@@ -84,18 +91,20 @@ class Client:
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.
+ 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')
+ raise BakeryException('unable to read info in discharge error '
+ 'response')
discharges = bakery.discharge_all(
error.info.macaroon,
self.acquire_discharge,
- self._key,
+ self.key,
)
- macaroons = '[' + ','.join(map(utils.macaroon_to_json_string, discharges)) + ']'
+ 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)
@@ -104,7 +113,6 @@ class Client:
else:
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'),
@@ -128,7 +136,8 @@ class Client:
raise DischargeError(cause.message)
if cause.info is None:
raise DischargeError(
- 'interaction-required response with no info: {}'.format(resp.json())
+ 'interaction-required response with no info: {}'.format(
+ resp.json())
)
loc = cav.location
if not loc.endswith('/'):
@@ -141,10 +150,10 @@ class Client:
# 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'))
+ return bakery.Macaroon.from_dict(resp.json().get('Macaroon'))
else:
- raise DischargeError()
+ raise DischargeError(
+ 'discharge failed with code {}'.format(resp.status_code))
def _acquire_discharge_with_token(self, cav, payload, token):
req = {}
@@ -167,14 +176,14 @@ class Client:
error response.
@return DischargeToken, bakery.Macaroon
'''
- if self._interaction_methods is None or len(self._interaction_methods) == 0:
+ 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:
@@ -184,7 +193,8 @@ class Client:
except InteractionMethodNotFound:
continue
if token is None:
- raise InteractionError('interaction method returned an empty token')
+ raise InteractionError('interaction method returned an empty '
+ 'token')
return token, None
raise InteractionError('no supported interaction method')
@@ -195,13 +205,13 @@ class Client:
method_urls = {
"interactive": visit_url
}
- if len(self._interaction_methods) > 1 or \
- self._interaction_methods[0].kind() != WEB_BROWSER_INTERACTION_KIND:
+ 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:
@@ -220,7 +230,10 @@ class Client:
interactor.legacy_interact(self, location, visit_url)
return _wait_for_macaroon(wait_url)
- raise InteractionError('no methods supported')
+ raise InteractionError('no methods supported; supported [{}]; provided [{}]'.format(
+ ' '.join([x.kind() for x in self._interaction_methods]),
+ ' '.join(method_urls.keys()),
+ ))
class _BakeryAuth:
@@ -290,11 +303,20 @@ def _prepare_discharge_hook(req, client):
return hook
-def extract_macaroons(headers):
+def extract_macaroons(headers_or_request):
''' 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
+ If the argument implements a get_header method, that will be used
+ instead of the get method to retrieve headers.
+ @param headers_or_request: dict of headers or a
+ urllib.request.Request-like object.
+ @return: A list of list of mpy macaroons
'''
+ def get_header(key, default=None):
+ try:
+ return headers_or_request.get_header(key, default)
+ except AttributeError:
+ return headers_or_request.get(key, default)
+
mss = []
def add_macaroon(data):
@@ -303,22 +325,22 @@ def extract_macaroons(headers):
ms = [utils.macaroon_from_dict(x) for x in data_as_objs]
mss.append(ms)
- cookieHeader = headers.get('Cookie')
- if cookieHeader is not None:
+ cookie_header = get_header('Cookie')
+ if cookie_header 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))
+ cs.load(str(cookie_header))
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(','):
+ # for HTTP1.1 compatibility anyway (see RFC 7230, section 3.2.2)
+ macaroon_header = get_header('Macaroons')
+ if macaroon_header is not None:
+ for h in macaroon_header.split(','):
add_macaroon(h)
return mss
@@ -345,7 +367,7 @@ def _wait_for_macaroon(wait_url):
}
resp = requests.get(url=wait_url, headers=headers)
if resp.status_code != 200:
- return InteractionError('cannot get {}'.format(wait_url))
+ raise InteractionError('cannot get {}'.format(wait_url))
return bakery.Macaroon.from_dict(resp.json().get('Macaroon'))
@@ -376,8 +398,7 @@ def _legacy_get_interaction_methods(u):
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])
+ 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
diff --git a/macaroonbakery/httpbakery/discharge.py b/macaroonbakery/httpbakery/_discharge.py
index ef3481a..f868d23 100644
--- a/macaroonbakery/httpbakery/discharge.py
+++ b/macaroonbakery/httpbakery/_discharge.py
@@ -1,7 +1,7 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-import macaroonbakery.utils as utils
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
+import macaroonbakery._utils as utils
def discharge(ctx, content, key, locator, checker):
@@ -11,7 +11,8 @@ def discharge(ctx, content, key, locator, checker):
@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}
+ @param checker {macaroonbakery.ThirdPartyCaveatChecker} Used to check third
+ party caveats.
@return The discharge macaroon {macaroonbakery.Macaroon}
'''
id = content.get('id')
diff --git a/macaroonbakery/httpbakery/error.py b/macaroonbakery/httpbakery/_error.py
index 422b346..ff75f13 100644
--- a/macaroonbakery/httpbakery/error.py
+++ b/macaroonbakery/httpbakery/_error.py
@@ -1,9 +1,9 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from collections import namedtuple
import json
+from collections import namedtuple
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
ERR_INTERACTION_REQUIRED = 'interaction required'
ERR_DISCHARGE_REQUIRED = 'macaroon discharge required'
@@ -19,7 +19,8 @@ class InteractionMethodNotFound(Exception):
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))
+ super(DischargeError, self).__init__(
+ 'third party refused discharge: {}'.format(msg))
class InteractionError(Exception):
@@ -27,7 +28,8 @@ class InteractionError(Exception):
interaction-required error
'''
def __init__(self, msg):
- super(InteractionError, self).__init__('cannot start interactive session: {}'.format(msg))
+ super(InteractionError, self).__init__(
+ 'cannot start interactive session: {}'.format(msg))
def discharge_required_response(macaroon, path, cookie_suffix_name,
diff --git a/macaroonbakery/httpbakery/interactor.py b/macaroonbakery/httpbakery/_interactor.py
index 0c15338..7fba4ef 100644
--- a/macaroonbakery/httpbakery/interactor.py
+++ b/macaroonbakery/httpbakery/_interactor.py
@@ -18,8 +18,7 @@ class Interactor(object):
the Error.interaction_methods type.
@return {str}
'''
- raise NotImplementedError('kind method must be defined in '
- 'subclass')
+ 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
@@ -37,8 +36,7 @@ class Interactor(object):
take place {Error}
@return {DischargeToken} The discharge token.
'''
- raise NotImplementedError('interact method must be defined in '
- 'subclass')
+ raise NotImplementedError('interact method must be defined in subclass')
class LegacyInteractor(object):
@@ -59,8 +57,7 @@ class LegacyInteractor(object):
@param visit_url The visit_url field from the error {str}
@return None
'''
- raise NotImplementedError('legacy_interact method must be defined in '
- 'subclass')
+ raise NotImplementedError('legacy_interact method must be defined in subclass')
class DischargeToken(namedtuple('DischargeToken', 'kind, value')):
diff --git a/macaroonbakery/httpbakery/keyring.py b/macaroonbakery/httpbakery/_keyring.py
index 01a4349..8d9ab43 100644
--- a/macaroonbakery/httpbakery/keyring.py
+++ b/macaroonbakery/httpbakery/_keyring.py
@@ -1,10 +1,10 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from six.moves.urllib.parse import urlparse
+import macaroonbakery.bakery as bakery
import requests
+from ._error import BAKERY_PROTOCOL_HEADER
-import macaroonbakery as bakery
-from macaroonbakery.httpbakery.error import BAKERY_PROTOCOL_HEADER
+from six.moves.urllib.parse import urlparse
class ThirdPartyLocator(bakery.ThirdPartyLocator):
diff --git a/macaroonbakery/httpbakery/agent/__init__.py b/macaroonbakery/httpbakery/agent/__init__.py
index db252de..c0a7523 100644
--- a/macaroonbakery/httpbakery/agent/__init__.py
+++ b/macaroonbakery/httpbakery/agent/__init__.py
@@ -1,8 +1,9 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from macaroonbakery.httpbakery.agent.agent import (
- load_agent_file,
+from ._agent import (
+ load_auth_info,
+ read_auth_info,
Agent,
AgentInteractor,
AgentFileFormatError,
@@ -13,5 +14,6 @@ __all__ = [
'AgentFileFormatError',
'AgentInteractor',
'AuthInfo',
- 'load_agent_file',
+ 'load_auth_info',
+ 'read_auth_info',
]
diff --git a/macaroonbakery/httpbakery/agent/agent.py b/macaroonbakery/httpbakery/agent/_agent.py
index ad56015..b717261 100644
--- a/macaroonbakery/httpbakery/agent/agent.py
+++ b/macaroonbakery/httpbakery/agent/_agent.py
@@ -1,20 +1,18 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-import base64
-from collections import namedtuple
+import copy
import json
+import logging
+from collections import namedtuple
-import nacl.public
-import nacl.encoding
-import nacl.exceptions
+import macaroonbakery.bakery as bakery
+import macaroonbakery.httpbakery as httpbakery
+import macaroonbakery._utils as utils
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
+log = logging.getLogger(__name__)
class AgentFileFormatError(Exception):
@@ -24,40 +22,41 @@ class AgentFileFormatError(Exception):
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.
+def load_auth_info(filename):
+ '''Loads agent authentication information from the specified file.
+ The returned information is suitable for passing as an argument
+ to the AgentInteractor constructor.
+ @param filename The name of the file to open (str)
+ @return AuthInfo The authentication information
+ @raises AgentFileFormatError when the file format is bad.
'''
-
with open(filename) as f:
- data = json.load(f)
+ return read_auth_info(f.read())
+
+
+def read_auth_info(agent_file_content):
+ '''Loads agent authentication information from the
+ specified content string, as read from an agents file.
+ The returned information is suitable for passing as an argument
+ to the AgentInteractor constructor.
+ @param agent_file_content The agent file content (str)
+ @return AuthInfo The authentication information
+ @raises AgentFileFormatError when the file format is bad.
+ '''
try:
- key = nacl.public.PrivateKey(
- data['key']['private'],
- nacl.encoding.Base64Encoder,
+ data = json.loads(agent_file_content)
+ return AuthInfo(
+ key=bakery.PrivateKey.deserialize(data['key']['private']),
+ agents=list(
+ Agent(url=a['url'], username=a['username'])
+ for a in data.get('agents', [])
+ ),
)
- 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, nacl.exceptions.TypeError) as e:
+ except (
+ KeyError,
+ ValueError,
+ TypeError,
+ ) as e:
raise AgentFileFormatError('invalid agent file', e)
@@ -110,9 +109,10 @@ class AgentInteractor(httpbakery.Interactor, httpbakery.LegacyInteractor):
if not location.endswith('/'):
location += '/'
login_url = urljoin(location, p.login_url)
+ # TODO use client to make the request.
resp = requests.get(login_url, json={
'Username': agent.username,
- 'PublicKey': self._auth_info.key.encode().decode('utf-8'),
+ 'PublicKey': str(self._auth_info.key),
})
if resp.status_code != 200:
raise httpbakery.InteractionError(
@@ -144,30 +144,31 @@ class AgentInteractor(httpbakery.Interactor, httpbakery.LegacyInteractor):
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(
+ # Shallow-copy the client so that we don't unexpectedly side-effect
+ # it by changing the key. Another possibility might be to
+ # set up agent authentication differently, in such a way that
+ # we're sure that client.key is the same as self._auth_info.key.
+ client = copy.copy(client)
+ client.key = self._auth_info.key
+ resp = client.request(
+ method='POST',
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())
+ json={
+ 'username': agent.username,
+ 'public_key': str(self._auth_info.key.public_key),
+ },
+ )
if resp.status_code != 200:
raise httpbakery.InteractionError(
- 'cannot acquire agent macaroon: {}'.format(resp.status_code))
- if not resp.json().get('agent-login', False):
+ 'cannot acquire agent macaroon from {}: {} (response body: {!r})'.format(visit_url, resp.status_code, resp.text))
+ 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 url(string) holds the URL of the discharger that knows about
+ the agent.
@param username holds the username agent (string).
'''
diff --git a/macaroonbakery/tests/common.py b/macaroonbakery/tests/common.py
index f238dfd..cfbfc52 100644
--- a/macaroonbakery/tests/common.py
+++ b/macaroonbakery/tests/common.py
@@ -2,10 +2,9 @@
# Licensed under the LGPLv3, see LICENCE file for details.
from datetime import datetime, timedelta
-import pytz
-
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
import macaroonbakery.checkers as checkers
+import pytz
class _StoppedClock(object):
diff --git a/macaroonbakery/tests/test_agent.py b/macaroonbakery/tests/test_agent.py
index 67f5b84..3b38337 100644
--- a/macaroonbakery/tests/test_agent.py
+++ b/macaroonbakery/tests/test_agent.py
@@ -1,27 +1,22 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-import base64
-from datetime import datetime, timedelta
import json
+import logging
import os
import tempfile
+from datetime import datetime, timedelta
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.bakery as bakery
import macaroonbakery.checkers as checkers
+import macaroonbakery.httpbakery as httpbakery
import macaroonbakery.httpbakery.agent as agent
+import requests.cookies
+
+from httmock import HTTMock, response, urlmatch
+from six.moves.urllib.parse import parse_qs
+
+log = logging.getLogger(__name__)
class TestAgents(TestCase):
@@ -44,73 +39,31 @@ class TestAgents(TestCase):
os.remove(self.bad_key_agent_filename)
os.remove(self.no_username_agent_filename)
- def test_load_agents(self):
- cookies, key = agent.load_agent_file(self.agent_filename)
- self.assertEqual(key.encode(nacl.encoding.Base64Encoder),
- b'CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=')
- self.assertEqual(
- key.public_key.encode(nacl.encoding.Base64Encoder),
- b'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
-
- value = cookies.get('agent-login', domain='1.example.com')
- jv = base64.b64decode(value)
- if six.PY3:
- jv = jv.decode('utf-8')
- data = json.loads(jv)
- self.assertEqual(data['username'], 'user-1')
- self.assertEqual(data['public_key'],
- 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
-
- value = cookies.get('agent-login', domain='2.example.com',
- path='/discharger')
- jv = base64.b64decode(value)
- if six.PY3:
- jv = jv.decode('utf-8')
- data = json.loads(jv)
- self.assertEqual(data['username'], 'user-2')
- self.assertEqual(data['public_key'],
- 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
-
- def test_load_agents_into_cookies(self):
- cookies = requests.cookies.RequestsCookieJar()
- 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.public_key.encode(nacl.encoding.Base64Encoder),
- b'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=',
- )
-
- value = cookies.get('agent-login', domain='1.example.com')
- jv = base64.b64decode(value)
- if six.PY3:
- jv = jv.decode('utf-8')
- data = json.loads(jv)
- self.assertEqual(data['username'], 'user-1')
- self.assertEqual(data['public_key'], 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
-
- value = cookies.get('agent-login', domain='2.example.com',
- path='/discharger')
- jv = base64.b64decode(value)
- if six.PY3:
- jv = jv.decode('utf-8')
- data = json.loads(jv)
- self.assertEqual(data['username'], 'user-2')
- self.assertEqual(data['public_key'], 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
-
- def test_load_agents_with_bad_key(self):
+ def test_load_auth_info(self):
+ auth_info = agent.load_auth_info(self.agent_filename)
+ self.assertEqual(str(auth_info.key), 'CqoSgj06Zcgb4/S6RT4DpTjLAfKoznEY3JsShSjKJEU=')
+ self.assertEqual(str(auth_info.key.public_key), 'YAhRSsth3a36mRYqQGQaLiS4QJax0p356nd+B8x7UQE=')
+ self.assertEqual(auth_info.agents, [
+ agent.Agent(url='https://1.example.com/', username='user-1'),
+ agent.Agent(url='https://2.example.com/discharger', username='user-2'),
+ agent.Agent(url='http://0.3.2.1', username='test-user'),
+ ])
+
+ def test_invalid_agent_json(self):
+ with self.assertRaises(agent.AgentFileFormatError):
+ agent.read_auth_info('}')
+
+ def test_invalid_read_auth_info_arg(self):
with self.assertRaises(agent.AgentFileFormatError):
- agent.load_agent_file(self.bad_key_agent_filename)
+ agent.read_auth_info(0)
- def test_load_agents_with_no_username(self):
+ def test_load_auth_info_with_bad_key(self):
with self.assertRaises(agent.AgentFileFormatError):
- agent.load_agent_file(self.no_username_agent_filename)
+ agent.load_auth_info(self.bad_key_agent_filename)
+
+ def test_load_auth_info_with_no_username(self):
+ with self.assertRaises(agent.AgentFileFormatError):
+ agent.load_auth_info(self.no_username_agent_filename)
def test_agent_login(self):
discharge_key = bakery.generate_key()
@@ -138,7 +91,8 @@ class TestAgents(TestCase):
content='done')
except bakery.PermissionDenied:
caveats = [
- checkers.Caveat(location='http://0.3.2.1', condition='is-ok')
+ checkers.Caveat(location='http://0.3.2.1',
+ condition='is-ok')
]
m = server_bakery.oven.macaroon(
version=bakery.LATEST_VERSION,
@@ -177,11 +131,11 @@ class TestAgents(TestCase):
return {
'status_code': 200,
'content': {
- 'Macaroon': m.serialize_json()
+ 'Macaroon': m.to_dict()
}
}
- key = bakery.generate_key()
+ auth_info = agent.load_auth_info(self.agent_filename)
@urlmatch(path='.*/login')
def login(url, request):
@@ -190,7 +144,7 @@ class TestAgents(TestCase):
version=bakery.LATEST_VERSION,
expiry=datetime.utcnow() + timedelta(days=1),
caveats=[bakery.local_third_party_caveat(
- key.public_key,
+ auth_info.key.public_key,
version=httpbakery.request_version(request.headers))],
ops=[bakery.Op(entity='agent', action='login')])
return {
@@ -204,17 +158,7 @@ class TestAgents(TestCase):
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'
- )
- ],
- ),
- ),
+ agent.AgentInteractor(auth_info),
])
resp = requests.get(
'http://0.1.2.3/here',
@@ -315,25 +259,26 @@ class TestAgents(TestCase):
key = bakery.generate_key()
- @urlmatch(path='.*/visit?$')
+ @urlmatch(path='.*/visit')
def visit(url, request):
if request.headers.get('Accept') == 'application/json':
return {
'status_code': 200,
'content': {
- 'agent': request.url
+ 'agent': '/agent-visit',
}
}
- 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'))
+ raise Exception('unexpected call to visit without Accept header')
+
+ @urlmatch(path='.*/agent-visit')
+ def agent_visit(url, request):
+ if request.method != "POST":
+ raise Exception('unexpected method')
+ log.info('agent_visit url {}'.format(url))
+ body = json.loads(request.body.decode('utf-8'))
+ if body['username'] != 'test-user':
+ raise Exception('unexpected username in body {!r}'.format(request.body))
+ public_key = bakery.PublicKey.deserialize(body['public_key'])
ms = httpbakery.extract_macaroons(request.headers)
if len(ms) == 0:
b = bakery.Bakery(key=discharge_key)
@@ -356,11 +301,11 @@ class TestAgents(TestCase):
return {
'status_code': 200,
'content': {
- 'agent-login': True
+ 'agent_login': True
}
}
- @urlmatch(path='.*/wait?$')
+ @urlmatch(path='.*/wait$')
def wait(url, request):
class EmptyChecker(bakery.ThirdPartyCaveatChecker):
def check_third_party_caveat(self, ctx, info):
@@ -385,12 +330,14 @@ class TestAgents(TestCase):
with HTTMock(server_get), \
HTTMock(discharge), \
HTTMock(visit), \
- HTTMock(wait):
+ HTTMock(wait), \
+ HTTMock(agent_visit):
client = httpbakery.Client(interaction_methods=[
agent.AgentInteractor(
agent.AuthInfo(
key=key,
- agents=[agent.Agent(username='test-user', url=u'http://0.3.2.1')],
+ agents=[agent.Agent(username='test-user',
+ url=u'http://0.3.2.1')],
),
),
])
@@ -414,11 +361,13 @@ agent_file = '''
}, {
"url": "https://2.example.com/discharger",
"username": "user-2"
+ }, {
+ "url": "http://0.3.2.1",
+ "username": "test-user"
}]
}
'''
-
bad_key_agent_file = '''
{
"key": {
diff --git a/macaroonbakery/tests/test_authorizer.py b/macaroonbakery/tests/test_authorizer.py
index f90d2b5..d5539b7 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 as bakery
+import macaroonbakery.bakery as bakery
import macaroonbakery.checkers as checkers
diff --git a/macaroonbakery/tests/test_bakery.py b/macaroonbakery/tests/test_bakery.py
index 5a13cff..a6c3e58 100644
--- a/macaroonbakery/tests/test_bakery.py
+++ b/macaroonbakery/tests/test_bakery.py
@@ -2,19 +2,11 @@
# Licensed under the LGPLv3, see LICENCE file for details.
from unittest import TestCase
+import macaroonbakery.httpbakery as httpbakery
import requests
+from mock import patch
-from mock import (
- patch,
-)
-
-from httmock import (
- HTTMock,
- urlmatch,
- response
-)
-
-import macaroonbakery.httpbakery as httpbakery
+from httmock import HTTMock, response, urlmatch
ID_PATH = 'http://example.com/someprotecteurl'
@@ -29,7 +21,7 @@ json_macaroon = {
}, {
u'cid': u'allow read-no-terms write'
}, {
- u'cid': u'time-before 2016-07-19T14:29:14.312669464Z'
+ u'cid': u'time-before 2158-07-19T14:29:14.312669464Z'
}],
u'location': u'charmstore',
u'signature': u'52d17cb11f5c84d58441bc0ffd7cc396'
@@ -41,7 +33,7 @@ discharge_token = [{
u'caveats': [{
u'cid': u'declared username someone'
}, {
- u'cid': u'time-before 2016-08-15T15:55:52.428319076Z'
+ u'cid': u'time-before 2158-08-15T15:55:52.428319076Z'
}, {
u'cid': u'origin '
}],
@@ -57,7 +49,7 @@ discharged_macaroon = {
}, {
u'cid': u'declared username someone'
}, {
- u'cid': u'time-before 2016-07-19T15:55:52.432439055Z'
+ u'cid': u'time-before 2158-07-19T15:55:52.432439055Z'
}],
u'location': u'',
u'signature': u'3513db5503ab17f9576760cd28'
@@ -167,6 +159,17 @@ def wait_after_401(url, request):
}
+@urlmatch(path='.*/wait')
+def wait_on_error(url, request):
+ return {
+ 'status_code': 500,
+ 'content': {
+ 'DischargeToken': discharge_token,
+ 'Macaroon': discharged_macaroon
+ }
+ }
+
+
class TestBakery(TestCase):
def assert_cookie_security(self, cookies, name, secure):
@@ -185,12 +188,14 @@ class TestBakery(TestCase):
auth=client.auth())
resp.raise_for_status()
assert 'macaroon-test' in client.cookies.keys()
- self.assert_cookie_security(client.cookies, 'macaroon-test', secure=False)
+ self.assert_cookie_security(client.cookies, 'macaroon-test',
+ secure=False)
@patch('webbrowser.open')
def test_407_then_401_on_discharge(self, mock_open):
client = httpbakery.Client()
- with HTTMock(first_407_then_200), HTTMock(discharge_401), HTTMock(wait_after_401):
+ with HTTMock(first_407_then_200), HTTMock(discharge_401), \
+ HTTMock(wait_after_401):
resp = requests.get(
ID_PATH,
cookies=client.cookies,
@@ -200,6 +205,53 @@ class TestBakery(TestCase):
mock_open.assert_called_once_with(u'http://example.com/visit', new=1)
assert 'macaroon-test' in client.cookies.keys()
+ @patch('webbrowser.open')
+ def test_407_then_error_on_wait(self, mock_open):
+ client = httpbakery.Client()
+ with HTTMock(first_407_then_200), HTTMock(discharge_401),\
+ HTTMock(wait_on_error):
+ with self.assertRaises(httpbakery.InteractionError) as exc:
+ requests.get(
+ ID_PATH,
+ cookies=client.cookies,
+ auth=client.auth(),
+ )
+ self.assertEqual(str(exc.exception),
+ 'cannot start interactive session: cannot get '
+ 'http://example.com/wait')
+ mock_open.assert_called_once_with(u'http://example.com/visit', new=1)
+
+ def test_407_then_no_interaction_methods(self):
+ client = httpbakery.Client(interaction_methods=[])
+ with HTTMock(first_407_then_200), HTTMock(discharge_401):
+ with self.assertRaises(httpbakery.InteractionError) as exc:
+ requests.get(
+ ID_PATH,
+ cookies=client.cookies,
+ auth=client.auth(),
+ )
+ self.assertEqual(str(exc.exception),
+ 'cannot start interactive session: interaction '
+ 'required but not possible')
+
+ def test_407_then_unknown_interaction_methods(self):
+ class UnknownInteractor(httpbakery.Interactor):
+ def kind(self):
+ return 'unknown'
+ client = httpbakery.Client(interaction_methods=[UnknownInteractor()])
+ with HTTMock(first_407_then_200), HTTMock(discharge_401):
+ with self.assertRaises(httpbakery.InteractionError) as exc:
+ requests.get(
+ ID_PATH,
+ cookies=client.cookies,
+ auth=client.auth(),
+ )
+ self.assertEqual(
+ str(exc.exception),
+ 'cannot start interactive session: no methods supported; '
+ 'supported [unknown]; provided [interactive]'
+ )
+
def test_cookie_with_port(self):
client = httpbakery.Client()
with HTTMock(first_407_then_200_with_port):
@@ -219,4 +271,5 @@ class TestBakery(TestCase):
auth=client.auth())
resp.raise_for_status()
assert 'macaroon-test' in client.cookies.keys()
- self.assert_cookie_security(client.cookies, 'macaroon-test', secure=True)
+ 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 643c756..6b61768 100644
--- a/macaroonbakery/tests/test_checker.py
+++ b/macaroonbakery/tests/test_checker.py
@@ -1,17 +1,16 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
import base64
-from collections import namedtuple
import json
-from unittest import TestCase
+from collections import namedtuple
from datetime import timedelta
+from unittest import TestCase
-from pymacaroons.verifier import Verifier, FirstPartyCaveatVerifierDelegate
-import pymacaroons
-
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
import macaroonbakery.checkers as checkers
-from macaroonbakery.tests.common import test_context, epoch, test_checker
+import pymacaroons
+from macaroonbakery.tests.common import epoch, test_checker, test_context
+from pymacaroons.verifier import FirstPartyCaveatVerifierDelegate, Verifier
class TestChecker(TestCase):
@@ -53,7 +52,8 @@ class TestChecker(TestCase):
client = _Client(locator)
ctx = test_context.with_value(_DISCHARGE_USER_KEY, 'bob')
- auth_info = client.do(ctx, ts, [bakery.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)
@@ -98,7 +98,8 @@ class TestChecker(TestCase):
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()
@@ -168,8 +169,10 @@ class TestChecker(TestCase):
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()
@@ -560,8 +563,10 @@ class TestChecker(TestCase):
# 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, [bakery.LOGIN_OP])
self.assertEqual(auth_info.identity.id(), 'alice')
@@ -890,7 +895,8 @@ class _BasicAuthIdService(bakery.IdentityClient):
return bakery.SimpleIdentity(user), None
def declared_identity(self, ctx, declared):
- raise bakery.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_checkers.py b/macaroonbakery/tests/test_checkers.py
index f552fa4..28da06e 100644
--- a/macaroonbakery/tests/test_checkers.py
+++ b/macaroonbakery/tests/test_checkers.py
@@ -3,11 +3,10 @@
from datetime import datetime, timedelta
from unittest import TestCase
-import six
-import pytz
-from pymacaroons import Macaroon, MACAROON_V2
-
import macaroonbakery.checkers as checkers
+import pytz
+import six
+from pymacaroons import MACAROON_V2, Macaroon
# A frozen time for the tests.
NOW = datetime(
diff --git a/macaroonbakery/tests/test_client.py b/macaroonbakery/tests/test_client.py
index e1a4009..ab20c3b 100644
--- a/macaroonbakery/tests/test_client.py
+++ b/macaroonbakery/tests/test_client.py
@@ -3,23 +3,24 @@
import base64
import datetime
import json
-from unittest import TestCase
-try:
- from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
-except ImportError:
- from http.server import HTTPServer, BaseHTTPRequestHandler
import threading
+from unittest import TestCase
-from httmock import (
- HTTMock,
- urlmatch
-)
+import macaroonbakery.bakery as bakery
+import macaroonbakery.checkers as checkers
+import macaroonbakery.httpbakery as httpbakery
+import pymacaroons
import requests
+import macaroonbakery._utils as utils
+
+from httmock import HTTMock, urlmatch
from six.moves.urllib.parse import parse_qs
+from six.moves.urllib.request import Request
-import macaroonbakery as bakery
-import macaroonbakery.httpbakery as httpbakery
-import macaroonbakery.checkers as checkers
+try:
+ from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+except ImportError:
+ from http.server import HTTPServer, BaseHTTPRequestHandler
AGES = datetime.datetime.utcnow() + datetime.timedelta(days=1)
TEST_OP = bakery.Op(entity='test', action='test')
@@ -30,7 +31,7 @@ class TestClient(TestCase):
b = new_bakery('loc', None, None)
def handler(*args):
- GetHandler(b, None, None, None, None, *args)
+ GetHandler(b, None, None, None, None, AGES, *args)
try:
httpd = HTTPServer(('', 0), handler)
thread = threading.Thread(target=httpd.serve_forever)
@@ -58,7 +59,7 @@ class TestClient(TestCase):
b = new_bakery('loc', None, None)
def handler(*args):
- GetHandler(b, None, None, None, None, *args)
+ GetHandler(b, None, None, None, None, AGES, *args)
try:
httpd = HTTPServer(('', 0), handler)
thread = threading.Thread(target=httpd.serve_forever)
@@ -81,7 +82,7 @@ class TestClient(TestCase):
finally:
httpd.shutdown()
- def test_repeated_request_with_body(self):
+ def test_expiry_cookie_is_set(self):
class _DischargerLocator(bakery.ThirdPartyLocator):
def __init__(self):
self.key = bakery.generate_key()
@@ -100,7 +101,8 @@ class TestClient(TestCase):
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)
+ m = httpbakery.discharge(checkers.AuthContext(), content, d.key, d,
+ alwaysOK3rd)
return {
'status_code': 200,
'content': {
@@ -108,8 +110,10 @@ class TestClient(TestCase):
}
}
+ ages = datetime.datetime.utcnow() + datetime.timedelta(days=1)
+
def handler(*args):
- GetHandler(b, 'http://1.2.3.4', None, None, None, *args)
+ GetHandler(b, 'http://1.2.3.4', None, None, None, ages, *args)
try:
httpd = HTTPServer(('', 0), handler)
thread = threading.Thread(target=httpd.serve_forever)
@@ -122,10 +126,64 @@ class TestClient(TestCase):
cookies=client.cookies,
auth=client.auth())
resp.raise_for_status()
+ m = bakery.Macaroon.from_dict(json.loads(
+ base64.b64decode(client.cookies.get('macaroon-test')).decode('utf-8'))[0])
+ t = checkers.macaroons_expiry_time(
+ checkers.Namespace(), [m.macaroon])
+ self.assertEquals(ages, t)
self.assertEquals(resp.text, 'done')
finally:
httpd.shutdown()
+ def test_expiry_cookie_set_in_past(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()
+ }
+ }
+
+ ages = datetime.datetime.utcnow() - datetime.timedelta(days=1)
+
+ def handler(*args):
+ GetHandler(b, 'http://1.2.3.4', None, None, None, ages, *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_too_many_discharge(self):
class _DischargerLocator(bakery.ThirdPartyLocator):
def __init__(self):
@@ -155,7 +213,7 @@ class TestClient(TestCase):
}
def handler(*args):
- GetHandler(b, 'http://1.2.3.4', None, None, None, *args)
+ GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args)
try:
httpd = HTTPServer(('', 0), handler)
thread = threading.Thread(target=httpd.serve_forever)
@@ -199,7 +257,7 @@ class TestClient(TestCase):
ThirdPartyCaveatCheckerF(check))
def handler(*args):
- GetHandler(b, 'http://1.2.3.4', None, None, None, *args)
+ GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args)
try:
httpd = HTTPServer(('', 0), handler)
thread = threading.Thread(target=httpd.serve_forever)
@@ -244,7 +302,7 @@ class TestClient(TestCase):
}
def handler(*args):
- GetHandler(b, 'http://1.2.3.4', None, None, None, *args)
+ GetHandler(b, 'http://1.2.3.4', None, None, None, AGES, *args)
try:
httpd = HTTPServer(('', 0), handler)
@@ -273,11 +331,34 @@ class TestClient(TestCase):
finally:
httpd.shutdown()
+ def test_extract_macaroons_from_request(self):
+ def encode_macaroon(m):
+ macaroons = '[' + utils.macaroon_to_json_string(m) + ']'
+ return base64.urlsafe_b64encode(utils.to_bytes(macaroons)).decode('ascii')
+
+ req = Request('http://example.com')
+ m1 = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V2, identifier='one')
+ req.add_header('Macaroons', encode_macaroon(m1))
+ m2 = pymacaroons.Macaroon(version=pymacaroons.MACAROON_V2, identifier='two')
+ jar = requests.cookies.RequestsCookieJar()
+ jar.set_cookie(utils.cookie(
+ name='macaroon-auth',
+ value=encode_macaroon(m2),
+ url='http://example.com',
+ ))
+ jar.add_cookie_header(req)
+
+ macaroons = httpbakery.extract_macaroons(req)
+ self.assertEquals(len(macaroons), 2)
+ macaroons.sort(key=lambda ms: ms[0].identifier)
+ self.assertEquals(macaroons[0][0].identifier, m1.identifier)
+ self.assertEquals(macaroons[1][0].identifier, m2.identifier)
+
class GetHandler(BaseHTTPRequestHandler):
'''A mock HTTP server that serves a GET request'''
def __init__(self, bakery, auth_location, mutate_error,
- caveats, version, *args):
+ caveats, version, expiry, *args):
'''
@param bakery used to check incoming requests and macaroons
for discharge-required errors.
@@ -288,14 +369,17 @@ class GetHandler(BaseHTTPRequestHandler):
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
+ @param version holds the version of the bakery that the
server will purport to serve.
+ @param expiry holds the expiry for the macaroon that will be created
+ in _write_discharge_error
'''
self._bakery = bakery
self._auth_location = auth_location
self._mutate_error = mutate_error
self._caveats = caveats
self._server_version = version
+ self._expiry = expiry
BaseHTTPRequestHandler.__init__(self, *args)
def do_GET(self):
@@ -333,7 +417,7 @@ class GetHandler(BaseHTTPRequestHandler):
caveats.extend(self._caveats)
m = self._bakery.oven.macaroon(
- version=bakery.LATEST_VERSION, expiry=AGES,
+ version=bakery.LATEST_VERSION, expiry=self._expiry,
caveats=caveats, ops=[TEST_OP])
content, headers = httpbakery.discharge_required_response(
diff --git a/macaroonbakery/tests/test_codec.py b/macaroonbakery/tests/test_codec.py
index d4fbc57..d82a794 100644
--- a/macaroonbakery/tests/test_codec.py
+++ b/macaroonbakery/tests/test_codec.py
@@ -3,12 +3,11 @@
import base64
from unittest import TestCase
+import macaroonbakery.bakery as bakery
+import macaroonbakery.checkers as checkers
import nacl.public
import six
-
-import macaroonbakery as bakery
-from macaroonbakery import codec
-import macaroonbakery.checkers as checkers
+from macaroonbakery.bakery import _codec as codec
class TestCodec(TestCase):
diff --git a/macaroonbakery/tests/test_discharge.py b/macaroonbakery/tests/test_discharge.py
index 433483a..27bae63 100644
--- a/macaroonbakery/tests/test_discharge.py
+++ b/macaroonbakery/tests/test_discharge.py
@@ -2,11 +2,10 @@
# Licensed under the LGPLv3, see LICENCE file for details.
import unittest
-from pymacaroons import MACAROON_V1, Macaroon
-
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
import macaroonbakery.checkers as checkers
from macaroonbakery.tests import common
+from pymacaroons import MACAROON_V1, Macaroon
class TestDischarge(unittest.TestCase):
diff --git a/macaroonbakery/tests/test_discharge_all.py b/macaroonbakery/tests/test_discharge_all.py
index 7999f5f..cab8a07 100644
--- a/macaroonbakery/tests/test_discharge_all.py
+++ b/macaroonbakery/tests/test_discharge_all.py
@@ -2,11 +2,10 @@
# Licensed under the LGPLv3, see LICENCE file for details.
import unittest
-from pymacaroons.verifier import Verifier
-
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
import macaroonbakery.checkers as checkers
from macaroonbakery.tests import common
+from pymacaroons.verifier import Verifier
def always_ok(predicate):
diff --git a/macaroonbakery/tests/test_keyring.py b/macaroonbakery/tests/test_keyring.py
index 438ab1b..3503145 100644
--- a/macaroonbakery/tests/test_keyring.py
+++ b/macaroonbakery/tests/test_keyring.py
@@ -2,11 +2,11 @@
# Licensed under the LGPLv3, see LICENCE file for details.
import unittest
-from httmock import urlmatch, HTTMock
-
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
import macaroonbakery.httpbakery as httpbakery
+from httmock import HTTMock, urlmatch
+
class TestKeyRing(unittest.TestCase):
@@ -19,7 +19,7 @@ class TestKeyRing(unittest.TestCase):
'status_code': 200,
'content': {
'Version': bakery.LATEST_VERSION,
- 'PublicKey': key.public_key.encode().decode('utf-8')
+ 'PublicKey': str(key.public_key),
}
}
@@ -41,7 +41,7 @@ class TestKeyRing(unittest.TestCase):
'status_code': 200,
'content': {
'Version': bakery.LATEST_VERSION,
- 'PublicKey': key.public_key.encode().decode('utf-8')
+ 'PublicKey': str(key.public_key),
}
}
@@ -64,7 +64,7 @@ class TestKeyRing(unittest.TestCase):
return {
'status_code': 200,
'content': {
- 'PublicKey': key.public_key.encode().decode('utf-8')
+ 'PublicKey': str(key.public_key),
}
}
@@ -79,7 +79,7 @@ class TestKeyRing(unittest.TestCase):
def test_allow_insecure(self):
kr = httpbakery.ThirdPartyLocator()
- with self.assertRaises(bakery.error.ThirdPartyInfoNotFound):
+ with self.assertRaises(bakery.ThirdPartyInfoNotFound):
kr.third_party_info('http://0.1.2.3/')
def test_fallback(self):
@@ -96,7 +96,7 @@ class TestKeyRing(unittest.TestCase):
return {
'status_code': 200,
'content': {
- 'PublicKey': key.public_key.encode().decode('utf-8')
+ 'PublicKey': str(key.public_key),
}
}
diff --git a/macaroonbakery/tests/test_macaroon.py b/macaroonbakery/tests/test_macaroon.py
index 93bbbb8..bcbbf80 100644
--- a/macaroonbakery/tests/test_macaroon.py
+++ b/macaroonbakery/tests/test_macaroon.py
@@ -3,13 +3,12 @@
import json
from unittest import TestCase
-import six
-import pymacaroons
-from pymacaroons import serializers
-
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
import macaroonbakery.checkers as checkers
+import pymacaroons
+import six
from macaroonbakery.tests import common
+from pymacaroons import serializers
class TestMacaroon(TestCase):
@@ -25,7 +24,8 @@ class TestMacaroon(TestCase):
self.assertEquals(m.version, bakery.LATEST_VERSION)
def test_add_first_party_caveat(self):
- m = bakery.Macaroon('rootkey', 'some id', 'here', bakery.LATEST_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)
diff --git a/macaroonbakery/tests/test_oven.py b/macaroonbakery/tests/test_oven.py
index ae235de..3c29767 100644
--- a/macaroonbakery/tests/test_oven.py
+++ b/macaroonbakery/tests/test_oven.py
@@ -1,11 +1,10 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-from unittest import TestCase
-
import copy
from datetime import datetime, timedelta
+from unittest import TestCase
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
EPOCH = datetime(1900, 11, 17, 19, 00, 13, 0, None)
AGES = EPOCH + timedelta(days=10)
@@ -88,7 +87,8 @@ class TestOven(TestCase):
ops_store=bakery.MemoryOpsStore())
ops = []
for i in range(30000):
- ops.append(bakery.Op(entity='entity' + str(i), action='action' + str(i)))
+ ops.append(bakery.Op(entity='entity' + str(i),
+ action='action' + str(i)))
m = test_oven.macaroon(bakery.LATEST_VERSION, AGES,
None, ops)
diff --git a/macaroonbakery/tests/test_store.py b/macaroonbakery/tests/test_store.py
index 5afa7be..8a54f59 100644
--- a/macaroonbakery/tests/test_store.py
+++ b/macaroonbakery/tests/test_store.py
@@ -2,7 +2,7 @@
# Licensed under the LGPLv3, see LICENCE file for details.
from unittest import TestCase
-import macaroonbakery as bakery
+import macaroonbakery.bakery as bakery
class TestOven(TestCase):
diff --git a/macaroonbakery/tests/test_time.py b/macaroonbakery/tests/test_time.py
index 38826e1..2685e56 100644
--- a/macaroonbakery/tests/test_time.py
+++ b/macaroonbakery/tests/test_time.py
@@ -1,16 +1,15 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
+from collections import namedtuple
from datetime import timedelta
from unittest import TestCase
-from collections import namedtuple
-import pyrfc3339
+import macaroonbakery.checkers as checkers
import pymacaroons
+import pyrfc3339
from pymacaroons import Macaroon
-import macaroonbakery.checkers as checkers
-
-t1 = pyrfc3339.parse('2017-10-26T16:19:47.441402074Z')
+t1 = pyrfc3339.parse('2017-10-26T16:19:47.441402074Z', produce_naive=True)
t2 = t1 + timedelta(hours=1)
t3 = t2 + timedelta(hours=1)
@@ -118,9 +117,17 @@ class TestExpireTime(TestCase):
]
for test in tests:
print('test ', test.about)
- t = checkers.macaroons_expiry_time(checkers.Namespace(), test.macaroons)
+ t = checkers.macaroons_expiry_time(checkers.Namespace(),
+ test.macaroons)
self.assertEqual(t, test.expectTime)
+ def test_macaroons_expire_time_skips_third_party(self):
+ m1 = newMacaroon([checkers.time_before_caveat(t1).condition])
+ m2 = newMacaroon()
+ m2.add_third_party_caveat('https://example.com', 'a-key', '123')
+ t = checkers.macaroons_expiry_time(checkers.Namespace(), [m1, m2])
+ self.assertEqual(t1, t)
+
def newMacaroon(conds=[]):
m = Macaroon(key='key', version=2)
diff --git a/macaroonbakery/tests/test_utils.py b/macaroonbakery/tests/test_utils.py
new file mode 100644
index 0000000..fcc8839
--- /dev/null
+++ b/macaroonbakery/tests/test_utils.py
@@ -0,0 +1,74 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+
+import json
+from datetime import datetime
+from unittest import TestCase
+
+import macaroonbakery.bakery as bakery
+import pymacaroons
+import pytz
+from macaroonbakery._utils import cookie
+from pymacaroons.serializers import json_serializer
+
+
+class CookieTest(TestCase):
+
+ def test_cookie_expires_naive(self):
+ timestamp = datetime.utcnow()
+ c = cookie('http://example.com', 'test', 'value', expires=timestamp)
+ self.assertEqual(
+ c.expires, int((timestamp - datetime(1970, 1, 1)).total_seconds()))
+
+ def test_cookie_expires_with_timezone(self):
+ timestamp = datetime.now(pytz.UTC)
+ self.assertRaises(
+ ValueError, cookie, 'http://example.com', 'test', 'value',
+ expires=timestamp)
+
+
+class TestB64Decode(TestCase):
+ def test_decode(self):
+ test_cases = [{
+ 'about': 'empty string',
+ 'input': '',
+ 'expect': '',
+ }, {
+ 'about': 'standard encoding, padded',
+ 'input': 'Z29+IQ==',
+ 'expect': 'go~!',
+ }, {
+ 'about': 'URL encoding, padded',
+ 'input': 'Z29-IQ==',
+ 'expect': 'go~!',
+ }, {
+ 'about': 'standard encoding, not padded',
+ 'input': 'Z29+IQ',
+ 'expect': 'go~!',
+ }, {
+ 'about': 'URL encoding, not padded',
+ 'input': 'Z29-IQ',
+ 'expect': 'go~!',
+ }, {
+ 'about': 'standard encoding, not enough much padding',
+ 'input': 'Z29+IQ=',
+ 'expect_error': 'illegal base64 data at input byte 8',
+ }]
+ for test in test_cases:
+ if test.get('expect_error'):
+ with self.assertRaises(ValueError, msg=test['about']) as e:
+ bakery.b64decode(test['input'])
+ self.assertEqual(str(e.exception), 'Incorrect padding')
+ else:
+ self.assertEqual(bakery.b64decode(test['input']), test['expect'].encode('utf-8'), msg=test['about'])
+
+
+class MacaroonToDictTest(TestCase):
+ def test_macaroon_to_dict(self):
+ m = pymacaroons.Macaroon(
+ key=b'rootkey', identifier=b'some id', location='here', version=2)
+ as_dict = bakery.macaroon_to_dict(m)
+ data = json.dumps(as_dict)
+ m1 = pymacaroons.Macaroon.deserialize(data, json_serializer.JsonSerializer())
+ self.assertEqual(m1.signature, m.signature)
+ pymacaroons.Verifier().verify(m1, b'rootkey')
diff --git a/setup.py b/setup.py
index 40155f9..eadbb41 100755
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ from setuptools import (
PROJECT_NAME = 'macaroonbakery'
-VERSION = (0, 0, 6)
+VERSION = (1, 1, 0)
def get_version():
diff --git a/tox.ini b/tox.ini
index b55483b..9a773b6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -22,7 +22,7 @@ commands =
[testenv:lint]
usedevelop = True
-commands = flake8 --ignore E501 --show-source macaroonbakery --exclude macaroonbakery/internal/id_pb2.py
+commands = flake8 --ignore E501 --show-source macaroonbakery --exclude macaroonbakery/bakery/_internal/id_pb2.py
[testenv:docs]
changedir = docs