Merge lp:~gary/charms/precise/juju-gui/authtoken3 into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- authtoken3
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+196414@code.launchpad.net |
Commit message
Description of the change
Fully integrate AuthenticationT
This might be the last of the charm branches for the authtoken feature. It hooks up the handler and tests the integration.
Gary Poster (gary) wrote : | # |
- 145. By Gary Poster
-
tweak encoding logic
- 146. By Gary Poster
-
tweak encoding
Gary Poster (gary) wrote : | # |
Please take a look.
- 147. By Gary Poster
-
get more paranoid about unicode handling
Gary Poster (gary) wrote : | # |
Please take a look.
Francesco Banconi (frankban) wrote : | # |
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://
https:/
File server/
https:/
server/
- make_request(
https:/
server/
implementation currently in use. Backends
They can also be used to create authentication requests... or similar?
https:/
server/
Since this docstring summarizes the purposes of objects in this module,
it would be nice to provide some info about the
AuthenticationT
https:/
server/
username, password):
"""Create and return an authentication request."""? <shrug>
https:/
server/
I know this is pre-existing code, but what do you think about logging
the token expiration here?
https:/
server/
... and maybe here we could log the fact that someone is actually using
the token?
https:/
File server/
https:/
server/
self.tokens.
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_
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:/
File server/
https:/
server/
- 148. By Gary Poster
-
respond to review
Gary Poster (gary) wrote : | # |
Please take a look.
https:/
File server/
https:/
server/
On 2013/11/25 11:59:34, frankban wrote:
> - make_request(
Done.
https:/
server/
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:/
server/
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 AuthenticationT
here.
Done.
https:/
server/
username, password):
On 2013/11/25 11:59:34, frankban wrote:
> """Create and return an authentication request."""? <shrug>
Done.
https:/
server/
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:/
server/
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:/
File server/
https:/
server/
self.tokens.
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_
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.
Francesco Banconi (frankban) wrote : | # |
Changes LGTM, thank you!
Gary Poster (gary) wrote : | # |
*** Submitted:
Fully integrate AuthenticationT
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:/
Gary Poster (gary) wrote : | # |
Thanks for the fantastic review(s), and for going above and beyond with
the QA, Francesco.
Preview Diff
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, |
Reviewers: mp+196414_ code.launchpad. net,
Message:
Please take a look.
Description: okenHandler
Fully integrate AuthenticationT
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: /code.launchpad .net/~gary/ charms/ precise/ juju-gui/ authtoken2/ +merge/ 196351
https:/
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/31290043/
Affected files (+240, -25 lines): guiserver/ auth.py guiserver/ handlers. py guiserver/ tests/helpers. py guiserver/ tests/test_ auth.py guiserver/ tests/test_ handlers. py
A [revision details]
M server/
M server/
M server/
M server/
M server/