Merge lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-2 into lp:ubuntu-sso-client/stable-1-2

Proposed by Alejandro J. Cura on 2011-11-18
Status: Merged
Approved by: Natalia Bidart on 2011-12-02
Approved revision: 692
Merged at revision: 689
Proposed branch: lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-2
Merge into: lp:ubuntu-sso-client/stable-1-2
Diff against target: 484 lines (+318/-20)
6 files modified
ubuntu_sso/account.py (+33/-7)
ubuntu_sso/credentials.py (+10/-3)
ubuntu_sso/tests/test_account.py (+60/-8)
ubuntu_sso/tests/test_credentials.py (+4/-1)
ubuntu_sso/utils/__init__.py (+63/-1)
ubuntu_sso/utils/tests/test_oauth_headers.py (+148/-0)
To merge this branch: bzr merge lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-2
Reviewer Review Type Date Requested Status
Natalia Bidart Approve on 2011-12-02
Diego Sarmentero (community) 2011-11-18 Abstain on 2011-11-23
Review via email: mp+82685@code.launchpad.net

This proposal supersedes a proposal from 2011-11-18.

Commit message

Do a HEAD request on the server to get accurate timestamp (LP: #692597 & LP: #891644)

Description of the change

Do a HEAD request on the server to get accurate timestamp (LP: #692597 & LP: #891644)

To post a comment you must log in.
Diego Sarmentero (diegosarmentero) wrote :

Text conflict in setup.py
Text conflict in ubuntu_sso/account.py
Text conflict in ubuntu_sso/credentials.py
Text conflict in ubuntu_sso/tests/test_credentials.py
Text conflict in ubuntu_sso/utils/__init__.py
Text conflict in ubuntu_sso/utils/tests/test_oauth_headers.py
Text conflict in ubuntu_sso/utils/tests/test_txsecrets.py
7 conflicts encountered.

review: Needs Fixing
review: Abstain
692. By Alejandro J. Cura on 2011-11-23

fix year in file headers

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/account.py'
2--- ubuntu_sso/account.py 2011-03-16 01:36:07 +0000
3+++ ubuntu_sso/account.py 2011-11-23 19:52:23 +0000
4@@ -1,8 +1,9 @@
5 # -*- coding: utf-8 -*-
6
7 # Author: Natalia Bidart <natalia.bidart@canonical.com>
8+# Author: Alejandro J. Cura <alecu@canonical.com>
9 #
10-# Copyright 2010 Canonical Ltd.
11+# Copyright 2010, 2011 Canonical Ltd.
12 #
13 # This program is free software: you can redistribute it and/or modify it
14 # under the terms of the GNU General Public License version 3, as published
15@@ -31,6 +32,7 @@
16 from oauth import oauth
17
18 from ubuntu_sso.logger import setup_logging
19+from ubuntu_sso.utils import timestamp_checker
20
21
22 logger = setup_logging("ubuntu_sso.account")
23@@ -39,6 +41,27 @@
24 SSO_STATUS_ERROR = 'error'
25
26
27+class TimestampedAuthorizer(OAuthAuthorizer):
28+ """Includes a custom timestamp on OAuth signatures."""
29+
30+ def __init__(self, get_timestamp, *args, **kwargs):
31+ """Store the get_timestamp method, and move on."""
32+ OAuthAuthorizer.__init__(self, *args, **kwargs)
33+ self.get_timestamp = get_timestamp
34+
35+ # pylint: disable=C0103,E1101
36+ def authorizeRequest(self, absolute_uri, method, body, headers):
37+ """Override authorizeRequest including the timestamp."""
38+ parameters = {"oauth_timestamp": self.get_timestamp()}
39+ oauth_request = oauth.OAuthRequest.from_consumer_and_token(
40+ self.consumer, self.access_token, http_url=absolute_uri,
41+ parameters=parameters)
42+ oauth_request.sign_request(
43+ oauth.OAuthSignatureMethod_PLAINTEXT(),
44+ self.consumer, self.access_token)
45+ headers.update(oauth_request.to_header(self.oauth_realm))
46+
47+
48 class InvalidEmailError(Exception):
49 """The email is not valid."""
50
51@@ -181,9 +204,11 @@
52 if sso_service is None:
53 oauth_token = oauth.OAuthToken(token['token'],
54 token['token_secret'])
55- authorizer = OAuthAuthorizer(token['consumer_key'],
56- token['consumer_secret'],
57- oauth_token)
58+ authorizer = TimestampedAuthorizer(
59+ timestamp_checker.get_faithful_time,
60+ token['consumer_key'],
61+ token['consumer_secret'],
62+ oauth_token)
63 sso_service = self.sso_service_class(authorizer, self.service_url)
64
65 me_info = sso_service.accounts.me()
66@@ -203,9 +228,10 @@
67 token_name=token_name)
68
69 oauth_token = oauth.OAuthToken(token['token'], token['token_secret'])
70- authorizer = OAuthAuthorizer(token['consumer_key'],
71- token['consumer_secret'],
72- oauth_token)
73+ authorizer = TimestampedAuthorizer(timestamp_checker.get_faithful_time,
74+ token['consumer_key'],
75+ token['consumer_secret'],
76+ oauth_token)
77 sso_service = self.sso_service_class(authorizer, self.service_url)
78 result = sso_service.accounts.validate_email(email_token=email_token)
79 logger.info('validate_email: email: %r result: %r', email, result)
80
81=== modified file 'ubuntu_sso/credentials.py'
82--- ubuntu_sso/credentials.py 2011-01-02 03:34:23 +0000
83+++ ubuntu_sso/credentials.py 2011-11-23 19:52:23 +0000
84@@ -3,7 +3,7 @@
85 # Author: Natalia Bidart <natalia.bidart@canonical.com>
86 # Author: Alejandro J. Cura <alecu@canonical.com>
87 #
88-# Copyright 2010 Canonical Ltd.
89+# Copyright 2010, 2011 Canonical Ltd.
90 #
91 # This program is free software: you can redistribute it and/or modify it
92 # under the terms of the GNU General Public License version 3, as published
93@@ -46,7 +46,7 @@
94 from oauth import oauth
95 from twisted.internet.defer import inlineCallbacks, returnValue
96
97-from ubuntu_sso import NO_OP
98+from ubuntu_sso import NO_OP, utils
99 from ubuntu_sso.keyring import Keyring
100 from ubuntu_sso.logger import setup_logging
101
102@@ -217,6 +217,10 @@
103 logger.warning('Login/registration was denied to app %r', app_name)
104 self.denial_cb(app_name)
105
106+ def _get_timestamp(self):
107+ """Get the timestamp calculated from the server."""
108+ return utils.timestamp_checker.get_faithful_time()
109+
110 @handle_failures(msg='Problem opening the ping_url')
111 @inlineCallbacks
112 def _ping_url(self, app_name, email, credentials):
113@@ -233,9 +237,12 @@
114 credentials['consumer_secret'])
115 token = oauth.OAuthToken(credentials['token'],
116 credentials['token_secret'])
117+ timestamp = self._get_timestamp()
118+ parameters = {"oauth_timestamp": timestamp}
119 get_request = oauth.OAuthRequest.from_consumer_and_token
120 oauth_req = get_request(oauth_consumer=consumer, token=token,
121- http_method='GET', http_url=url)
122+ http_method='GET', http_url=url,
123+ parameters=parameters)
124 oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
125 consumer, token)
126 request = urllib2.Request(url, headers=oauth_req.to_header())
127
128=== modified file 'ubuntu_sso/tests/test_account.py'
129--- ubuntu_sso/tests/test_account.py 2011-03-16 01:36:07 +0000
130+++ ubuntu_sso/tests/test_account.py 2011-11-23 19:52:23 +0000
131@@ -1,8 +1,9 @@
132 # -*- coding: utf-8 -*-
133 #
134 # Author: Natalia Bidart <natalia.bidart@canonical.com>
135+# Author: Alejandro J. Cura <alecu@canonical.com>
136 #
137-# Copyright 2010 Canonical Ltd.
138+# Copyright 2010, 2011 Canonical Ltd.
139 #
140 # This program is free software: you can redistribute it and/or modify it
141 # under the terms of the GNU General Public License version 3, as published
142@@ -23,15 +24,36 @@
143 # pylint: disable=F0401
144 from lazr.restfulclient.errors import HTTPError
145 # pylint: enable=F0401
146+from oauth import oauth
147 from twisted.trial.unittest import TestCase
148
149-from ubuntu_sso.account import (Account, AuthenticationError, EmailTokenError,
150- InvalidEmailError, InvalidPasswordError, NewPasswordError, SERVICE_URL,
151- RegistrationError, ResetPasswordTokenError,
152- SSO_STATUS_OK, SSO_STATUS_ERROR)
153-from ubuntu_sso.tests import (APP_NAME, CAPTCHA_ID, CAPTCHA_PATH,
154- CAPTCHA_SOLUTION, EMAIL, EMAIL_TOKEN, NAME, PASSWORD, RESET_PASSWORD_TOKEN,
155- TOKEN, TOKEN_NAME)
156+from ubuntu_sso.account import (
157+ Account,
158+ AuthenticationError,
159+ EmailTokenError,
160+ InvalidEmailError,
161+ InvalidPasswordError,
162+ NewPasswordError,
163+ SERVICE_URL,
164+ RegistrationError,
165+ ResetPasswordTokenError,
166+ SSO_STATUS_OK,
167+ SSO_STATUS_ERROR,
168+ TimestampedAuthorizer,
169+)
170+from ubuntu_sso.tests import (
171+ APP_NAME,
172+ CAPTCHA_ID,
173+ CAPTCHA_PATH,
174+ CAPTCHA_SOLUTION,
175+ EMAIL,
176+ EMAIL_TOKEN,
177+ NAME,
178+ PASSWORD,
179+ RESET_PASSWORD_TOKEN,
180+ TOKEN,
181+ TOKEN_NAME,
182+)
183
184
185 CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \
186@@ -142,6 +164,36 @@
187 self.accounts = FakedAccounts()
188
189
190+class TimestampedAuthorizerTestCase(TestCase):
191+ """Test suite for the TimestampedAuthorizer."""
192+
193+ def test_authorize_request_includes_timestamp(self):
194+ """The authorizeRequest method includes the timestamp."""
195+ fromcandt_call = []
196+ fake_uri = "http://protocultura.net"
197+ fake_timestamp = 1234
198+ get_fake_timestamp = lambda: fake_timestamp
199+ original_oauthrequest = oauth.OAuthRequest
200+
201+ class FakeOAuthRequest(oauth.OAuthRequest):
202+ """A Fake OAuthRequest class."""
203+
204+ @staticmethod
205+ def from_consumer_and_token(*args, **kwargs):
206+ """A fake from_consumer_and_token."""
207+ fromcandt_call.append((args, kwargs))
208+ builder = original_oauthrequest.from_consumer_and_token
209+ return builder(*args, **kwargs)
210+
211+ self.patch(oauth, "OAuthRequest", FakeOAuthRequest)
212+
213+ authorizer = TimestampedAuthorizer(get_fake_timestamp, "ubuntuone")
214+ authorizer.authorizeRequest(fake_uri, "POST", None, {})
215+ call_kwargs = fromcandt_call[0][1]
216+ parameters = call_kwargs["parameters"]
217+ self.assertEqual(parameters["oauth_timestamp"], fake_timestamp)
218+
219+
220 class AccountTestCase(TestCase):
221 """Test suite for the SSO login processor."""
222
223
224=== modified file 'ubuntu_sso/tests/test_credentials.py'
225--- ubuntu_sso/tests/test_credentials.py 2011-01-02 03:34:23 +0000
226+++ ubuntu_sso/tests/test_credentials.py 2011-11-23 19:52:23 +0000
227@@ -3,7 +3,7 @@
228 # Author: Natalia Bidart <natalia.bidart@canonical.com>
229 # Author: Alejandro J. Cura <alecu@canonical.com>
230 #
231-# Copyright 2010 Canonical Ltd.
232+# Copyright 2010, 2011 Canonical Ltd.
233 #
234 # This program is free software: you can redistribute it and/or modify it
235 # under the terms of the GNU General Public License version 3, as published
236@@ -20,6 +20,7 @@
237
238 import logging
239 import urllib
240+import time
241
242 from twisted.internet import defer
243 from twisted.internet.defer import inlineCallbacks
244@@ -359,6 +360,8 @@
245 return response
246
247 self.patch(credentials.urllib2, 'urlopen', faked_urlopen)
248+ self.patch(credentials.utils.timestamp_checker, "get_faithful_time",
249+ time.time)
250
251 @inlineCallbacks
252 def test_ping_url_if_url_is_none(self):
253
254=== modified file 'ubuntu_sso/utils/__init__.py'
255--- ubuntu_sso/utils/__init__.py 2010-11-15 23:06:52 +0000
256+++ ubuntu_sso/utils/__init__.py 2011-11-23 19:52:23 +0000
257@@ -2,7 +2,7 @@
258
259 # Author: Alejandro J. Cura <alecu@canonical.com>
260 #
261-# Copyright 2010 Canonical Ltd.
262+# Copyright 2010, 2011 Canonical Ltd.
263 #
264 # This program is free software: you can redistribute it and/or modify it
265 # under the terms of the GNU General Public License version 3, as published
266@@ -17,3 +17,65 @@
267 # with this program. If not, see <http://www.gnu.org/licenses/>.
268
269 """Utility modules that may find use outside ubuntu_sso."""
270+import time
271+import urllib2
272+from twisted.web import http
273+
274+from ubuntu_sso.logger import setup_logging
275+logger = setup_logging("ubuntu_sso.utils")
276+
277+
278+class RequestHead(urllib2.Request):
279+ """A request with the method set to HEAD."""
280+
281+ _request_method = "HEAD"
282+
283+ def get_method(self):
284+ """Return the desired method."""
285+ return self._request_method
286+
287+
288+class SyncTimestampChecker(object):
289+ """A timestamp that's regularly checked with a server."""
290+
291+ CHECKING_INTERVAL = 60 * 60 # in seconds
292+ ERROR_INTERVAL = 30 # in seconds
293+ SERVER_URL = "http://one.ubuntu.com/api/time"
294+
295+ def __init__(self):
296+ """Initialize this instance."""
297+ self.next_check = time.time()
298+ self.skew = 0
299+
300+ def get_server_time(self):
301+ """Get the time at the server."""
302+ headers = {"Cache-Control": "no-cache"}
303+ request = RequestHead(self.SERVER_URL, headers=headers)
304+ response = urllib2.urlopen(request)
305+ date_string = response.info()["Date"]
306+ timestamp = http.stringToDatetime(date_string)
307+ return timestamp
308+
309+ def get_faithful_time(self):
310+ """Get an accurate timestamp."""
311+ local_time = time.time()
312+ if local_time >= self.next_check:
313+ try:
314+ server_time = self.get_server_time()
315+ self.next_check = local_time + self.CHECKING_INTERVAL
316+ self.skew = server_time - local_time
317+ logger.debug("Calculated server-local time skew: %r",
318+ self.skew)
319+ #pylint: disable=W0703
320+ except Exception, server_error:
321+ logger.debug("Error while verifying the server time skew: %r",
322+ server_error)
323+ self.next_check = local_time + self.ERROR_INTERVAL
324+ logger.debug("Using corrected timestamp: %r",
325+ http.datetimeToString(local_time + self.skew))
326+ return int(local_time + self.skew)
327+
328+
329+# pylint: disable=C0103
330+timestamp_checker = SyncTimestampChecker()
331+# pylint: enable=C0103
332
333=== added file 'ubuntu_sso/utils/tests/test_oauth_headers.py'
334--- ubuntu_sso/utils/tests/test_oauth_headers.py 1970-01-01 00:00:00 +0000
335+++ ubuntu_sso/utils/tests/test_oauth_headers.py 2011-11-23 19:52:23 +0000
336@@ -0,0 +1,148 @@
337+# -*- coding: utf-8 -*-
338+
339+# Author: Alejandro J. Cura <alecu@canonical.com>
340+#
341+# Copyright 2011 Canonical Ltd.
342+#
343+# This program is free software: you can redistribute it and/or modify it
344+# under the terms of the GNU General Public License version 3, as published
345+# by the Free Software Foundation.
346+#
347+# This program is distributed in the hope that it will be useful, but
348+# WITHOUT ANY WARRANTY; without even the implied warranties of
349+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
350+# PURPOSE. See the GNU General Public License for more details.
351+#
352+# You should have received a copy of the GNU General Public License along
353+# with this program. If not, see <http://www.gnu.org/licenses/>.
354+
355+"""Tests for the oauth_headers helper function."""
356+
357+import time
358+
359+from twisted.application import internet, service
360+from twisted.internet import defer
361+from twisted.internet.threads import deferToThread
362+from twisted.trial.unittest import TestCase
363+from twisted.web import server, resource
364+
365+from ubuntu_sso.utils import SyncTimestampChecker
366+
367+
368+class RootResource(resource.Resource):
369+ """A root resource that logs the number of calls."""
370+
371+ isLeaf = True
372+
373+ def __init__(self, *args, **kwargs):
374+ """Initialize this fake instance."""
375+ resource.Resource.__init__(self, *args, **kwargs)
376+ self.count = 0
377+ self.request_headers = []
378+
379+ # pylint: disable=C0103
380+ def render_HEAD(self, request):
381+ """Increase the counter on each render."""
382+ self.count += 1
383+ self.request_headers.append(request.requestHeaders)
384+ return ""
385+
386+
387+class MockWebServer(object):
388+ """A mock webserver for testing."""
389+
390+ def __init__(self):
391+ """Start up this instance."""
392+ # pylint: disable=E1101
393+ self.root = RootResource()
394+ site = server.Site(self.root)
395+ application = service.Application('web')
396+ self.service_collection = service.IServiceCollection(application)
397+ self.tcpserver = internet.TCPServer(0, site)
398+ self.tcpserver.setServiceParent(self.service_collection)
399+ self.service_collection.startService()
400+
401+ def get_url(self):
402+ """Build the url for this mock server."""
403+ # pylint: disable=W0212
404+ port_num = self.tcpserver._port.getHost().port
405+ return "http://localhost:%d/" % port_num
406+
407+ def stop(self):
408+ """Shut it down."""
409+ # pylint: disable=E1101
410+ self.service_collection.stopService()
411+
412+
413+class FakedError(Exception):
414+ """A mock, test, sample, and fake exception."""
415+
416+
417+class TimestampCheckerTestCase(TestCase):
418+ """Tests for the timestamp checker."""
419+
420+ def setUp(self):
421+ """Initialize a fake webserver."""
422+ self.ws = MockWebServer()
423+ self.addCleanup(self.ws.stop)
424+ self.patch(SyncTimestampChecker, "SERVER_URL", self.ws.get_url())
425+
426+ @defer.inlineCallbacks
427+ def test_returned_value_is_int(self):
428+ """The returned value is an integer."""
429+ checker = SyncTimestampChecker()
430+ timestamp = yield deferToThread(checker.get_faithful_time)
431+ self.assertEqual(type(timestamp), int)
432+
433+ @defer.inlineCallbacks
434+ def test_first_call_does_head(self):
435+ """The first call gets the clock from our web."""
436+ checker = SyncTimestampChecker()
437+ yield deferToThread(checker.get_faithful_time)
438+ self.assertEqual(self.ws.root.count, 1)
439+
440+ @defer.inlineCallbacks
441+ def test_second_call_is_cached(self):
442+ """For the second call, the time is cached."""
443+ checker = SyncTimestampChecker()
444+ yield deferToThread(checker.get_faithful_time)
445+ yield deferToThread(checker.get_faithful_time)
446+ self.assertEqual(self.ws.root.count, 1)
447+
448+ @defer.inlineCallbacks
449+ def test_after_timeout_cache_expires(self):
450+ """After some time, the cache expires."""
451+ fake_timestamp = 1
452+ self.patch(time, "time", lambda: fake_timestamp)
453+ checker = SyncTimestampChecker()
454+ yield deferToThread(checker.get_faithful_time)
455+ fake_timestamp += SyncTimestampChecker.CHECKING_INTERVAL
456+ yield deferToThread(checker.get_faithful_time)
457+ self.assertEqual(self.ws.root.count, 2)
458+
459+ @defer.inlineCallbacks
460+ def test_server_date_sends_nocache_headers(self):
461+ """Getting the server date sends the no-cache headers."""
462+ checker = SyncTimestampChecker()
463+ yield deferToThread(checker.get_server_time)
464+ assert len(self.ws.root.request_headers) == 1
465+ headers = self.ws.root.request_headers[0]
466+ result = headers.getRawHeaders("Cache-Control")
467+ self.assertEqual(result, ["no-cache"])
468+
469+ @defer.inlineCallbacks
470+ def test_server_error_means_skew_not_updated(self):
471+ """When server can't be reached, the skew is not updated."""
472+ fake_timestamp = 1
473+ self.patch(time, "time", lambda: fake_timestamp)
474+ checker = SyncTimestampChecker()
475+
476+ def failing_get_server_time():
477+ """Let's fail while retrieving the server time."""
478+ raise FakedError()
479+
480+ self.patch(checker, "get_server_time", failing_get_server_time)
481+ yield deferToThread(checker.get_faithful_time)
482+ self.assertEqual(checker.skew, 0)
483+ self.assertEqual(checker.next_check,
484+ fake_timestamp + SyncTimestampChecker.ERROR_INTERVAL)

Subscribers

People subscribed via source and target branches