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

Proposed by Alejandro J. Cura
Status: Merged
Approved by: Alejandro J. Cura
Approved revision: 796
Merged at revision: 802
Proposed branch: lp:~alecu/ubuntu-sso-client/timestamp-autofix
Merge into: lp:ubuntu-sso-client
Diff against target: 469 lines (+310/-17)
5 files modified
ubuntu_sso/account.py (+32/-6)
ubuntu_sso/tests/test_account.py (+59/-7)
ubuntu_sso/tests/test_credentials.py (+3/-0)
ubuntu_sso/utils/__init__.py (+64/-1)
ubuntu_sso/utils/tests/test_oauth_headers.py (+152/-3)
To merge this branch: bzr merge lp:~alecu/ubuntu-sso-client/timestamp-autofix
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Diego Sarmentero (community) Approve
Review via email: mp+78507@code.launchpad.net

Commit message

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

Description of the change

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

To post a comment you must log in.
796. By Alejandro J. Cura

fixing punctuation.

Revision history for this message
Diego Sarmentero (diegosarmentero) wrote :

+1

review: Approve
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Looks good, I have the same concerns I pointed out in the storage-protocol branch.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'ubuntu_sso/account.py'
--- ubuntu_sso/account.py 2011-03-16 01:36:07 +0000
+++ ubuntu_sso/account.py 2011-10-06 21:50:30 +0000
@@ -1,6 +1,7 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
22
3# Author: Natalia Bidart <natalia.bidart@canonical.com>3# Author: Natalia Bidart <natalia.bidart@canonical.com>
4# Author: Alejandro J. Cura <alecu@canonical.com>
4#5#
5# Copyright 2010 Canonical Ltd.6# Copyright 2010 Canonical Ltd.
6#7#
@@ -31,6 +32,7 @@
31from oauth import oauth32from oauth import oauth
3233
33from ubuntu_sso.logger import setup_logging34from ubuntu_sso.logger import setup_logging
35from ubuntu_sso.utils import timestamp_checker
3436
3537
36logger = setup_logging("ubuntu_sso.account")38logger = setup_logging("ubuntu_sso.account")
@@ -39,6 +41,27 @@
39SSO_STATUS_ERROR = 'error'41SSO_STATUS_ERROR = 'error'
4042
4143
44class TimestampedAuthorizer(OAuthAuthorizer):
45 """Includes a custom timestamp on OAuth signatures."""
46
47 def __init__(self, get_timestamp, *args, **kwargs):
48 """Store the get_timestamp method, and move on."""
49 super(TimestampedAuthorizer, self).__init__(*args, **kwargs)
50 self.get_timestamp = get_timestamp
51
52 # pylint: disable=C0103
53 def authorizeRequest(self, absolute_uri, method, body, headers):
54 """Override authorizeRequest including the timestamp."""
55 parameters = {"oauth_timestamp": self.get_timestamp()}
56 oauth_request = oauth.OAuthRequest.from_consumer_and_token(
57 self.consumer, self.access_token, http_url=absolute_uri,
58 parameters=parameters)
59 oauth_request.sign_request(
60 oauth.OAuthSignatureMethod_PLAINTEXT(),
61 self.consumer, self.access_token)
62 headers.update(oauth_request.to_header(self.oauth_realm))
63
64
42class InvalidEmailError(Exception):65class InvalidEmailError(Exception):
43 """The email is not valid."""66 """The email is not valid."""
4467
@@ -181,9 +204,11 @@
181 if sso_service is None:204 if sso_service is None:
182 oauth_token = oauth.OAuthToken(token['token'],205 oauth_token = oauth.OAuthToken(token['token'],
183 token['token_secret'])206 token['token_secret'])
184 authorizer = OAuthAuthorizer(token['consumer_key'],207 authorizer = TimestampedAuthorizer(
185 token['consumer_secret'],208 timestamp_checker.get_faithful_time,
186 oauth_token)209 token['consumer_key'],
210 token['consumer_secret'],
211 oauth_token)
187 sso_service = self.sso_service_class(authorizer, self.service_url)212 sso_service = self.sso_service_class(authorizer, self.service_url)
188213
189 me_info = sso_service.accounts.me()214 me_info = sso_service.accounts.me()
@@ -203,9 +228,10 @@
203 token_name=token_name)228 token_name=token_name)
204229
205 oauth_token = oauth.OAuthToken(token['token'], token['token_secret'])230 oauth_token = oauth.OAuthToken(token['token'], token['token_secret'])
206 authorizer = OAuthAuthorizer(token['consumer_key'],231 authorizer = TimestampedAuthorizer(timestamp_checker.get_faithful_time,
207 token['consumer_secret'],232 token['consumer_key'],
208 oauth_token)233 token['consumer_secret'],
234 oauth_token)
209 sso_service = self.sso_service_class(authorizer, self.service_url)235 sso_service = self.sso_service_class(authorizer, self.service_url)
210 result = sso_service.accounts.validate_email(email_token=email_token)236 result = sso_service.accounts.validate_email(email_token=email_token)
211 logger.info('validate_email: email: %r result: %r', email, result)237 logger.info('validate_email: email: %r result: %r', email, result)
212238
=== modified file 'ubuntu_sso/tests/test_account.py'
--- ubuntu_sso/tests/test_account.py 2011-09-02 13:05:06 +0000
+++ ubuntu_sso/tests/test_account.py 2011-10-06 21:50:30 +0000
@@ -1,6 +1,7 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2#2#
3# Author: Natalia Bidart <natalia.bidart@canonical.com>3# Author: Natalia Bidart <natalia.bidart@canonical.com>
4# Author: Alejandro J. Cura <alecu@canonical.com>
4#5#
5# Copyright 2010 Canonical Ltd.6# Copyright 2010 Canonical Ltd.
6#7#
@@ -24,15 +25,36 @@
24# pylint: disable=F040125# pylint: disable=F0401
25from lazr.restfulclient.errors import HTTPError26from lazr.restfulclient.errors import HTTPError
26# pylint: enable=F040127# pylint: enable=F0401
28from oauth import oauth
27from twisted.trial.unittest import TestCase29from twisted.trial.unittest import TestCase
2830
29from ubuntu_sso.account import (Account, AuthenticationError, EmailTokenError,31from ubuntu_sso.account import (
30 InvalidEmailError, InvalidPasswordError, NewPasswordError, SERVICE_URL,32 Account,
31 RegistrationError, ResetPasswordTokenError,33 AuthenticationError,
32 SSO_STATUS_OK, SSO_STATUS_ERROR)34 EmailTokenError,
33from ubuntu_sso.tests import (APP_NAME, CAPTCHA_ID, CAPTCHA_PATH,35 InvalidEmailError,
34 CAPTCHA_SOLUTION, EMAIL, EMAIL_TOKEN, NAME, PASSWORD, RESET_PASSWORD_TOKEN,36 InvalidPasswordError,
35 TOKEN, TOKEN_NAME)37 NewPasswordError,
38 SERVICE_URL,
39 RegistrationError,
40 ResetPasswordTokenError,
41 SSO_STATUS_OK,
42 SSO_STATUS_ERROR,
43 TimestampedAuthorizer,
44)
45from ubuntu_sso.tests import (
46 APP_NAME,
47 CAPTCHA_ID,
48 CAPTCHA_PATH,
49 CAPTCHA_SOLUTION,
50 EMAIL,
51 EMAIL_TOKEN,
52 NAME,
53 PASSWORD,
54 RESET_PASSWORD_TOKEN,
55 TOKEN,
56 TOKEN_NAME,
57)
3658
3759
38CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \60CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \
@@ -145,6 +167,36 @@
145 self.accounts = FakedAccounts()167 self.accounts = FakedAccounts()
146168
147169
170class TimestampedAuthorizerTestCase(TestCase):
171 """Test suite for the TimestampedAuthorizer."""
172
173 def test_authorize_request_includes_timestamp(self):
174 """The authorizeRequest method includes the timestamp."""
175 fromcandt_call = []
176 fake_uri = "http://protocultura.net"
177 fake_timestamp = 1234
178 get_fake_timestamp = lambda: fake_timestamp
179 original_oauthrequest = oauth.OAuthRequest
180
181 class FakeOAuthRequest(oauth.OAuthRequest):
182 """A Fake OAuthRequest class."""
183
184 @staticmethod
185 def from_consumer_and_token(*args, **kwargs):
186 """A fake from_consumer_and_token."""
187 fromcandt_call.append((args, kwargs))
188 builder = original_oauthrequest.from_consumer_and_token
189 return builder(*args, **kwargs)
190
191 self.patch(oauth, "OAuthRequest", FakeOAuthRequest)
192
193 authorizer = TimestampedAuthorizer(get_fake_timestamp, "ubuntuone")
194 authorizer.authorizeRequest(fake_uri, "POST", None, {})
195 call_kwargs = fromcandt_call[0][1]
196 parameters = call_kwargs["parameters"]
197 self.assertEqual(parameters["oauth_timestamp"], fake_timestamp)
198
199
148class AccountTestCase(TestCase):200class AccountTestCase(TestCase):
149 """Test suite for the SSO login processor."""201 """Test suite for the SSO login processor."""
150202
151203
=== modified file 'ubuntu_sso/tests/test_credentials.py'
--- ubuntu_sso/tests/test_credentials.py 2011-08-22 16:21:36 +0000
+++ ubuntu_sso/tests/test_credentials.py 2011-10-06 21:50:30 +0000
@@ -20,6 +20,7 @@
2020
21import logging21import logging
22import os22import os
23import time
23import urllib224import urllib2
2425
25from twisted.internet import defer26from twisted.internet import defer
@@ -373,6 +374,8 @@
373 return response374 return response
374375
375 self.patch(credentials.urllib2, 'urlopen', faked_urlopen)376 self.patch(credentials.urllib2, 'urlopen', faked_urlopen)
377 self.patch(credentials.utils.timestamp_checker, "get_faithful_time",
378 time.time)
376379
377 @defer.inlineCallbacks380 @defer.inlineCallbacks
378 def test_ping_url_if_url_is_none(self):381 def test_ping_url_if_url_is_none(self):
379382
=== modified file 'ubuntu_sso/utils/__init__.py'
--- ubuntu_sso/utils/__init__.py 2011-08-12 12:54:36 +0000
+++ ubuntu_sso/utils/__init__.py 2011-10-06 21:50:30 +0000
@@ -2,7 +2,7 @@
22
3# Author: Alejandro J. Cura <alecu@canonical.com>3# Author: Alejandro J. Cura <alecu@canonical.com>
4#4#
5# Copyright 2010 Canonical Ltd.5# Copyright 2010, 2011 Canonical Ltd.
6#6#
7# This program is free software: you can redistribute it and/or modify it7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published8# under the terms of the GNU General Public License version 3, as published
@@ -19,9 +19,71 @@
19"""Utility modules that may find use outside ubuntu_sso."""19"""Utility modules that may find use outside ubuntu_sso."""
2020
21import cgi21import cgi
22import time
23import urllib2
2224
23from oauth import oauth25from oauth import oauth
24from urlparse import urlparse26from urlparse import urlparse
27from twisted.web import http
28
29from ubuntu_sso.logger import setup_logging
30logger = setup_logging("ubuntu_sso.utils")
31
32
33class RequestHead(urllib2.Request):
34 """A request with the method set to HEAD."""
35
36 _request_method = "HEAD"
37
38 def get_method(self):
39 """Return the desired method."""
40 return self._request_method
41
42
43class SyncTimestampChecker(object):
44 """A timestamp that's regularly checked with a server."""
45
46 CHECKING_INTERVAL = 60 * 60 # in seconds
47 ERROR_INTERVAL = 30 # in seconds
48 SERVER_URL = "http://one.ubuntu.com/"
49
50 def __init__(self):
51 """Initialize this instance."""
52 self.next_check = time.time()
53 self.skew = 0
54
55 def get_server_time(self):
56 """Get the time at the server."""
57 headers = {"Cache-Control": "no-cache"}
58 request = RequestHead(self.SERVER_URL, headers=headers)
59 response = urllib2.urlopen(request)
60 date_string = response.info()["Date"]
61 timestamp = http.stringToDatetime(date_string)
62 return timestamp
63
64 def get_faithful_time(self):
65 """Get an accurate timestamp."""
66 local_time = time.time()
67 if local_time >= self.next_check:
68 try:
69 server_time = self.get_server_time()
70 self.next_check = local_time + self.CHECKING_INTERVAL
71 self.skew = server_time - local_time
72 logger.debug("Calculated server-local time skew: %r",
73 self.skew)
74 #pylint: disable=W0703
75 except Exception, server_error:
76 logger.debug("Error while verifying the server time skew: %r",
77 server_error)
78 self.next_check = local_time + self.ERROR_INTERVAL
79 logger.debug("Using corrected timestamp: %r",
80 http.datetimeToString(local_time + self.skew))
81 return int(local_time + self.skew)
82
83
84# pylint: disable=C0103
85timestamp_checker = SyncTimestampChecker()
86# pylint: enable=C0103
2587
2688
27def oauth_headers(url, credentials, http_method='GET'):89def oauth_headers(url, credentials, http_method='GET'):
@@ -37,6 +99,7 @@
37 url = url.encode('utf-8')99 url = url.encode('utf-8')
38 _, _, _, _, query, _ = urlparse(url)100 _, _, _, _, query, _ = urlparse(url)
39 parameters = dict(cgi.parse_qsl(query))101 parameters = dict(cgi.parse_qsl(query))
102 parameters["oauth_timestamp"] = timestamp_checker.get_faithful_time()
40103
41 consumer = oauth.OAuthConsumer(credentials['consumer_key'],104 consumer = oauth.OAuthConsumer(credentials['consumer_key'],
42 credentials['consumer_secret'])105 credentials['consumer_secret'])
43106
=== modified file 'ubuntu_sso/utils/tests/test_oauth_headers.py'
--- ubuntu_sso/utils/tests/test_oauth_headers.py 2011-08-12 12:54:36 +0000
+++ ubuntu_sso/utils/tests/test_oauth_headers.py 2011-10-06 21:50:30 +0000
@@ -1,6 +1,7 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
22
3# Author: Natalia B. Bidart <natalia.bidart@canonical.com>3# Author: Natalia B. Bidart <natalia.bidart@canonical.com>
4# Author: Alejandro J. Cura <alecu@canonical.com>
4#5#
5# Copyright 2011 Canonical Ltd.6# Copyright 2011 Canonical Ltd.
6#7#
@@ -18,9 +19,16 @@
1819
19"""Tests for the oauth_headers helper function."""20"""Tests for the oauth_headers helper function."""
2021
22import time
23
24from twisted.application import internet, service
25from twisted.internet import defer
26from twisted.internet.threads import deferToThread
21from twisted.trial.unittest import TestCase27from twisted.trial.unittest import TestCase
28from twisted.web import server, resource
2229
23from ubuntu_sso.utils import oauth, oauth_headers30from ubuntu_sso import utils
31from ubuntu_sso.utils import oauth, oauth_headers, SyncTimestampChecker
24from ubuntu_sso.tests import TOKEN32from ubuntu_sso.tests import TOKEN
2533
2634
@@ -44,6 +52,21 @@
44 """Test suite for the oauth_headers method."""52 """Test suite for the oauth_headers method."""
4553
46 url = u'http://example.com'54 url = u'http://example.com'
55 fake_timestamp_value = 1
56
57 @defer.inlineCallbacks
58 def setUp(self):
59 """Initialize this test suite."""
60 yield super(SignWithCredentialsTestCase, self).setUp()
61 self.timestamp_called = False
62
63 def fake_timestamp():
64 """A fake timestamp that records the call."""
65 self.timestamp_called = True
66 return self.fake_timestamp_value
67
68 self.patch(utils.timestamp_checker, "get_faithful_time",
69 fake_timestamp)
4770
48 def build_header(self, url, http_method='GET'):71 def build_header(self, url, http_method='GET'):
49 """Build an Oauth header for comparison."""72 """Build an Oauth header for comparison."""
@@ -105,5 +128,131 @@
105 oauth_headers(url=self.url + path, credentials=TOKEN)128 oauth_headers(url=self.url + path, credentials=TOKEN)
106129
107 self.assertIn('parameters', FakedOAuthRequest.params)130 self.assertIn('parameters', FakedOAuthRequest.params)
108 self.assertEqual(FakedOAuthRequest.params['parameters'],131 params = FakedOAuthRequest.params['parameters']
109 {'foo': 'bar'})132 del(params["oauth_timestamp"])
133 self.assertEqual(params, {'foo': 'bar'})
134
135 def test_oauth_headers_uses_timestamp_checker(self):
136 """The oauth_headers function uses the timestamp_checker."""
137 oauth_headers(u"http://protocultura.net", TOKEN)
138 self.assertTrue(self.timestamp_called,
139 "the timestamp MUST be requested.")
140
141
142class RootResource(resource.Resource):
143 """A root resource that logs the number of calls."""
144
145 isLeaf = True
146
147 def __init__(self, *args, **kwargs):
148 """Initialize this fake instance."""
149 resource.Resource.__init__(self, *args, **kwargs)
150 self.count = 0
151 self.request_headers = []
152
153 # pylint: disable=C0103
154 def render_HEAD(self, request):
155 """Increase the counter on each render."""
156 self.count += 1
157 self.request_headers.append(request.requestHeaders)
158 return ""
159
160
161class MockWebServer(object):
162 """A mock webserver for testing."""
163
164 def __init__(self):
165 """Start up this instance."""
166 # pylint: disable=E1101
167 self.root = RootResource()
168 site = server.Site(self.root)
169 application = service.Application('web')
170 self.service_collection = service.IServiceCollection(application)
171 self.tcpserver = internet.TCPServer(0, site)
172 self.tcpserver.setServiceParent(self.service_collection)
173 self.service_collection.startService()
174
175 def get_url(self):
176 """Build the url for this mock server."""
177 # pylint: disable=W0212
178 port_num = self.tcpserver._port.getHost().port
179 return "http://localhost:%d/" % port_num
180
181 def stop(self):
182 """Shut it down."""
183 # pylint: disable=E1101
184 self.service_collection.stopService()
185
186
187class FakedError(Exception):
188 """A mock, test, sample, and fake exception."""
189
190
191class TimestampCheckerTestCase(TestCase):
192 """Tests for the timestamp checker."""
193
194 def setUp(self):
195 """Initialize a fake webserver."""
196 self.ws = MockWebServer()
197 self.addCleanup(self.ws.stop)
198 self.patch(SyncTimestampChecker, "SERVER_URL", self.ws.get_url())
199
200 @defer.inlineCallbacks
201 def test_returned_value_is_int(self):
202 """The returned value is an integer."""
203 checker = SyncTimestampChecker()
204 timestamp = yield deferToThread(checker.get_faithful_time)
205 self.assertEqual(type(timestamp), int)
206
207 @defer.inlineCallbacks
208 def test_first_call_does_head(self):
209 """The first call gets the clock from our web."""
210 checker = SyncTimestampChecker()
211 yield deferToThread(checker.get_faithful_time)
212 self.assertEqual(self.ws.root.count, 1)
213
214 @defer.inlineCallbacks
215 def test_second_call_is_cached(self):
216 """For the second call, the time is cached."""
217 checker = SyncTimestampChecker()
218 yield deferToThread(checker.get_faithful_time)
219 yield deferToThread(checker.get_faithful_time)
220 self.assertEqual(self.ws.root.count, 1)
221
222 @defer.inlineCallbacks
223 def test_after_timeout_cache_expires(self):
224 """After some time, the cache expires."""
225 fake_timestamp = 1
226 self.patch(time, "time", lambda: fake_timestamp)
227 checker = SyncTimestampChecker()
228 yield deferToThread(checker.get_faithful_time)
229 fake_timestamp += SyncTimestampChecker.CHECKING_INTERVAL
230 yield deferToThread(checker.get_faithful_time)
231 self.assertEqual(self.ws.root.count, 2)
232
233 @defer.inlineCallbacks
234 def test_server_date_sends_nocache_headers(self):
235 """Getting the server date sends the no-cache headers."""
236 checker = SyncTimestampChecker()
237 yield deferToThread(checker.get_server_time)
238 assert len(self.ws.root.request_headers) == 1
239 headers = self.ws.root.request_headers[0]
240 result = headers.getRawHeaders("Cache-Control")
241 self.assertEqual(result, ["no-cache"])
242
243 @defer.inlineCallbacks
244 def test_server_error_means_skew_not_updated(self):
245 """When server can't be reached, the skew is not updated."""
246 fake_timestamp = 1
247 self.patch(time, "time", lambda: fake_timestamp)
248 checker = SyncTimestampChecker()
249
250 def failing_get_server_time():
251 """Let's fail while retrieving the server time."""
252 raise FakedError()
253
254 self.patch(checker, "get_server_time", failing_get_server_time)
255 yield deferToThread(checker.get_faithful_time)
256 self.assertEqual(checker.skew, 0)
257 self.assertEqual(checker.next_check,
258 fake_timestamp + SyncTimestampChecker.ERROR_INTERVAL)

Subscribers

People subscribed via source and target branches