1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
# Copyright 2017 Canonical Ltd.
# Licensed under the LGPLv3, see LICENCE file for details.
from collections import namedtuple
import json
import macaroonbakery 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)
|