Merge lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-0 into lp:ubuntu-sso-client/stable-1-0
- timestamp-autofix-1-0
- Merge into stable-1-0
Proposed by
Alejandro J. Cura
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Natalia Bidart | ||||||||
Approved revision: | 649 | ||||||||
Merged at revision: | 647 | ||||||||
Proposed branch: | lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-0 | ||||||||
Merge into: | lp:ubuntu-sso-client/stable-1-0 | ||||||||
Diff against target: |
431 lines (+315/-6) 5 files modified
ubuntu_sso/main.py (+32/-4) ubuntu_sso/tests/test_main.py (+38/-2) ubuntu_sso/utils/__init__.py (+81/-0) ubuntu_sso/utils/tests/__init__.py (+16/-0) ubuntu_sso/utils/tests/test_oauth_headers.py (+148/-0) |
||||||||
To merge this branch: | bzr merge lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-0 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Natalia Bidart (community) | Approve | ||
Diego Sarmentero (community) | Approve | ||
Review via email: mp+82737@code.launchpad.net |
To post a comment you must log in.
Revision history for this message
Diego Sarmentero (diegosarmentero) : | # |
review:
Abstain
- 649. By Alejandro J. Cura
-
fix year in file headers
Revision history for this message
Diego Sarmentero (diegosarmentero) wrote : | # |
+1 looks great!
review:
Approve
Revision history for this message
Natalia Bidart (nataliabidart) wrote : | # |
Tested locally and IRL. It works great, and all tests are green.
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'ubuntu_sso/main.py' |
2 | --- ubuntu_sso/main.py 2011-03-29 17:26:26 +0000 |
3 | +++ ubuntu_sso/main.py 2011-11-23 19:50:27 +0000 |
4 | @@ -3,7 +3,7 @@ |
5 | # Author: Natalia Bidart <natalia.bidart@canonical.com> |
6 | # Author: Alejandro J. Cura <alecu@canonical.com> |
7 | # |
8 | -# Copyright 2009 Canonical Ltd. |
9 | +# Copyright 2009, 2011 Canonical Ltd. |
10 | # |
11 | # This program is free software: you can redistribute it and/or modify it |
12 | # under the terms of the GNU General Public License version 3, as published |
13 | @@ -46,6 +46,7 @@ |
14 | from ubuntu_sso import DBUS_IFACE_USER_NAME, DBUS_IFACE_CRED_NAME |
15 | from ubuntu_sso.keyring import Keyring, get_token_name, U1_APP_NAME |
16 | from ubuntu_sso.logger import setup_logging |
17 | +from ubuntu_sso.utils import timestamp_checker |
18 | |
19 | |
20 | # Disable the invalid name warning, as we have a lot of DBus style names |
21 | @@ -117,6 +118,27 @@ |
22 | return creds |
23 | |
24 | |
25 | +class TimestampedAuthorizer(OAuthAuthorizer): |
26 | + """Includes a custom timestamp on OAuth signatures.""" |
27 | + |
28 | + def __init__(self, get_timestamp, *args, **kwargs): |
29 | + """Store the get_timestamp method, and move on.""" |
30 | + OAuthAuthorizer.__init__(self, *args, **kwargs) |
31 | + self.get_timestamp = get_timestamp |
32 | + |
33 | + # pylint: disable=C0103,E1101 |
34 | + def authorizeRequest(self, absolute_uri, method, body, headers): |
35 | + """Override authorizeRequest including the timestamp.""" |
36 | + parameters = {"oauth_timestamp": self.get_timestamp()} |
37 | + oauth_request = oauth.OAuthRequest.from_consumer_and_token( |
38 | + self.consumer, self.access_token, http_url=absolute_uri, |
39 | + parameters=parameters) |
40 | + oauth_request.sign_request( |
41 | + oauth.OAuthSignatureMethod_PLAINTEXT(), |
42 | + self.consumer, self.access_token) |
43 | + headers.update(oauth_request.to_header(self.oauth_realm)) |
44 | + |
45 | + |
46 | class SSOLoginProcessor(object): |
47 | """Login and register users using the Ubuntu Single Sign On service.""" |
48 | |
49 | @@ -236,7 +258,9 @@ |
50 | if sso_service is None: |
51 | oauth_token = oauth.OAuthToken(token['token'], |
52 | token['token_secret']) |
53 | - authorizer = OAuthAuthorizer(token['consumer_key'], |
54 | + authorizer = TimestampedAuthorizer( |
55 | + timestamp_checker.get_faithful_time, |
56 | + token['consumer_key'], |
57 | token['consumer_secret'], |
58 | oauth_token) |
59 | sso_service = self.sso_service_class(authorizer, self.service_url) |
60 | @@ -258,7 +282,8 @@ |
61 | token_name=token_name) |
62 | |
63 | oauth_token = oauth.OAuthToken(token['token'], token['token_secret']) |
64 | - authorizer = OAuthAuthorizer(token['consumer_key'], |
65 | + authorizer = TimestampedAuthorizer( |
66 | + timestamp_checker.get_faithful_time, |
67 | token['consumer_secret'], |
68 | oauth_token) |
69 | sso_service = self.sso_service_class(authorizer, self.service_url) |
70 | @@ -607,9 +632,12 @@ |
71 | credentials['consumer_secret']) |
72 | token = oauth.OAuthToken(credentials['token'], |
73 | credentials['token_secret']) |
74 | + timestamp = timestamp_checker.get_faithful_time() |
75 | + parameters = {"oauth_timestamp": timestamp} |
76 | get_request = oauth.OAuthRequest.from_consumer_and_token |
77 | oauth_req = get_request(oauth_consumer=consumer, token=token, |
78 | - http_method="GET", http_url=url) |
79 | + http_method="GET", http_url=url, |
80 | + parameters=parameters) |
81 | oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(), |
82 | consumer, token) |
83 | request = urllib2.Request(url, headers=oauth_req.to_header()) |
84 | |
85 | === modified file 'ubuntu_sso/tests/test_main.py' |
86 | --- ubuntu_sso/tests/test_main.py 2011-03-29 17:26:26 +0000 |
87 | +++ ubuntu_sso/tests/test_main.py 2011-11-23 19:50:27 +0000 |
88 | @@ -5,7 +5,7 @@ |
89 | # Author: Natalia Bidart <natalia.bidart@canonical.com> |
90 | # Author: Alejandro J. Cura <alecu@canonical.com> |
91 | # |
92 | -# Copyright 2009-2010 Canonical Ltd. |
93 | +# Copyright 2009-2011 Canonical Ltd. |
94 | # |
95 | # This program is free software: you can redistribute it and/or modify it |
96 | # under the terms of the GNU General Public License version 3, as published |
97 | @@ -22,6 +22,7 @@ |
98 | |
99 | import logging |
100 | import os |
101 | +import time |
102 | import urllib2 |
103 | |
104 | import gobject |
105 | @@ -31,6 +32,7 @@ |
106 | from lazr.restfulclient.errors import HTTPError |
107 | # pylint: enable=F0401 |
108 | from mocker import Mocker, MockerTestCase, ARGS, KWARGS, ANY |
109 | +from oauth import oauth |
110 | from twisted.internet.defer import Deferred |
111 | from twisted.trial.unittest import TestCase |
112 | |
113 | @@ -46,7 +48,7 @@ |
114 | keyring_get_credentials, keyring_store_credentials, logger, |
115 | NewPasswordError, PING_URL, SERVICE_URL, |
116 | RegistrationError, ResetPasswordTokenError, |
117 | - SSOCredentials, SSOLogin, SSOLoginProcessor) |
118 | + SSOCredentials, SSOLogin, SSOLoginProcessor, TimestampedAuthorizer) |
119 | |
120 | |
121 | # Access to a protected member 'yyy' of a client class |
122 | @@ -193,6 +195,38 @@ |
123 | self.accounts = FakedAccounts() |
124 | |
125 | |
126 | +class TimestampedAuthorizerTestCase(TestCase): |
127 | + """Test suite for the TimestampedAuthorizer.""" |
128 | + |
129 | + def test_authorize_request_includes_timestamp(self): |
130 | + """The authorizeRequest method includes the timestamp.""" |
131 | + fromcandt_call = [] |
132 | + fake_uri = "http://protocultura.net" |
133 | + fake_timestamp = 1234 |
134 | + get_fake_timestamp = lambda: fake_timestamp |
135 | + original_oauthrequest = oauth.OAuthRequest |
136 | + |
137 | + class FakeOAuthRequest(oauth.OAuthRequest): |
138 | + """A Fake OAuthRequest class.""" |
139 | + |
140 | + # pylint: disable=W0221 |
141 | + @staticmethod |
142 | + def from_consumer_and_token(*args, **kwargs): |
143 | + """A fake from_consumer_and_token.""" |
144 | + fromcandt_call.append((args, kwargs)) |
145 | + builder = original_oauthrequest.from_consumer_and_token |
146 | + return builder(*args, **kwargs) |
147 | + # pylint: enable=W0221 |
148 | + |
149 | + self.patch(oauth, "OAuthRequest", FakeOAuthRequest) |
150 | + |
151 | + authorizer = TimestampedAuthorizer(get_fake_timestamp, "ubuntuone") |
152 | + authorizer.authorizeRequest(fake_uri, "POST", None, {}) |
153 | + call_kwargs = fromcandt_call[0][1] |
154 | + parameters = call_kwargs["parameters"] |
155 | + self.assertEqual(parameters["oauth_timestamp"], fake_timestamp) |
156 | + |
157 | + |
158 | class SSOLoginProcessorTestCase(TestCase, MockerTestCase): |
159 | """Test suite for the SSO login processor.""" |
160 | |
161 | @@ -1426,6 +1460,8 @@ |
162 | return FakedResponse(code=200) |
163 | |
164 | self.patch(urllib2, 'urlopen', fake_it) |
165 | + self.patch(ubuntu_sso.main.timestamp_checker, "get_faithful_time", |
166 | + time.time) |
167 | |
168 | self.client = SSOCredentials(None) |
169 | |
170 | |
171 | === added directory 'ubuntu_sso/utils' |
172 | === added file 'ubuntu_sso/utils/__init__.py' |
173 | --- ubuntu_sso/utils/__init__.py 1970-01-01 00:00:00 +0000 |
174 | +++ ubuntu_sso/utils/__init__.py 2011-11-23 19:50:27 +0000 |
175 | @@ -0,0 +1,81 @@ |
176 | +# -*- coding: utf-8 -*- |
177 | + |
178 | +# Author: Alejandro J. Cura <alecu@canonical.com> |
179 | +# |
180 | +# Copyright 2010, 2011 Canonical Ltd. |
181 | +# |
182 | +# This program is free software: you can redistribute it and/or modify it |
183 | +# under the terms of the GNU General Public License version 3, as published |
184 | +# by the Free Software Foundation. |
185 | +# |
186 | +# This program is distributed in the hope that it will be useful, but |
187 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
188 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
189 | +# PURPOSE. See the GNU General Public License for more details. |
190 | +# |
191 | +# You should have received a copy of the GNU General Public License along |
192 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
193 | + |
194 | +"""Utility modules that may find use outside ubuntu_sso.""" |
195 | +import time |
196 | +import urllib2 |
197 | +from twisted.web import http |
198 | + |
199 | +from ubuntu_sso.logger import setup_logging |
200 | +logger = setup_logging("ubuntu_sso.utils") |
201 | + |
202 | + |
203 | +class RequestHead(urllib2.Request): |
204 | + """A request with the method set to HEAD.""" |
205 | + |
206 | + _request_method = "HEAD" |
207 | + |
208 | + def get_method(self): |
209 | + """Return the desired method.""" |
210 | + return self._request_method |
211 | + |
212 | + |
213 | +class SyncTimestampChecker(object): |
214 | + """A timestamp that's regularly checked with a server.""" |
215 | + |
216 | + CHECKING_INTERVAL = 60 * 60 # in seconds |
217 | + ERROR_INTERVAL = 30 # in seconds |
218 | + SERVER_URL = "http://one.ubuntu.com/api/time" |
219 | + |
220 | + def __init__(self): |
221 | + """Initialize this instance.""" |
222 | + self.next_check = time.time() |
223 | + self.skew = 0 |
224 | + |
225 | + def get_server_time(self): |
226 | + """Get the time at the server.""" |
227 | + headers = {"Cache-Control": "no-cache"} |
228 | + request = RequestHead(self.SERVER_URL, headers=headers) |
229 | + response = urllib2.urlopen(request) |
230 | + date_string = response.info()["Date"] |
231 | + timestamp = http.stringToDatetime(date_string) |
232 | + return timestamp |
233 | + |
234 | + def get_faithful_time(self): |
235 | + """Get an accurate timestamp.""" |
236 | + local_time = time.time() |
237 | + if local_time >= self.next_check: |
238 | + try: |
239 | + server_time = self.get_server_time() |
240 | + self.next_check = local_time + self.CHECKING_INTERVAL |
241 | + self.skew = server_time - local_time |
242 | + logger.debug("Calculated server-local time skew: %r", |
243 | + self.skew) |
244 | + #pylint: disable=W0703 |
245 | + except Exception, server_error: |
246 | + logger.debug("Error while verifying the server time skew: %r", |
247 | + server_error) |
248 | + self.next_check = local_time + self.ERROR_INTERVAL |
249 | + logger.debug("Using corrected timestamp: %r", |
250 | + http.datetimeToString(local_time + self.skew)) |
251 | + return int(local_time + self.skew) |
252 | + |
253 | + |
254 | +# pylint: disable=C0103 |
255 | +timestamp_checker = SyncTimestampChecker() |
256 | +# pylint: enable=C0103 |
257 | |
258 | === added directory 'ubuntu_sso/utils/tests' |
259 | === added file 'ubuntu_sso/utils/tests/__init__.py' |
260 | --- ubuntu_sso/utils/tests/__init__.py 1970-01-01 00:00:00 +0000 |
261 | +++ ubuntu_sso/utils/tests/__init__.py 2011-11-23 19:50:27 +0000 |
262 | @@ -0,0 +1,16 @@ |
263 | +# ubuntu_sso - Ubuntu Single Sign On client support for desktop apps |
264 | +# |
265 | +# Copyright 2011 Canonical Ltd. |
266 | +# |
267 | +# This program is free software: you can redistribute it and/or modify it |
268 | +# under the terms of the GNU General Public License version 3, as published |
269 | +# by the Free Software Foundation. |
270 | +# |
271 | +# This program is distributed in the hope that it will be useful, but |
272 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
273 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
274 | +# PURPOSE. See the GNU General Public License for more details. |
275 | +# |
276 | +# You should have received a copy of the GNU General Public License along |
277 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
278 | +"""Ubuntu Single Sign On utils tests.""" |
279 | |
280 | === added file 'ubuntu_sso/utils/tests/test_oauth_headers.py' |
281 | --- ubuntu_sso/utils/tests/test_oauth_headers.py 1970-01-01 00:00:00 +0000 |
282 | +++ ubuntu_sso/utils/tests/test_oauth_headers.py 2011-11-23 19:50:27 +0000 |
283 | @@ -0,0 +1,148 @@ |
284 | +# -*- coding: utf-8 -*- |
285 | + |
286 | +# Author: Alejandro J. Cura <alecu@canonical.com> |
287 | +# |
288 | +# Copyright 2011 Canonical Ltd. |
289 | +# |
290 | +# This program is free software: you can redistribute it and/or modify it |
291 | +# under the terms of the GNU General Public License version 3, as published |
292 | +# by the Free Software Foundation. |
293 | +# |
294 | +# This program is distributed in the hope that it will be useful, but |
295 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
296 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
297 | +# PURPOSE. See the GNU General Public License for more details. |
298 | +# |
299 | +# You should have received a copy of the GNU General Public License along |
300 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
301 | + |
302 | +"""Tests for the oauth_headers helper function.""" |
303 | + |
304 | +import time |
305 | + |
306 | +from twisted.application import internet, service |
307 | +from twisted.internet import defer |
308 | +from twisted.internet.threads import deferToThread |
309 | +from twisted.trial.unittest import TestCase |
310 | +from twisted.web import server, resource |
311 | + |
312 | +from ubuntu_sso.utils import SyncTimestampChecker |
313 | + |
314 | + |
315 | +class RootResource(resource.Resource): |
316 | + """A root resource that logs the number of calls.""" |
317 | + |
318 | + isLeaf = True |
319 | + |
320 | + def __init__(self, *args, **kwargs): |
321 | + """Initialize this fake instance.""" |
322 | + resource.Resource.__init__(self, *args, **kwargs) |
323 | + self.count = 0 |
324 | + self.request_headers = [] |
325 | + |
326 | + # pylint: disable=C0103 |
327 | + def render_HEAD(self, request): |
328 | + """Increase the counter on each render.""" |
329 | + self.count += 1 |
330 | + self.request_headers.append(request.requestHeaders) |
331 | + return "" |
332 | + |
333 | + |
334 | +class MockWebServer(object): |
335 | + """A mock webserver for testing.""" |
336 | + |
337 | + def __init__(self): |
338 | + """Start up this instance.""" |
339 | + # pylint: disable=E1101 |
340 | + self.root = RootResource() |
341 | + site = server.Site(self.root) |
342 | + application = service.Application('web') |
343 | + self.service_collection = service.IServiceCollection(application) |
344 | + self.tcpserver = internet.TCPServer(0, site) |
345 | + self.tcpserver.setServiceParent(self.service_collection) |
346 | + self.service_collection.startService() |
347 | + |
348 | + def get_url(self): |
349 | + """Build the url for this mock server.""" |
350 | + # pylint: disable=W0212 |
351 | + port_num = self.tcpserver._port.getHost().port |
352 | + return "http://localhost:%d/" % port_num |
353 | + |
354 | + def stop(self): |
355 | + """Shut it down.""" |
356 | + # pylint: disable=E1101 |
357 | + self.service_collection.stopService() |
358 | + |
359 | + |
360 | +class FakedError(Exception): |
361 | + """A mock, test, sample, and fake exception.""" |
362 | + |
363 | + |
364 | +class TimestampCheckerTestCase(TestCase): |
365 | + """Tests for the timestamp checker.""" |
366 | + |
367 | + def setUp(self): |
368 | + """Initialize a fake webserver.""" |
369 | + self.ws = MockWebServer() |
370 | + self.addCleanup(self.ws.stop) |
371 | + self.patch(SyncTimestampChecker, "SERVER_URL", self.ws.get_url()) |
372 | + |
373 | + @defer.inlineCallbacks |
374 | + def test_returned_value_is_int(self): |
375 | + """The returned value is an integer.""" |
376 | + checker = SyncTimestampChecker() |
377 | + timestamp = yield deferToThread(checker.get_faithful_time) |
378 | + self.assertEqual(type(timestamp), int) |
379 | + |
380 | + @defer.inlineCallbacks |
381 | + def test_first_call_does_head(self): |
382 | + """The first call gets the clock from our web.""" |
383 | + checker = SyncTimestampChecker() |
384 | + yield deferToThread(checker.get_faithful_time) |
385 | + self.assertEqual(self.ws.root.count, 1) |
386 | + |
387 | + @defer.inlineCallbacks |
388 | + def test_second_call_is_cached(self): |
389 | + """For the second call, the time is cached.""" |
390 | + checker = SyncTimestampChecker() |
391 | + yield deferToThread(checker.get_faithful_time) |
392 | + yield deferToThread(checker.get_faithful_time) |
393 | + self.assertEqual(self.ws.root.count, 1) |
394 | + |
395 | + @defer.inlineCallbacks |
396 | + def test_after_timeout_cache_expires(self): |
397 | + """After some time, the cache expires.""" |
398 | + fake_timestamp = 1 |
399 | + self.patch(time, "time", lambda: fake_timestamp) |
400 | + checker = SyncTimestampChecker() |
401 | + yield deferToThread(checker.get_faithful_time) |
402 | + fake_timestamp += SyncTimestampChecker.CHECKING_INTERVAL |
403 | + yield deferToThread(checker.get_faithful_time) |
404 | + self.assertEqual(self.ws.root.count, 2) |
405 | + |
406 | + @defer.inlineCallbacks |
407 | + def test_server_date_sends_nocache_headers(self): |
408 | + """Getting the server date sends the no-cache headers.""" |
409 | + checker = SyncTimestampChecker() |
410 | + yield deferToThread(checker.get_server_time) |
411 | + assert len(self.ws.root.request_headers) == 1 |
412 | + headers = self.ws.root.request_headers[0] |
413 | + result = headers.getRawHeaders("Cache-Control") |
414 | + self.assertEqual(result, ["no-cache"]) |
415 | + |
416 | + @defer.inlineCallbacks |
417 | + def test_server_error_means_skew_not_updated(self): |
418 | + """When server can't be reached, the skew is not updated.""" |
419 | + fake_timestamp = 1 |
420 | + self.patch(time, "time", lambda: fake_timestamp) |
421 | + checker = SyncTimestampChecker() |
422 | + |
423 | + def failing_get_server_time(): |
424 | + """Let's fail while retrieving the server time.""" |
425 | + raise FakedError() |
426 | + |
427 | + self.patch(checker, "get_server_time", failing_get_server_time) |
428 | + yield deferToThread(checker.get_faithful_time) |
429 | + self.assertEqual(checker.skew, 0) |
430 | + self.assertEqual(checker.next_check, |
431 | + fake_timestamp + SyncTimestampChecker.ERROR_INTERVAL) |
Text conflict in bin/ubuntu- sso-login sso/gtk/ gui.py sso/gtk/ tests/test_ gui.py sso/keyring/ linux.py sso/keyring/ tests/test_ linux.py sso/main/ linux.py sso/main/ tests/test_ linux.py sso/utils. moved.
Text conflict in data/gtk/ui.glade
Text conflict in run-tests
Text conflict in setup.py
Text conflict in ubuntu_
Text conflict in ubuntu_
Text conflict in ubuntu_
Text conflict in ubuntu_
Text conflict in ubuntu_
Text conflict in ubuntu_
Conflict adding file ubuntu_sso/utils. Moved existing file to ubuntu_
11 conflicts encountered.