Merge ~cjwatson/launchpad:py3-loggerhead-secure-cookie into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 8d2e473bee7b3ebd094c2207c86eaf47d5232f80
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:py3-loggerhead-secure-cookie
Merge into: launchpad:master
Diff against target: 172 lines (+54/-37)
4 files modified
lib/launchpad_loggerhead/session.py (+48/-32)
lib/launchpad_loggerhead/tests.py (+2/-5)
requirements/launchpad.txt (+2/-0)
setup.py (+2/-0)
Reviewer Review Type Date Requested Status
Cristian Gonzalez (community) Approve
Review via email: mp+399127@code.launchpad.net

Commit message

Replace Loggerhead cookie handling using secure-cookie

Description of the change

paste.auth.cookie is deprecated, and getting it to work with Python 3 is cumbersome at best. Switch to secure-cookie, which seems to have a nicer API for our purposes, appears to be better-maintained (by the Werkzeug folks), and allows us to fix a long-standing minor deficiency around deleting session cookies on logout.

This will break existing Loggerhead sessions. I think that's OK since it's a one-time thing, but it's worth noting.

Dependencies MP: https://code.launchpad.net/~cjwatson/lp-source-dependencies/+git/lp-source-dependencies/+merge/399126

To post a comment you must log in.
8d2e473... by Colin Watson

Replace Loggerhead cookie handling using secure-cookie

paste.auth.cookie is deprecated, and getting it to work with Python 3 is
cumbersome at best. Switch to secure-cookie, which seems to have a
nicer API for our purposes, appears to be better-maintained (by the
Werkzeug folks), and allows us to fix a long-standing minor deficiency
around deleting session cookies on logout.

This will break existing Loggerhead sessions. I think that's OK since
it's a one-time thing, but it's worth noting.

Revision history for this message
Cristian Gonzalez (cristiangsp) wrote :

Nice change even with the trade off, totally worth it.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/launchpad_loggerhead/session.py b/lib/launchpad_loggerhead/session.py
index c78c021..2adb69c 100644
--- a/lib/launchpad_loggerhead/session.py
+++ b/lib/launchpad_loggerhead/session.py
@@ -1,13 +1,36 @@
1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the1# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Simple paste-y session manager tuned for the needs of launchpad-loggerhead.4"""Simple session manager tuned for the needs of launchpad-loggerhead."""
5"""
65
6import hashlib
7import pickle7import pickle
88
9from paste.auth.cookie import AuthCookieHandler9from secure_cookie.cookie import SecureCookie
10import six10from werkzeug.http import (
11 dump_cookie,
12 parse_cookie,
13 )
14
15from lp.services.config import config
16
17
18class LaunchpadSecureCookie(SecureCookie):
19
20 # The default of sha1 is a bit too weak.
21 hash_method = staticmethod(hashlib.sha256)
22
23 # The OpenID consumer stores non-JSON-encodable objects in the session.
24 class serialization_method(object):
25
26 @classmethod
27 def dumps(cls, value):
28 # Use protocol 2 for Python 2 compatibility.
29 return pickle.dumps(value, protocol=2)
30
31 @classmethod
32 def loads(cls, value):
33 return pickle.loads(value)
1134
1235
13class SessionHandler(object):36class SessionHandler(object):
@@ -30,40 +53,33 @@ class SessionHandler(object):
30 SessionHandler.53 SessionHandler.
31 """54 """
32 self.application = application55 self.application = application
33 self.cookie_handler = AuthCookieHandler(
34 self._process, scanlist=[session_var], secret=secret)
35 self.session_var = session_var56 self.session_var = session_var
57 self._secret = secret
58 self.cookie_name = "%s.lh" % config.launchpad_session.cookie
3659
37 def __call__(self, environ, start_response):60 def __call__(self, environ, start_response):
38 # We need to put the request through the cookie handler first, so we61 """Process a request."""
39 # can access the validated string in the environ in `_process` below.62 cookie = parse_cookie(environ).get(self.cookie_name, "")
40 return self.cookie_handler(environ, start_response)63 session = LaunchpadSecureCookie.unserialize(cookie, self._secret)
41
42 def _process(self, environ, start_response):
43 """Process a request.
44
45 AuthCookieHandler takes care of getting the text value of the session
46 in and out of the cookie (and validating the text using HMAC) so we
47 just need to convert that string to and from a real dictionary using
48 pickle.
49 """
50 if self.session_var in environ:
51 session = pickle.loads(six.ensure_binary(
52 environ[self.session_var], encoding="ISO-8859-1"))
53 else:
54 session = {}
55 existed = bool(session)64 existed = bool(session)
56 environ[self.session_var] = session65 environ[self.session_var] = session
66
57 def response_hook(status, response_headers, exc_info=None):67 def response_hook(status, response_headers, exc_info=None):
58 session = environ.pop(self.session_var)68 session = environ.pop(self.session_var)
59 # paste.auth.cookie does not delete cookies (see69 cookie_kwargs = {
60 # http://trac.pythonpaste.org/pythonpaste/ticket/139). A70 "path": "/",
61 # reasonable workaround is to make the value empty. Therefore,71 "httponly": True,
62 # we explicitly set the value in the session (to be encrypted)72 "secure": environ["wsgi.url_scheme"] == "https",
63 # if the value is non-empty *or* if it was non-empty at the start73 }
64 # of the request.74 if session:
65 if existed or session:75 cookie = dump_cookie(
66 # Use protocol 2 for Python 2 compatibility.76 self.cookie_name, session.serialize(), **cookie_kwargs)
67 environ[self.session_var] = pickle.dumps(session, protocol=2)77 response_headers.append(("Set-Cookie", cookie))
78 elif existed:
79 # Delete the cookie.
80 cookie = dump_cookie(
81 self.cookie_name, "", expires=0, **cookie_kwargs)
82 response_headers.append(("Set-Cookie", cookie))
68 return start_response(status, response_headers, exc_info)83 return start_response(status, response_headers, exc_info)
84
69 return self.application(environ, response_hook)85 return self.application(environ, response_hook)
diff --git a/lib/launchpad_loggerhead/tests.py b/lib/launchpad_loggerhead/tests.py
index 4c8482c..f0b8244 100644
--- a/lib/launchpad_loggerhead/tests.py
+++ b/lib/launchpad_loggerhead/tests.py
@@ -73,7 +73,7 @@ class TestLogout(TestCase):
73 app = session_scribbler(app, self)73 app = session_scribbler(app, self)
74 app = HTTPExceptionHandler(app)74 app = HTTPExceptionHandler(app)
75 app = SessionHandler(app, SESSION_VAR, SECRET)75 app = SessionHandler(app, SESSION_VAR, SECRET)
76 self.cookie_name = app.cookie_handler.cookie_name76 self.cookie_name = app.cookie_name
77 self.browser = Browser(wsgi_app=app)77 self.browser = Browser(wsgi_app=app)
78 self.browser.open(78 self.browser.open(
79 config.codehosting.secure_codebrowse_root + '+login')79 config.codehosting.secure_codebrowse_root + '+login')
@@ -95,10 +95,7 @@ class TestLogout(TestCase):
95 self.assertEqual(95 self.assertEqual(
96 self.browser.url, allvhosts.configs['mainsite'].rooturl)96 self.browser.url, allvhosts.configs['mainsite'].rooturl)
9797
98 # The session cookie still exists, because of how98 # The user has an empty session now.
99 # paste.auth.cookie works (see
100 # http://trac.pythonpaste.org/pythonpaste/ticket/139 ) but the user
101 # does in fact have an empty session now.
102 self.browser.open(99 self.browser.open(
103 config.codehosting.secure_codebrowse_root + 'favicon.ico')100 config.codehosting.secure_codebrowse_root + 'favicon.ico')
104 self.assertEqual(self.session, {})101 self.assertEqual(self.session, {})
diff --git a/requirements/launchpad.txt b/requirements/launchpad.txt
index 3444639..35eb07d 100644
--- a/requirements/launchpad.txt
+++ b/requirements/launchpad.txt
@@ -136,6 +136,7 @@ requests-toolbelt==0.9.1
136responses==0.9.0136responses==0.9.0
137s3transfer==0.3.3137s3transfer==0.3.3
138scandir==1.7138scandir==1.7
139secure-cookie==0.1.0
139service-identity==18.1.0140service-identity==18.1.0
140setproctitle==1.1.7141setproctitle==1.1.7
141setuptools-git==1.2142setuptools-git==1.2
@@ -169,6 +170,7 @@ waitress==1.3.1
169webencodings==0.5.1170webencodings==0.5.1
170WebOb==1.8.5171WebOb==1.8.5
171WebTest==2.0.35172WebTest==2.0.35
173Werkzeug==1.0.1
172wsgi-intercept==1.9.2174wsgi-intercept==1.9.2
173WSGIProxy2==0.4.6175WSGIProxy2==0.4.6
174wsgiref==0.1.2176wsgiref==0.1.2
diff --git a/setup.py b/setup.py
index 1e7495d..4114644 100644
--- a/setup.py
+++ b/setup.py
@@ -226,6 +226,7 @@ setup(
226 'requests-toolbelt',226 'requests-toolbelt',
227 'responses',227 'responses',
228 'scandir',228 'scandir',
229 'secure-cookie',
229 'setproctitle',230 'setproctitle',
230 'setuptools',231 'setuptools',
231 'six',232 'six',
@@ -247,6 +248,7 @@ setup(
247 'wadllib',248 'wadllib',
248 'WebOb',249 'WebOb',
249 'WebTest',250 'WebTest',
251 'Werkzeug',
250 'WSGIProxy2',252 'WSGIProxy2',
251 'z3c.ptcompat',253 'z3c.ptcompat',
252 'zc.zservertracelog',254 'zc.zservertracelog',

Subscribers

People subscribed via source and target branches

to status/vote changes: