Merge lp:~leonardr/launchpad/no-cookie-vary-header into lp:launchpad

Proposed by Leonard Richardson
Status: Merged
Approved by: Aaron Bentley
Approved revision: 10618
Merge reported by: Leonard Richardson
Merged at revision: not available
Proposed branch: lp:~leonardr/launchpad/no-cookie-vary-header
Merge into: lp:launchpad
Diff against target: 215 lines (+135/-24) (has conflicts)
5 files modified
lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt (+91/-19)
lib/canonical/launchpad/pagetests/webservice/xx-service.txt (+22/-0)
lib/canonical/launchpad/webapp/servers.py (+20/-2)
lib/canonical/launchpad/webapp/tests/test_servers.py (+1/-2)
versions.cfg (+1/-1)
Text conflict in lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt
Contents conflict in lib/lp/bugs/scripts/checkwatches/core.py
Path conflict: lib/lp/bugs/scripts/checkwatches/core.py / lib/lp/bugs/scripts/checkwatches/core.py
Contents conflict in lib/lp/bugs/scripts/checkwatches/tests/test_core.py
Path conflict: lib/lp/bugs/scripts/checkwatches/tests/test_core.py / lib/lp/bugs/scripts/checkwatches/tests/test_core.py
Contents conflict in lib/lp/registry/model/projectgroup.py
Path conflict: lib/lp/registry/model/projectgroup.py / lib/lp/registry/model/projectgroup.py
To merge this branch: bzr merge lp:~leonardr/launchpad/no-cookie-vary-header
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Gary Poster (community) Approve
Review via email: mp+22812@code.launchpad.net

Description of the change

This branch stops Launchpad from including 'Cookie' in its Vary header when serving web service requests. Although not technically incorrect (the Launchpad web service doesn't use cookies at all), it's not necessary to mention this header, and mentioning it triggers an httplib2 bug (http://code.google.com/p/httplib2/issues/detail?id=94) which manifests in launchpadlib as bug 521927.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

I think there are supposed to be two blank lines, not one, before a new header, as on line 8 of the patch. Otherwise, good.

review: Approve
Revision history for this message
Leonard Richardson (leonardr) wrote :

I decided to use my new powers to write launchpadlib tests for this in Launchpad, and discovered that removing Cookie doesn't solve the problem: we also need to remove Authorization, because every OAuth nonce is different.

I gave this some thought and decided that this is fine with our existing web service, but depending on the performance improvements we make in the future, we might want to change launchpadlib to keep a different cache for every authorization token. This would degrade client-side performance but it would make it impossible for less-privileged tokens to accidentally get cached data obtained by more privileged tokens.

I don't know how much this matters, and it certainly wouldn't affect a malicious program, because a malicious program can just scour the .launchpadlib directory for a more-privileged token and use that token instead.

10619. By Leonard Richardson

Fixed spacing.

10620. By Leonard Richardson

Merged in the launchpadlib pagetest branch.

10621. By Leonard Richardson

Added test, removed Authorization from Vary so the test would work.

10622. By Leonard Richardson

Merge trunk.

10623. By Leonard Richardson

Fixed test.

Revision history for this message
Gary Poster (gary) wrote :

The launchpadlib test is awesome. Yay.

The potential security issues are concerning, but they do seem fine now.

IRC discussion:

[10:23am] gary_poster: leonardr: xx-service.txt looks like it would fail now. Your code says self.response.setHeader('Vary', 'Accept') but your test says
[10:23am] gary_poster: 83+ >>> response = webservice.get(
[10:23am] gary_poster: 84+ ... 'http://bugs.launchpad.dev/api/devel/')
[10:23am] gary_poster: 85+ >>> print response.getheader('Vary')
[10:23am] gary_poster: 86+ Authorization, Accept
[10:23am] leonardr: gary: thanks, don't know how i missed that

review: Approve
Revision history for this message
Aaron Bentley (abentley) wrote :

I hearby reapprove this with the new authorization non-variance.

review: Approve
10624. By Leonard Richardson

Fixed test again.

10625. By Leonard Richardson

Fixed test failure.

10626. By Leonard Richardson

Updated versions.cfg to seek a new version of launchpadlib.

10627. By Leonard Richardson

Merge with trunk.

10628. By Leonard Richardson

Merged from trunk and resolved conflict.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt'
--- lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt 2010-04-23 18:53:53 +0000
+++ lib/canonical/launchpad/pagetests/webservice/launchpadlib.txt 2010-04-27 15:30:57 +0000
@@ -1,5 +1,6 @@
1*******************************
1Using launchpadlib in pagetests2Using launchpadlib in pagetests
2===============================3*******************************
34
4As an alternative to crafting HTTP requests with the 'webservice'5As an alternative to crafting HTTP requests with the 'webservice'
5object, you can write pagetests using launchpadlib.6object, you can write pagetests using launchpadlib.
@@ -50,21 +51,92 @@
50 >>> launchpad = Launchpad(credentials, 'http://api.launchpad.dev/')51 >>> launchpad = Launchpad(credentials, 'http://api.launchpad.dev/')
51 >>> print launchpad.me.name52 >>> print launchpad.me.name
52 no-priv53 no-priv
5354<<<<<<< TREE
54 >>> lp_anon = Launchpad.login_anonymously('launchpadlib test',55
55 ... 'http://api.launchpad.dev/')56 >>> lp_anon = Launchpad.login_anonymously('launchpadlib test',
5657 ... 'http://api.launchpad.dev/')
57The Launchpad object for the anonymous user can be used to access58
58public information.59The Launchpad object for the anonymous user can be used to access
5960public information.
60 >>> apache_results = lp_anon.project_groups.search(text="Apache")61
61 >>> print apache_results[0].name62 >>> apache_results = lp_anon.project_groups.search(text="Apache")
62 apache63 >>> print apache_results[0].name
6364 apache
64But trying to access information that requires a logged in user65
65results in an error.66But trying to access information that requires a logged in user
6667results in an error.
67 >>> print lp_anon.me.name68
68 Traceback (most recent call last):69 >>> print lp_anon.me.name
69 ...70 Traceback (most recent call last):
70 HTTPError: HTTP Error 401: Unauthorized...71 ...
72 HTTPError: HTTP Error 401: Unauthorized...
73=======
74
75Anonymous access
76================
77
78 >>> lp_anon = Launchpad.login_anonymously('launchpadlib test',
79 ... 'http://api.launchpad.dev/')
80
81The Launchpad object for the anonymous user can be used to access
82public information.
83
84 >>> apache_results = lp_anon.project_groups.search(text="Apache")
85 >>> print apache_results[0].name
86 apache
87
88But trying to access information that requires a logged in user
89results in an error.
90
91 >>> print lp_anon.me.name
92 Traceback (most recent call last):
93 ...
94 HTTPError: HTTP Error 401: Unauthorized...
95
96Caching
97=======
98
99Let's make sure Launchpad serves caching-related headers that make
100launchpadlib work correctly. First, we set up a temporary directory to
101store the cache.
102
103 >>> import tempfile
104 >>> cache = tempfile.mkdtemp()
105
106Then we make it possible to view the HTTP traffic between launchpadlib
107and Launchpad.
108
109 >>> import httplib2
110 >>> old_debug_level = httplib2.debuglevel
111 >>> httplib2.debuglevel = 1
112
113Now create a Launchpad object and observe how it populates the cache.
114
115 >>> launchpad = Launchpad(
116 ... credentials, 'http://api.launchpad.dev/', cache=cache)
117 send: 'GET /1.0/ ...accept: application/vnd.sun.wadl+xml...'
118 reply: 'HTTP/1.0 200 Ok\n'
119 ...
120 send: 'GET /1.0/ ...accept: application/json...'
121 reply: 'HTTP/1.0 200 Ok\n'
122 ...
123
124Create a second Launchpad object, and the cached documents will be
125used to make conditional HTTP requests. The conditions will succeed,
126and launchpadlib will not have to download the documents again.
127
128 >>> launchpad = Launchpad(
129 ... credentials, 'http://api.launchpad.dev/', cache=cache)
130 send: 'GET /1.0/ ...accept: application/vnd.sun.wadl+xml...'
131 reply: 'HTTP/1.0 304 Not Modified\n'
132 ...
133 send: 'GET /1.0/ ...accept: application/json...'
134 reply: 'HTTP/1.0 304 Not Modified\n'
135 ...
136
137Cleanup.
138
139 >>> import shutil
140 >>> shutil.rmtree(cache)
141 >>> httplib2.debuglevel = old_debug_level
142>>>>>>> MERGE-SOURCE
71143
=== modified file 'lib/canonical/launchpad/pagetests/webservice/xx-service.txt'
--- lib/canonical/launchpad/pagetests/webservice/xx-service.txt 2010-02-16 17:05:07 +0000
+++ lib/canonical/launchpad/pagetests/webservice/xx-service.txt 2010-04-27 15:30:57 +0000
@@ -209,3 +209,25 @@
209 HTTP/1.1 401 Unauthorized209 HTTP/1.1 401 Unauthorized
210 ...210 ...
211 Request is missing an OAuth consumer key.211 Request is missing an OAuth consumer key.
212
213
214The 'Vary' Header
215=================
216
217Launchpad's web service sets the Vary header differently from other
218parts of Launchpad.
219
220 >>> browser.open("http://launchpad.dev/")
221 >>> print browser.headers['Vary']
222 Cookie, Authorization
223
224 >>> response = webservice.get(
225 ... 'http://bugs.launchpad.dev/api/devel/')
226 >>> print response.getheader('Vary')
227 Accept
228
229The web service's Vary header does not mention the 'Cookie' header,
230because the web service doesn't use cookies. It doesn't mention the
231'Authorization' header, because every web service request has a
232distinct 'Authorization' header. It does mention the 'Accept' header,
233because the web service does use content negotiation.
212234
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2010-03-19 20:02:25 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2010-04-27 15:30:57 +0000
@@ -1267,8 +1267,26 @@
1267 def __init__(self, body_instream, environ, response=None):1267 def __init__(self, body_instream, environ, response=None):
1268 super(WebServiceClientRequest, self).__init__(1268 super(WebServiceClientRequest, self).__init__(
1269 body_instream, environ, response)1269 body_instream, environ, response)
1270 # Web service requests use content negotiation.1270 # Web service requests use content negotiation, so we put
1271 self.response.setHeader('Vary', 'Cookie, Authorization, Accept')1271 # 'Accept' in the Vary header. They don't use cookies, so
1272 # there's no point in putting 'Cookie' in the Vary header, and
1273 # putting 'Authorization' in the Vary header totally destroys
1274 # caching because every web service request contains a
1275 # distinct OAuth nonce in its Authorization header.
1276 #
1277 # Because 'Authorization' is not in the Vary header, a client
1278 # that reuses a single cache for different OAuth credentials
1279 # could conceivably leak private information to an
1280 # unprivileged user via the cache. This won't happen for the
1281 # web service root resource because the service root is the
1282 # same for everybody. It won't happen for entry resources
1283 # because if two users have a different representation of an
1284 # entry, the ETag will also be different and a conditional
1285 # request will fail.
1286 #
1287 # Once lazr.restful starts setting caching directives other
1288 # than ETag, we may have to revisit this.
1289 self.response.setHeader('Vary', 'Accept')
12721290
1273 def getRootURL(self, rootsite):1291 def getRootURL(self, rootsite):
1274 """See IBasicLaunchpadRequest."""1292 """See IBasicLaunchpadRequest."""
12751293
=== modified file 'lib/canonical/launchpad/webapp/tests/test_servers.py'
--- lib/canonical/launchpad/webapp/tests/test_servers.py 2010-03-16 13:36:29 +0000
+++ lib/canonical/launchpad/webapp/tests/test_servers.py 2010-04-27 15:30:57 +0000
@@ -315,8 +315,7 @@
315 def test_response_should_vary_based_on_content_type(self):315 def test_response_should_vary_based_on_content_type(self):
316 request = WebServiceClientRequest(StringIO.StringIO(''), {})316 request = WebServiceClientRequest(StringIO.StringIO(''), {})
317 self.assertEquals(317 self.assertEquals(
318 request.response.getHeader('Vary'),318 request.response.getHeader('Vary'), 'Accept')
319 'Cookie, Authorization, Accept')
320319
321320
322class TestBasicLaunchpadRequest(TestCase):321class TestBasicLaunchpadRequest(TestCase):
323322
=== renamed file 'lib/lp/bugs/scripts/checkwatches/core.py' => 'lib/lp/bugs/scripts/checkwatches/core.py.THIS'
=== renamed file 'lib/lp/bugs/scripts/checkwatches/tests/test_core.py' => 'lib/lp/bugs/scripts/checkwatches/tests/test_core.py.THIS'
=== renamed file 'lib/lp/registry/model/projectgroup.py' => 'lib/lp/registry/model/projectgroup.py.THIS'
=== modified file 'versions.cfg'
--- versions.cfg 2010-04-22 04:56:27 +0000
+++ versions.cfg 2010-04-27 15:30:57 +0000
@@ -21,7 +21,7 @@
21grokcore.component = 1.621grokcore.component = 1.6
22httplib2 = 0.6.022httplib2 = 0.6.0
23ipython = 0.9.123ipython = 0.9.1
24launchpadlib = 1.5.824launchpadlib = 1.6.0
25lazr.authentication = 0.1.125lazr.authentication = 0.1.1
26lazr.batchnavigator = 1.126lazr.batchnavigator = 1.1
27lazr.config = 1.1.327lazr.config = 1.1.3