17
17
"""Juju GUI server authentication management.
19
This module includes the pieces required to process user authentication:
19
This module includes the pieces required to process user authentication.
21
- User: a simple data structure representing a logged in or anonymous user;
22
- authentication backends (GoBackend and PythonBackend): any object
23
implementing the following interface:
21
- User: this is a simple data structure representing a logged in or
23
- Authentication backends (GoBackend and PythonBackend): the primary
24
purpose of auth backends is to provide the logic to parse requests' data
25
based on the API implementation currently in use. They can also be used
26
to create authentication requests. They must implement the following
24
28
- get_request_id(data) -> id or None;
25
29
- request_is_login(data) -> bool;
26
30
- get_credentials(data) -> (str, str);
27
- login_succeeded(data) -> bool.
28
The only purpose of auth backends is to provide the logic to parse
29
requests' data based on the API implementation currently in use. Backends
30
don't know anything about the authentication process or the current user,
31
and are not intended to store state: one backend (the one suitable for
32
the current API implementation) is instantiated once when the application
33
is bootstrapped and used as a singleton by all WebSocket requests;
34
- AuthMiddleware: process authentication requests and responses, using
35
the backend to parse the WebSocket messages, logging in the current user
36
if the authentication succeeds.
31
- login_succeeded(data) -> bool; and
32
- make_request(request_id, username, password) -> dict.
33
Backends don't know anything about the authentication process or the
34
current user, and are not intended to store state: one backend (the one
35
suitable for the current API implementation) is instantiated once when
36
the application is bootstrapped and used as a singleton by all WebSocket
38
- AuthMiddleware: this middleware processes authentication requests and
39
responses, using the backend to parse the WebSocket messages, logging in
40
the current user if the authentication succeeds.
41
- AuthenticationTokenHandler: This handles authentication token creation
42
and usage requests. It is used both by the AuthMiddleware and by
43
handlers.WebSocketHandler in the ``on_message`` and ``on_juju_message``
76
84
self._backend = backend
77
85
self._tokens = tokens
78
86
self._write_message = write_message
79
self._request_id = None
87
self._request_ids = {}
81
89
def in_progress(self):
82
"""Return True if the authentication is in progress, False otherwise.
90
"""Return True if authentication is in progress, False otherwise.
84
return self._request_id is not None
92
return bool(self._request_ids)
86
94
def process_request(self, data):
87
95
"""Parse the WebSocket data arriving from the client.
90
98
performed by the GUI user.
92
100
backend = self._backend
101
tokens = self._tokens
93
102
request_id = backend.get_request_id(data)
94
if request_id is not None and backend.request_is_login(data):
95
self._request_id = request_id
96
credentials = backend.get_credentials(data)
97
self._user.username, self._user.password = credentials
103
if request_id is not None:
106
if backend.request_is_login(data):
107
credentials = backend.get_credentials(data)
108
elif tokens.authentication_requested(data):
110
credentials = tokens.process_authentication_request(
111
data, self._write_message)
112
if credentials is None:
113
# This means that the tokens object handled the request.
116
# We need a "real" authentication request.
117
data = backend.make_request(request_id, *credentials)
118
if credentials is not None:
119
# Stashing credentials is a security risk. We currently deem
120
# this risk to be acceptably small. Even keeping an
121
# authenticated websocket in memory seems to be of a similar
122
# risk profile, and we cannot operate without that.
123
self._request_ids[request_id] = dict(
125
username=credentials[0],
126
password=credentials[1])
99
129
def process_response(self, data):
100
130
"""Parse the WebSocket data arriving from the Juju API server.
104
134
authentication succeeded.
106
136
request_id = self._backend.get_request_id(data)
107
if request_id == self._request_id:
137
if request_id in self._request_ids:
138
info = self._request_ids.pop(request_id)
108
140
logged_in = self._backend.login_succeeded(data)
110
logging.info('auth: user {} logged in'.format(self._user))
111
self._user.is_authenticated = True
113
self._user.username = self._user.password = ''
114
self._request_id = None
142
# Stashing credentials is a security risk. We currently deem
143
# this risk to be acceptably small. Even keeping an
144
# authenticated websocket in memory seems to be of a similar
145
# risk profile, and we cannot operate without that.
146
user.username = info['username']
147
user.password = info['password']
148
logging.info('auth: user {} logged in'.format(user))
149
user.is_authenticated = True
151
data = self._tokens.process_authentication_response(
117
156
class GoBackend(object):
165
204
return 'Error' not in data
206
def make_request(self, request_id, username, password):
207
"""Create and return an authentication request."""
209
RequestId=request_id,
212
Params=dict(AuthTag=username, Password=password))
168
215
class PythonBackend(object):
169
216
"""Authentication backend for the Juju Python implementation.
217
264
return data.get('result') and not data.get('err')
266
def make_request(self, request_id, username, password):
267
"""Create and return an authentication request."""
269
request_id=request_id,
220
275
def get_backend(apiversion):
221
276
"""Return the auth backend instance to use for the given API version."""
304
If the user is not authenticated, the failure response will look like this.
308
'Error': 'tokens can only be created by authenticated users.',
309
'ErrorCode': 'unauthorized access',
249
313
A token authentication request looks like the following:
300
364
def process_token_request(self, data, user, write_message):
301
365
"""Create a single-use, time-expired token and send it back."""
366
if not user.is_authenticated:
368
RequestId=data['RequestId'],
369
Error='tokens can only be created by authenticated users.',
370
ErrorCode='unauthorized access',
302
373
token = uuid.uuid4().hex
304
375
def expire_token():
305
376
self._data.pop(token, None)
377
logging.info('auth: expired token {}'.format(token))
306
378
handle = self._io_loop.add_timeout(self._max_life, expire_token)
307
379
now = datetime.datetime.utcnow()
308
380
# Stashing these is a security risk. We currently deem this risk to
337
409
def process_authentication_request(self, data, write_message):
338
410
"""Get the credentials for the token, or send an error."""
339
credentials = self._data.pop(data['Params']['Token'], None)
411
token = data['Params']['Token']
412
credentials = self._data.pop(token, None)
340
413
if credentials is not None:
414
logging.info('auth: using token {}'.format(token))
341
415
self._io_loop.remove_timeout(credentials['handle'])
342
416
return credentials['username'], credentials['password']