summaryrefslogtreecommitdiff
path: root/macaroonbakery/httpbakery
diff options
context:
space:
mode:
Diffstat (limited to 'macaroonbakery/httpbakery')
-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
9 files changed, 161 insertions, 134 deletions
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).
'''