~benji/charms/precise/juju-gui/test-failure

« back to all changes in this revision

Viewing changes to server/guiserver/auth.py

  • Committer: Gary Poster
  • Date: 2013-11-25 14:18:26 UTC
  • mfrom: (134.4.7 authtoken3)
  • Revision ID: gary.poster@canonical.com-20131125141826-yatj3l58m4wg1tjw
Fully integrate AuthenticationTokenHandler

This might be the last of the charm branches for the authtoken feature.  It hooks up the handler and tests the integration.

R=frankban
CC=
https://codereview.appspot.com/31290043

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
"""Juju GUI server authentication management.
18
18
 
19
 
This module includes the pieces required to process user authentication:
 
19
This module includes the pieces required to process user authentication.
20
20
 
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
 
22
      anonymous user.
 
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
 
27
      interface:
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
 
37
      requests.
 
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``
 
44
      methods.
37
45
"""
38
46
 
39
47
import datetime
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 = {}
80
88
 
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.
83
91
        """
84
 
        return self._request_id is not None
 
92
        return bool(self._request_ids)
85
93
 
86
94
    def process_request(self, data):
87
95
        """Parse the WebSocket data arriving from the client.
90
98
        performed by the GUI user.
91
99
        """
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:
 
104
            credentials = None
 
105
            is_token = False
 
106
            if backend.request_is_login(data):
 
107
                credentials = backend.get_credentials(data)
 
108
            elif tokens.authentication_requested(data):
 
109
                is_token = True
 
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.
 
114
                    return None
 
115
                else:
 
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(
 
124
                    is_token=is_token,
 
125
                    username=credentials[0],
 
126
                    password=credentials[1])
 
127
        return data
98
128
 
99
129
    def process_response(self, data):
100
130
        """Parse the WebSocket data arriving from the Juju API server.
104
134
        authentication succeeded.
105
135
        """
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)
 
139
            user = self._user
108
140
            logged_in = self._backend.login_succeeded(data)
109
141
            if logged_in:
110
 
                logging.info('auth: user {} logged in'.format(self._user))
111
 
                self._user.is_authenticated = True
112
 
            else:
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
 
150
                if info['is_token']:
 
151
                    data = self._tokens.process_authentication_response(
 
152
                        data, user)
 
153
        return data
115
154
 
116
155
 
117
156
class GoBackend(object):
164
203
        """
165
204
        return 'Error' not in data
166
205
 
 
206
    def make_request(self, request_id, username, password):
 
207
        """Create and return an authentication request."""
 
208
        return dict(
 
209
            RequestId=request_id,
 
210
            Type='Admin',
 
211
            Request='Login',
 
212
            Params=dict(AuthTag=username, Password=password))
 
213
 
167
214
 
168
215
class PythonBackend(object):
169
216
    """Authentication backend for the Juju Python implementation.
216
263
        """
217
264
        return data.get('result') and not data.get('err')
218
265
 
 
266
    def make_request(self, request_id, username, password):
 
267
        """Create and return an authentication request."""
 
268
        return dict(
 
269
            request_id=request_id,
 
270
            op='login',
 
271
            user=username,
 
272
            password=password)
 
273
 
219
274
 
220
275
def get_backend(apiversion):
221
276
    """Return the auth backend instance to use for the given API version."""
235
290
            'Params': {},
236
291
        }
237
292
 
238
 
    Here is an example of a token creation response.
 
293
    Here is an example of a successful token creation response.
239
294
 
240
295
        {
241
296
            'RequestId': 42,
246
301
            }
247
302
        }
248
303
 
 
304
    If the user is not authenticated, the failure response will look like this.
 
305
 
 
306
        {
 
307
            'RequestId': 42,
 
308
            'Error': 'tokens can only be created by authenticated users.',
 
309
            'ErrorCode': 'unauthorized access',
 
310
            'Response': {},
 
311
        }
 
312
 
249
313
    A token authentication request looks like the following:
250
314
 
251
315
        {
299
363
 
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:
 
367
            write_message(dict(
 
368
                RequestId=data['RequestId'],
 
369
                Error='tokens can only be created by authenticated users.',
 
370
                ErrorCode='unauthorized access',
 
371
                Response={}))
 
372
            return
302
373
        token = uuid.uuid4().hex
303
374
 
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
336
408
 
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']
343
417
        else: