diff --git a/docs/glossary.rst b/docs/glossary.rst index 2b006da209..96dd826d12 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -818,9 +818,12 @@ Glossary application. session factory - A callable, which, when called with a single argument named - ``request`` (a :term:`request` object), returns a - :term:`session` object. + A callable, which, when called with a single argument named ``request`` + (a :term:`request` object), returns a :term:`session` object. See + :ref:`using_the_default_session_factory`, + :ref:`using_alternate_session_factories` and + :meth:`pyramid.config.Configurator.set_session_factory` for more + information. Mako `Mako `_ is a template language language diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 6bfaf11c05..b88f3f0c80 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -393,6 +393,10 @@ introspectables in categories not described here. The ``match_param`` argument passed to ``add_view``. + ``csrf_token`` + + The ``csrf_token`` argument passed to ``add_view``. + ``callable`` The (resolved) ``view`` argument passed to ``add_view``. Represents the diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index 23b4fde682..f65435cc60 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -394,6 +394,28 @@ configured view. consideration when deciding whether or not to invoke the associated view callable. +``check_csrf`` + If specified, this value should be one of ``None``, ``True``, ``False``, or + a string representing the 'check name'. If the value is ``True`` or a + string, CSRF checking will be performed. If the value is ``False`` or + ``None``, CSRF checking will not be performed. + + If the value provided is a string, that string will be used as the 'check + name'. If the value provided is ``True``, ``csrf_token`` will be used as + the check name. + + If CSRF checking is performed, the checked value will be the value of + ``request.params[check_name]``. This value will be compared against the + value of ``request.session.get_csrf_token()``, and the check will pass if + these two values are the same. If the check passes, the associated view + will be permitted to execute. If the check fails, the associated view + will not be permitted to execute. + + Note that using this feature requires a :term:`session factory` to have + been configured. + + .. versionadded:: 1.4a2 + ``custom_predicates`` If ``custom_predicates`` is specified, it must be a sequence of references to custom predicate callables. Use custom predicates when no set of @@ -407,6 +429,15 @@ configured view. If ``custom_predicates`` is not specified, no custom predicates are used. +``predicates`` + Pass a key/value pair here to use a third-party predicate registered via + :meth:`pyramid.config.Configurator.add_view_predicate`. More than one + key/value pair can be used at the same time. See + :ref:`view_and_route_predicates` for more information about third-party + predicates. + + .. versionadded:: 1.4a1 + .. index:: single: view_config decorator diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst index 76320f6e69..86bfc7c0a0 100644 --- a/docs/whatsnew-1.4.rst +++ b/docs/whatsnew-1.4.rst @@ -156,6 +156,12 @@ Minor Feature Additions - A new :func:`pyramid.session.check_csrf_token` convenience API function was added. +- A ``check_csrf`` view predicate was added. For example, you can now do + ``config.add_view(someview, check_csrf=True)``. When the predicate is + checked, if the ``csrf_token`` value in ``request.params`` matches the csrf + token in the request's session, the view will be permitted to execute. + Otherwise, it will not be permitted to execute. + Backwards Incompatibilities --------------------------- diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 9e0ee28c1d..77b55d9b36 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -13,6 +13,8 @@ from pyramid.util import object_description +from pyramid.session import check_csrf_token + from .util import as_sorted_tuple class XHRPredicate(object): @@ -226,3 +228,24 @@ def __call__(self, context, request): # injects ``traverse`` into the matchdict. As a result, we just # return True. return True + +class CheckCSRFTokenPredicate(object): + + check_csrf_token = staticmethod(check_csrf_token) # testing + + def __init__(self, val, config): + self.val = val + + def text(self): + return 'check_csrf = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + val = self.val + if val: + if val is True: + val = 'csrf_token' + return self.check_csrf_token(request, val, raises=False) + return True + diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 36896a17e2..9ace96c1d9 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -662,6 +662,7 @@ def add_view( mapper=None, http_cache=None, match_param=None, + check_csrf=None, **predicates): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken @@ -989,6 +990,29 @@ def add_view( variable. If the regex matches, this predicate will be ``True``. + check_csrf + + If specified, this value should be one of ``None``, ``True``, + ``False``, or a string representing the 'check name'. If the value + is ``True`` or a string, CSRF checking will be performed. If the + value is ``False`` or ``None``, CSRF checking will not be performed. + + If the value provided is a string, that string will be used as the + 'check name'. If the value provided is ``True``, ``csrf_token`` will + be used as the check name. + + If CSRF checking is performed, the checked value will be the value + of ``request.params[check_name]``. This value will be compared + against the value of ``request.session.get_csrf_token()``, and the + check will pass if these two values are the same. If the check + passes, the associated view will be permitted to execute. If the + check fails, the associated view will not be permitted to execute. + + Note that using this feature requires a :term:`session factory` to + have been configured. + + .. versionadded:: 1.4a2 + custom_predicates This value should be a sequence of references to custom @@ -1007,7 +1031,9 @@ def add_view( :meth:`pyramid.config.Configurator.add_view_predicate`. More than one key/value pair can be used at the same time. See :ref:`view_and_route_predicates` for more information about - third-party predicates. This argument is new as of Pyramid 1.4. + third-party predicates. + + .. versionadded: 1.4a1 """ view = self.maybe_dotted(view) @@ -1061,6 +1087,7 @@ def view(context, request): containment=containment, request_type=request_type, match_param=match_param, + check_csrf=check_csrf, custom=predvalseq(custom_predicates), ) ) @@ -1098,6 +1125,7 @@ def discrim_func(): header=header, path_info=path_info, match_param=match_param, + check_csrf=check_csrf, callable=view, mapper=mapper, decorator=decorator, @@ -1340,6 +1368,7 @@ def add_default_view_predicates(self): ('containment', p.ContainmentPredicate), ('request_type', p.RequestTypePredicate), ('match_param', p.MatchParamPredicate), + ('check_csrf', p.CheckCSRFTokenPredicate), ('custom', p.CustomPredicate), ): self.add_view_predicate(name, factory) diff --git a/pyramid/session.py b/pyramid/session.py index 3f700564d5..3b28346932 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -81,19 +81,26 @@ def signed_deserialize(serialized, secret, hmac=hmac): return pickle.loads(pickled) -def check_csrf_token(request, token='csrf_token'): +def check_csrf_token(request, token='csrf_token', raises=True): """ Check the CSRF token in the request's session against the value in ``request.params.get(token)``. If ``token`` is not supplied, the string value ``csrf_token`` will be used as the token value. If the value in ``request.params.get(token)`` doesn't match the value supplied by - ``request.session.get_csrf_token()``, this function will raise an - :exc:`pyramid.httpexceptions.HTTPBadRequest` exception. If the CSRF - check is successful, this function will return ``True``. + ``request.session.get_csrf_token()``, and ``raises`` is ``True``, this + function will raise an :exc:`pyramid.httpexceptions.HTTPBadRequest` + 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. + + Note that using this function requires that a :term:`session factory` is + configured. .. versionadded:: 1.4a2 """ if request.params.get(token) != request.session.get_csrf_token(): - raise HTTPBadRequest('incorrect CSRF token') + if raises: + raise HTTPBadRequest('incorrect CSRF token') + return False return True def UnencryptedCookieSessionFactoryConfig( diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index e33a314583..005b1b27af 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -256,6 +256,49 @@ def test_phash(self): inst = self._makeOne('/abc') self.assertEqual(inst.phash(), '') +class Test_CheckCSRFTokenPredicate(unittest.TestCase): + def _makeOne(self, val, config): + from pyramid.config.predicates import CheckCSRFTokenPredicate + return CheckCSRFTokenPredicate(val, config) + + def test_text(self): + inst = self._makeOne(True, None) + self.assertEqual(inst.text(), 'check_csrf = True') + + def test_phash(self): + inst = self._makeOne(True, None) + self.assertEqual(inst.phash(), 'check_csrf = True') + + def test_it_call_val_True(self): + inst = self._makeOne(True, None) + request = Dummy() + def check_csrf_token(req, val, raises=True): + self.assertEqual(req, request) + self.assertEqual(val, 'csrf_token') + self.assertEqual(raises, False) + return True + inst.check_csrf_token = check_csrf_token + result = inst(None, request) + self.assertEqual(result, True) + + def test_it_call_val_str(self): + inst = self._makeOne('abc', None) + request = Dummy() + def check_csrf_token(req, val, raises=True): + self.assertEqual(req, request) + self.assertEqual(val, 'abc') + self.assertEqual(raises, False) + return True + inst.check_csrf_token = check_csrf_token + result = inst(None, request) + self.assertEqual(result, True) + + def test_it_call_val_False(self): + inst = self._makeOne(False, None) + request = Dummy() + result = inst(None, request) + self.assertEqual(result, True) + class predicate(object): def __repr__(self): return 'predicate' diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py index 21cf16b123..b3e0e20c46 100644 --- a/pyramid/tests/test_session.py +++ b/pyramid/tests/test_session.py @@ -356,9 +356,9 @@ def test_it_bad_encoding(self): self.assertRaises(ValueError, self._callFUT, serialized, 'secret') class Test_check_csrf_token(unittest.TestCase): - def _callFUT(self, request, token): + def _callFUT(self, request, token, raises=True): from ..session import check_csrf_token - return check_csrf_token(request, token) + return check_csrf_token(request, token, raises=raises) def test_success(self): request = testing.DummyRequest() @@ -371,11 +371,16 @@ def test_success_default_token(self): request.params['csrf_token'] = request.session.get_csrf_token() self.assertEqual(check_csrf_token(request), True) - def test_failure(self): + def test_failure_raises(self): from pyramid.httpexceptions import HTTPBadRequest request = testing.DummyRequest() self.assertRaises(HTTPBadRequest, self._callFUT, request, 'csrf_token') + def test_failure_no_raises(self): + request = testing.DummyRequest() + result = self._callFUT(request, 'csrf_token', raises=False) + self.assertEqual(result, False) + class DummySessionFactory(dict): _dirty = False _cookie_name = 'session' diff --git a/pyramid/view.py b/pyramid/view.py index 12a2efde60..76f466b83e 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -170,7 +170,7 @@ def my_view(context, request): ``request_type``, ``route_name``, ``request_method``, ``request_param``, ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, - ``match_param``, and ``predicates``. + ``match_param``, ``csrf_token``, and ``predicates``. The meanings of these arguments are the same as the arguments passed to :meth:`pyramid.config.Configurator.add_view`. If any argument is left