Merge lp:~bac/launchpad/bug-750984 into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 13127
Proposed branch: lp:~bac/launchpad/bug-750984
Merge into: lp:launchpad
Diff against target: 418 lines (+92/-55)
3 files modified
lib/canonical/launchpad/doc/webapp-publication.txt (+50/-33)
lib/canonical/launchpad/interfaces/oauth.py (+22/-6)
lib/canonical/launchpad/webapp/servers.py (+20/-16)
To merge this branch: bzr merge lp:~bac/launchpad/bug-750984
Reviewer Review Type Date Requested Status
j.c.sackett (community) Approve
Review via email: mp+62543@code.launchpad.net

Commit message

[r=jcsackett][bug=750984] Don't OOPS on bad API token/nonce/timestamp.

Description of the change

= Summary =

Previously problems with API token, nonce, or timestamp resulted in an
Unauthorized exception being raised which caused an OOPS to be
generated. Since the likely cause of those problems is bad client data
(though server side processing could be at fault) it is inappropriate to
generated an OOPS.

== Proposed fix ==

Rather than returning Unauthorized return custom exceptions that have
been marked with webservice_error as 401. The client still sees an HTTP
401 but no OOPS is reported.

== Pre-implementation notes ==

Talks with Gary and lots of conversation on the bug between Robert and
Martin.

== Tests ==

bin/test -vvt webapp-publication.txt

== Demo and Q/A ==

It should be possible to force bad client data but the details are not
obvious at the moment.

= Launchpad lint =

The lint issues are not fixable.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/interfaces/oauth.py
  lib/canonical/launchpad/doc/webapp-publication.txt
  lib/canonical/launchpad/webapp/servers.py

./lib/canonical/launchpad/doc/webapp-publication.txt
     195: want exceeds 78 characters.
     203: want exceeds 78 characters.
     325: want exceeds 78 characters.

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

This looks good to land.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/doc/webapp-publication.txt'
--- lib/canonical/launchpad/doc/webapp-publication.txt 2011-04-21 01:30:30 +0000
+++ lib/canonical/launchpad/doc/webapp-publication.txt 2011-05-26 19:11:35 +0000
@@ -1,4 +1,5 @@
1= Launchpad Publication =1Launchpad Publication
2=====================
23
3Launchpad uses the generic Zope3 publisher. It registers several4Launchpad uses the generic Zope3 publisher. It registers several
4factories that are responsible for instantiating the appropriate5factories that are responsible for instantiating the appropriate
@@ -6,7 +7,8 @@
6zope.publisher.IPublication for the request.7zope.publisher.IPublication for the request.
78
89
9== Virtual host configurations ==10Virtual host configurations
11---------------------------
1012
11The configuration defines a number of domains, one for the main13The configuration defines a number of domains, one for the main
12Launchpad site and one for the sites of the various applications.14Launchpad site and one for the sites of the various applications.
@@ -118,7 +120,8 @@
118 xmlrpc.launchpad.dev120 xmlrpc.launchpad.dev
119121
120122
121== VirtualHostRequestPublicationFactory ==123VirtualHostRequestPublicationFactory
124------------------------------------
122125
123A number of VirtualHostRequestPublicationFactories are registered with126A number of VirtualHostRequestPublicationFactories are registered with
124Zope to handle requests for a particular vhost, port, and set of HTTP127Zope to handle requests for a particular vhost, port, and set of HTTP
@@ -322,7 +325,8 @@
322 <class 'canonical.launchpad.webapp.publication.LaunchpadBrowserPublication'>325 <class 'canonical.launchpad.webapp.publication.LaunchpadBrowserPublication'>
323326
324327
325== Zope Publisher integration ==328Zope Publisher integration
329--------------------------
326330
327A factory is registered for each of our available virtual host. This331A factory is registered for each of our available virtual host. This
328is done by the register_launchpad_request_publication_factories332is done by the register_launchpad_request_publication_factories
@@ -417,7 +421,8 @@
417 Allow: POST421 Allow: POST
418422
419 >>> print_request_and_publication(423 >>> print_request_and_publication(
420 ... 'xmlrpc.launchpad.dev', method='POST', mime_type='application/xml')424 ... 'xmlrpc.launchpad.dev', method='POST',
425 ... mime_type='application/xml')
421 ProtocolErrorRequest426 ProtocolErrorRequest
422 ProtocolErrorPublication: status=415427 ProtocolErrorPublication: status=415
423428
@@ -483,7 +488,8 @@
483 >>> login(ANONYMOUS)488 >>> login(ANONYMOUS)
484489
485490
486== ILaunchpadBrowserApplicationRequest ==491ILaunchpadBrowserApplicationRequest
492-----------------------------------
487493
488All Launchpad requests provides the ILaunchpadBrowserApplicationRequest494All Launchpad requests provides the ILaunchpadBrowserApplicationRequest
489interface. That interface is an extension of the zope standard495interface. That interface is an extension of the zope standard
@@ -497,7 +503,8 @@
497 True503 True
498504
499505
500== Handling form data using IBrowserFormNG ==506Handling form data using IBrowserFormNG
507---------------------------------------
501508
502Submitted form data is available in the form_ng request attribute. This509Submitted form data is available in the form_ng request attribute. This
503is an object providing the IBrowserFormNG interface which offers two510is an object providing the IBrowserFormNG interface which offers two
@@ -582,7 +589,8 @@
582 items_field589 items_field
583590
584591
585== Page ID ==592Page ID
593-------
586594
587Our publication implementation sets a WSGI variable 'launchpad.pageid'.595Our publication implementation sets a WSGI variable 'launchpad.pageid'.
588This is an identifier of the form ContextName:ViewName.596This is an identifier of the form ContextName:ViewName.
@@ -654,7 +662,8 @@
654 ''662 ''
655663
656664
657== Tick counts ==665Tick counts
666-----------
658667
659Similarly to our page IDs, our publication implementation will store the668Similarly to our page IDs, our publication implementation will store the
660tick counts for the traversal and object publication processes in WSGI669tick counts for the traversal and object publication processes in WSGI
@@ -832,7 +841,8 @@
832 >>> login(ANONYMOUS)841 >>> login(ANONYMOUS)
833842
834843
835== Transaction Logging ==844Transaction Logging
845-------------------
836846
837The publication implementation is responsible for putting the name847The publication implementation is responsible for putting the name
838of the logged in user in the transaction. (The afterCall() hook is848of the logged in user in the transaction. (The afterCall() hook is
@@ -872,7 +882,8 @@
872 / 16882 / 16
873883
874884
875== Read-Only Requests ==885Read-Only Requests
886------------------
876887
877Our publication implementation make sure that requests supposed to be888Our publication implementation make sure that requests supposed to be
878read-only (GET and HEAD) don't change anything in the database.889read-only (GET and HEAD) don't change anything in the database.
@@ -943,7 +954,8 @@
943 Darwin954 Darwin
944955
945956
946=== GET requests on api.launchpad.net ===957GET requests on api.launchpad.net
958.................................
947959
948In the case of WebServicePublication, though, we have to commit the960In the case of WebServicePublication, though, we have to commit the
949transaction after GET requests in order to persist new entries added to961transaction after GET requests in order to persist new entries added to
@@ -992,13 +1004,14 @@
992 ... 'api.launchpad.dev', 'GET',1004 ... 'api.launchpad.dev', 'GET',
993 ... extra_environment=dict(QUERY_STRING=urlencode(form)))1005 ... extra_environment=dict(QUERY_STRING=urlencode(form)))
994 >>> test_request.processInputs()1006 >>> test_request.processInputs()
995 >>> print publication.getPrincipal(test_request).title1007 >>> publication.getPrincipal(test_request)
996 Traceback (most recent call last):1008 Traceback (most recent call last):
997 ...1009 ...
998 Unauthorized: Invalid nonce/timestamp: This nonce has been used already.1010 NonceAlreadyUsed: This nonce has been used already.
9991011
10001012
1001== Doomed transactions are aborted ==1013Doomed transactions are aborted
1014-------------------------------
10021015
1003Doomed transactions are aborted.1016Doomed transactions are aborted.
10041017
@@ -1032,7 +1045,8 @@
1032 >>> del txn.abort # Clean up test fixture.1045 >>> del txn.abort # Clean up test fixture.
10331046
10341047
1035== Requests on Python C Methods succeed ==1048Requests on Python C Methods succeed
1049------------------------------------
10361050
1037Rarely but occasionally, it is possible to traverse to a Python C method.1051Rarely but occasionally, it is possible to traverse to a Python C method.
1038For instance, an XMLRPC proxy might allow a traversal to __repr__.1052For instance, an XMLRPC proxy might allow a traversal to __repr__.
@@ -1048,7 +1062,8 @@
1048 '{}'1062 '{}'
10491063
10501064
1051== HEAD requests have empty body ==1065HEAD requests have empty body
1066-----------------------------
10521067
1053The publication implementation also makes sure that no body is1068The publication implementation also makes sure that no body is
1054returned as part of HEAD requests. (Again this is handled by the1069returned as part of HEAD requests. (Again this is handled by the
@@ -1085,7 +1100,8 @@
1085 Some boring content.1100 Some boring content.
10861101
10871102
1088== Authentication of requests ==1103Authentication of requests
1104--------------------------
10891105
1090In LaunchpadBrowserPublication, authentication happens in the1106In LaunchpadBrowserPublication, authentication happens in the
1091beforeTraversal() hook. Our publication will set the principal to1107beforeTraversal() hook. Our publication will set the principal to
@@ -1183,10 +1199,11 @@
1183 >>> form2 = form.copy()1199 >>> form2 = form.copy()
1184 >>> form2['oauth_nonce'] = '1764572616e48616d6d65724c61686'1200 >>> form2['oauth_nonce'] = '1764572616e48616d6d65724c61686'
1185 >>> test_request = LaunchpadTestRequest(form=form2)1201 >>> test_request = LaunchpadTestRequest(form=form2)
1186 >>> print publication.getPrincipal(test_request).title1202 >>> publication.getPrincipal(test_request)
1187 Traceback (most recent call last):1203 Traceback (most recent call last):
1188 ...1204 ...
1189 Unauthorized: Expired token...1205 TokenException: Expired token...
1206
1190 >>> access_token.date_expires = now + timedelta(days=1)1207 >>> access_token.date_expires = now + timedelta(days=1)
1191 >>> syncUpdate(access_token)1208 >>> syncUpdate(access_token)
11921209
@@ -1194,10 +1211,10 @@
1194 >>> form2['oauth_token'] += 'z'1211 >>> form2['oauth_token'] += 'z'
1195 >>> form2['oauth_nonce'] = '4572616e48616d6d65724c61686176'1212 >>> form2['oauth_nonce'] = '4572616e48616d6d65724c61686176'
1196 >>> test_request = LaunchpadTestRequest(form=form2)1213 >>> test_request = LaunchpadTestRequest(form=form2)
1197 >>> print publication.getPrincipal(test_request).title1214 >>> publication.getPrincipal(test_request)
1198 Traceback (most recent call last):1215 Traceback (most recent call last):
1199 ...1216 ...
1200 Unauthorized: Unknown access token...1217 TokenException: Unknown access token...
12011218
1202The consumer must be registered as well, and the signature must be1219The consumer must be registered as well, and the signature must be
1203correct.1220correct.
@@ -1205,7 +1222,7 @@
1205 >>> form2 = form.copy()1222 >>> form2 = form.copy()
1206 >>> form2['oauth_consumer_key'] += 'z'1223 >>> form2['oauth_consumer_key'] += 'z'
1207 >>> test_request = LaunchpadTestRequest(form=form2)1224 >>> test_request = LaunchpadTestRequest(form=form2)
1208 >>> print publication.getPrincipal(test_request).title1225 >>> publication.getPrincipal(test_request)
1209 Traceback (most recent call last):1226 Traceback (most recent call last):
1210 ...1227 ...
1211 Unauthorized: Unknown consumer (foobar123451432z).1228 Unauthorized: Unknown consumer (foobar123451432z).
@@ -1214,10 +1231,10 @@
1214 >>> form2['oauth_signature'] += 'z'1231 >>> form2['oauth_signature'] += 'z'
1215 >>> form2['oauth_nonce'] = '2616e48616d6d65724c61686176457'1232 >>> form2['oauth_nonce'] = '2616e48616d6d65724c61686176457'
1216 >>> test_request = LaunchpadTestRequest(form=form2)1233 >>> test_request = LaunchpadTestRequest(form=form2)
1217 >>> print publication.getPrincipal(test_request).title1234 >>> publication.getPrincipal(test_request)
1218 Traceback (most recent call last):1235 Traceback (most recent call last):
1219 ...1236 ...
1220 Unauthorized: Invalid signature.1237 TokenException: Invalid signature.
12211238
1222The nonce specified in the request must not have been used before in a request1239The nonce specified in the request must not have been used before in a request
1223with this same token and timestamp.1240with this same token and timestamp.
@@ -1229,10 +1246,10 @@
1229 Guilherme Salgado1246 Guilherme Salgado
12301247
1231 >>> test_request = LaunchpadTestRequest(form=form2)1248 >>> test_request = LaunchpadTestRequest(form=form2)
1232 >>> print publication.getPrincipal(test_request).title1249 >>> publication.getPrincipal(test_request)
1233 Traceback (most recent call last):1250 Traceback (most recent call last):
1234 ...1251 ...
1235 Unauthorized: Invalid nonce/timestamp: This nonce has been used already.1252 NonceAlreadyUsed: This nonce has been used already.
12361253
1237The timestamp must not be older than TIMESTAMP_ACCEPTANCE_WINDOW from the most1254The timestamp must not be older than TIMESTAMP_ACCEPTANCE_WINDOW from the most
1238recent request for this token.1255recent request for this token.
@@ -1247,10 +1264,10 @@
12471264
1248 >>> form2['oauth_timestamp'] -= 101265 >>> form2['oauth_timestamp'] -= 10
1249 >>> test_request = LaunchpadTestRequest(form=form2)1266 >>> test_request = LaunchpadTestRequest(form=form2)
1250 >>> print publication.getPrincipal(test_request).title1267 >>> publication.getPrincipal(test_request)
1251 Traceback (most recent call last):1268 Traceback (most recent call last):
1252 ...1269 ...
1253 Unauthorized: Invalid nonce/timestamp: Timestamp too old compared ...1270 TimestampOrderingError: Timestamp too old compared ...
12541271
1255Last but not least, the timestamp must not be too far in the future, as1272Last but not least, the timestamp must not be too far in the future, as
1256defined by TIMESTAMP_SKEW_WINDOW.1273defined by TIMESTAMP_SKEW_WINDOW.
@@ -1260,10 +1277,10 @@
1260 >>> form2 = form.copy()1277 >>> form2 = form.copy()
1261 >>> form2['oauth_timestamp'] += (TIMESTAMP_SKEW_WINDOW+10)1278 >>> form2['oauth_timestamp'] += (TIMESTAMP_SKEW_WINDOW+10)
1262 >>> test_request = LaunchpadTestRequest(form=form2)1279 >>> test_request = LaunchpadTestRequest(form=form2)
1263 >>> print publication.getPrincipal(test_request).title1280 >>> publication.getPrincipal(test_request)
1264 Traceback (most recent call last):1281 Traceback (most recent call last):
1265 ...1282 ...
1266 Unauthorized: Invalid nonce/timestamp: Timestamp ... from bad system clock1283 ClockSkew: Timestamp ... from bad system clock
12671284
1268Close the bogus request that was started by the call to1285Close the bogus request that was started by the call to
1269beforeTraversal, in order to ensure we leave our state sane.1286beforeTraversal, in order to ensure we leave our state sane.
12701287
=== modified file 'lib/canonical/launchpad/interfaces/oauth.py'
--- lib/canonical/launchpad/interfaces/oauth.py 2010-10-18 11:34:31 +0000
+++ lib/canonical/launchpad/interfaces/oauth.py 2011-05-26 19:11:35 +0000
@@ -20,8 +20,10 @@
20 'NonceAlreadyUsed',20 'NonceAlreadyUsed',
21 'TimestampOrderingError',21 'TimestampOrderingError',
22 'ClockSkew',22 'ClockSkew',
23 'TokenException',
23 ]24 ]
2425
26import httplib
25from zope.interface import (27from zope.interface import (
26 Attribute,28 Attribute,
27 Interface,29 Interface,
@@ -34,6 +36,8 @@
34 TextLine,36 TextLine,
35 )37 )
3638
39from lazr.restful.declarations import webservice_error
40
37from canonical.launchpad import _41from canonical.launchpad import _
38from canonical.launchpad.webapp.interfaces import (42from canonical.launchpad.webapp.interfaces import (
39 AccessLevel,43 AccessLevel,
@@ -300,14 +304,26 @@
300 """Marker interface for a request signed with OAuth credentials."""304 """Marker interface for a request signed with OAuth credentials."""
301305
302306
303# Note that these three exceptions are converted to Unauthorized (equating to307# Note that these exceptions are marked as UNAUTHORIZED (401 status)
304# 401 status) in webapp/servers.py, WebServicePublication.getPrincipal.308# so they may be raised but will not cause an OOPS to be generated. The
305309# client will see them as an UNAUTHORIZED error.
306class NonceAlreadyUsed(Exception):310
311class _TokenException(Exception):
312 """Base class for token, nonce, and timestamp exceptions."""
313 webservice_error(httplib.UNAUTHORIZED)
314
315
316class NonceAlreadyUsed(_TokenException):
307 """Nonce has been used together with same token but another timestamp."""317 """Nonce has been used together with same token but another timestamp."""
308318
309class TimestampOrderingError(Exception):319
320class TimestampOrderingError(_TokenException):
310 """Timestamp is too old, compared to the last request."""321 """Timestamp is too old, compared to the last request."""
311322
312class ClockSkew(Exception):323
324class ClockSkew(_TokenException):
313 """Timestamp is too far off from server's clock."""325 """Timestamp is too far off from server's clock."""
326
327
328class TokenException(_TokenException):
329 """Token has expired."""
314330
=== modified file 'lib/canonical/launchpad/webapp/servers.py'
--- lib/canonical/launchpad/webapp/servers.py 2011-05-20 19:35:04 +0000
+++ lib/canonical/launchpad/webapp/servers.py 2011-05-26 19:11:35 +0000
@@ -67,11 +67,9 @@
67 IWebServiceApplication,67 IWebServiceApplication,
68 )68 )
69from canonical.launchpad.interfaces.oauth import (69from canonical.launchpad.interfaces.oauth import (
70 ClockSkew,
71 IOAuthConsumerSet,70 IOAuthConsumerSet,
72 IOAuthSignedRequest,71 IOAuthSignedRequest,
73 NonceAlreadyUsed,72 TokenException,
74 TimestampOrderingError,
75 )73 )
76import canonical.launchpad.layers74import canonical.launchpad.layers
77from canonical.launchpad.webapp.authentication import (75from canonical.launchpad.webapp.authentication import (
@@ -1025,7 +1023,7 @@
10251023
10261024
1027http = wsgi.ServerType(1025http = wsgi.ServerType(
1028 ZServerTracelogServer, # subclass of WSGIHTTPServer1026 ZServerTracelogServer, # subclass of WSGIHTTPServer
1029 WSGIPublisherApplication,1027 WSGIPublisherApplication,
1030 LaunchpadAccessLogger,1028 LaunchpadAccessLogger,
1031 8080,1029 8080,
@@ -1039,7 +1037,7 @@
1039 True)1037 True)
10401038
1041debughttp = wsgi.ServerType(1039debughttp = wsgi.ServerType(
1042 ZServerTracelogServer, # subclass of WSGIHTTPServer1040 ZServerTracelogServer, # subclass of WSGIHTTPServer
1043 WSGIPublisherApplication,1041 WSGIPublisherApplication,
1044 LaunchpadAccessLogger,1042 LaunchpadAccessLogger,
1045 8082,1043 8082,
@@ -1047,7 +1045,7 @@
1047 requestFactory=DebugLayerRequestFactory)1045 requestFactory=DebugLayerRequestFactory)
10481046
1049privatexmlrpc = wsgi.ServerType(1047privatexmlrpc = wsgi.ServerType(
1050 ZServerTracelogServer, # subclass of WSGIHTTPServer1048 ZServerTracelogServer, # subclass of WSGIHTTPServer
1051 WSGIPublisherApplication,1049 WSGIPublisherApplication,
1052 LaunchpadAccessLogger,1050 LaunchpadAccessLogger,
1053 8080,1051 8080,
@@ -1208,6 +1206,14 @@
12081206
1209 Web service requests are authenticated using OAuth, except for the1207 Web service requests are authenticated using OAuth, except for the
1210 one made using (presumably) JavaScript on the /api override path.1208 one made using (presumably) JavaScript on the /api override path.
1209
1210 Raises a variety of token errors (ClockSkew, NonceAlreadyUsed,
1211 TimestampOrderingError, TokenException) which have a webservice error
1212 status of Unauthorized - 401. All of these exceptions represent
1213 errors on the part of the client.
1214
1215 Raises Unauthorized directly in the case where the consumer is None
1216 for a non-anonymous request as it may represent a server error.
1211 """1217 """
1212 # Use the regular HTTP authentication, when the request is not1218 # Use the regular HTTP authentication, when the request is not
1213 # on the API virtual host but comes through the path_override on1219 # on the API virtual host but comes through the path_override on
@@ -1250,7 +1256,7 @@
1250 # transactions committed so that we can keep track of1256 # transactions committed so that we can keep track of
1251 # the OAuth nonces and prevent replay attacks.1257 # the OAuth nonces and prevent replay attacks.
1252 if consumer_key == '' or consumer_key is None:1258 if consumer_key == '' or consumer_key is None:
1253 raise Unauthorized("No consumer key specified.")1259 raise TokenException("No consumer key specified.")
1254 consumer = consumers.new(consumer_key, '')1260 consumer = consumers.new(consumer_key, '')
1255 else:1261 else:
1256 # An unknown consumer can never make a non-anonymous1262 # An unknown consumer can never make a non-anonymous
@@ -1270,19 +1276,16 @@
1270 return auth_utility.unauthenticatedPrincipal()1276 return auth_utility.unauthenticatedPrincipal()
1271 token = consumer.getAccessToken(token_key)1277 token = consumer.getAccessToken(token_key)
1272 if token is None:1278 if token is None:
1273 raise Unauthorized('Unknown access token (%s).' % token_key)1279 raise TokenException('Unknown access token (%s).' % token_key)
1274 nonce = form.get('oauth_nonce')1280 nonce = form.get('oauth_nonce')
1275 timestamp = form.get('oauth_timestamp')1281 timestamp = form.get('oauth_timestamp')
1276 try:1282 token.checkNonceAndTimestamp(nonce, timestamp)
1277 token.checkNonceAndTimestamp(nonce, timestamp)
1278 except (NonceAlreadyUsed, TimestampOrderingError, ClockSkew), e:
1279 raise Unauthorized('Invalid nonce/timestamp: %s' % e)
1280 if token.permission == OAuthPermission.UNAUTHORIZED:1283 if token.permission == OAuthPermission.UNAUTHORIZED:
1281 raise Unauthorized('Unauthorized token (%s).' % token.key)1284 raise TokenException('Unauthorized token (%s).' % token.key)
1282 elif token.is_expired:1285 elif token.is_expired:
1283 raise Unauthorized('Expired token (%s).' % token.key)1286 raise TokenException('Expired token (%s).' % token.key)
1284 elif not check_oauth_signature(request, consumer, token):1287 elif not check_oauth_signature(request, consumer, token):
1285 raise Unauthorized('Invalid signature.')1288 raise TokenException('Invalid signature.')
1286 else:1289 else:
1287 # Everything is fine, let's return the principal.1290 # Everything is fine, let's return the principal.
1288 pass1291 pass
@@ -1529,7 +1532,8 @@
1529 # len(factories)+1.1532 # len(factories)+1.
1530 for priority, factory in enumerate(factories):1533 for priority, factory in enumerate(factories):
1531 publisher_factory_registry.register(1534 publisher_factory_registry.register(
1532 "*", "*", factory.vhost_name, len(factories)-priority+1, factory)1535 "*", "*", factory.vhost_name, len(factories) - priority + 1,
1536 factory)
15331537
1534 # Register a catch-all "not found" handler at the lowest priority.1538 # Register a catch-all "not found" handler at the lowest priority.
1535 publisher_factory_registry.register(1539 publisher_factory_registry.register(