diff options
author | Colin Watson <cjwatson@debian.org> | 2017-12-12 15:20:49 +0000 |
---|---|---|
committer | Colin Watson <cjwatson@debian.org> | 2017-12-12 15:20:49 +0000 |
commit | 9e4403035a9953c99117083e6373ae3c441a76b5 (patch) | |
tree | d91b137df6767bfb8cb72de6b9fd21efb0c3dee4 /macaroonbakery/httpbakery/_error.py | |
parent | 949b7072cabce0daed6c94993ad44c8ea8648dbd (diff) |
Import py-macaroon-bakery_1.1.0.orig.tar.gz
Diffstat (limited to 'macaroonbakery/httpbakery/_error.py')
-rw-r--r-- | macaroonbakery/httpbakery/_error.py | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/macaroonbakery/httpbakery/_error.py b/macaroonbakery/httpbakery/_error.py new file mode 100644 index 0000000..ff75f13 --- /dev/null +++ b/macaroonbakery/httpbakery/_error.py @@ -0,0 +1,202 @@ +# Copyright 2017 Canonical Ltd. +# Licensed under the LGPLv3, see LICENCE file for details. +import json +from collections import namedtuple + +import macaroonbakery.bakery as bakery + +ERR_INTERACTION_REQUIRED = 'interaction required' +ERR_DISCHARGE_REQUIRED = 'macaroon discharge required' + + +class InteractionMethodNotFound(Exception): + '''This is thrown by client-side interaction methods when + they find that a given interaction isn't supported by the + client for a location''' + pass + + +class DischargeError(Exception): + '''This is thrown by Client when a third party has refused a discharge''' + def __init__(self, msg): + super(DischargeError, self).__init__( + 'third party refused discharge: {}'.format(msg)) + + +class InteractionError(Exception): + '''This is thrown by Client when it fails to deal with an + interaction-required error + ''' + def __init__(self, msg): + super(InteractionError, self).__init__( + 'cannot start interactive session: {}'.format(msg)) + + +def discharge_required_response(macaroon, path, cookie_suffix_name, + message=None): + ''' Get response content and headers from a discharge macaroons error. + + @param macaroon may hold a macaroon that, when discharged, may + 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). + ''' + if message is None: + message = 'discharge required' + content = json.dumps( + { + 'Code': 'macaroon discharge required', + 'Message': message, + 'Info': { + 'Macaroon': macaroon.to_dict(), + 'MacaroonPath': path, + 'CookieNameSuffix': cookie_suffix_name + }, + } + ).encode('utf-8') + return content, { + 'WWW-Authenticate': 'Macaroon', + 'Content-Type': 'application/json' + } + +# 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.VERSION_1) + ''' + vs = req_headers.get(BAKERY_PROTOCOL_HEADER) + if vs is None: + # No header - use backward compatibility mode. + return bakery.VERSION_1 + try: + x = int(vs) + except ValueError: + # Badly formed header - use backward compatibility mode. + return bakery.VERSION_1 + if x > bakery.LATEST_VERSION: + # Later version than we know about - use the + # latest version that we can. + return bakery.LATEST_VERSION + return x + + +class Error(namedtuple('Error', 'code, message, version, info')): + '''This class defines an error value as returned from + an httpbakery API. + ''' + @classmethod + def from_dict(cls, serialized): + '''Create an error from a JSON-deserialized object + @param serialized the object holding the serialized error {dict} + ''' + code = serialized.get('Code') + message = serialized.get('Message') + info = ErrorInfo.from_dict(serialized.get('Info')) + return Error(code=code, message=message, info=info, + version=bakery.LATEST_VERSION) + + def interaction_method(self, kind, x): + ''' Checks whether the error is an InteractionRequired error + that implements the method with the given name, and JSON-unmarshals the + method-specific data into x by calling its from_dict method + with the deserialized JSON object. + @param kind The interaction method kind (string). + @param x A class with a class method from_dict that returns a new + instance of the interaction info for the given kind. + @return The result of x.from_dict. + ''' + if self.info is None or self.code != ERR_INTERACTION_REQUIRED: + raise InteractionError( + 'not an interaction-required error (code {})'.format( + self.code) + ) + entry = self.info.interaction_methods.get(kind) + if entry is None: + raise InteractionMethodNotFound( + 'interaction method {} not found'.format(kind) + ) + return x.from_dict(entry) + + +class ErrorInfo( + namedtuple('ErrorInfo', 'macaroon, macaroon_path, cookie_name_suffix, ' + 'interaction_methods, visit_url, wait_url')): + ''' Holds additional information provided + by an error. + + @param macaroon may hold a macaroon that, when + discharged, may allow access to a service. + This field is associated with the ERR_DISCHARGE_REQUIRED + error code. + + @param macaroon_path holds the URL path to be associated + with the macaroon. The macaroon is potentially + valid for all URLs under the given path. + If it is empty, the macaroon will be associated with + the original URL from which the error was returned. + + @param cookie_name_suffix holds the desired cookie name suffix to be + associated with the macaroon. The actual name used will be + ("macaroon-" + cookie_name_suffix). Clients may ignore this field - + older clients will always use ("macaroon-" + + macaroon.signature() in hex). + + @param visit_url holds a URL that the client should visit + in a web browser to authenticate themselves. + + @param wait_url holds a URL that the client should visit + to acquire the discharge macaroon. A GET on + this URL will block until the client has authenticated, + and then it will return the discharge macaroon. + ''' + + __slots__ = () + + @classmethod + def from_dict(cls, serialized): + '''Create a new ErrorInfo object from a JSON deserialized + dictionary + @param serialized The JSON object {dict} + @return ErrorInfo object + ''' + if serialized is None: + return None + macaroon = serialized.get('Macaroon') + if macaroon is not None: + macaroon = bakery.Macaroon.from_dict(macaroon) + path = serialized.get('MacaroonPath') + cookie_name_suffix = serialized.get('CookieNameSuffix') + visit_url = serialized.get('VisitURL') + wait_url = serialized.get('WaitURL') + interaction_methods = serialized.get('InteractionMethods') + return ErrorInfo(macaroon=macaroon, macaroon_path=path, + cookie_name_suffix=cookie_name_suffix, + visit_url=visit_url, wait_url=wait_url, + interaction_methods=interaction_methods) + + def __new__(cls, macaroon=None, macaroon_path=None, + cookie_name_suffix=None, interaction_methods=None, + visit_url=None, wait_url=None): + '''Override the __new__ method so that we can + have optional arguments, which namedtuple doesn't + allow''' + return super(ErrorInfo, cls).__new__( + cls, macaroon, macaroon_path, cookie_name_suffix, + interaction_methods, visit_url, wait_url) |