Merge lp:~gary/charms/precise/juju-gui/authtoken3 into lp:~juju-gui/charms/precise/juju-gui/trunk

Proposed by Gary Poster
Status: Merged
Merged at revision: 138
Proposed branch: lp:~gary/charms/precise/juju-gui/authtoken3
Merge into: lp:~juju-gui/charms/precise/juju-gui/trunk
Prerequisite: lp:~gary/charms/precise/juju-gui/authtoken2
Diff against target: 593 lines (+315/-45)
5 files modified
server/guiserver/auth.py (+103/-29)
server/guiserver/handlers.py (+20/-4)
server/guiserver/tests/helpers.py (+10/-0)
server/guiserver/tests/test_auth.py (+91/-12)
server/guiserver/tests/test_handlers.py (+91/-0)
To merge this branch: bzr merge lp:~gary/charms/precise/juju-gui/authtoken3
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+196414@code.launchpad.net

Description of the change

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.

https://codereview.appspot.com/31290043/

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Reviewers: mp+196414_code.launchpad.net,

Message:
Please take a look.

Description:
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. We might need more
work once we actually have a way to QA this! It looks good so far
though.

https://code.launchpad.net/~gary/charms/precise/juju-gui/authtoken3/+merge/196414

Requires:
https://code.launchpad.net/~gary/charms/precise/juju-gui/authtoken2/+merge/196351

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/31290043/

Affected files (+240, -25 lines):
   A [revision details]
   M server/guiserver/auth.py
   M server/guiserver/handlers.py
   M server/guiserver/tests/helpers.py
   M server/guiserver/tests/test_auth.py
   M server/guiserver/tests/test_handlers.py

145. By Gary Poster

tweak encoding logic

146. By Gary Poster

tweak encoding

Revision history for this message
Gary Poster (gary) wrote :
147. By Gary Poster

get more paranoid about unicode handling

Revision history for this message
Gary Poster (gary) wrote :
Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (3.2 KiB)

Very nice branch Gary, thank you!
LGTM (with some comments below), and QA ok!
To QA this branch I deployed the charm in a local environment using
"make deploy" and then I successfully run this script:
http://pastebin.ubuntu.com/6473403/

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py
File server/guiserver/auth.py (right):

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode27
server/guiserver/auth.py:27: - login_succeeded(data) -> bool.
- make_request(request_id, username, password) -> dict

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode29
server/guiserver/auth.py:29: requests' data based on the API
implementation currently in use. Backends
They can also be used to create authentication requests... or similar?

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode36
server/guiserver/auth.py:36: if the authentication succeeds.
Since this docstring summarizes the purposes of objects in this module,
it would be nice to provide some info about the
AuthenticationTokenHandler here.

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode198
server/guiserver/auth.py:198: def make_request(self, request_id,
username, password):
"""Create and return an authentication request."""? <shrug>

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode349
server/guiserver/auth.py:349: def expire_token():
I know this is pre-existing code, but what do you think about logging
the token expiration here?

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode385
server/guiserver/auth.py:385: if credentials is not None:
... and maybe here we could log the fact that someone is actually using
the token?

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/handlers.py
File server/guiserver/handlers.py (right):

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/handlers.py#newcode157
server/guiserver/handlers.py:157: elif
self.tokens.token_requested(data):
A token request is only handled by the guiserver if the user is
authenticated. This means that a GUIToken Create request from an
"anonymous" client is propagated to juju-core, and the response will be
something like this:
{
  'RequestId': 0,
  'Error': 'unknown object type "GUIToken"',
  'Response': {},
}
This can be ok but seems confusing from the client perspective; what we
might want to send back is probably something like "unauthorized access:
no user logged in".
The quick fix is to always check if a token is requested (s/elif/if,
similar to what already done with deployer requests at line 145) and
ensure the user is authenticated in the process_token_request method
itself, returning a response or an error accordingly. Of course this can
be done (if you agree with the proposed change) in another branch.

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/tests/test_auth.py
File server/guiserver/tests/test_auth.py (right):

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/tests/test_auth.py#newcode151
server/guiserver/tests/test_auth.py...

Read more...

148. By Gary Poster

respond to review

Revision history for this message
Gary Poster (gary) wrote :

Please take a look.

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py
File server/guiserver/auth.py (right):

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode27
server/guiserver/auth.py:27: - login_succeeded(data) -> bool.
On 2013/11/25 11:59:34, frankban wrote:
> - make_request(request_id, username, password) -> dict

Done.

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode29
server/guiserver/auth.py:29: requests' data based on the API
implementation currently in use. Backends
On 2013/11/25 11:59:34, frankban wrote:
> They can also be used to create authentication requests... or similar?

Done.

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode36
server/guiserver/auth.py:36: if the authentication succeeds.
On 2013/11/25 11:59:34, frankban wrote:
> Since this docstring summarizes the purposes of objects in this
module, it would
> be nice to provide some info about the AuthenticationTokenHandler
here.

Done.

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode198
server/guiserver/auth.py:198: def make_request(self, request_id,
username, password):
On 2013/11/25 11:59:34, frankban wrote:
> """Create and return an authentication request."""? <shrug>

Done.

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode349
server/guiserver/auth.py:349: def expire_token():
On 2013/11/25 11:59:34, frankban wrote:
> I know this is pre-existing code, but what do you think about logging
the token
> expiration here?

Done.

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/auth.py#newcode385
server/guiserver/auth.py:385: if credentials is not None:
On 2013/11/25 11:59:34, frankban wrote:
> ... and maybe here we could log the fact that someone is actually
using the
> token?

Done.

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/handlers.py
File server/guiserver/handlers.py (right):

https://codereview.appspot.com/31290043/diff/30001/server/guiserver/handlers.py#newcode157
server/guiserver/handlers.py:157: elif
self.tokens.token_requested(data):
On 2013/11/25 11:59:34, frankban wrote:
> A token request is only handled by the guiserver if the user is
authenticated.
> This means that a GUIToken Create request from an "anonymous" client
is
> propagated to juju-core, and the response will be something like this:
> {
> 'RequestId': 0,
> 'Error': 'unknown object type "GUIToken"',
> 'Response': {},
> }
> This can be ok but seems confusing from the client perspective; what
we might
> want to send back is probably something like "unauthorized access: no
user
> logged in".
> The quick fix is to always check if a token is requested (s/elif/if,
similar to
> what already done with deployer requests at line 145) and ensure the
user is
> authenticated in the process_token_request method itself, returning a
response
> or an error accordingly. Of course this can be done (if you agree with
the
> proposed change) in another branch.

Good idea, thank you. I went ahead and did it.

https://codereview.appspot.com/31290043/

Revision history for this message
Francesco Banconi (frankban) wrote :
Revision history for this message
Gary Poster (gary) wrote :

*** Submitted:

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

https://codereview.appspot.com/31290043/

Revision history for this message
Gary Poster (gary) wrote :

Thanks for the fantastic review(s), and for going above and beyond with
the QA, Francesco.

https://codereview.appspot.com/31290043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'server/guiserver/auth.py'
2--- server/guiserver/auth.py 2013-11-23 02:14:15 +0000
3+++ server/guiserver/auth.py 2013-11-25 14:02:21 +0000
4@@ -16,24 +16,32 @@
5
6 """Juju GUI server authentication management.
7
8-This module includes the pieces required to process user authentication:
9+This module includes the pieces required to process user authentication.
10
11- - User: a simple data structure representing a logged in or anonymous user;
12- - authentication backends (GoBackend and PythonBackend): any object
13- implementing the following interface:
14+ - User: this is a simple data structure representing a logged in or
15+ anonymous user.
16+ - Authentication backends (GoBackend and PythonBackend): the primary
17+ purpose of auth backends is to provide the logic to parse requests' data
18+ based on the API implementation currently in use. They can also be used
19+ to create authentication requests. They must implement the following
20+ interface:
21 - get_request_id(data) -> id or None;
22 - request_is_login(data) -> bool;
23 - get_credentials(data) -> (str, str);
24- - login_succeeded(data) -> bool.
25- The only purpose of auth backends is to provide the logic to parse
26- requests' data based on the API implementation currently in use. Backends
27- don't know anything about the authentication process or the current user,
28- and are not intended to store state: one backend (the one suitable for
29- the current API implementation) is instantiated once when the application
30- is bootstrapped and used as a singleton by all WebSocket requests;
31- - AuthMiddleware: process authentication requests and responses, using
32- the backend to parse the WebSocket messages, logging in the current user
33- if the authentication succeeds.
34+ - login_succeeded(data) -> bool; and
35+ - make_request(request_id, username, password) -> dict.
36+ Backends don't know anything about the authentication process or the
37+ current user, and are not intended to store state: one backend (the one
38+ suitable for the current API implementation) is instantiated once when
39+ the application is bootstrapped and used as a singleton by all WebSocket
40+ requests.
41+ - AuthMiddleware: this middleware processes authentication requests and
42+ responses, using the backend to parse the WebSocket messages, logging in
43+ the current user if the authentication succeeds.
44+ - AuthenticationTokenHandler: This handles authentication token creation
45+ and usage requests. It is used both by the AuthMiddleware and by
46+ handlers.WebSocketHandler in the ``on_message`` and ``on_juju_message``
47+ methods.
48 """
49
50 import datetime
51@@ -76,12 +84,12 @@
52 self._backend = backend
53 self._tokens = tokens
54 self._write_message = write_message
55- self._request_id = None
56+ self._request_ids = {}
57
58 def in_progress(self):
59- """Return True if the authentication is in progress, False otherwise.
60+ """Return True if authentication is in progress, False otherwise.
61 """
62- return self._request_id is not None
63+ return bool(self._request_ids)
64
65 def process_request(self, data):
66 """Parse the WebSocket data arriving from the client.
67@@ -90,11 +98,33 @@
68 performed by the GUI user.
69 """
70 backend = self._backend
71+ tokens = self._tokens
72 request_id = backend.get_request_id(data)
73- if request_id is not None and backend.request_is_login(data):
74- self._request_id = request_id
75- credentials = backend.get_credentials(data)
76- self._user.username, self._user.password = credentials
77+ if request_id is not None:
78+ credentials = None
79+ is_token = False
80+ if backend.request_is_login(data):
81+ credentials = backend.get_credentials(data)
82+ elif tokens.authentication_requested(data):
83+ is_token = True
84+ credentials = tokens.process_authentication_request(
85+ data, self._write_message)
86+ if credentials is None:
87+ # This means that the tokens object handled the request.
88+ return None
89+ else:
90+ # We need a "real" authentication request.
91+ data = backend.make_request(request_id, *credentials)
92+ if credentials is not None:
93+ # Stashing credentials is a security risk. We currently deem
94+ # this risk to be acceptably small. Even keeping an
95+ # authenticated websocket in memory seems to be of a similar
96+ # risk profile, and we cannot operate without that.
97+ self._request_ids[request_id] = dict(
98+ is_token=is_token,
99+ username=credentials[0],
100+ password=credentials[1])
101+ return data
102
103 def process_response(self, data):
104 """Parse the WebSocket data arriving from the Juju API server.
105@@ -104,14 +134,23 @@
106 authentication succeeded.
107 """
108 request_id = self._backend.get_request_id(data)
109- if request_id == self._request_id:
110+ if request_id in self._request_ids:
111+ info = self._request_ids.pop(request_id)
112+ user = self._user
113 logged_in = self._backend.login_succeeded(data)
114 if logged_in:
115- logging.info('auth: user {} logged in'.format(self._user))
116- self._user.is_authenticated = True
117- else:
118- self._user.username = self._user.password = ''
119- self._request_id = None
120+ # Stashing credentials is a security risk. We currently deem
121+ # this risk to be acceptably small. Even keeping an
122+ # authenticated websocket in memory seems to be of a similar
123+ # risk profile, and we cannot operate without that.
124+ user.username = info['username']
125+ user.password = info['password']
126+ logging.info('auth: user {} logged in'.format(user))
127+ user.is_authenticated = True
128+ if info['is_token']:
129+ data = self._tokens.process_authentication_response(
130+ data, user)
131+ return data
132
133
134 class GoBackend(object):
135@@ -164,6 +203,14 @@
136 """
137 return 'Error' not in data
138
139+ def make_request(self, request_id, username, password):
140+ """Create and return an authentication request."""
141+ return dict(
142+ RequestId=request_id,
143+ Type='Admin',
144+ Request='Login',
145+ Params=dict(AuthTag=username, Password=password))
146+
147
148 class PythonBackend(object):
149 """Authentication backend for the Juju Python implementation.
150@@ -216,6 +263,14 @@
151 """
152 return data.get('result') and not data.get('err')
153
154+ def make_request(self, request_id, username, password):
155+ """Create and return an authentication request."""
156+ return dict(
157+ request_id=request_id,
158+ op='login',
159+ user=username,
160+ password=password)
161+
162
163 def get_backend(apiversion):
164 """Return the auth backend instance to use for the given API version."""
165@@ -235,7 +290,7 @@
166 'Params': {},
167 }
168
169- Here is an example of a token creation response.
170+ Here is an example of a successful token creation response.
171
172 {
173 'RequestId': 42,
174@@ -246,6 +301,15 @@
175 }
176 }
177
178+ If the user is not authenticated, the failure response will look like this.
179+
180+ {
181+ 'RequestId': 42,
182+ 'Error': 'tokens can only be created by authenticated users.',
183+ 'ErrorCode': 'unauthorized access',
184+ 'Response': {},
185+ }
186+
187 A token authentication request looks like the following:
188
189 {
190@@ -299,10 +363,18 @@
191
192 def process_token_request(self, data, user, write_message):
193 """Create a single-use, time-expired token and send it back."""
194+ if not user.is_authenticated:
195+ write_message(dict(
196+ RequestId=data['RequestId'],
197+ Error='tokens can only be created by authenticated users.',
198+ ErrorCode='unauthorized access',
199+ Response={}))
200+ return
201 token = uuid.uuid4().hex
202
203 def expire_token():
204 self._data.pop(token, None)
205+ logging.info('auth: expired token {}'.format(token))
206 handle = self._io_loop.add_timeout(self._max_life, expire_token)
207 now = datetime.datetime.utcnow()
208 # Stashing these is a security risk. We currently deem this risk to
209@@ -336,8 +408,10 @@
210
211 def process_authentication_request(self, data, write_message):
212 """Get the credentials for the token, or send an error."""
213- credentials = self._data.pop(data['Params']['Token'], None)
214+ token = data['Params']['Token']
215+ credentials = self._data.pop(token, None)
216 if credentials is not None:
217+ logging.info('auth: using token {}'.format(token))
218 self._io_loop.remove_timeout(credentials['handle'])
219 return credentials['username'], credentials['password']
220 else:
221
222=== modified file 'server/guiserver/handlers.py'
223--- server/guiserver/handlers.py 2013-11-22 15:53:02 +0000
224+++ server/guiserver/handlers.py 2013-11-25 14:02:21 +0000
225@@ -22,6 +22,7 @@
226 import time
227
228 from tornado import (
229+ escape,
230 gen,
231 web,
232 websocket,
233@@ -138,15 +139,27 @@
234 established are queued for later delivery.
235 """
236 data = json_decode_dict(message)
237+ encoded = None
238 if data is not None:
239 # Handle deployment requests.
240 if self.deployment.requested(data):
241 return self.deployment.process_request(data)
242 # Handle authentication requests.
243 if not self.user.is_authenticated:
244- self.auth.process_request(data)
245+ new_data = self.auth.process_request(data)
246+ if new_data is None:
247+ # The None marker indicates that a response was sent.
248+ return
249+ elif new_data != data:
250+ encoded = escape.json_encode(new_data)
251+ message = encoded.decode('utf8')
252+ # Handle authentication token requests.
253+ if self.tokens.token_requested(data):
254+ return self.tokens.process_token_request(
255+ data, self.user, wrap_write_message(self))
256 # Propagate messages to the Juju API server.
257- encoded = message.encode('utf-8')
258+ if encoded is None:
259+ encoded = message.encode('utf-8')
260 if self.juju_connected:
261 logging.debug(self._summary + 'client -> juju: {}'.format(encoded))
262 return self.juju_connection.write_message(message)
263@@ -163,8 +176,11 @@
264 return self.on_juju_close()
265 data = json_decode_dict(message)
266 if (data is not None) and self.auth.in_progress():
267- self.auth.process_response(data)
268- encoded = message.encode('utf-8')
269+ encoded = escape.json_encode(
270+ self.auth.process_response(data))
271+ message = encoded.decode('utf8')
272+ else:
273+ encoded = message.encode('utf-8')
274 logging.debug(self._summary + 'juju -> client: {}'.format(encoded))
275 self.write_message(message)
276
277
278=== modified file 'server/guiserver/tests/helpers.py'
279--- server/guiserver/tests/helpers.py 2013-11-07 11:17:54 +0000
280+++ server/guiserver/tests/helpers.py 2013-11-25 14:02:21 +0000
281@@ -96,6 +96,16 @@
282 data['Error'] = 'invalid entity name or password'
283 return json.dumps(data) if encoded else data
284
285+ def make_token_login_request(self, tokens=None, request_id=42,
286+ token='DEFACED', username=None,
287+ password=None):
288+ if username is not None and password is not None:
289+ tokens._data[token] = dict(
290+ username=username, password=password, handle="handle")
291+ return dict(
292+ RequestId=request_id, Type='GUIToken', Request='Login',
293+ Params={'Token': token})
294+
295
296 class PythonAPITestMixin(object):
297 """Add helper methods for testing the Python API implementation."""
298
299=== modified file 'server/guiserver/tests/test_auth.py'
300--- server/guiserver/tests/test_auth.py 2013-11-23 02:14:15 +0000
301+++ server/guiserver/tests/test_auth.py 2013-11-25 14:02:21 +0000
302@@ -78,17 +78,20 @@
303
304 def test_login_request(self):
305 # The authentication process starts if a login request is processed.
306+
307 request = self.make_login_request(username='user', password='passwd')
308- self.auth.process_request(request)
309+ response = self.auth.process_request(request)
310+ self.assertEqual(request, response)
311 self.assertTrue(self.auth.in_progress())
312- self.assert_user('user', 'passwd', False)
313+ self.assert_user('', '', False)
314
315 def test_login_success(self):
316 # The user is logged in if the authentication process completes.
317 request = self.make_login_request(username='user', password='passwd')
318 self.auth.process_request(request)
319 response = self.make_login_response()
320- self.auth.process_response(response)
321+ result = self.auth.process_response(response)
322+ self.assertEqual(response, result)
323 self.assertFalse(self.auth.in_progress())
324 self.assert_user('user', 'passwd', True)
325
326@@ -97,7 +100,8 @@
327 request = self.make_login_request()
328 self.auth.process_request(request)
329 response = self.make_login_response(successful=False)
330- self.auth.process_response(response)
331+ result = self.auth.process_response(response)
332+ self.assertEqual(response, result)
333 self.assertFalse(self.auth.in_progress())
334 self.assert_user('', '', False)
335
336@@ -110,10 +114,10 @@
337 response = self.make_login_response(request_id=47)
338 self.auth.process_response(response)
339 self.assertTrue(self.auth.in_progress())
340- self.assert_user('user', 'passwd', False)
341+ self.assert_user('', '', False)
342
343 def test_multiple_auth_requests(self):
344- # Only the last authentication request is taken into consideration.
345+ # The last authentication request is honored.
346 request1 = self.make_login_request(request_id=1)
347 request2 = self.make_login_request(
348 request_id=2, username='user2', password='passwd2')
349@@ -122,8 +126,8 @@
350 # The first response arrives.
351 response = self.make_login_response(request_id=1)
352 self.auth.process_response(response)
353- # The user is still not autheticated and the auth is in progress.
354- self.assertFalse(self.user.is_authenticated)
355+ # The user is authenticated but the auth is still in progress.
356+ self.assertTrue(self.user.is_authenticated)
357 self.assertTrue(self.auth.in_progress())
358 # The second response arrives.
359 response = self.make_login_response(request_id=2)
360@@ -143,7 +147,61 @@
361 class TestGoAuthMiddleware(
362 helpers.GoAPITestMixin, AuthMiddlewareTestMixin,
363 LogTrapTestCase, unittest.TestCase):
364- pass
365+
366+ def test_token_login_request(self):
367+ # The authentication process starts with a token login request also.
368+ request = self.make_token_login_request(
369+ self.tokens, username='user', password='passwd')
370+ response = self.auth.process_request(request)
371+ # The response now looks as if it were made without a token.
372+ self.assertEqual(
373+ self.make_login_request(username='user', password='passwd'),
374+ response)
375+ self.assertTrue(self.auth.in_progress())
376+ self.assert_user('', '', False)
377+ self.assertFalse(self.write_message.called)
378+
379+ def test_token_login_success(self):
380+ # The user is logged in if the authentication process completes.
381+ request = self.make_token_login_request(
382+ self.tokens, username='user', password='passwd')
383+ self.auth.process_request(request)
384+ response = self.make_login_response()
385+ result = self.auth.process_response(response)
386+ self.assertEqual(
387+ dict(RequestId=42,
388+ Response=dict(AuthTag='user', Password='passwd')),
389+ result)
390+ self.assertFalse(self.auth.in_progress())
391+ self.assert_user('user', 'passwd', True)
392+ self.assertFalse(self.write_message.called)
393+
394+ def test_token_login_failure(self):
395+ # The user is not logged in if the authentication process fails.
396+ request = self.make_token_login_request(
397+ self.tokens, username='user', password='passwd')
398+ self.auth.process_request(request)
399+ response = self.make_login_response(successful=False)
400+ result = self.auth.process_response(response)
401+ self.assertEqual(response, result)
402+ self.assertFalse(self.auth.in_progress())
403+ self.assert_user('', '', False)
404+ self.assertFalse(self.write_message.called)
405+
406+ def test_token_login_missing(self):
407+ # The user is not logged in if the authentication process fails.
408+ request = self.make_token_login_request()
409+ response = self.auth.process_request(request)
410+ # None is a marker indicating that the request has been handled and
411+ # should not be continued on through to Juju.
412+ self.assertIsNone(response)
413+ self.write_message.assert_called_once_with(dict(
414+ RequestId=42,
415+ Error='unknown, fulfilled, or expired token',
416+ ErrorCode='unauthorized access',
417+ Response={}))
418+ self.assertFalse(self.auth.in_progress())
419+ self.assert_user('', '', False)
420
421
422 class TestPythonAuthMiddleware(
423@@ -192,6 +250,12 @@
424 response = self.make_login_response(successful=False)
425 self.assertFalse(self.backend.login_succeeded(response))
426
427+ def test_make_request(self):
428+ expected = self.make_login_request(
429+ request_id=42, username='user', password='passwd')
430+ self.assertEqual(
431+ expected, self.backend.make_request(42, 'user', 'passwd'))
432+
433
434 class TestGoBackend(
435 helpers.GoAPITestMixin, BackendTestMixin, unittest.TestCase):
436@@ -304,7 +368,7 @@
437 datetime.datetime(2013, 11, 21, 21)}))
438 def test_process_token_request(self):
439 # It correctly responds to token requests.
440- user = auth.User('user-admin', 'ADMINSECRET')
441+ user = auth.User('user-admin', 'ADMINSECRET', True)
442 write_message = mock.Mock()
443 data = dict(RequestId=42, Type='GUIToken', Request='Create')
444 self.tokens.process_token_request(data, user, write_message)
445@@ -331,6 +395,21 @@
446 expire_token()
447 self.assertFalse('DEFACED' in self.tokens._data)
448
449+ def test_unauthenticated_process_token_request(self):
450+ # Unauthenticated token requests get an informative error.
451+ user = auth.User(is_authenticated=False)
452+ write_message = mock.Mock()
453+ data = dict(RequestId=42, Type='GUIToken', Request='Create')
454+ self.tokens.process_token_request(data, user, write_message)
455+ write_message.assert_called_once_with(dict(
456+ RequestId=42,
457+ Error='tokens can only be created by authenticated users.',
458+ ErrorCode='unauthorized access',
459+ Response={}
460+ ))
461+ self.assertEqual({}, self.tokens._data)
462+ self.assertFalse(self.io_loop.add_timeout.called)
463+
464 def test_authentication_requested(self):
465 # It recognizes an authentication request.
466 request = dict(
467@@ -393,7 +472,7 @@
468 def test_token_request_and_authentication_collaborate(self):
469 # process_token_request and process_authentication_request collaborate.
470 # This is a small integration test of the two functions' interaction.
471- user = auth.User('user-admin', 'ADMINSECRET')
472+ user = auth.User('user-admin', 'ADMINSECRET', True)
473 write_message = mock.Mock()
474 request = dict(RequestId=42, Type='GUIToken', Request='Create')
475 self.tokens.process_token_request(request, user, write_message)
476@@ -406,7 +485,7 @@
477
478 def test_process_authentication_response(self):
479 # It translates a normal authentication success.
480- user = auth.User('user-admin', 'ADMINSECRET')
481+ user = auth.User('user-admin', 'ADMINSECRET', True)
482 response = {'RequestId': 42, 'Response': {}}
483 self.assertEqual(
484 dict(RequestId=42,
485
486=== modified file 'server/guiserver/tests/test_handlers.py'
487--- server/guiserver/tests/test_handlers.py 2013-11-22 15:53:02 +0000
488+++ server/guiserver/tests/test_handlers.py 2013-11-25 14:02:21 +0000
489@@ -16,6 +16,7 @@
490
491 """Tests for the Juju GUI server handlers."""
492
493+import datetime
494 import json
495 import os
496 import shutil
497@@ -330,6 +331,96 @@
498 self.assertFalse(self.handler.user.is_authenticated)
499 self.assertFalse(self.handler.auth.in_progress())
500
501+ @mock.patch('uuid.uuid4', mock.Mock(return_value=mock.Mock(hex='DEFACED')))
502+ @mock.patch('datetime.datetime',
503+ mock.Mock(
504+ **{'utcnow.return_value':
505+ datetime.datetime(2013, 11, 21, 21)}))
506+ def test_token_request(self):
507+ # It supports requesting a token when authenticated.
508+ self.handler.user.username = 'user'
509+ self.handler.user.password = 'passwd'
510+ self.handler.user.is_authenticated = True
511+ request = json.dumps(
512+ dict(RequestId=42, Type='GUIToken', Request='Create'))
513+ self.handler.on_message(request)
514+ message = self.handler.ws_connection.write_message.call_args[0][0]
515+ self.assertEqual(
516+ dict(
517+ RequestId=42,
518+ Response=dict(
519+ Token='DEFACED',
520+ Created='2013-11-21T21:00:00Z',
521+ Expires='2013-11-21T21:02:00Z'
522+ )
523+ ),
524+ json.loads(message))
525+ self.assertFalse(self.handler.juju_connected)
526+ self.assertEqual(0, len(self.handler._juju_message_queue))
527+
528+ def test_unauthenticated_token_request(self):
529+ # When not authenticated, the request is passed on to Juju for error.
530+ self.assertFalse(self.handler.user.is_authenticated)
531+ request = json.dumps(
532+ dict(RequestId=42, Type='GUIToken', Request='Create'))
533+ self.handler.on_message(request)
534+ message = self.handler.ws_connection.write_message.call_args[0][0]
535+ self.assertEqual(
536+ dict(
537+ RequestId=42,
538+ Error='tokens can only be created by authenticated users.',
539+ ErrorCode='unauthorized access',
540+ Response={},
541+ ),
542+ json.loads(message))
543+ self.assertFalse(self.handler.juju_connected)
544+ self.assertEqual(0, len(self.handler._juju_message_queue))
545+
546+ def test_token_authentication_success(self):
547+ # It supports authenticating with a token.
548+ request = self.make_token_login_request(
549+ self.tokens, username='user', password='passwd')
550+ with mock.patch.object(self.io_loop,
551+ 'remove_timeout') as mock_remove_timeout:
552+ self.handler.on_message(json.dumps(request))
553+ mock_remove_timeout.assert_called_once_with('handle')
554+ self.assertEqual(
555+ self.make_login_request(
556+ request_id=42, username='user', password='passwd'),
557+ json.loads(self.handler._juju_message_queue[0]))
558+ self.assertTrue(self.handler.auth.in_progress())
559+ self.send_login_response(True)
560+ self.assertEqual(
561+ dict(RequestId=42,
562+ Response={'AuthTag': 'user', 'Password': 'passwd'}),
563+ json.loads(
564+ self.handler.ws_connection.write_message.call_args[0][0]))
565+
566+ def test_token_authentication_failure(self):
567+ # It correctly handles a token that will not authenticate.
568+ request = self.make_token_login_request(
569+ self.tokens, username='user', password='passwd')
570+ with mock.patch.object(self.io_loop,
571+ 'remove_timeout') as mock_remove_timeout:
572+ self.handler.on_message(json.dumps(request))
573+ mock_remove_timeout.assert_called_once_with('handle')
574+ self.send_login_response(False)
575+ message = self.handler.ws_connection.write_message.call_args[0][0]
576+ self.assertEqual(
577+ 'invalid entity name or password',
578+ json.loads(message)['Error'])
579+
580+ def test_unknown_authentication_token(self):
581+ # It correctly handles an unknown token.
582+ request = self.make_token_login_request()
583+ self.handler.on_message(json.dumps(request))
584+ message = self.handler.ws_connection.write_message.call_args[0][0]
585+ self.assertEqual(
586+ 'unknown, fulfilled, or expired token',
587+ json.loads(message)['Error'])
588+ self.assertFalse(self.handler.juju_connected)
589+ self.assertEqual(0, len(self.handler._juju_message_queue))
590+
591
592 class TestWebSocketHandlerBundles(
593 WebSocketHandlerTestMixin, helpers.WSSTestMixin,

Subscribers

People subscribed via source and target branches

to all changes: