Merge ~pappacena/turnip:py3-http-auth-flow into turnip:master

Proposed by Thiago F. Pappacena
Status: Merged
Approved by: Thiago F. Pappacena
Approved revision: a2ff896e2bfb9f21c8132d43fbea5f14a26ccf85
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~pappacena/turnip:py3-http-auth-flow
Merge into: turnip:master
Diff against target: 238 lines (+120/-8)
2 files modified
turnip/pack/http.py (+46/-7)
turnip/pack/tests/test_http.py (+74/-1)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+395807@code.launchpad.net

Commit message

Making HTTP auth flow compatible with python3

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

At some point it would be nice to find a non-deprecated replacement for paste.auth.cookie that we could use both here and in Launchpad. But I'm OK with this for now, thanks.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/turnip/pack/http.py b/turnip/pack/http.py
2index f138c77..3e627d0 100644
3--- a/turnip/pack/http.py
4+++ b/turnip/pack/http.py
5@@ -7,6 +7,7 @@ from __future__ import (
6 unicode_literals,
7 )
8
9+import base64
10 import io
11 import json
12 import os.path
13@@ -24,12 +25,14 @@ from openid.extensions.sreg import (
14 SRegRequest,
15 SRegResponse,
16 )
17+import paste.auth.cookie
18 from paste.auth.cookie import (
19 AuthCookieSigner,
20 decode as decode_cookie,
21 encode as encode_cookie,
22 )
23 import six
24+import time
25 from twisted.internet import (
26 defer,
27 error,
28@@ -476,18 +479,53 @@ class CGitScriptResource(twcgi.CGIScript):
29 twcgi.CGIScript.runProcess(self, env, request, *args, **kwargs)
30
31
32+class TurnipAuthCookieSigner(AuthCookieSigner):
33+ def auth(self, cookie):
34+ """
35+ Authenticate the cooke using the signature, verify that it
36+ has not expired; and return the cookie's content.
37+
38+ This was moved from the original AuthCookieSigner to make it python3
39+ compatible, since paste.auth.* is marked to be deprecated according to
40+ https://pythonpaste.readthedocs.io/en/latest/future.html#to-deprecate.
41+ """
42+ _signature_size = paste.auth.cookie._signature_size
43+ _header_size = paste.auth.cookie._header_size
44+ sha1 = paste.auth.cookie.sha1
45+ hmac = paste.auth.cookie.hmac
46+ make_time = paste.auth.cookie.make_time
47+ decode = base64.decodestring(
48+ cookie.replace(b"_", b"/").replace(b"~", b"="))
49+ signature = decode[:_signature_size]
50+ expires = decode[_signature_size:_header_size]
51+ content = decode[_header_size:]
52+ if signature == hmac.new(self.secret, content, sha1).digest():
53+ if int(expires) > int(make_time(time.time())):
54+ return content
55+ else:
56+ # This is the normal case of an expired cookie; just
57+ # don't bother doing anything here.
58+ pass
59+ else:
60+ # This case can happen if the server is restarted with a
61+ # different secret; or if the user's IP address changed
62+ # due to a proxy. However, it could also be a break-in
63+ # attempt -- so should it be reported?
64+ pass
65+
66+
67 class BaseHTTPAuthResource(resource.Resource):
68 """Base HTTP resource for OpenID authentication handling."""
69
70 session_var = 'turnip.session'
71- cookie_name = 'TURNIP_COOKIE'
72+ cookie_name = b'TURNIP_COOKIE'
73 anonymous_id = '+launchpad-anonymous'
74
75 def __init__(self, root):
76 resource.Resource.__init__(self)
77 self.root = root
78 if root.cgit_secret is not None:
79- self.signer = AuthCookieSigner(root.cgit_secret)
80+ self.signer = TurnipAuthCookieSigner(root.cgit_secret)
81 else:
82 self.signer = None
83
84@@ -497,14 +535,14 @@ class BaseHTTPAuthResource(resource.Resource):
85 if cookie is not None:
86 content = self.signer.auth(cookie)
87 if content:
88- return json.loads(decode_cookie(content))
89+ return json.loads(decode_cookie(six.ensure_text(content)))
90 return {}
91
92 def _putSession(self, request, session):
93 if self.signer is not None:
94 content = self.signer.sign(encode_cookie(json.dumps(session)))
95- cookie = '%s=%s; Path=/; secure;' % (self.cookie_name, content)
96- request.setHeader(b'Set-Cookie', cookie.encode('UTF-8'))
97+ cookie = b'%s=%s; Path=/; secure;' % (self.cookie_name, content)
98+ request.setHeader(b'Set-Cookie', cookie)
99
100 def _setErrorCode(self, request, code=http.INTERNAL_SERVER_ERROR):
101 request.setResponseCode(code)
102@@ -534,7 +572,8 @@ class HTTPAuthLoginResource(BaseHTTPAuthResource):
103 wrong.
104 """
105 session = self._getSession(request)
106- query = {k: v[-1] for k, v in request.args.items()}
107+ query = {six.ensure_text(k): six.ensure_text(v[-1])
108+ for k, v in request.args.items()}
109 response = self._makeConsumer(session).complete(
110 query, query['openid.return_to'])
111 if response.status == consumer.SUCCESS:
112@@ -551,7 +590,7 @@ class HTTPAuthLoginResource(BaseHTTPAuthResource):
113 session['identity_url'] = response.identity_url
114 session['user'] = sreg_info['nickname']
115 self._putSession(request, session)
116- request.redirect(query['back_to'])
117+ request.redirect(six.ensure_binary(query['back_to']))
118 return b''
119 elif response.status == consumer.FAILURE:
120 log.msg('OpenID response: FAILURE: %s' % response.message)
121diff --git a/turnip/pack/tests/test_http.py b/turnip/pack/tests/test_http.py
122index 86a5c95..18c5893 100644
123--- a/turnip/pack/tests/test_http.py
124+++ b/turnip/pack/tests/test_http.py
125@@ -8,10 +8,13 @@ from __future__ import (
126 )
127
128 from io import BytesIO
129+import json
130 import os
131
132 from fixtures import TempDir
133 import six
134+from openid.consumer import consumer
135+from paste.auth.cookie import encode as encode_cookie
136 from testtools import TestCase
137 from testtools.deferredruntest import AsynchronousDeferredRunTest
138 from twisted.internet import (
139@@ -31,7 +34,10 @@ from turnip.pack import (
140 http,
141 )
142 from turnip.pack.helpers import encode_packet
143-from turnip.pack.http import get_protocol_version_from_request
144+from turnip.pack.http import (
145+ get_protocol_version_from_request,
146+ HTTPAuthLoginResource,
147+ )
148 from turnip.pack.tests.fake_servers import FakeVirtInfoService
149 from turnip.tests.compat import mock
150 from turnip.version_info import version_info
151@@ -44,6 +50,7 @@ class LessDummyRequest(requesthelper.DummyRequest):
152 def __init__(self, *args, **kwargs):
153 super(LessDummyRequest, self).__init__(*args, **kwargs)
154 self.content = BytesIO()
155+ self.cookies = {}
156
157 @property
158 def value(self):
159@@ -68,6 +75,9 @@ class LessDummyRequest(requesthelper.DummyRequest):
160 def getClientAddress(self):
161 return IPv4Address('TCP', '127.0.0.1', '80')
162
163+ def getCookie(self, name):
164+ return self.cookies.get(name)
165+
166
167 class AuthenticatedLessDummyRequest(LessDummyRequest):
168 def getUser(self):
169@@ -304,6 +314,69 @@ class TestSmartHTTPCommandResource(ErrorTestMixin, TestCase):
170 self.request.value)
171
172
173+class TestHTTPAuthLoginResource(TestCase):
174+ """Unit tests for login resource."""
175+ def setUp(self):
176+ super(TestHTTPAuthLoginResource, self).setUp()
177+ self.root = FakeRoot(self.useFixture(TempDir()).path)
178+ self.root.cgit_secret = b'dont-tell-anyone shuuu'
179+
180+ def getResourceInstance(self, mock_response):
181+ resource = HTTPAuthLoginResource(self.root)
182+ resource._makeConsumer = mock.Mock()
183+ resource._makeConsumer.return_value.complete.return_value = (
184+ mock_response)
185+ return resource
186+
187+ def test_render_GET_success(self):
188+ response = mock.Mock()
189+ response.status = consumer.SUCCESS
190+ response.identity_url = 'http://lp.test/XopAlqp'
191+ response.getSignedNS.return_value = {
192+ 'nickname': 'pappacena', 'country': 'BR'
193+ }
194+
195+ request = LessDummyRequest([''])
196+ request.method = b'GET'
197+ request.path = b'/example'
198+ request.args = {
199+ b'openid.return_to': [b'https://return.to.test'],
200+ b'back_to': [b'https://return.to.test']
201+ }
202+
203+ resource = self.getResourceInstance(response)
204+ self.assertEqual(b'', resource.render_GET(request))
205+ encoded_cookie = resource.signer.sign(encode_cookie(json.dumps({
206+ 'identity_url': response.identity_url,
207+ 'user': 'pappacena'
208+ })))
209+ expected_cookie = b'TURNIP_COOKIE=%s; Path=/; secure;' % encoded_cookie
210+ self.assertEqual({
211+ b'Set-Cookie': [expected_cookie],
212+ b'Location': [b'https://return.to.test']
213+ }, dict(request.responseHeaders.getAllRawHeaders()))
214+
215+ def test_getSession(self):
216+ response = mock.Mock()
217+ request = LessDummyRequest([''])
218+ request.method = b'GET'
219+ request.path = b'/example'
220+ request.args = {
221+ b'openid.return_to': [b'https://return.to.test'],
222+ b'back_to': [b'https://return.to.test']
223+ }
224+
225+ resource = self.getResourceInstance(response)
226+ cookie_data = {
227+ 'identity_url': 'http://localhost',
228+ 'user': 'pappacena'}
229+ cookie_content = resource.signer.sign(
230+ encode_cookie(json.dumps(cookie_data)))
231+ request.cookies[resource.cookie_name] = cookie_content
232+
233+ self.assertEqual(cookie_data, resource._getSession(request))
234+
235+
236 class TestHTTPAuthRootResource(TestCase):
237
238 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)

Subscribers

People subscribed via source and target branches