summaryrefslogtreecommitdiff
path: root/macaroonbakery/httpbakery
diff options
context:
space:
mode:
authorColin Watson <cjwatson@debian.org>2017-11-03 12:13:13 +0000
committerColin Watson <cjwatson@debian.org>2017-11-03 12:13:13 +0000
commit3d9eaeb5dacee168a93da090e2c0d46eedbe51a2 (patch)
tree779d797fb3cf6cc9552cb08c40662b5d3d8397fd /macaroonbakery/httpbakery
parent79ff2842fa477ee0693ea167c0a74cd7cf080d27 (diff)
Import py-macaroon-bakery_0.0.4.orig.tar.gz
Diffstat (limited to 'macaroonbakery/httpbakery')
-rw-r--r--macaroonbakery/httpbakery/__init__.py18
-rw-r--r--macaroonbakery/httpbakery/agent.py11
-rw-r--r--macaroonbakery/httpbakery/client.py26
-rw-r--r--macaroonbakery/httpbakery/error.py67
-rw-r--r--macaroonbakery/httpbakery/keyring.py56
5 files changed, 170 insertions, 8 deletions
diff --git a/macaroonbakery/httpbakery/__init__.py b/macaroonbakery/httpbakery/__init__.py
index 4ebcf23..3b40dc2 100644
--- a/macaroonbakery/httpbakery/__init__.py
+++ b/macaroonbakery/httpbakery/__init__.py
@@ -1 +1,17 @@
-from .client import BakeryAuth # NOQA
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+from macaroonbakery.httpbakery.client import BakeryAuth, extract_macaroons
+from macaroonbakery.httpbakery.error import (
+ BAKERY_PROTOCOL_HEADER, discharged_required_response, request_version
+)
+from macaroonbakery.httpbakery.keyring import ThirdPartyLocator
+
+
+__all__ = [
+ 'BAKERY_PROTOCOL_HEADER',
+ 'BakeryAuth',
+ 'ThirdPartyLocator',
+ 'discharged_required_response',
+ 'extract_macaroons',
+ 'request_version',
+]
diff --git a/macaroonbakery/httpbakery/agent.py b/macaroonbakery/httpbakery/agent.py
index 3676bae..e5a09e4 100644
--- a/macaroonbakery/httpbakery/agent.py
+++ b/macaroonbakery/httpbakery/agent.py
@@ -1,6 +1,5 @@
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
-
import base64
import json
@@ -12,20 +11,20 @@ from six.moves.urllib.parse import urlparse
class AgentFileFormatError(Exception):
- """ AgentFileFormatError is the exception raised when an agent file has a bad
+ ''' AgentFileFormatError is the exception raised when an agent file has a bad
structure.
- """
+ '''
pass
def load_agent_file(filename, cookies=None):
- """ Loads agent information from the specified file.
+ ''' Loads agent information from the specified file.
The agent cookies are added to cookies, or a newly created cookie jar
if cookies is not specified. The updated cookies is returned along
with the private key associated with the agent. These can be passed
directly as the cookies and key parameter to BakeryAuth.
- """
+ '''
with open(filename) as f:
data = json.load(f)
@@ -50,4 +49,4 @@ def load_agent_file(filename, cookies=None):
cookies.set_cookie(cookie)
return cookies, key
except (KeyError, ValueError) as e:
- raise AgentFileFormatError("invalid agent file", e)
+ raise AgentFileFormatError('invalid agent file', e)
diff --git a/macaroonbakery/httpbakery/client.py b/macaroonbakery/httpbakery/client.py
index 32f35dd..b62c61d 100644
--- a/macaroonbakery/httpbakery/client.py
+++ b/macaroonbakery/httpbakery/client.py
@@ -1,12 +1,16 @@
# Copyright 2017 Canonical Ltd.
# 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.http_cookiejar import Cookie
from six.moves.urllib.parse import urljoin
from six.moves.urllib.parse import urlparse
+from pymacaroons import Macaroon
+from pymacaroons.serializers.json_serializer import JsonSerializer
+
from macaroonbakery.bakery import discharge_all
from macaroonbakery import utils
@@ -155,3 +159,23 @@ def _visit_page_for_agent(cookies, key):
auth=BakeryAuth(cookies=cookies, key=key))
resp.raise_for_status()
return visit_page_for_agent
+
+
+def extract_macaroons(headers):
+ ''' Returns an array of any macaroons found in the given slice of cookies.
+ @param headers: dict of headers
+ @return: An array of array of mpy macaroons
+ '''
+ cookie_string = "\n".join(headers.get_all('Cookie', failobj=[]))
+ cs = SimpleCookie()
+ cs.load(cookie_string)
+ mss = []
+ for c in cs:
+ if not c.startswith('macaroon-'):
+ continue
+ data = base64.b64decode(cs[c].value)
+ data_as_objs = json.loads(data.decode('utf-8'))
+ ms = [Macaroon.deserialize(json.dumps(x), serializer=JsonSerializer())
+ for x in data_as_objs]
+ mss.append(ms)
+ return mss
diff --git a/macaroonbakery/httpbakery/error.py b/macaroonbakery/httpbakery/error.py
new file mode 100644
index 0000000..e138c66
--- /dev/null
+++ b/macaroonbakery/httpbakery/error.py
@@ -0,0 +1,67 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+import json
+
+import macaroonbakery
+
+
+def discharged_required_response(macaroon, path, cookie_suffix_name):
+ ''' Get response content and headers from a discharge macaroons error.
+
+ @param macaroon may hold a macaroon that, when discharged, may
+ allow access to a service.
+ @param path holds the URL path to be associated with the macaroon.
+ The macaroon is potentially valid for all URLs under the given path.
+ @param cookie_suffix_name holds the desired cookie name suffix to be
+ associated with the macaroon. The actual name used will be
+ ("macaroon-" + CookieName). Clients may ignore this field -
+ older clients will always use ("macaroon-" + macaroon.signature() in hex)
+ @return content(bytes) and the headers to set on the response(dict).
+ '''
+ content = json.dumps(
+ {
+ 'Code': 'macaroon discharge required',
+ 'Message': 'discharge required',
+ 'Info': {
+ 'Macaroon': macaroon.to_dict(),
+ 'MacaroonPath': path,
+ 'CookieNameSuffix': cookie_suffix_name
+ },
+ }
+ )
+ return content, {
+ 'WWW-Authenticate': 'Macaroon',
+ 'Content-Type': 'application/json'
+ }
+
+# BAKERY_PROTOCOL_HEADER is the header that HTTP clients should set
+# to determine the bakery protocol version. If it is 0 or missing,
+# a discharge-required error response will be returned with HTTP status 407;
+# if it is greater than 0, the response will have status 401 with the
+# WWW-Authenticate header set to "Macaroon".
+BAKERY_PROTOCOL_HEADER = 'Bakery-Protocol-Version'
+
+
+def request_version(req_headers):
+ ''' Determines the bakery protocol version from a client request.
+ If the protocol cannot be determined, or is invalid, the original version
+ of the protocol is used. If a later version is found, the latest known
+ version is used, which is OK because versions are backwardly compatible.
+
+ @param req_headers: the request headers as a dict.
+ @return: bakery protocol version (for example macaroonbakery.BAKERY_V1)
+ '''
+ vs = req_headers.get(BAKERY_PROTOCOL_HEADER)
+ if vs is None:
+ # No header - use backward compatibility mode.
+ return macaroonbakery.BAKERY_V1
+ try:
+ x = int(vs)
+ except ValueError:
+ # Badly formed header - use backward compatibility mode.
+ return macaroonbakery.BAKERY_V1
+ if x > macaroonbakery.LATEST_BAKERY_VERSION:
+ # Later version than we know about - use the
+ # latest version that we can.
+ return macaroonbakery.LATEST_BAKERY_VERSION
+ return x
diff --git a/macaroonbakery/httpbakery/keyring.py b/macaroonbakery/httpbakery/keyring.py
new file mode 100644
index 0000000..f4e93f7
--- /dev/null
+++ b/macaroonbakery/httpbakery/keyring.py
@@ -0,0 +1,56 @@
+# Copyright 2017 Canonical Ltd.
+# Licensed under the LGPLv3, see LICENCE file for details.
+from six.moves.urllib.parse import urlparse
+import requests
+
+import macaroonbakery
+
+
+class ThirdPartyLocator(macaroonbakery.ThirdPartyLocator):
+ ''' Implements macaroonbakery.ThirdPartyLocator by first looking in the
+ backing cache and, if that fails, making an HTTP request to find the
+ information associated with the given discharge location.
+ '''
+
+ def __init__(self, allow_insecure=False):
+ '''
+ @param url: the url to retrieve public_key
+ @param allow_insecure: By default it refuses to use insecure URLs.
+ '''
+ self._allow_insecure = allow_insecure
+ self._cache = {}
+
+ def third_party_info(self, loc):
+ u = urlparse(loc)
+ if u.scheme != 'https' and not self._allow_insecure:
+ raise macaroonbakery.ThirdPartyInfoNotFound(
+ 'untrusted discharge URL {}'.format(loc))
+ loc = loc.rstrip('/')
+ info = self._cache.get(loc)
+ if info is not None:
+ return info
+ url_endpoint = '/discharge/info'
+ resp = requests.get(loc + url_endpoint)
+ status_code = resp.status_code
+ if status_code == 404:
+ url_endpoint = '/publickey'
+ resp = requests.get(loc + url_endpoint)
+ status_code = resp.status_code
+ if status_code != 200:
+ raise macaroonbakery.ThirdPartyInfoNotFound(
+ 'unable to get info from {}'.format(url_endpoint))
+ json_resp = resp.json()
+ if json_resp is None:
+ raise macaroonbakery.ThirdPartyInfoNotFound(
+ 'no response from /discharge/info')
+ pk = json_resp.get('PublicKey')
+ if pk is None:
+ raise macaroonbakery.ThirdPartyInfoNotFound(
+ 'no public key found in /discharge/info')
+ idm_pk = macaroonbakery.PublicKey.deserialize(pk)
+ version = json_resp.get('Version', macaroonbakery.BAKERY_V1)
+ self._cache[loc] = macaroonbakery.ThirdPartyInfo(
+ version=version,
+ public_key=idm_pk
+ )
+ return self._cache.get(loc)