Merge lp:~alecu/ubuntu-sso-client/timestamp-autofix-1-0 into lp:ubuntu-sso-client/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
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Diego Sarmentero (community) Approve
Review via email: mp+82737@code.launchpad.net

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 bin/ubuntu-sso-login
Text conflict in data/gtk/ui.glade
Text conflict in run-tests
Text conflict in setup.py
Text conflict in ubuntu_sso/gtk/gui.py
Text conflict in ubuntu_sso/gtk/tests/test_gui.py
Text conflict in ubuntu_sso/keyring/linux.py
Text conflict in ubuntu_sso/keyring/tests/test_linux.py
Text conflict in ubuntu_sso/main/linux.py
Text conflict in ubuntu_sso/main/tests/test_linux.py
Conflict adding file ubuntu_sso/utils. Moved existing file to ubuntu_sso/utils.moved.
11 conflicts encountered.

review: Needs Fixing
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
=== modified file 'ubuntu_sso/main.py'
--- ubuntu_sso/main.py 2011-03-29 17:26:26 +0000
+++ ubuntu_sso/main.py 2011-11-23 19:50:27 +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 2009 Canonical Ltd.6# Copyright 2009, 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,6 +46,7 @@
46from ubuntu_sso import DBUS_IFACE_USER_NAME, DBUS_IFACE_CRED_NAME46from ubuntu_sso import DBUS_IFACE_USER_NAME, DBUS_IFACE_CRED_NAME
47from ubuntu_sso.keyring import Keyring, get_token_name, U1_APP_NAME47from ubuntu_sso.keyring import Keyring, get_token_name, U1_APP_NAME
48from ubuntu_sso.logger import setup_logging48from ubuntu_sso.logger import setup_logging
49from ubuntu_sso.utils import timestamp_checker
4950
5051
51# Disable the invalid name warning, as we have a lot of DBus style names52# Disable the invalid name warning, as we have a lot of DBus style names
@@ -117,6 +118,27 @@
117 return creds118 return creds
118119
119120
121class TimestampedAuthorizer(OAuthAuthorizer):
122 """Includes a custom timestamp on OAuth signatures."""
123
124 def __init__(self, get_timestamp, *args, **kwargs):
125 """Store the get_timestamp method, and move on."""
126 OAuthAuthorizer.__init__(self, *args, **kwargs)
127 self.get_timestamp = get_timestamp
128
129 # pylint: disable=C0103,E1101
130 def authorizeRequest(self, absolute_uri, method, body, headers):
131 """Override authorizeRequest including the timestamp."""
132 parameters = {"oauth_timestamp": self.get_timestamp()}
133 oauth_request = oauth.OAuthRequest.from_consumer_and_token(
134 self.consumer, self.access_token, http_url=absolute_uri,
135 parameters=parameters)
136 oauth_request.sign_request(
137 oauth.OAuthSignatureMethod_PLAINTEXT(),
138 self.consumer, self.access_token)
139 headers.update(oauth_request.to_header(self.oauth_realm))
140
141
120class SSOLoginProcessor(object):142class SSOLoginProcessor(object):
121 """Login and register users using the Ubuntu Single Sign On service."""143 """Login and register users using the Ubuntu Single Sign On service."""
122144
@@ -236,7 +258,9 @@
236 if sso_service is None:258 if sso_service is None:
237 oauth_token = oauth.OAuthToken(token['token'],259 oauth_token = oauth.OAuthToken(token['token'],
238 token['token_secret'])260 token['token_secret'])
239 authorizer = OAuthAuthorizer(token['consumer_key'],261 authorizer = TimestampedAuthorizer(
262 timestamp_checker.get_faithful_time,
263 token['consumer_key'],
240 token['consumer_secret'],264 token['consumer_secret'],
241 oauth_token)265 oauth_token)
242 sso_service = self.sso_service_class(authorizer, self.service_url)266 sso_service = self.sso_service_class(authorizer, self.service_url)
@@ -258,7 +282,8 @@
258 token_name=token_name)282 token_name=token_name)
259283
260 oauth_token = oauth.OAuthToken(token['token'], token['token_secret'])284 oauth_token = oauth.OAuthToken(token['token'], token['token_secret'])
261 authorizer = OAuthAuthorizer(token['consumer_key'],285 authorizer = TimestampedAuthorizer(
286 timestamp_checker.get_faithful_time,
262 token['consumer_secret'],287 token['consumer_secret'],
263 oauth_token)288 oauth_token)
264 sso_service = self.sso_service_class(authorizer, self.service_url)289 sso_service = self.sso_service_class(authorizer, self.service_url)
@@ -607,9 +632,12 @@
607 credentials['consumer_secret'])632 credentials['consumer_secret'])
608 token = oauth.OAuthToken(credentials['token'],633 token = oauth.OAuthToken(credentials['token'],
609 credentials['token_secret'])634 credentials['token_secret'])
635 timestamp = timestamp_checker.get_faithful_time()
636 parameters = {"oauth_timestamp": timestamp}
610 get_request = oauth.OAuthRequest.from_consumer_and_token637 get_request = oauth.OAuthRequest.from_consumer_and_token
611 oauth_req = get_request(oauth_consumer=consumer, token=token,638 oauth_req = get_request(oauth_consumer=consumer, token=token,
612 http_method="GET", http_url=url)639 http_method="GET", http_url=url,
640 parameters=parameters)
613 oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),641 oauth_req.sign_request(oauth.OAuthSignatureMethod_HMAC_SHA1(),
614 consumer, token)642 consumer, token)
615 request = urllib2.Request(url, headers=oauth_req.to_header())643 request = urllib2.Request(url, headers=oauth_req.to_header())
616644
=== modified file 'ubuntu_sso/tests/test_main.py'
--- ubuntu_sso/tests/test_main.py 2011-03-29 17:26:26 +0000
+++ ubuntu_sso/tests/test_main.py 2011-11-23 19:50:27 +0000
@@ -5,7 +5,7 @@
5# Author: Natalia Bidart <natalia.bidart@canonical.com>5# Author: Natalia Bidart <natalia.bidart@canonical.com>
6# Author: Alejandro J. Cura <alecu@canonical.com>6# Author: Alejandro J. Cura <alecu@canonical.com>
7#7#
8# Copyright 2009-2010 Canonical Ltd.8# Copyright 2009-2011 Canonical Ltd.
9#9#
10# This program is free software: you can redistribute it and/or modify it10# This program is free software: you can redistribute it and/or modify it
11# under the terms of the GNU General Public License version 3, as published11# under the terms of the GNU General Public License version 3, as published
@@ -22,6 +22,7 @@
2222
23import logging23import logging
24import os24import os
25import time
25import urllib226import urllib2
2627
27import gobject28import gobject
@@ -31,6 +32,7 @@
31from lazr.restfulclient.errors import HTTPError32from lazr.restfulclient.errors import HTTPError
32# pylint: enable=F040133# pylint: enable=F0401
33from mocker import Mocker, MockerTestCase, ARGS, KWARGS, ANY34from mocker import Mocker, MockerTestCase, ARGS, KWARGS, ANY
35from oauth import oauth
34from twisted.internet.defer import Deferred36from twisted.internet.defer import Deferred
35from twisted.trial.unittest import TestCase37from twisted.trial.unittest import TestCase
3638
@@ -46,7 +48,7 @@
46 keyring_get_credentials, keyring_store_credentials, logger,48 keyring_get_credentials, keyring_store_credentials, logger,
47 NewPasswordError, PING_URL, SERVICE_URL,49 NewPasswordError, PING_URL, SERVICE_URL,
48 RegistrationError, ResetPasswordTokenError,50 RegistrationError, ResetPasswordTokenError,
49 SSOCredentials, SSOLogin, SSOLoginProcessor)51 SSOCredentials, SSOLogin, SSOLoginProcessor, TimestampedAuthorizer)
5052
5153
52# Access to a protected member 'yyy' of a client class54# Access to a protected member 'yyy' of a client class
@@ -193,6 +195,38 @@
193 self.accounts = FakedAccounts()195 self.accounts = FakedAccounts()
194196
195197
198class TimestampedAuthorizerTestCase(TestCase):
199 """Test suite for the TimestampedAuthorizer."""
200
201 def test_authorize_request_includes_timestamp(self):
202 """The authorizeRequest method includes the timestamp."""
203 fromcandt_call = []
204 fake_uri = "http://protocultura.net"
205 fake_timestamp = 1234
206 get_fake_timestamp = lambda: fake_timestamp
207 original_oauthrequest = oauth.OAuthRequest
208
209 class FakeOAuthRequest(oauth.OAuthRequest):
210 """A Fake OAuthRequest class."""
211
212 # pylint: disable=W0221
213 @staticmethod
214 def from_consumer_and_token(*args, **kwargs):
215 """A fake from_consumer_and_token."""
216 fromcandt_call.append((args, kwargs))
217 builder = original_oauthrequest.from_consumer_and_token
218 return builder(*args, **kwargs)
219 # pylint: enable=W0221
220
221 self.patch(oauth, "OAuthRequest", FakeOAuthRequest)
222
223 authorizer = TimestampedAuthorizer(get_fake_timestamp, "ubuntuone")
224 authorizer.authorizeRequest(fake_uri, "POST", None, {})
225 call_kwargs = fromcandt_call[0][1]
226 parameters = call_kwargs["parameters"]
227 self.assertEqual(parameters["oauth_timestamp"], fake_timestamp)
228
229
196class SSOLoginProcessorTestCase(TestCase, MockerTestCase):230class SSOLoginProcessorTestCase(TestCase, MockerTestCase):
197 """Test suite for the SSO login processor."""231 """Test suite for the SSO login processor."""
198232
@@ -1426,6 +1460,8 @@
1426 return FakedResponse(code=200)1460 return FakedResponse(code=200)
14271461
1428 self.patch(urllib2, 'urlopen', fake_it)1462 self.patch(urllib2, 'urlopen', fake_it)
1463 self.patch(ubuntu_sso.main.timestamp_checker, "get_faithful_time",
1464 time.time)
14291465
1430 self.client = SSOCredentials(None)1466 self.client = SSOCredentials(None)
14311467
14321468
=== added directory 'ubuntu_sso/utils'
=== added file 'ubuntu_sso/utils/__init__.py'
--- ubuntu_sso/utils/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/__init__.py 2011-11-23 19:50:27 +0000
@@ -0,0 +1,81 @@
1# -*- coding: utf-8 -*-
2
3# Author: Alejandro J. Cura <alecu@canonical.com>
4#
5# Copyright 2010, 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"""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
082
=== added directory 'ubuntu_sso/utils/tests'
=== added file 'ubuntu_sso/utils/tests/__init__.py'
--- ubuntu_sso/utils/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ ubuntu_sso/utils/tests/__init__.py 2011-11-23 19:50:27 +0000
@@ -0,0 +1,16 @@
1# ubuntu_sso - Ubuntu Single Sign On client support for desktop apps
2#
3# Copyright 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it
6# under the terms of the GNU General Public License version 3, as published
7# by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but
10# WITHOUT ANY WARRANTY; without even the implied warranties of
11# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
12# PURPOSE. See the GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program. If not, see <http://www.gnu.org/licenses/>.
16"""Ubuntu Single Sign On utils tests."""
017
=== 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:50:27 +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