summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2013-10-20 16:15:34 -0400
committerChris McDonough <chrism@plope.com>2013-10-20 16:15:34 -0400
commit2a6e6b3776b07589f8bbaaf4c72419f2bdf5e218 (patch)
tree5074ba1d0d378b91b640e234c9c9267fcbe06015
parent6c98b17ed9aadbe485c6473c3f76e1b2b529dc78 (diff)
parent0264cc0c3d4ca091d4d5c8bd0c2fda38a3f8a0c7 (diff)
downloadpyramid-2a6e6b3776b07589f8bbaaf4c72419f2bdf5e218.tar.gz
pyramid-2a6e6b3776b07589f8bbaaf4c72419f2bdf5e218.tar.bz2
pyramid-2a6e6b3776b07589f8bbaaf4c72419f2bdf5e218.zip
Merge branch 'master' into fix.basic-authentication-encodings
-rw-r--r--CHANGES.txt30
-rw-r--r--docs/api/exceptions.rst2
-rw-r--r--docs/api/httpexceptions.rst6
-rw-r--r--docs/api/session.rst8
-rw-r--r--docs/narr/security.rst28
-rw-r--r--docs/narr/sessions.rst19
-rw-r--r--docs/quick_tour/package/hello_world/__init__.py4
-rw-r--r--docs/quick_tour/package/hello_world/init.py4
-rw-r--r--docs/quick_tutorial/sessions/tutorial/__init__.py6
-rw-r--r--pyramid/authentication.py4
-rw-r--r--pyramid/exceptions.py15
-rw-r--r--pyramid/httpexceptions.py8
-rw-r--r--pyramid/session.py480
-rw-r--r--pyramid/tests/test_exceptions.py6
-rw-r--r--pyramid/tests/test_session.py305
15 files changed, 751 insertions, 174 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index f170308b0..15b1b910e 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -10,6 +10,25 @@ 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
+
+- Added a new ``SignedCookieSessionFactory`` which is very similar to the
+ ``UnencryptedCookieSessionFactoryConfig`` but with a clearer focus on
+ signing content. The custom serializer arguments to this function should
+ only focus on serializing, unlike its predecessor which required the
+ serializer to also perform signing.
+ See https://github.com/Pylons/pyramid/pull/1142
+
+- Added a new ``BaseCookieSessionFactory`` which acts as a generic cookie
+ factory that can be used by framework implementors to create their own
+ session implementations. It provides a reusable API which focuses strictly
+ on providing a dictionary-like object that properly handles renewals,
+ timeouts, and conformance with the ``ISession`` API.
+ See https://github.com/Pylons/pyramid/pull/1142
+
Bug Fixes
---------
@@ -56,6 +75,17 @@ Backwards Incompatibilities
situation, leaving a query string of ``a=b&key=``.
See https://github.com/Pylons/pyramid/issues/1119
+Deprecations
+------------
+
+- The ``pyramid.session.UnencryptedCookieSessionFactoryConfig`` API has been
+ deprecated and is superseded by the
+ ``pyramid.session.SignedCookieSessionFactory``. Note that while the cookies
+ generated by the ``UnencryptedCookieSessionFactoryConfig``
+ are compatible with cookies generated by old releases, cookies generated by
+ the SignedCookieSessionFactory are not. See
+ https://github.com/Pylons/pyramid/pull/1142
+
1.5a2 (2013-09-22)
==================
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 6a08d1048..b50f10beb 100644
--- a/docs/api/httpexceptions.rst
+++ b/docs/api/httpexceptions.rst
@@ -7,9 +7,9 @@
.. 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`,
.. autofunction:: exception_response
diff --git a/docs/api/session.rst b/docs/api/session.rst
index 31bc196ad..dde9d20e9 100644
--- a/docs/api/session.rst
+++ b/docs/api/session.rst
@@ -5,12 +5,16 @@
.. automodule:: pyramid.session
- .. autofunction:: UnencryptedCookieSessionFactoryConfig
-
.. autofunction:: signed_serialize
.. autofunction:: signed_deserialize
.. autofunction:: check_csrf_token
+ .. autofunction:: SignedCookieSessionFactory
+
+ .. autofunction:: UnencryptedCookieSessionFactoryConfig
+
+ .. autofunction:: BaseCookieSessionFactory
+
diff --git a/docs/narr/security.rst b/docs/narr/security.rst
index 6517fedf8..e85ed823a 100644
--- a/docs/narr/security.rst
+++ b/docs/narr/security.rst
@@ -669,3 +669,31 @@ following interface:
After you do so, you can pass an instance of such a class into the
:class:`~pyramid.config.Configurator.set_authorization_policy` method at
configuration time to use it.
+
+.. _admonishment_against_secret_sharing:
+
+Admonishment Against Secret-Sharing
+-----------------------------------
+
+A "secret" is required by various components of Pyramid. For example, the
+:term:`authentication policy` below uses a secret value ``seekrit``::
+
+ authn_policy = AuthTktAuthenticationPolicy('seekrit', hashalg='sha512')
+
+A :term:`session factory` also requires a secret::
+
+ my_session_factory = SignedCookieSessionFactory('itsaseekreet')
+
+It is tempting to use the same secret for multiple Pyramid subsystems. For
+example, you might be tempted to use the value ``seekrit`` as the secret for
+both the authentication policy and the session factory defined above. This is
+a bad idea, because in both cases, these secrets are used to sign the payload
+of the data.
+
+If you use the same secret for two different parts of your application for
+signing purposes, it may allow an attacker to get his chosen plaintext signed,
+which would allow the attacker to control the content of the payload. Re-using
+a secret across two different subsystems might drop the security of signing to
+zero. Keys should not be re-used across different contexts where an attacker
+has the possibility of providing a chosen plaintext.
+
diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst
index f33bc6132..fb5035373 100644
--- a/docs/narr/sessions.rst
+++ b/docs/narr/sessions.rst
@@ -43,24 +43,23 @@ limitations:
It is digitally signed, however, and thus its data cannot easily be
tampered with.
-You can configure this session factory in your :app:`Pyramid`
-application by using the ``session_factory`` argument to the
-:class:`~pyramid.config.Configurator` class:
+You can configure this session factory in your :app:`Pyramid` application
+by using the :meth:`pyramid.config.Configurator.set_session_factory`` method.
.. code-block:: python
:linenos:
- from pyramid.session import UnencryptedCookieSessionFactoryConfig
- my_session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
-
+ from pyramid.session import SignedCookieSessionFactory
+ my_session_factory = SignedCookieSessionFactory('itsaseekreet')
+
from pyramid.config import Configurator
- config = Configurator(session_factory = my_session_factory)
+ config = Configurator()
+ config.set_session_factory(my_session_factory)
.. warning::
- Note the very long, very explicit name for
- ``UnencryptedCookieSessionFactoryConfig``. It's trying to tell you that
- this implementation is, by default, *unencrypted*. You should not use it
+ By default the :func:`~pyramid.session.SignedCookieSessionFactory`
+ implementation is *unencrypted*. You should not use it
when you keep sensitive information in the session object, as the
information can be easily read by both users of your application and third
parties who have access to your users' network traffic. And if you use this
diff --git a/docs/quick_tour/package/hello_world/__init__.py b/docs/quick_tour/package/hello_world/__init__.py
index 6e66bf40a..4a4fbec30 100644
--- a/docs/quick_tour/package/hello_world/__init__.py
+++ b/docs/quick_tour/package/hello_world/__init__.py
@@ -1,7 +1,7 @@
from pyramid.config import Configurator
from pyramid_jinja2 import renderer_factory
# Start Sphinx Include 1
-from pyramid.session import UnencryptedCookieSessionFactoryConfig
+from pyramid.session import SignedCookieSessionFactory
# End Sphinx Include 1
from hello_world.models import get_root
@@ -16,7 +16,7 @@ def main(global_config, **settings):
settings.setdefault('jinja2.i18n.domain', 'hello_world')
# Start Sphinx Include 2
- my_session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
+ my_session_factory = SignedCookieSessionFactory('itsaseekreet')
config = Configurator(root_factory=get_root, settings=settings,
session_factory=my_session_factory)
# End Sphinx Include 2
diff --git a/docs/quick_tour/package/hello_world/init.py b/docs/quick_tour/package/hello_world/init.py
index 9d7ec43d8..5b5f6a118 100644
--- a/docs/quick_tour/package/hello_world/init.py
+++ b/docs/quick_tour/package/hello_world/init.py
@@ -1,7 +1,7 @@
from pyramid.config import Configurator
from pyramid_jinja2 import renderer_factory
# Start Sphinx 1
-from pyramid.session import UnencryptedCookieSessionFactoryConfig
+from pyramid.session import SignedCookieSessionFactory
# End Sphinx 1
from hello_world.models import get_root
@@ -22,7 +22,7 @@ def main(global_config, **settings):
# End Include
# Start Sphinx Include 2
- my_session_factory = UnencryptedCookieSessionFactoryConfig('itsaseekreet')
+ my_session_factory = SignedCookieSessionFactory('itsaseekreet')
config = Configurator(session_factory=my_session_factory)
# End Sphinx Include 2
diff --git a/docs/quick_tutorial/sessions/tutorial/__init__.py b/docs/quick_tutorial/sessions/tutorial/__init__.py
index ecf57bb32..9ddc2e1b1 100644
--- a/docs/quick_tutorial/sessions/tutorial/__init__.py
+++ b/docs/quick_tutorial/sessions/tutorial/__init__.py
@@ -1,9 +1,9 @@
from pyramid.config import Configurator
-from pyramid.session import UnencryptedCookieSessionFactoryConfig
+from pyramid.session import SignedCookieSessionFactory
def main(global_config, **settings):
- my_session_factory = UnencryptedCookieSessionFactoryConfig(
+ my_session_factory = SignedCookieSessionFactory(
'itsaseekreet')
config = Configurator(settings=settings,
session_factory=my_session_factory)
@@ -11,4 +11,4 @@ def main(global_config, **settings):
config.add_route('home', '/')
config.add_route('hello', '/howdy')
config.scan('.views')
- return config.make_wsgi_app() \ No newline at end of file
+ return config.make_wsgi_app()
diff --git a/pyramid/authentication.py b/pyramid/authentication.py
index 6b6fbd041..ec8ac0a41 100644
--- a/pyramid/authentication.py
+++ b/pyramid/authentication.py
@@ -424,7 +424,9 @@ class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
``secret``
- The secret (a string) used for auth_tkt cookie signing.
+ The secret (a string) used for auth_tkt cookie signing. This value
+ should be unique across all values provided to Pyramid for various
+ subsystem secrets (see :ref:`admonishment_against_secret_sharing`).
Required.
``callback``
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 fff17b2df..ebee39ada 100644
--- a/pyramid/httpexceptions.py
+++ b/pyramid/httpexceptions.py
@@ -565,6 +565,14 @@ class HTTPClientError(HTTPError):
'it is either malformed or otherwise incorrect.')
class HTTPBadRequest(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the body or headers failed validity checks,
+ preventing the server from being able to continue processing.
+
+ code: 400, title: Bad Request
+ """
pass
class HTTPUnauthorized(HTTPClientError):
diff --git a/pyramid/session.py b/pyramid/session.py
index 3708ef879..9e0733661 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -1,10 +1,11 @@
-from hashlib import sha1
import base64
import binascii
+import hashlib
import hmac
-import time
import os
+import time
+from zope.deprecation import deprecated
from zope.interface import implementer
from pyramid.compat import (
@@ -15,25 +16,31 @@ from pyramid.compat import (
native_,
)
-from pyramid.httpexceptions import HTTPBadRequest
+from pyramid.exceptions import BadCSRFToken
from pyramid.interfaces import ISession
from pyramid.util import strings_differ
def manage_accessed(wrapped):
- """ Decorator which causes a cookie to be set when a wrapped
- method is called"""
+ """ Decorator which causes a cookie to be renewed when an accessor
+ method is called."""
def accessed(session, *arg, **kw):
- session.accessed = int(time.time())
- if not session._dirty:
- session._dirty = True
- def set_cookie_callback(request, response):
- session._set_cookie(response)
- session.request = None # explicitly break cycle for gc
- session.request.add_response_callback(set_cookie_callback)
+ session.accessed = now = int(time.time())
+ if now - session.renewed > session._reissue_time:
+ session.changed()
return wrapped(session, *arg, **kw)
accessed.__doc__ = wrapped.__doc__
return accessed
+def manage_changed(wrapped):
+ """ Decorator which causes a cookie to be set when a setter method
+ is called."""
+ def changed(session, *arg, **kw):
+ session.accessed = int(time.time())
+ session.changed()
+ return wrapped(session, *arg, **kw)
+ changed.__doc__ = wrapped.__doc__
+ return changed
+
def signed_serialize(data, secret):
""" Serialize any pickleable structure (``data``) and sign it
using the ``secret`` (must be a string). Return the
@@ -48,7 +55,7 @@ def signed_serialize(data, secret):
response.set_cookie('signed_cookie', cookieval)
"""
pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
- sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest()
+ sig = hmac.new(bytes_(secret), pickled, hashlib.sha1).hexdigest()
return sig + native_(base64.b64encode(pickled))
def signed_deserialize(serialized, secret, hmac=hmac):
@@ -66,13 +73,13 @@ def signed_deserialize(serialized, secret, hmac=hmac):
"""
# hmac parameterized only for unit tests
try:
- input_sig, pickled = (serialized[:40],
+ input_sig, pickled = (bytes_(serialized[:40]),
base64.b64decode(bytes_(serialized[40:])))
except (binascii.Error, TypeError) as e:
# Badly formed data can make base64 die
raise ValueError('Badly formed base64 data: %s' % e)
- sig = hmac.new(bytes_(secret), pickled, sha1).hexdigest()
+ sig = bytes_(hmac.new(bytes_(secret), pickled, hashlib.sha1).hexdigest())
# Avoid timing attacks (see
# http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf)
@@ -95,7 +102,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.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,93 +115,115 @@ 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 BadCSRFToken('check_csrf_token(): Invalid token')
return False
return True
-def UnencryptedCookieSessionFactoryConfig(
- secret,
- timeout=1200,
+def BaseCookieSessionFactory(
+ serialize,
+ deserialize,
cookie_name='session',
- cookie_max_age=None,
- cookie_path='/',
- cookie_domain=None,
- cookie_secure=False,
- cookie_httponly=False,
- cookie_on_exception=True,
- signed_serialize=signed_serialize,
- signed_deserialize=signed_deserialize,
+ max_age=None,
+ path='/',
+ domain=None,
+ secure=False,
+ httponly=False,
+ timeout=1200,
+ reissue_time=0,
+ set_on_exception=True,
):
"""
- Configure a :term:`session factory` which will provide unencrypted
- (but signed) cookie-based sessions. The return value of this
- function is a :term:`session factory`, which may be provided as
- the ``session_factory`` argument of a
- :class:`pyramid.config.Configurator` constructor, or used
- as the ``session_factory`` argument of the
- :meth:`pyramid.config.Configurator.set_session_factory`
- method.
+ .. versionadded:: 1.5
+
+ Configure a :term:`session factory` which will provide cookie-based
+ sessions. The return value of this function is a :term:`session factory`,
+ which may be provided as the ``session_factory`` argument of a
+ :class:`pyramid.config.Configurator` constructor, or used as the
+ ``session_factory`` argument of the
+ :meth:`pyramid.config.Configurator.set_session_factory` method.
The session factory returned by this function will create sessions
which are limited to storing fewer than 4000 bytes of data (as the
payload must fit into a single cookie).
+ .. warning:
+
+ This class provides no protection from tampering and is only intended
+ to be used by framework authors to create their own cookie-based
+ session factories.
+
Parameters:
- ``secret``
- A string which is used to sign the cookie.
+ ``serialize``
+ A callable accepting a Python object and returning a bytestring. A
+ ``ValueError`` should be raised for malformed inputs.
- ``timeout``
- A number of seconds of inactivity before a session times out.
+ ``deserialize``
+ A callable accepting a bytestring and returning a Python object. A
+ ``ValueError`` should be raised for malformed inputs.
``cookie_name``
- The name of the cookie used for sessioning.
+ The name of the cookie used for sessioning. Default: ``'session'``.
- ``cookie_max_age``
+ ``max_age``
The maximum age of the cookie used for sessioning (in seconds).
Default: ``None`` (browser scope).
- ``cookie_path``
- The path used for the session cookie.
+ ``path``
+ The path used for the session cookie. Default: ``'/'``.
- ``cookie_domain``
+ ``domain``
The domain used for the session cookie. Default: ``None`` (no domain).
- ``cookie_secure``
- The 'secure' flag of the session cookie.
+ ``secure``
+ The 'secure' flag of the session cookie. Default: ``False``.
- ``cookie_httponly``
- The 'httpOnly' flag of the session cookie.
+ ``httponly``
+ Hide the cookie from Javascript by setting the 'HttpOnly' flag of the
+ session cookie. Default: ``False``.
- ``cookie_on_exception``
+ ``timeout``
+ A number of seconds of inactivity before a session times out. If
+ ``None`` then the cookie never expires. Default: 1200.
+
+ ``reissue_time``
+ The number of seconds that must pass before the cookie is automatically
+ reissued as the result of a request which accesses the session. The
+ duration is measured as the number of seconds since the last session
+ cookie was issued and 'now'. If this value is ``0``, a new cookie
+ will be reissued on every request accesses the session. If ``None``
+ then the cookie's lifetime will never be extended.
+
+ A good rule of thumb: if you want auto-expired cookies based on
+ inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
+ ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
+ (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
+ than the ``reissue_time`` value, as the ticket will never be reissued.
+ However, such a configuration is not explicitly prevented.
+
+ Default: ``0``.
+
+ ``set_on_exception``
If ``True``, set a session cookie even if an exception occurs
- while rendering a view.
+ while rendering a view. Default: ``True``.
- ``signed_serialize``
- A callable which takes more or less arbitrary Python data structure and
- a secret and returns a signed serialization in bytes.
- Default: ``signed_serialize`` (using pickle).
-
- ``signed_deserialize``
- A callable which takes a signed and serialized data structure in bytes
- and a secret and returns the original data structure if the signature
- is valid. Default: ``signed_deserialize`` (using pickle).
+ .. versionadded: 1.5a3
"""
@implementer(ISession)
- class UnencryptedCookieSessionFactory(dict):
+ class CookieSession(dict):
""" Dictionary-like session object """
# configuration parameters
_cookie_name = cookie_name
- _cookie_max_age = cookie_max_age
- _cookie_path = cookie_path
- _cookie_domain = cookie_domain
- _cookie_secure = cookie_secure
- _cookie_httponly = cookie_httponly
- _cookie_on_exception = cookie_on_exception
- _secret = secret
+ _cookie_max_age = max_age
+ _cookie_path = path
+ _cookie_domain = domain
+ _cookie_secure = secure
+ _cookie_httponly = httponly
+ _cookie_on_exception = set_on_exception
_timeout = timeout
+ _reissue_time = reissue_time
# dirty flag
_dirty = False
@@ -202,33 +231,45 @@ def UnencryptedCookieSessionFactoryConfig(
def __init__(self, request):
self.request = request
now = time.time()
- created = accessed = now
+ created = renewed = now
new = True
value = None
state = {}
cookieval = request.cookies.get(self._cookie_name)
if cookieval is not None:
try:
- value = signed_deserialize(cookieval, self._secret)
+ value = deserialize(bytes_(cookieval))
except ValueError:
+ # the cookie failed to deserialize, dropped
value = None
if value is not None:
- accessed, created, state = value
- new = False
- if now - accessed > self._timeout:
+ try:
+ renewed, created, state = value
+ new = False
+ if now - renewed > self._timeout:
+ # expire the session because it was not renewed
+ # before the timeout threshold
+ state = {}
+ except TypeError:
+ # value failed to unpack properly or renewed was not
+ # a numeric type so we'll fail deserialization here
state = {}
self.created = created
- self.accessed = accessed
+ self.accessed = renewed
+ self.renewed = renewed
self.new = new
dict.__init__(self, state)
# ISession methods
def changed(self):
- """ This is intentionally a noop; the session is
- serialized on every access, so unnecessary"""
- pass
+ if not self._dirty:
+ self._dirty = True
+ def set_cookie_callback(request, response):
+ self._set_cookie(response)
+ self.request = None # explicitly break cycle for gc
+ self.request.add_response_callback(set_cookie_callback)
def invalidate(self):
self.clear() # XXX probably needs to unset cookie
@@ -250,22 +291,22 @@ def UnencryptedCookieSessionFactoryConfig(
has_key = manage_accessed(dict.has_key)
# modifying dictionary methods
- clear = manage_accessed(dict.clear)
- update = manage_accessed(dict.update)
- setdefault = manage_accessed(dict.setdefault)
- pop = manage_accessed(dict.pop)
- popitem = manage_accessed(dict.popitem)
- __setitem__ = manage_accessed(dict.__setitem__)
- __delitem__ = manage_accessed(dict.__delitem__)
+ clear = manage_changed(dict.clear)
+ update = manage_changed(dict.update)
+ setdefault = manage_changed(dict.setdefault)
+ pop = manage_changed(dict.pop)
+ popitem = manage_changed(dict.popitem)
+ __setitem__ = manage_changed(dict.__setitem__)
+ __delitem__ = manage_changed(dict.__delitem__)
# flash API methods
- @manage_accessed
+ @manage_changed
def flash(self, msg, queue='', allow_duplicate=True):
storage = self.setdefault('_f_' + queue, [])
if allow_duplicate or (msg not in storage):
storage.append(msg)
- @manage_accessed
+ @manage_changed
def pop_flash(self, queue=''):
storage = self.pop('_f_' + queue, [])
return storage
@@ -276,7 +317,7 @@ def UnencryptedCookieSessionFactoryConfig(
return storage
# CSRF API methods
- @manage_accessed
+ @manage_changed
def new_csrf_token(self):
token = text_(binascii.hexlify(os.urandom(20)))
self['_csrft_'] = token
@@ -295,9 +336,9 @@ def UnencryptedCookieSessionFactoryConfig(
exception = getattr(self.request, 'exception', None)
if exception is not None: # dont set a cookie during exceptions
return False
- cookieval = signed_serialize(
- (self.accessed, self.created, dict(self)), self._secret
- )
+ cookieval = native_(serialize(
+ (self.accessed, self.created, dict(self))
+ ))
if len(cookieval) > 4064:
raise ValueError(
'Cookie value is too long to store (%s bytes)' %
@@ -306,12 +347,259 @@ def UnencryptedCookieSessionFactoryConfig(
response.set_cookie(
self._cookie_name,
value=cookieval,
- max_age = self._cookie_max_age,
- path = self._cookie_path,
- domain = self._cookie_domain,
- secure = self._cookie_secure,
- httponly = self._cookie_httponly,
+ max_age=self._cookie_max_age,
+ path=self._cookie_path,
+ domain=self._cookie_domain,
+ secure=self._cookie_secure,
+ httponly=self._cookie_httponly,
)
return True
- return UnencryptedCookieSessionFactory
+ return CookieSession
+
+
+def UnencryptedCookieSessionFactoryConfig(
+ secret,
+ timeout=1200,
+ cookie_name='session',
+ cookie_max_age=None,
+ cookie_path='/',
+ cookie_domain=None,
+ cookie_secure=False,
+ cookie_httponly=False,
+ cookie_on_exception=True,
+ signed_serialize=signed_serialize,
+ signed_deserialize=signed_deserialize,
+ ):
+ """
+ .. deprecated:: 1.5
+ Use :func:`pyramid.session.SignedCookieSessionFactory` instead.
+
+ Configure a :term:`session factory` which will provide unencrypted
+ (but signed) cookie-based sessions. The return value of this
+ function is a :term:`session factory`, which may be provided as
+ the ``session_factory`` argument of a
+ :class:`pyramid.config.Configurator` constructor, or used
+ as the ``session_factory`` argument of the
+ :meth:`pyramid.config.Configurator.set_session_factory`
+ method.
+
+ The session factory returned by this function will create sessions
+ which are limited to storing fewer than 4000 bytes of data (as the
+ payload must fit into a single cookie).
+
+ Parameters:
+
+ ``secret``
+ A string which is used to sign the cookie.
+
+ ``timeout``
+ A number of seconds of inactivity before a session times out.
+
+ ``cookie_name``
+ The name of the cookie used for sessioning.
+
+ ``cookie_max_age``
+ The maximum age of the cookie used for sessioning (in seconds).
+ Default: ``None`` (browser scope).
+
+ ``cookie_path``
+ The path used for the session cookie.
+
+ ``cookie_domain``
+ The domain used for the session cookie. Default: ``None`` (no domain).
+
+ ``cookie_secure``
+ The 'secure' flag of the session cookie.
+
+ ``cookie_httponly``
+ The 'httpOnly' flag of the session cookie.
+
+ ``cookie_on_exception``
+ If ``True``, set a session cookie even if an exception occurs
+ while rendering a view.
+
+ ``signed_serialize``
+ A callable which takes more or less arbitrary Python data structure and
+ a secret and returns a signed serialization in bytes.
+ Default: ``signed_serialize`` (using pickle).
+
+ ``signed_deserialize``
+ A callable which takes a signed and serialized data structure in bytes
+ and a secret and returns the original data structure if the signature
+ is valid. Default: ``signed_deserialize`` (using pickle).
+ """
+
+ return BaseCookieSessionFactory(
+ lambda v: signed_serialize(v, secret),
+ lambda v: signed_deserialize(v, secret),
+ cookie_name=cookie_name,
+ max_age=cookie_max_age,
+ path=cookie_path,
+ domain=cookie_domain,
+ secure=cookie_secure,
+ httponly=cookie_httponly,
+ timeout=timeout,
+ reissue_time=0, # to keep session.accessed == session.renewed
+ set_on_exception=cookie_on_exception,
+ )
+
+deprecated(
+ 'UnencryptedCookieSessionFactoryConfig',
+ 'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of '
+ 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead.'
+ )
+
+def SignedCookieSessionFactory(
+ secret,
+ cookie_name='session',
+ max_age=None,
+ path='/',
+ domain=None,
+ secure=False,
+ httponly=False,
+ set_on_exception=True,
+ timeout=1200,
+ reissue_time=0,
+ hashalg='sha512',
+ salt='pyramid.session.',
+ serialize=None,
+ deserialize=None,
+ ):
+ """
+ .. versionadded:: 1.5
+
+ Configure a :term:`session factory` which will provide signed
+ cookie-based sessions. The return value of this
+ function is a :term:`session factory`, which may be provided as
+ the ``session_factory`` argument of a
+ :class:`pyramid.config.Configurator` constructor, or used
+ as the ``session_factory`` argument of the
+ :meth:`pyramid.config.Configurator.set_session_factory`
+ method.
+
+ The session factory returned by this function will create sessions
+ which are limited to storing fewer than 4000 bytes of data (as the
+ payload must fit into a single cookie).
+
+ Parameters:
+
+ ``secret``
+ A string which is used to sign the cookie. The secret should be at
+ least as long as the block size of the selected hash algorithm. For
+ ``sha512`` this would mean a 128 bit (64 character) secret. It should
+ be unique within the set of secret values provided to Pyramid for
+ its various subsystems (see :ref:`admonishment_against_secret_sharing`).
+
+ ``hashalg``
+ The HMAC digest algorithm to use for signing. The algorithm must be
+ supported by the :mod:`hashlib` library. Default: ``'sha512'``.
+
+ ``salt``
+ A namespace to avoid collisions between different uses of a shared
+ secret. Reusing a secret for different parts of an application is
+ strongly discouraged (see :ref:`admonishment_against_secret_sharing`).
+ Default: ``'pyramid.session.'``.
+
+ ``cookie_name``
+ The name of the cookie used for sessioning. Default: ``'session'``.
+
+ ``max_age``
+ The maximum age of the cookie used for sessioning (in seconds).
+ Default: ``None`` (browser scope).
+
+ ``path``
+ The path used for the session cookie. Default: ``'/'``.
+
+ ``domain``
+ The domain used for the session cookie. Default: ``None`` (no domain).
+
+ ``secure``
+ The 'secure' flag of the session cookie. Default: ``False``.
+
+ ``httponly``
+ Hide the cookie from Javascript by setting the 'HttpOnly' flag of the
+ session cookie. Default: ``False``.
+
+ ``timeout``
+ A number of seconds of inactivity before a session times out. If
+ ``None`` then the cookie never expires. Default: 1200.
+
+ ``reissue_time``
+ The number of seconds that must pass before the cookie is automatically
+ reissued as the result of a request which accesses the session. The
+ duration is measured as the number of seconds since the last session
+ cookie was issued and 'now'. If this value is ``0``, a new cookie
+ will be reissued on every request accesses the session. If ``None``
+ then the cookie's lifetime will never be extended.
+
+ A good rule of thumb: if you want auto-expired cookies based on
+ inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
+ ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
+ (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
+ than the ``reissue_time`` value, as the ticket will never be reissued.
+ However, such a configuration is not explicitly prevented.
+
+ Default: ``0``.
+
+ ``set_on_exception``
+ If ``True``, set a session cookie even if an exception occurs
+ while rendering a view. Default: ``True``.
+
+ ``serialize``
+ A callable accepting a Python object and returning a bytestring. A
+ ``ValueError`` should be raised for malformed inputs.
+ Default: :func:`pickle.dumps`.
+
+ ``deserialize``
+ A callable accepting a bytestring and returning a Python object. A
+ ``ValueError`` should be raised for malformed inputs.
+ Default: :func:`pickle.loads`.
+
+ .. versionadded: 1.5a3
+ """
+
+ if serialize is None:
+ serialize = lambda v: pickle.dumps(v, pickle.HIGHEST_PROTOCOL)
+
+ if deserialize is None:
+ deserialize = pickle.loads
+
+ digestmod = lambda: hashlib.new(hashalg)
+ digest_size = digestmod().digest_size
+
+ salted_secret = bytes_(salt or '') + bytes_(secret)
+
+ def signed_serialize(appstruct):
+ cstruct = serialize(appstruct)
+ sig = hmac.new(salted_secret, cstruct, digestmod).digest()
+ return base64.b64encode(cstruct + sig)
+
+ def signed_deserialize(bstruct):
+ try:
+ fstruct = base64.b64decode(bstruct)
+ except (binascii.Error, TypeError) as e:
+ raise ValueError('Badly formed base64 data: %s' % e)
+
+ cstruct = fstruct[:-digest_size]
+ expected_sig = fstruct[-digest_size:]
+
+ sig = hmac.new(salted_secret, cstruct, digestmod).digest()
+ if strings_differ(sig, expected_sig):
+ raise ValueError('Invalid signature')
+
+ return deserialize(cstruct)
+
+ return BaseCookieSessionFactory(
+ signed_serialize,
+ signed_deserialize,
+ cookie_name=cookie_name,
+ max_age=max_age,
+ path=path,
+ domain=domain,
+ secure=secure,
+ httponly=httponly,
+ timeout=timeout,
+ reissue_time=reissue_time,
+ set_on_exception=set_on_exception,
+ )
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 35e2b5c27..c13d3ce5c 100644
--- a/pyramid/tests/test_session.py
+++ b/pyramid/tests/test_session.py
@@ -1,10 +1,8 @@
+import json
import unittest
from pyramid import testing
-class TestUnencryptedCookieSession(unittest.TestCase):
- def _makeOne(self, request, **kw):
- from pyramid.session import UnencryptedCookieSessionFactoryConfig
- return UnencryptedCookieSessionFactoryConfig('secret', **kw)(request)
+class SharedCookieSessionTests(object):
def test_ctor_no_cookie(self):
request = testing.DummyRequest()
@@ -18,36 +16,47 @@ class TestUnencryptedCookieSession(unittest.TestCase):
session = self._makeOne(request)
verifyObject(ISession, session)
- def _serialize(self, accessed, state, secret='secret'):
- from pyramid.session import signed_serialize
- return signed_serialize((accessed, accessed, state), secret)
-
def test_ctor_with_cookie_still_valid(self):
import time
request = testing.DummyRequest()
- cookieval = self._serialize(time.time(), {'state':1})
+ cookieval = self._serialize((time.time(), 0, {'state': 1}))
request.cookies['session'] = cookieval
session = self._makeOne(request)
self.assertEqual(dict(session), {'state':1})
-
+
def test_ctor_with_cookie_expired(self):
request = testing.DummyRequest()
- cookieval = self._serialize(0, {'state':1})
+ cookieval = self._serialize((0, 0, {'state': 1}))
request.cookies['session'] = cookieval
session = self._makeOne(request)
self.assertEqual(dict(session), {})
- def test_ctor_with_bad_cookie(self):
+ def test_ctor_with_bad_cookie_cannot_deserialize(self):
+ request = testing.DummyRequest()
+ request.cookies['session'] = 'abc'
+ session = self._makeOne(request)
+ self.assertEqual(dict(session), {})
+
+ def test_ctor_with_bad_cookie_not_tuple(self):
request = testing.DummyRequest()
- cookieval = 'abc'
+ cookieval = self._serialize('abc')
request.cookies['session'] = cookieval
session = self._makeOne(request)
self.assertEqual(dict(session), {})
+ def test_timeout(self):
+ import time
+ request = testing.DummyRequest()
+ cookieval = self._serialize((time.time() - 5, 0, {'state': 1}))
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, timeout=1)
+ self.assertEqual(dict(session), {})
+
def test_changed(self):
request = testing.DummyRequest()
session = self._makeOne(request)
self.assertEqual(session.changed(), None)
+ self.assertTrue(session._dirty)
def test_invalidate(self):
request = testing.DummyRequest()
@@ -56,6 +65,15 @@ class TestUnencryptedCookieSession(unittest.TestCase):
self.assertEqual(session.invalidate(), None)
self.assertFalse('a' in session)
+ def test_reissue_triggered(self):
+ import time
+ request = testing.DummyRequest()
+ cookieval = self._serialize((time.time() - 2, 0, {'state': 1}))
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request)
+ self.assertEqual(session['state'], 1)
+ self.assertTrue(session._dirty)
+
def test__set_cookie_on_exception(self):
request = testing.DummyRequest()
request.exception = True
@@ -95,16 +113,16 @@ class TestUnencryptedCookieSession(unittest.TestCase):
request = testing.DummyRequest()
request.exception = None
session = self._makeOne(request,
- cookie_name = 'abc',
- cookie_path = '/foo',
- cookie_domain = 'localhost',
- cookie_secure = True,
- cookie_httponly = True,
+ cookie_name='abc',
+ path='/foo',
+ domain='localhost',
+ secure=True,
+ httponly=True,
)
session['abc'] = 'x'
response = Response()
self.assertEqual(session._set_cookie(response), True)
- cookieval= response.headerlist[-1][1]
+ cookieval = response.headerlist[-1][1]
val, domain, path, secure, httponly = [x.strip() for x in
cookieval.split(';')]
self.assertTrue(val.startswith('abc='))
@@ -205,6 +223,181 @@ class TestUnencryptedCookieSession(unittest.TestCase):
self.assertTrue(token)
self.assertTrue('_csrft_' in session)
+ def test_no_set_cookie_with_exception(self):
+ import webob
+ request = testing.DummyRequest()
+ request.exception = True
+ session = self._makeOne(request, set_on_exception=False)
+ session['a'] = 1
+ callbacks = request.response_callbacks
+ self.assertEqual(len(callbacks), 1)
+ response = webob.Response()
+ result = callbacks[0](request, response)
+ self.assertEqual(result, None)
+ self.assertFalse('Set-Cookie' in dict(response.headerlist))
+
+ def test_set_cookie_with_exception(self):
+ import webob
+ request = testing.DummyRequest()
+ request.exception = True
+ session = self._makeOne(request)
+ session['a'] = 1
+ callbacks = request.response_callbacks
+ self.assertEqual(len(callbacks), 1)
+ response = webob.Response()
+ result = callbacks[0](request, response)
+ self.assertEqual(result, None)
+ self.assertTrue('Set-Cookie' in dict(response.headerlist))
+
+ def test_cookie_is_set(self):
+ import webob
+ request = testing.DummyRequest()
+ session = self._makeOne(request)
+ session['a'] = 1
+ callbacks = request.response_callbacks
+ self.assertEqual(len(callbacks), 1)
+ response = webob.Response()
+ result = callbacks[0](request, response)
+ self.assertEqual(result, None)
+ self.assertTrue('Set-Cookie' in dict(response.headerlist))
+
+class TestBaseCookieSession(SharedCookieSessionTests, unittest.TestCase):
+ def _makeOne(self, request, **kw):
+ from pyramid.session import BaseCookieSessionFactory
+ return BaseCookieSessionFactory(
+ dummy_serialize, dummy_deserialize, **kw)(request)
+
+ def _serialize(self, value):
+ return json.dumps(value)
+
+ def test_reissue_not_triggered(self):
+ import time
+ request = testing.DummyRequest()
+ cookieval = self._serialize((time.time(), 0, {'state': 1}))
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, reissue_time=1)
+ self.assertEqual(session['state'], 1)
+ self.assertFalse(session._dirty)
+
+class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase):
+ def _makeOne(self, request, **kw):
+ from pyramid.session import SignedCookieSessionFactory
+ kw.setdefault('secret', 'secret')
+ return SignedCookieSessionFactory(**kw)(request)
+
+ def _serialize(self, value, salt=b'pyramid.session.', hashalg='sha512'):
+ import base64
+ import hashlib
+ import hmac
+ import pickle
+
+ digestmod = lambda: hashlib.new(hashalg)
+ cstruct = pickle.dumps(value, pickle.HIGHEST_PROTOCOL)
+ sig = hmac.new(salt + b'secret', cstruct, digestmod).digest()
+ return base64.b64encode(cstruct + sig)
+
+ def test_reissue_not_triggered(self):
+ import time
+ request = testing.DummyRequest()
+ cookieval = self._serialize((time.time(), 0, {'state': 1}))
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, reissue_time=1)
+ self.assertEqual(session['state'], 1)
+ self.assertFalse(session._dirty)
+
+ def test_custom_salt(self):
+ import time
+ request = testing.DummyRequest()
+ cookieval = self._serialize((time.time(), 0, {'state': 1}), salt=b'f.')
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, salt=b'f.')
+ self.assertEqual(session['state'], 1)
+
+ def test_salt_mismatch(self):
+ import time
+ request = testing.DummyRequest()
+ cookieval = self._serialize((time.time(), 0, {'state': 1}), salt=b'f.')
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, salt=b'g.')
+ self.assertEqual(session, {})
+
+ def test_custom_hashalg(self):
+ import time
+ request = testing.DummyRequest()
+ cookieval = self._serialize((time.time(), 0, {'state': 1}),
+ hashalg='sha1')
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, hashalg='sha1')
+ self.assertEqual(session['state'], 1)
+
+ def test_hashalg_mismatch(self):
+ import time
+ request = testing.DummyRequest()
+ cookieval = self._serialize((time.time(), 0, {'state': 1}),
+ hashalg='sha1')
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, hashalg='sha256')
+ self.assertEqual(session, {})
+
+ def test_secret_mismatch(self):
+ import time
+ request = testing.DummyRequest()
+ cookieval = self._serialize((time.time(), 0, {'state': 1}))
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, secret='evilsecret')
+ self.assertEqual(session, {})
+
+ def test_custom_serializer(self):
+ import base64
+ from hashlib import sha512
+ import hmac
+ import time
+ request = testing.DummyRequest()
+ cstruct = dummy_serialize((time.time(), 0, {'state': 1}))
+ sig = hmac.new(b'pyramid.session.secret', cstruct, sha512).digest()
+ cookieval = base64.b64encode(cstruct + sig)
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request, deserialize=dummy_deserialize)
+ self.assertEqual(session['state'], 1)
+
+ def test_invalid_data_size(self):
+ from hashlib import sha512
+ import base64
+ request = testing.DummyRequest()
+ num_bytes = sha512().digest_size - 1
+ cookieval = base64.b64encode(b' ' * num_bytes)
+ request.cookies['session'] = cookieval
+ session = self._makeOne(request)
+ self.assertEqual(session, {})
+
+class TestUnencryptedCookieSession(SharedCookieSessionTests, unittest.TestCase):
+ def setUp(self):
+ super(TestUnencryptedCookieSession, self).setUp()
+ from zope.deprecation import __show__
+ __show__.off()
+
+ def tearDown(self):
+ super(TestUnencryptedCookieSession, self).tearDown()
+ from zope.deprecation import __show__
+ __show__.on()
+
+ def _makeOne(self, request, **kw):
+ from pyramid.session import UnencryptedCookieSessionFactoryConfig
+ self._rename_cookie_var(kw, 'path', 'cookie_path')
+ self._rename_cookie_var(kw, 'domain', 'cookie_domain')
+ self._rename_cookie_var(kw, 'secure', 'cookie_secure')
+ self._rename_cookie_var(kw, 'httponly', 'cookie_httponly')
+ self._rename_cookie_var(kw, 'set_on_exception', 'cookie_on_exception')
+ return UnencryptedCookieSessionFactoryConfig('secret', **kw)(request)
+
+ def _rename_cookie_var(self, kw, src, dest):
+ if src in kw:
+ kw.setdefault(dest, kw.pop(src))
+
+ def _serialize(self, value):
+ from pyramid.session import signed_serialize
+ return signed_serialize(value, 'secret')
+
def test_serialize_option(self):
from pyramid.response import Response
secret = 'secret'
@@ -255,54 +448,48 @@ class Test_manage_accessed(unittest.TestCase):
def test_accessed_set(self):
request = testing.DummyRequest()
session = DummySessionFactory(request)
- session.accessed = None
+ session.renewed = 0
wrapper = self._makeOne(session.__class__.get)
wrapper(session, 'a')
self.assertNotEqual(session.accessed, None)
-
- def test_already_dirty(self):
+ self.assertTrue(session._dirty)
+
+ def test_accessed_without_renew(self):
+ import time
request = testing.DummyRequest()
session = DummySessionFactory(request)
- session._dirty = True
- session['a'] = 1
+ session._reissue_time = 5
+ session.renewed = time.time()
wrapper = self._makeOne(session.__class__.get)
- self.assertEqual(wrapper.__doc__, session.get.__doc__)
- result = wrapper(session, 'a')
- self.assertEqual(result, 1)
- callbacks = request.response_callbacks
- self.assertEqual(len(callbacks), 0)
+ wrapper(session, 'a')
+ self.assertNotEqual(session.accessed, None)
+ self.assertFalse(session._dirty)
- def test_with_exception(self):
- import webob
+ def test_already_dirty(self):
request = testing.DummyRequest()
- request.exception = True
session = DummySessionFactory(request)
+ session.renewed = 0
+ session._dirty = True
session['a'] = 1
wrapper = self._makeOne(session.__class__.get)
self.assertEqual(wrapper.__doc__, session.get.__doc__)
result = wrapper(session, 'a')
self.assertEqual(result, 1)
callbacks = request.response_callbacks
- self.assertEqual(len(callbacks), 1)
- response = webob.Response()
- result = callbacks[0](request, response)
- self.assertEqual(result, None)
- self.assertFalse('Set-Cookie' in dict(response.headerlist))
+ self.assertEqual(len(callbacks), 0)
- def test_cookie_is_set(self):
+class Test_manage_changed(unittest.TestCase):
+ def _makeOne(self, wrapped):
+ from pyramid.session import manage_changed
+ return manage_changed(wrapped)
+
+ def test_it(self):
request = testing.DummyRequest()
session = DummySessionFactory(request)
- session['a'] = 1
- wrapper = self._makeOne(session.__class__.get)
- self.assertEqual(wrapper.__doc__, session.get.__doc__)
- result = wrapper(session, 'a')
- self.assertEqual(result, 1)
- callbacks = request.response_callbacks
- self.assertEqual(len(callbacks), 1)
- response = DummyResponse()
- result = callbacks[0](request, response)
- self.assertEqual(result, None)
- self.assertEqual(session.response, response)
+ wrapper = self._makeOne(session.__class__.__setitem__)
+ wrapper(session, 'a', 1)
+ self.assertNotEqual(session.accessed, None)
+ self.assertTrue(session._dirty)
def serialize(data, secret):
import hmac
@@ -354,7 +541,7 @@ class Test_signed_deserialize(unittest.TestCase):
def test_it_bad_encoding(self):
serialized = 'bad' + serialize('123', 'secret')
self.assertRaises(ValueError, self._callFUT, serialized, 'secret')
-
+
class Test_check_csrf_token(unittest.TestCase):
def _callFUT(self, *args, **kwargs):
from ..session import check_csrf_token
@@ -381,15 +568,22 @@ 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.exceptions import BadCSRFToken
request = testing.DummyRequest()
- self.assertRaises(HTTPBadRequest, self._callFUT, request, 'csrf_token')
+ self.assertRaises(BadCSRFToken, 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)
+def dummy_serialize(value):
+ return json.dumps(value).encode('utf-8')
+
+def dummy_deserialize(value):
+ return json.loads(value.decode('utf-8'))
+
class DummySessionFactory(dict):
_dirty = False
_cookie_name = 'session'
@@ -399,13 +593,14 @@ class DummySessionFactory(dict):
_cookie_secure = False
_cookie_httponly = False
_timeout = 1200
- _secret = 'secret'
+ _reissue_time = 0
+
def __init__(self, request):
self.request = request
dict.__init__(self, {})
- def _set_cookie(self, response):
- self.response = response
+ def changed(self):
+ self._dirty = True
class DummyResponse(object):
def __init__(self):