Merge lp:~gary/launchpad/loggerheadlogout into lp:launchpad

Proposed by Gary Poster
Status: Merged
Approved by: Gary Poster
Approved revision: no longer in the source branch.
Merged at revision: 10855
Proposed branch: lp:~gary/launchpad/loggerheadlogout
Merge into: lp:launchpad
Diff against target: 408 lines (+258/-13)
8 files modified
buildout-templates/bin/test.in (+1/-1)
lib/canonical/launchpad/tests/test_login.py (+60/-2)
lib/canonical/launchpad/webapp/login.py (+5/-4)
lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt (+14/-3)
lib/launchpad_loggerhead/app.py (+15/-1)
lib/launchpad_loggerhead/session.py (+9/-2)
lib/launchpad_loggerhead/tests.py (+146/-0)
lib/lp/testopenid/browser/server.py (+8/-0)
To merge this branch: bzr merge lp:~gary/launchpad/loggerheadlogout
Reviewer Review Type Date Requested Status
Paul Hummer (community) Approve
Review via email: mp+25108@code.launchpad.net

Commit message

log out from bzr and openid after logging out from Launchpad.

Description of the change

This is a merge of changes from a branch that's been applied to production. Please see the previous MP for details:

https://code.edge.launchpad.net/~bac/launchpad/bug-574493/+merge/24853

Thank you

Gary

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'buildout-templates/bin/test.in'
--- buildout-templates/bin/test.in 2010-04-26 16:00:31 +0000
+++ buildout-templates/bin/test.in 2010-05-11 22:19:24 +0000
@@ -162,7 +162,7 @@
162 # Find tests in the tests and ftests directories162 # Find tests in the tests and ftests directories
163 'tests_pattern': '^f?tests$',163 'tests_pattern': '^f?tests$',
164 'test_path': [${buildout:directory/lib|path-repr}],164 'test_path': [${buildout:directory/lib|path-repr}],
165 'package': ['canonical', 'lp', 'devscripts'],165 'package': ['canonical', 'lp', 'devscripts', 'launchpad_loggerhead'],
166 'layer': ['!(MailmanLayer)'],166 'layer': ['!(MailmanLayer)'],
167 }167 }
168168
169169
=== modified file 'lib/canonical/launchpad/tests/test_login.py'
--- lib/canonical/launchpad/tests/test_login.py 2010-03-30 20:02:53 +0000
+++ lib/canonical/launchpad/tests/test_login.py 2010-05-11 22:19:24 +0000
@@ -1,9 +1,11 @@
1# Copyright 2009 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
4import cgi
4from datetime import datetime5from datetime import datetime
5import unittest6import unittest
67
8import lazr.uri
7from zope.component import getUtility9from zope.component import getUtility
8from zope.event import notify10from zope.event import notify
9from zope.session.interfaces import ISession11from zope.session.interfaces import ISession
@@ -17,7 +19,8 @@
17from canonical.launchpad.webapp.authentication import LaunchpadPrincipal19from canonical.launchpad.webapp.authentication import LaunchpadPrincipal
18from canonical.launchpad.webapp.interfaces import (20from canonical.launchpad.webapp.interfaces import (
19 CookieAuthLoggedInEvent, ILaunchpadPrincipal, IPlacelessAuthUtility)21 CookieAuthLoggedInEvent, ILaunchpadPrincipal, IPlacelessAuthUtility)
20from canonical.launchpad.webapp.login import logInPrincipal, logoutPerson22from canonical.launchpad.webapp.login import (
23 CookieLogoutPage, logInPrincipal, logoutPerson)
21from canonical.launchpad.webapp.servers import LaunchpadTestRequest24from canonical.launchpad.webapp.servers import LaunchpadTestRequest
22from canonical.testing import DatabaseFunctionalLayer25from canonical.testing import DatabaseFunctionalLayer
2326
@@ -71,6 +74,61 @@
71 self.request)74 self.request)
72 self.failUnless(principal is None)75 self.failUnless(principal is None)
7376
77 def test_CookieLogoutPage(self):
78 # This test shows that the CookieLogoutPage redirects as we expect:
79 # first to loggerhead for it to log out (see bug 574493) and then
80 # to our OpenId provider for it to log out (see bug 568106). This
81 # will need to be readdressed when we want to accept other OpenId
82 # providers, unfortunately.
83
84 # This is to setup an interaction so that we can call logInPrincipal
85 # below.
86 login('foo.bar@example.com')
87
88 logInPrincipal(self.request, self.principal, 'foo.bar@example.com')
89
90 # Normally CookieLogoutPage is magically mixed in with a base class
91 # that accepts context and request and sets up other things. We're
92 # just going to put the request on the base class ourselves for this
93 # test.
94
95 view = CookieLogoutPage()
96 view.request = self.request
97
98 # We need to set the session cookie so it can be expired.
99 self.request.response.setCookie(
100 config.launchpad_session.cookie, 'xxx')
101
102 # Now we logout.
103
104 result = view.logout()
105
106 # We should, in fact, be logged out (this calls logoutPerson).
107
108 principal = getUtility(IPlacelessAuthUtility).authenticate(
109 self.request)
110 self.failUnless(principal is None)
111
112 # The view should have redirected us, with no actual response body.
113
114 self.assertEquals(self.request.response.getStatus(), 302)
115 self.assertEquals(result, '')
116
117 # We are redirecting to Loggerhead, to ask it to logout.
118
119 location = lazr.uri.URI(self.request.response.getHeader('location'))
120 self.assertEquals(location.host, 'bazaar.launchpad.dev')
121 self.assertEquals(location.scheme, 'https')
122 self.assertEquals(location.path, '/+logout')
123
124 # That page should then redirect to our OpenId provider to logout,
125 # which we provide in our query string. See
126 # launchpad_loggerhead.tests.TestLogout for the pertinent tests.
127
128 query = cgi.parse_qs(location.query)
129 self.assertEquals(
130 query['next_to'][0], 'http://testopenid.dev/+logout')
131
74 def test_logging_in_and_logging_out_the_old_way(self):132 def test_logging_in_and_logging_out_the_old_way(self):
75 # A test showing that we can authenticate a request that had the133 # A test showing that we can authenticate a request that had the
76 # person/account ID stored in the 'personid' session variable instead134 # person/account ID stored in the 'personid' session variable instead
77135
=== modified file 'lib/canonical/launchpad/webapp/login.py'
--- lib/canonical/launchpad/webapp/login.py 2010-04-27 18:48:31 +0000
+++ lib/canonical/launchpad/webapp/login.py 2010-05-11 22:19:24 +0000
@@ -502,10 +502,11 @@
502502
503 def logout(self):503 def logout(self):
504 logoutPerson(self.request)504 logoutPerson(self.request)
505 self.request.response.addNoticeNotification(505 openid_vhost = config.launchpad.openid_provider_vhost
506 _(u'You have been logged out')506 openid_root = allvhosts.configs[openid_vhost].rooturl
507 )507 target = '%s+logout?%s' % (
508 target = '%s/?loggingout=1' % self.request.URL[-1]508 config.codehosting.secure_codebrowse_root,
509 urllib.urlencode(dict(next_to='%s+logout' % (openid_root,))))
509 self.request.response.redirect(target)510 self.request.response.redirect(target)
510 return ''511 return ''
511512
512513
=== modified file 'lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt'
--- lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt 2010-02-25 10:50:31 +0000
+++ lib/canonical/launchpad/webapp/tests/no-anonymous-session-cookies.txt 2010-05-11 22:19:24 +0000
@@ -1,5 +1,5 @@
1We will verify that we do not put session cookies in anonymous requests. This1We will verify that we do not put session cookies in anonymous requests. This
2is important for cacheing anonymous requests in front of Zope, such as with2is important for caching anonymous requests in front of Zope, such as with
3Squid. Note that we are checking whether the browser has a session cookie3Squid. Note that we are checking whether the browser has a session cookie
4set, not whether the server has sent a "set-cookie" header.4set, not whether the server has sent a "set-cookie" header.
55
@@ -49,8 +49,19 @@
49minute time interval (set in canonical.launchpad.webapp.login and enforced49minute time interval (set in canonical.launchpad.webapp.login and enforced
50with an assert in canonical.launchpad.webapp.session) is intended to be fudge50with an assert in canonical.launchpad.webapp.session) is intended to be fudge
51time for browsers with bad system clocks.51time for browsers with bad system clocks.
5252 >>> # XXX 2010-05-08 bac bug=577596 This work-around for the fact
53 >>> browser.getControl('Log Out').click()53 >>> # that loggerhead is not running needs to be replaced with
54 >>> # something more robust and clear.
55 >>> from urllib2 import HTTPError, URLError
56 >>> try:
57 ... browser.getControl('Log Out').click()
58 ... except (HTTPError, URLError):
59 ... pass
60
61After ensuring the browser has not left the launchpad.dev domain, the
62single cookie is shown to have the ten minute expiration.
63
64 >>> browser.open('http://launchpad.dev:8085')
54 >>> len(browser.cookies)65 >>> len(browser.cookies)
55 166 1
56 >>> expires = browser.cookies.getinfo('launchpad_tests')['expires']67 >>> expires = browser.cookies.getinfo('launchpad_tests')['expires']
5768
=== modified file 'lib/launchpad_loggerhead/app.py'
--- lib/launchpad_loggerhead/app.py 2010-05-04 23:42:28 +0000
+++ lib/launchpad_loggerhead/app.py 2010-05-11 22:19:24 +0000
@@ -123,7 +123,7 @@
123 elif response.status == CANCEL:123 elif response.status == CANCEL:
124 self.log.error('open id response: CANCEL')124 self.log.error('open id response: CANCEL')
125 exc = HTTPUnauthorized()125 exc = HTTPUnauthorized()
126 exc.explanation = "Authetication cancelled."126 exc.explanation = "Authentication cancelled."
127 raise exc127 raise exc
128 else:128 else:
129 self.log.error('open id response: UNKNOWN')129 self.log.error('open id response: UNKNOWN')
@@ -131,6 +131,18 @@
131 exc.explanation = "Unknown OpenID response."131 exc.explanation = "Unknown OpenID response."
132 raise exc132 raise exc
133133
134 def _logout(self, environ, start_response):
135 """Logout of loggerhead.
136
137 Clear the cookie and redirect to `next_to`.
138 """
139 environ[self.session_var].clear()
140 query = dict(parse_querystring(environ))
141 next_url = query.get('next_to')
142 if next_url is None:
143 next_url = allvhosts.configs['mainsite'].rooturl
144 raise HTTPMovedPermanently(next_url)
145
134 def __call__(self, environ, start_response):146 def __call__(self, environ, start_response):
135 environ['loggerhead.static.url'] = environ['SCRIPT_NAME']147 environ['loggerhead.static.url'] = environ['SCRIPT_NAME']
136 if environ['PATH_INFO'].startswith('/static/'):148 if environ['PATH_INFO'].startswith('/static/'):
@@ -142,6 +154,8 @@
142 return robots_app(environ, start_response)154 return robots_app(environ, start_response)
143 elif environ['PATH_INFO'].startswith('/+login'):155 elif environ['PATH_INFO'].startswith('/+login'):
144 return self._complete_login(environ, start_response)156 return self._complete_login(environ, start_response)
157 elif environ['PATH_INFO'].startswith('/+logout'):
158 return self._logout(environ, start_response)
145 path = environ['PATH_INFO']159 path = environ['PATH_INFO']
146 trailingSlashCount = len(path) - len(path.rstrip('/'))160 trailingSlashCount = len(path) - len(path.rstrip('/'))
147 user = environ[self.session_var].get('user', LAUNCHPAD_ANONYMOUS)161 user = environ[self.session_var].get('user', LAUNCHPAD_ANONYMOUS)
148162
=== modified file 'lib/launchpad_loggerhead/session.py'
--- lib/launchpad_loggerhead/session.py 2010-04-27 01:35:56 +0000
+++ lib/launchpad_loggerhead/session.py 2010-05-11 22:19:24 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009 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 paste-y session manager tuned for the needs of launchpad-loggerhead.
@@ -64,10 +64,17 @@
64 session = pickle.loads(environ[self.session_var])64 session = pickle.loads(environ[self.session_var])
65 else:65 else:
66 session = {}66 session = {}
67 existed = bool(session)
67 environ[self.session_var] = session68 environ[self.session_var] = session
68 def response_hook(status, response_headers, exc_info=None):69 def response_hook(status, response_headers, exc_info=None):
69 session = environ.pop(self.session_var)70 session = environ.pop(self.session_var)
70 if session:71 # paste.auth.cookie does not delete cookies (see
72 # http://trac.pythonpaste.org/pythonpaste/ticket/139). A
73 # reasonable workaround is to make the value empty. Therefore,
74 # we explicitly set the value in the session (to be encrypted)
75 # if the value is non-empty *or* if it was non-empty at the start
76 # of the request.
77 if existed or session:
71 environ[self.session_var] = pickle.dumps(session)78 environ[self.session_var] = pickle.dumps(session)
72 return start_response(status, response_headers, exc_info)79 return start_response(status, response_headers, exc_info)
73 return self.application(environ, response_hook)80 return self.application(environ, response_hook)
7481
=== added file 'lib/launchpad_loggerhead/tests.py'
--- lib/launchpad_loggerhead/tests.py 1970-01-01 00:00:00 +0000
+++ lib/launchpad_loggerhead/tests.py 2010-05-11 22:19:24 +0000
@@ -0,0 +1,146 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4import unittest
5import urllib
6
7import lazr.uri
8import wsgi_intercept
9from wsgi_intercept.urllib2_intercept import install_opener, uninstall_opener
10import wsgi_intercept.zope_testbrowser
11from paste.httpexceptions import HTTPExceptionHandler
12
13from canonical.config import config
14from canonical.launchpad.webapp.vhosts import allvhosts
15from canonical.testing import DatabaseFunctionalLayer
16from launchpad_loggerhead.app import RootApp
17from launchpad_loggerhead.session import SessionHandler
18from lp.testing import TestCase
19
20SESSION_VAR = 'lh.session'
21
22# See sourcecode/launchpad-loggerhead/start-loggerhead.py for the production
23# mechanism for getting the secret.
24SECRET = 'secret'
25
26
27def session_scribbler(app, test):
28 """Squirrel away the session variable."""
29 def scribble(environ, start_response):
30 test.session = environ[SESSION_VAR] # Yay for mutables.
31 return app(environ, start_response)
32 return scribble
33
34
35def dummy_destination(environ, start_response):
36 """Return a fake response."""
37 start_response('200 OK', [('Content-type','text/plain')])
38 return ['This is a dummy destination.\n']
39
40
41class SimpleLogInRootApp(RootApp):
42 """A mock root app that doesn't require open id."""
43 def _complete_login(self, environ, start_response):
44 environ[SESSION_VAR]['user'] = 'bob'
45 start_response('200 OK', [('Content-type','text/plain')])
46 return ['\n']
47
48
49class TestLogout(TestCase):
50 layer = DatabaseFunctionalLayer
51
52 def intercept(self, uri, app):
53 """Install wsgi interceptors for the uri, app tuple."""
54 if isinstance(uri, basestring):
55 uri = lazr.uri.URI(uri)
56 port = uri.port
57 if port is None:
58 if uri.scheme == 'http':
59 port = 80
60 elif uri.scheme == 'https':
61 port = 443
62 else:
63 raise NotImplementedError(uri.scheme)
64 else:
65 port = int(port)
66 wsgi_intercept.add_wsgi_intercept(uri.host, port, lambda: app)
67 self.intercepted.append((uri.host, port))
68
69 def setUp(self):
70 TestCase.setUp(self)
71 self.intercepted = []
72 self.session = None
73 self.root = app = SimpleLogInRootApp(SESSION_VAR)
74 app = session_scribbler(app, self)
75 app = HTTPExceptionHandler(app)
76 app = SessionHandler(app, SESSION_VAR, SECRET)
77 self.cookie_name = app.cookie_handler.cookie_name
78 self.intercept(config.codehosting.codebrowse_root, app)
79 self.intercept(config.codehosting.secure_codebrowse_root, app)
80 self.intercept(allvhosts.configs['mainsite'].rooturl,
81 dummy_destination)
82 install_opener()
83 self.browser = wsgi_intercept.zope_testbrowser.WSGI_Browser()
84 # We want to pretend we are not a robot, or else mechanize will honor
85 # robots.txt.
86 self.browser.mech_browser.set_handle_robots(False)
87 self.browser.open(
88 config.codehosting.secure_codebrowse_root + '+login')
89
90 def tearDown(self):
91 uninstall_opener()
92 for host, port in self.intercepted:
93 wsgi_intercept.remove_wsgi_intercept(host, port)
94 TestCase.tearDown(self)
95
96 def testLoggerheadLogout(self):
97 # We start logged in as 'bob'.
98 self.assertEqual(self.session['user'], 'bob')
99 self.browser.open(
100 config.codehosting.secure_codebrowse_root + 'favicon.ico')
101 self.assertEqual(self.session['user'], 'bob')
102 self.failUnless(self.browser.cookies.get(self.cookie_name))
103
104 # When we visit +logout, our session is gone.
105 self.browser.open(
106 config.codehosting.secure_codebrowse_root + '+logout')
107 self.assertEqual(self.session, {})
108
109 # By default, we have been redirected to the Launchpad root.
110 self.assertEqual(
111 self.browser.url, allvhosts.configs['mainsite'].rooturl)
112
113 # The session cookie still exists, because of how
114 # paste.auth.cookie works (see
115 # http://trac.pythonpaste.org/pythonpaste/ticket/139 ) but the user
116 # does in fact have an empty session now.
117 self.browser.open(
118 config.codehosting.secure_codebrowse_root + 'favicon.ico')
119 self.assertEqual(self.session, {})
120
121 def testLoggerheadLogoutRedirect(self):
122 # When we visit +logout with a 'next_to' value in the query string,
123 # the logout page will redirect to the given URI. As of this
124 # writing, this is used by Launchpad to redirect to our OpenId
125 # provider (see canonical.launchpad.tests.test_login.
126 # TestLoginAndLogout.test_CookieLogoutPage).
127
128 # Here, we will have a more useless example of the basic machinery.
129 dummy_root = 'http://dummy.dev/'
130 self.intercept(dummy_root, dummy_destination)
131 self.browser.open(
132 config.codehosting.secure_codebrowse_root +
133 '+logout?' +
134 urllib.urlencode(dict(next_to=dummy_root + '+logout')))
135
136 # We are logged out, as before.
137 self.assertEqual(self.session, {})
138
139 # Now, though, we are redirected to the ``next_to`` destination.
140 self.assertEqual(self.browser.url, dummy_root + '+logout')
141 self.assertEqual(self.browser.contents,
142 'This is a dummy destination.\n')
143
144
145def test_suite():
146 return unittest.TestLoader().loadTestsFromName(__name__)
0147
=== modified file 'lib/lp/testopenid/browser/server.py'
--- lib/lp/testopenid/browser/server.py 2010-03-31 19:38:32 +0000
+++ lib/lp/testopenid/browser/server.py 2010-05-11 22:19:24 +0000
@@ -24,6 +24,7 @@
24from openid import oidutil24from openid import oidutil
25from openid.server.server import CheckIDRequest, Server25from openid.server.server import CheckIDRequest, Server
26from openid.store.memstore import MemoryStore26from openid.store.memstore import MemoryStore
27from openid.extensions.sreg import SRegRequest, SRegResponse
2728
28from canonical.cachedproperty import cachedproperty29from canonical.cachedproperty import cachedproperty
29from canonical.launchpad import _30from canonical.launchpad import _
@@ -41,6 +42,7 @@
41from lp.testopenid.interfaces.server import (42from lp.testopenid.interfaces.server import (
42 get_server_url, ITestOpenIDApplication, ITestOpenIDLoginForm,43 get_server_url, ITestOpenIDApplication, ITestOpenIDLoginForm,
43 ITestOpenIDPersistentIdentity)44 ITestOpenIDPersistentIdentity)
45from lp.registry.interfaces.person import IPerson
4446
4547
46OPENID_REQUEST_SESSION_KEY = 'testopenid.request'48OPENID_REQUEST_SESSION_KEY = 'testopenid.request'
@@ -196,6 +198,12 @@
196 else:198 else:
197 response = self.openid_request.answer(True)199 response = self.openid_request.answer(True)
198200
201 sreg_fields = dict(nickname=IPerson(self.account).name)
202 sreg_request = SRegRequest.fromOpenIDRequest(self.openid_request)
203 sreg_response = SRegResponse.extractResponse(
204 sreg_request, sreg_fields)
205 response.addExtension(sreg_response)
206
199 return response207 return response
200208
201 def createFailedResponse(self):209 def createFailedResponse(self):