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

Proposed by Alejandro J. Cura
Status: Merged
Approved by: Natalia Bidart
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 (community) Approve
Diego Sarmentero (community) Abstain
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.
Revision history for this message
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
Revision history for this message
Diego Sarmentero (diegosarmentero) :
review: Abstain
692. By Alejandro J. Cura

fix year in file headers

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
=== modified file 'ubuntu_sso/account.py'
--- ubuntu_sso/account.py 2011-03-16 01:36:07 +0000
+++ ubuntu_sso/account.py 2011-11-23 19:52:23 +0000
@@ -1,8 +1,9 @@
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, 2011 Canonical Ltd.
6#7#
7# This program is free software: you can redistribute it and/or modify it8# 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 published9# under the terms of the GNU General Public License version 3, as published
@@ -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 OAuthAuthorizer.__init__(self, *args, **kwargs)
50 self.get_timestamp = get_timestamp
51
52 # pylint: disable=C0103,E1101
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/credentials.py'
--- ubuntu_sso/credentials.py 2011-01-02 03:34:23 +0000
+++ ubuntu_sso/credentials.py 2011-11-23 19:52:23 +0000
@@ -3,7 +3,7 @@
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# Author: Alejandro J. Cura <alecu@canonical.com>
5#5#
6# Copyright 2010 Canonical Ltd.6# Copyright 2010, 2011 Canonical Ltd.
7#7#
8# This program is free software: you can redistribute it and/or modify it8# This program is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License version 3, as published9# under the terms of the GNU General Public License version 3, as published
@@ -46,7 +46,7 @@
46from oauth import oauth46from oauth import oauth
47from twisted.internet.defer import inlineCallbacks, returnValue47from twisted.internet.defer import inlineCallbacks, returnValue
4848
49from ubuntu_sso import NO_OP49from ubuntu_sso import NO_OP, utils
50from ubuntu_sso.keyring import Keyring50from ubuntu_sso.keyring import Keyring
51from ubuntu_sso.logger import setup_logging51from ubuntu_sso.logger import setup_logging
5252
@@ -217,6 +217,10 @@
217 logger.warning('Login/registration was denied to app %r', app_name)217 logger.warning('Login/registration was denied to app %r', app_name)
218 self.denial_cb(app_name)218 self.denial_cb(app_name)
219219
220 def _get_timestamp(self):
221 """Get the timestamp calculated from the server."""
222 return utils.timestamp_checker.get_faithful_time()
223
220 @handle_failures(msg='Problem opening the ping_url')224 @handle_failures(msg='Problem opening the ping_url')
221 @inlineCallbacks225 @inlineCallbacks
222 def _ping_url(self, app_name, email, credentials):226 def _ping_url(self, app_name, email, credentials):
@@ -233,9 +237,12 @@
233 credentials['consumer_secret'])237 credentials['consumer_secret'])
234 token = oauth.OAuthToken(credentials['token'],238 token = oauth.OAuthToken(credentials['token'],
235 credentials['token_secret'])239 credentials['token_secret'])
240 timestamp = self._get_timestamp()
241 parameters = {"oauth_timestamp": timestamp}
236 get_request = oauth.OAuthRequest.from_consumer_and_token242 get_request = oauth.OAuthRequest.from_consumer_and_token
237 oauth_req = get_request(oauth_consumer=consumer, token=token,243 oauth_req = get_request(oauth_consumer=consumer, token=token,
238 http_method='GET', http_url=url)244 http_method='GET', http_url=url,
245 parameters=parameters)
239 oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),246 oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
240 consumer, token)247 consumer, token)
241 request = urllib2.Request(url, headers=oauth_req.to_header())248 request = urllib2.Request(url, headers=oauth_req.to_header())
242249
=== modified file 'ubuntu_sso/tests/test_account.py'
--- ubuntu_sso/tests/test_account.py 2011-03-16 01:36:07 +0000
+++ ubuntu_sso/tests/test_account.py 2011-11-23 19:52:23 +0000
@@ -1,8 +1,9 @@
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, 2011 Canonical Ltd.
6#7#
7# This program is free software: you can redistribute it and/or modify it8# 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 published9# under the terms of the GNU General Public License version 3, as published
@@ -23,15 +24,36 @@
23# pylint: disable=F040124# pylint: disable=F0401
24from lazr.restfulclient.errors import HTTPError25from lazr.restfulclient.errors import HTTPError
25# pylint: enable=F040126# pylint: enable=F0401
27from oauth import oauth
26from twisted.trial.unittest import TestCase28from twisted.trial.unittest import TestCase
2729
28from ubuntu_sso.account import (Account, AuthenticationError, EmailTokenError,30from ubuntu_sso.account import (
29 InvalidEmailError, InvalidPasswordError, NewPasswordError, SERVICE_URL,31 Account,
30 RegistrationError, ResetPasswordTokenError,32 AuthenticationError,
31 SSO_STATUS_OK, SSO_STATUS_ERROR)33 EmailTokenError,
32from ubuntu_sso.tests import (APP_NAME, CAPTCHA_ID, CAPTCHA_PATH,34 InvalidEmailError,
33 CAPTCHA_SOLUTION, EMAIL, EMAIL_TOKEN, NAME, PASSWORD, RESET_PASSWORD_TOKEN,35 InvalidPasswordError,
34 TOKEN, TOKEN_NAME)36 NewPasswordError,
37 SERVICE_URL,
38 RegistrationError,
39 ResetPasswordTokenError,
40 SSO_STATUS_OK,
41 SSO_STATUS_ERROR,
42 TimestampedAuthorizer,
43)
44from ubuntu_sso.tests import (
45 APP_NAME,
46 CAPTCHA_ID,
47 CAPTCHA_PATH,
48 CAPTCHA_SOLUTION,
49 EMAIL,
50 EMAIL_TOKEN,
51 NAME,
52 PASSWORD,
53 RESET_PASSWORD_TOKEN,
54 TOKEN,
55 TOKEN_NAME,
56)
3557
3658
37CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \59CANT_RESET_PASSWORD_CONTENT = "CanNotResetPassowrdError: " \
@@ -142,6 +164,36 @@
142 self.accounts = FakedAccounts()164 self.accounts = FakedAccounts()
143165
144166
167class TimestampedAuthorizerTestCase(TestCase):
168 """Test suite for the TimestampedAuthorizer."""
169
170 def test_authorize_request_includes_timestamp(self):
171 """The authorizeRequest method includes the timestamp."""
172 fromcandt_call = []
173 fake_uri = "http://protocultura.net"
174 fake_timestamp = 1234
175 get_fake_timestamp = lambda: fake_timestamp
176 original_oauthrequest = oauth.OAuthRequest
177
178 class FakeOAuthRequest(oauth.OAuthRequest):
179 """A Fake OAuthRequest class."""
180
181 @staticmethod
182 def from_consumer_and_token(*args, **kwargs):
183 """A fake from_consumer_and_token."""
184 fromcandt_call.append((args, kwargs))
185 builder = original_oauthrequest.from_consumer_and_token
186 return builder(*args, **kwargs)
187
188 self.patch(oauth, "OAuthRequest", FakeOAuthRequest)
189
190 authorizer = TimestampedAuthorizer(get_fake_timestamp, "ubuntuone")
191 authorizer.authorizeRequest(fake_uri, "POST", None, {})
192 call_kwargs = fromcandt_call[0][1]
193 parameters = call_kwargs["parameters"]
194 self.assertEqual(parameters["oauth_timestamp"], fake_timestamp)
195
196
145class AccountTestCase(TestCase):197class AccountTestCase(TestCase):
146 """Test suite for the SSO login processor."""198 """Test suite for the SSO login processor."""
147199
148200
=== modified file 'ubuntu_sso/tests/test_credentials.py'
--- ubuntu_sso/tests/test_credentials.py 2011-01-02 03:34:23 +0000
+++ ubuntu_sso/tests/test_credentials.py 2011-11-23 19:52:23 +0000
@@ -3,7 +3,7 @@
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# Author: Alejandro J. Cura <alecu@canonical.com>
5#5#
6# Copyright 2010 Canonical Ltd.6# Copyright 2010, 2011 Canonical Ltd.
7#7#
8# This program is free software: you can redistribute it and/or modify it8# This program is free software: you can redistribute it and/or modify it
9# under the terms of the GNU General Public License version 3, as published9# under the terms of the GNU General Public License version 3, as published
@@ -20,6 +20,7 @@
2020
21import logging21import logging
22import urllib22import urllib
23import time
2324
24from twisted.internet import defer25from twisted.internet import defer
25from twisted.internet.defer import inlineCallbacks26from twisted.internet.defer import inlineCallbacks
@@ -359,6 +360,8 @@
359 return response360 return response
360361
361 self.patch(credentials.urllib2, 'urlopen', faked_urlopen)362 self.patch(credentials.urllib2, 'urlopen', faked_urlopen)
363 self.patch(credentials.utils.timestamp_checker, "get_faithful_time",
364 time.time)
362365
363 @inlineCallbacks366 @inlineCallbacks
364 def test_ping_url_if_url_is_none(self):367 def test_ping_url_if_url_is_none(self):
365368
=== modified file 'ubuntu_sso/utils/__init__.py'
--- ubuntu_sso/utils/__init__.py 2010-11-15 23:06:52 +0000
+++ ubuntu_sso/utils/__init__.py 2011-11-23 19:52:23 +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
@@ -17,3 +17,65 @@
17# with this program. If not, see <http://www.gnu.org/licenses/>.17# with this program. If not, see <http://www.gnu.org/licenses/>.
1818
19"""Utility modules that may find use outside ubuntu_sso."""19"""Utility modules that may find use outside ubuntu_sso."""
20import time
21import urllib2
22from twisted.web import http
23
24from ubuntu_sso.logger import setup_logging
25logger = setup_logging("ubuntu_sso.utils")
26
27
28class RequestHead(urllib2.Request):
29 """A request with the method set to HEAD."""
30
31 _request_method = "HEAD"
32
33 def get_method(self):
34 """Return the desired method."""
35 return self._request_method
36
37
38class SyncTimestampChecker(object):
39 """A timestamp that's regularly checked with a server."""
40
41 CHECKING_INTERVAL = 60 * 60 # in seconds
42 ERROR_INTERVAL = 30 # in seconds
43 SERVER_URL = "http://one.ubuntu.com/api/time"
44
45 def __init__(self):
46 """Initialize this instance."""
47 self.next_check = time.time()
48 self.skew = 0
49
50 def get_server_time(self):
51 """Get the time at the server."""
52 headers = {"Cache-Control": "no-cache"}
53 request = RequestHead(self.SERVER_URL, headers=headers)
54 response = urllib2.urlopen(request)
55 date_string = response.info()["Date"]
56 timestamp = http.stringToDatetime(date_string)
57 return timestamp
58
59 def get_faithful_time(self):
60 """Get an accurate timestamp."""
61 local_time = time.time()
62 if local_time >= self.next_check:
63 try:
64 server_time = self.get_server_time()
65 self.next_check = local_time + self.CHECKING_INTERVAL
66 self.skew = server_time - local_time
67 logger.debug("Calculated server-local time skew: %r",
68 self.skew)
69 #pylint: disable=W0703
70 except Exception, server_error:
71 logger.debug("Error while verifying the server time skew: %r",
72 server_error)
73 self.next_check = local_time + self.ERROR_INTERVAL
74 logger.debug("Using corrected timestamp: %r",
75 http.datetimeToString(local_time + self.skew))
76 return int(local_time + self.skew)
77
78
79# pylint: disable=C0103
80timestamp_checker = SyncTimestampChecker()
81# pylint: enable=C0103
2082
=== added file 'ubuntu_sso/utils/tests/test_oauth_headers.py'
--- ubuntu_sso/utils/tests/test_oauth_headers.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/tests/test_oauth_headers.py 2011-11-23 19:52:23 +0000
@@ -0,0 +1,148 @@
1# -*- coding: utf-8 -*-
2
3# Author: Alejandro J. Cura <alecu@canonical.com>
4#
5# Copyright 2011 Canonical Ltd.
6#
7# 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 published
9# by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.
18
19"""Tests for the oauth_headers helper function."""
20
21import time
22
23from twisted.application import internet, service
24from twisted.internet import defer
25from twisted.internet.threads import deferToThread
26from twisted.trial.unittest import TestCase
27from twisted.web import server, resource
28
29from ubuntu_sso.utils import SyncTimestampChecker
30
31
32class RootResource(resource.Resource):
33 """A root resource that logs the number of calls."""
34
35 isLeaf = True
36
37 def __init__(self, *args, **kwargs):
38 """Initialize this fake instance."""
39 resource.Resource.__init__(self, *args, **kwargs)
40 self.count = 0
41 self.request_headers = []
42
43 # pylint: disable=C0103
44 def render_HEAD(self, request):
45 """Increase the counter on each render."""
46 self.count += 1
47 self.request_headers.append(request.requestHeaders)
48 return ""
49
50
51class MockWebServer(object):
52 """A mock webserver for testing."""
53
54 def __init__(self):
55 """Start up this instance."""
56 # pylint: disable=E1101
57 self.root = RootResource()
58 site = server.Site(self.root)
59 application = service.Application('web')
60 self.service_collection = service.IServiceCollection(application)
61 self.tcpserver = internet.TCPServer(0, site)
62 self.tcpserver.setServiceParent(self.service_collection)
63 self.service_collection.startService()
64
65 def get_url(self):
66 """Build the url for this mock server."""
67 # pylint: disable=W0212
68 port_num = self.tcpserver._port.getHost().port
69 return "http://localhost:%d/" % port_num
70
71 def stop(self):
72 """Shut it down."""
73 # pylint: disable=E1101
74 self.service_collection.stopService()
75
76
77class FakedError(Exception):
78 """A mock, test, sample, and fake exception."""
79
80
81class TimestampCheckerTestCase(TestCase):
82 """Tests for the timestamp checker."""
83
84 def setUp(self):
85 """Initialize a fake webserver."""
86 self.ws = MockWebServer()
87 self.addCleanup(self.ws.stop)
88 self.patch(SyncTimestampChecker, "SERVER_URL", self.ws.get_url())
89
90 @defer.inlineCallbacks
91 def test_returned_value_is_int(self):
92 """The returned value is an integer."""
93 checker = SyncTimestampChecker()
94 timestamp = yield deferToThread(checker.get_faithful_time)
95 self.assertEqual(type(timestamp), int)
96
97 @defer.inlineCallbacks
98 def test_first_call_does_head(self):
99 """The first call gets the clock from our web."""
100 checker = SyncTimestampChecker()
101 yield deferToThread(checker.get_faithful_time)
102 self.assertEqual(self.ws.root.count, 1)
103
104 @defer.inlineCallbacks
105 def test_second_call_is_cached(self):
106 """For the second call, the time is cached."""
107 checker = SyncTimestampChecker()
108 yield deferToThread(checker.get_faithful_time)
109 yield deferToThread(checker.get_faithful_time)
110 self.assertEqual(self.ws.root.count, 1)
111
112 @defer.inlineCallbacks
113 def test_after_timeout_cache_expires(self):
114 """After some time, the cache expires."""
115 fake_timestamp = 1
116 self.patch(time, "time", lambda: fake_timestamp)
117 checker = SyncTimestampChecker()
118 yield deferToThread(checker.get_faithful_time)
119 fake_timestamp += SyncTimestampChecker.CHECKING_INTERVAL
120 yield deferToThread(checker.get_faithful_time)
121 self.assertEqual(self.ws.root.count, 2)
122
123 @defer.inlineCallbacks
124 def test_server_date_sends_nocache_headers(self):
125 """Getting the server date sends the no-cache headers."""
126 checker = SyncTimestampChecker()
127 yield deferToThread(checker.get_server_time)
128 assert len(self.ws.root.request_headers) == 1
129 headers = self.ws.root.request_headers[0]
130 result = headers.getRawHeaders("Cache-Control")
131 self.assertEqual(result, ["no-cache"])
132
133 @defer.inlineCallbacks
134 def test_server_error_means_skew_not_updated(self):
135 """When server can't be reached, the skew is not updated."""
136 fake_timestamp = 1
137 self.patch(time, "time", lambda: fake_timestamp)
138 checker = SyncTimestampChecker()
139
140 def failing_get_server_time():
141 """Let's fail while retrieving the server time."""
142 raise FakedError()
143
144 self.patch(checker, "get_server_time", failing_get_server_time)
145 yield deferToThread(checker.get_faithful_time)
146 self.assertEqual(checker.skew, 0)
147 self.assertEqual(checker.next_check,
148 fake_timestamp + SyncTimestampChecker.ERROR_INTERVAL)

Subscribers

People subscribed via source and target branches