From 0905d2015e35e827c3fdb2135695710b80d549a5 Mon Sep 17 00:00:00 2001 From: "Karl O. Pinc" Date: Tue, 8 Oct 2013 11:50:11 -0500 Subject: Subclass HTTPBadCSRFToken from HTTPBadRequest and have request.session.check_csrf_token use the new exception. This supports a more fine-grained exception trapping. --- docs/api/httpexceptions.rst | 19 ++++++++++++++++--- pyramid/httpexceptions.py | 40 ++++++++++++++++++++++++++++++++++++---- pyramid/session.py | 6 +++--- pyramid/tests/test_session.py | 5 +++-- 4 files changed, 58 insertions(+), 12 deletions(-) diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 6a08d1048..0fdd0f0e9 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -7,9 +7,12 @@ .. attribute:: status_map - A mapping of integer status code to exception class (eg. the - integer "401" maps to - :class:`pyramid.httpexceptions.HTTPUnauthorized`). + A mapping of integer status code to HTTP exception class (eg. the integer + "401" maps to :class:`pyramid.httpexceptions.HTTPUnauthorized`). All + mapped exception classes are children of :class:`pyramid.httpexceptions`, + i.e. the :ref:`pyramid_specific_http_exceptions` such as + :class:`pyramid.httpexceptions.HTTPBadRequest.BadCSRFToken` are not + mapped. .. autofunction:: exception_response @@ -106,3 +109,13 @@ .. autoclass:: HTTPVersionNotSupported .. autoclass:: HTTPInsufficientStorage + + +.. _pyramid_specific_http_exceptions: + +Pyramid-specific HTTP Exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each Pyramid-specific HTTP exception has the status code of it's parent. + + .. autoclass:: HTTPBadCSRFToken diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index fff17b2df..21d862a6b 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -2,10 +2,13 @@ HTTP Exceptions --------------- -This module contains Pyramid HTTP exception classes. Each class relates to a -single HTTP status code. Each class is a subclass of the -:class:`~HTTPException`. Each exception class is also a :term:`response` -object. +This module contains Pyramid HTTP exception classes. Each class is a subclass +of the :class:`~HTTPException`. Each class relates to a single HTTP status +code, although the reverse is not true. There are +:ref:`pyramid_specific_http_exceptions` which are sub-classes of the +:rfc:`2608` HTTP status codes. Each of these Pyramid-specific exceptions have +the status code of it's parent. Each exception class is also a +:term:`response` object. Each exception class has a status code according to :rfc:`2068`: codes with 100-300 are not really errors; 400s are client errors, @@ -32,6 +35,9 @@ Exception HTTPError HTTPClientError * 400 - HTTPBadRequest + + * 400 - HTTPBadCSRFToken + * 401 - HTTPUnauthorized * 402 - HTTPPaymentRequired * 403 - HTTPForbidden @@ -565,8 +571,34 @@ class HTTPClientError(HTTPError): 'it is either malformed or otherwise incorrect.') class HTTPBadRequest(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + base class for Pyramid-specific validity checks of the client's request + + This class and it's sub-classes result in a '400 Bad Request' HTTP status, + although it's sub-classes specialize the 'Bad Request' text. + """ pass +class HTTPBadCSRFToken(HTTPClientError): + """ + subclass of :class:`~HTTPBadRequest` + + This indicates the request has failed cross-site request forgery token + validation. + + title: Bad CSRF Token + """ + title = 'Bad CSRF Token' + explanation = ( + 'Access is denied. This server can not verify that your cross-site ' + 'request forgery token belongs to your login session. Either you ' + 'supplied the wrong cross-site request forgery token or your session ' + 'no longer exists. This may be due to session timeout or because ' + 'browser is not supplying the credentials required, as can happen ' + 'when the browser has cookies turned off.') + class HTTPUnauthorized(HTTPClientError): """ subclass of :class:`~HTTPClientError` diff --git a/pyramid/session.py b/pyramid/session.py index 3708ef879..72b69117c 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -15,7 +15,7 @@ from pyramid.compat import ( native_, ) -from pyramid.httpexceptions import HTTPBadRequest +from pyramid.httpexceptions import HTTPBadCSRFToken from pyramid.interfaces import ISession from pyramid.util import strings_differ @@ -95,7 +95,7 @@ def check_csrf_token(request, If the value supplied by param or by header doesn't match the value supplied by ``request.session.get_csrf_token()``, and ``raises`` is ``True``, this function will raise an - :exc:`pyramid.httpexceptions.HTTPBadRequest` exception. + :exc:`pyramid.httpexceptions.HTTPBadCSRFToken` exception. If the check does succeed and ``raises`` is ``False``, this function will return ``False``. If the CSRF check is successful, this function will return ``True`` unconditionally. @@ -108,7 +108,7 @@ def check_csrf_token(request, supplied_token = request.params.get(token, request.headers.get(header)) if supplied_token != request.session.get_csrf_token(): if raises: - raise HTTPBadRequest('incorrect CSRF token') + raise HTTPBadCSRFToken('check_csrf_token(): Invalid token') return False return True diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 35e2b5c27..a928af43e 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -381,9 +381,10 @@ class Test_check_csrf_token(unittest.TestCase): self.assertEqual(self._callFUT(request), True) def test_failure_raises(self): - from pyramid.httpexceptions import HTTPBadRequest + from pyramid.httpexceptions import HTTPBadCSRFToken request = testing.DummyRequest() - self.assertRaises(HTTPBadRequest, self._callFUT, request, 'csrf_token') + self.assertRaises(HTTPBadCSRFToken, self._callFUT, request, + 'csrf_token') def test_failure_no_raises(self): request = testing.DummyRequest() -- cgit v1.2.3 From 0e2914bc0d5f6f4cab1cfe11e3c6e88dd96ecbb6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 19 Oct 2013 01:43:17 -0500 Subject: move HTTPBadCSRFToken to p.exceptions.BadCSRFToken --- pyramid/exceptions.py | 15 +++++++++++++++ pyramid/httpexceptions.py | 21 --------------------- pyramid/session.py | 6 +++--- pyramid/tests/test_exceptions.py | 6 ++++++ pyramid/tests/test_session.py | 4 ++-- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pyramid/exceptions.py b/pyramid/exceptions.py index a8fca1d84..c59d109df 100644 --- a/pyramid/exceptions.py +++ b/pyramid/exceptions.py @@ -1,4 +1,5 @@ from pyramid.httpexceptions import ( + HTTPBadRequest, HTTPNotFound, HTTPForbidden, ) @@ -8,6 +9,20 @@ Forbidden = HTTPForbidden # bw compat CR = '\n' +class BadCSRFToken(HTTPBadRequest): + """ + This exception indicates the request has failed cross-site request + forgery token validation. + """ + title = 'Bad CSRF Token' + explanation = ( + 'Access is denied. This server can not verify that your cross-site ' + 'request forgery token belongs to your login session. Either you ' + 'supplied the wrong cross-site request forgery token or your session ' + 'no longer exists. This may be due to session timeout or because ' + 'browser is not supplying the credentials required, as can happen ' + 'when the browser has cookies turned off.') + class PredicateMismatch(HTTPNotFound): """ This exception is raised by multiviews when no view matches diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 21d862a6b..5e8d8ccd8 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -35,9 +35,6 @@ Exception HTTPError HTTPClientError * 400 - HTTPBadRequest - - * 400 - HTTPBadCSRFToken - * 401 - HTTPUnauthorized * 402 - HTTPPaymentRequired * 403 - HTTPForbidden @@ -581,24 +578,6 @@ class HTTPBadRequest(HTTPClientError): """ pass -class HTTPBadCSRFToken(HTTPClientError): - """ - subclass of :class:`~HTTPBadRequest` - - This indicates the request has failed cross-site request forgery token - validation. - - title: Bad CSRF Token - """ - title = 'Bad CSRF Token' - explanation = ( - 'Access is denied. This server can not verify that your cross-site ' - 'request forgery token belongs to your login session. Either you ' - 'supplied the wrong cross-site request forgery token or your session ' - 'no longer exists. This may be due to session timeout or because ' - 'browser is not supplying the credentials required, as can happen ' - 'when the browser has cookies turned off.') - class HTTPUnauthorized(HTTPClientError): """ subclass of :class:`~HTTPClientError` diff --git a/pyramid/session.py b/pyramid/session.py index 72b69117c..d3318cbda 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -15,7 +15,7 @@ from pyramid.compat import ( native_, ) -from pyramid.httpexceptions import HTTPBadCSRFToken +from pyramid.exceptions import BadCSRFToken from pyramid.interfaces import ISession from pyramid.util import strings_differ @@ -95,7 +95,7 @@ def check_csrf_token(request, If the value supplied by param or by header doesn't match the value supplied by ``request.session.get_csrf_token()``, and ``raises`` is ``True``, this function will raise an - :exc:`pyramid.httpexceptions.HTTPBadCSRFToken` exception. + :exc:`pyramid.exceptions.BadCSRFToken` exception. If the check does succeed and ``raises`` is ``False``, this function will return ``False``. If the CSRF check is successful, this function will return ``True`` unconditionally. @@ -108,7 +108,7 @@ def check_csrf_token(request, supplied_token = request.params.get(token, request.headers.get(header)) if supplied_token != request.session.get_csrf_token(): if raises: - raise HTTPBadCSRFToken('check_csrf_token(): Invalid token') + raise BadCSRFToken('check_csrf_token(): Invalid token') return False return True diff --git a/pyramid/tests/test_exceptions.py b/pyramid/tests/test_exceptions.py index aa5ebb376..993209046 100644 --- a/pyramid/tests/test_exceptions.py +++ b/pyramid/tests/test_exceptions.py @@ -11,6 +11,12 @@ class TestBWCompat(unittest.TestCase): from pyramid.httpexceptions import HTTPForbidden as two self.assertTrue(one is two) +class TestBadCSRFToken(unittest.TestCase): + def test_response_equivalence(self): + from pyramid.exceptions import BadCSRFToken + from pyramid.httpexceptions import HTTPBadRequest + self.assertTrue(isinstance(BadCSRFToken(), HTTPBadRequest)) + class TestNotFound(unittest.TestCase): def _makeOne(self, message): from pyramid.exceptions import NotFound diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index a928af43e..9337ab8eb 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -381,9 +381,9 @@ class Test_check_csrf_token(unittest.TestCase): self.assertEqual(self._callFUT(request), True) def test_failure_raises(self): - from pyramid.httpexceptions import HTTPBadCSRFToken + from pyramid.exceptions import BadCSRFToken request = testing.DummyRequest() - self.assertRaises(HTTPBadCSRFToken, self._callFUT, request, + self.assertRaises(BadCSRFToken, self._callFUT, request, 'csrf_token') def test_failure_no_raises(self): -- cgit v1.2.3 From 6b0889cc8f3711d5f77cb663f8f2fa432eb3ad06 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 19 Oct 2013 01:52:11 -0500 Subject: update doc references --- CHANGES.txt | 5 +++++ docs/api/exceptions.rst | 2 ++ docs/api/httpexceptions.rst | 13 ------------- pyramid/httpexceptions.py | 17 +++++++---------- 4 files changed, 14 insertions(+), 23 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index a228fbb3a..fcfb83e4f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,6 +10,11 @@ Features python -3 -m pyramid.scripts.pserve development.ini +- Added a specific subclass of ``HTTPBadRequest`` named + ``pyramid.exceptions.BadCSRFToken`` which will now be raised in response + to failures in ``check_csrf_token``. + See https://github.com/Pylons/pyramid/pull/1149 + Bug Fixes --------- diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst index ab158f18d..0c630571f 100644 --- a/docs/api/exceptions.rst +++ b/docs/api/exceptions.rst @@ -5,6 +5,8 @@ .. automodule:: pyramid.exceptions + .. autoclass:: BadCSRFToken + .. autoclass:: PredicateMismatch .. autoclass:: Forbidden diff --git a/docs/api/httpexceptions.rst b/docs/api/httpexceptions.rst index 0fdd0f0e9..b50f10beb 100644 --- a/docs/api/httpexceptions.rst +++ b/docs/api/httpexceptions.rst @@ -10,9 +10,6 @@ A mapping of integer status code to HTTP exception class (eg. the integer "401" maps to :class:`pyramid.httpexceptions.HTTPUnauthorized`). All mapped exception classes are children of :class:`pyramid.httpexceptions`, - i.e. the :ref:`pyramid_specific_http_exceptions` such as - :class:`pyramid.httpexceptions.HTTPBadRequest.BadCSRFToken` are not - mapped. .. autofunction:: exception_response @@ -109,13 +106,3 @@ .. autoclass:: HTTPVersionNotSupported .. autoclass:: HTTPInsufficientStorage - - -.. _pyramid_specific_http_exceptions: - -Pyramid-specific HTTP Exceptions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Each Pyramid-specific HTTP exception has the status code of it's parent. - - .. autoclass:: HTTPBadCSRFToken diff --git a/pyramid/httpexceptions.py b/pyramid/httpexceptions.py index 5e8d8ccd8..ebee39ada 100644 --- a/pyramid/httpexceptions.py +++ b/pyramid/httpexceptions.py @@ -2,13 +2,10 @@ HTTP Exceptions --------------- -This module contains Pyramid HTTP exception classes. Each class is a subclass -of the :class:`~HTTPException`. Each class relates to a single HTTP status -code, although the reverse is not true. There are -:ref:`pyramid_specific_http_exceptions` which are sub-classes of the -:rfc:`2608` HTTP status codes. Each of these Pyramid-specific exceptions have -the status code of it's parent. Each exception class is also a -:term:`response` object. +This module contains Pyramid HTTP exception classes. Each class relates to a +single HTTP status code. Each class is a subclass of the +:class:`~HTTPException`. Each exception class is also a :term:`response` +object. Each exception class has a status code according to :rfc:`2068`: codes with 100-300 are not really errors; 400s are client errors, @@ -571,10 +568,10 @@ class HTTPBadRequest(HTTPClientError): """ subclass of :class:`~HTTPClientError` - base class for Pyramid-specific validity checks of the client's request + This indicates that the body or headers failed validity checks, + preventing the server from being able to continue processing. - This class and it's sub-classes result in a '400 Bad Request' HTTP status, - although it's sub-classes specialize the 'Bad Request' text. + code: 400, title: Bad Request """ pass -- cgit v1.2.3