pyramid_cubicweb/session.py
author Christophe de Vienne <christophe@unlish.com>
Sat, 03 Jan 2015 22:06:03 +0100
changeset 11537 caf268942436
parent 11506 bfc1aa1dba30
child 11579 78ba3e88a549
permissions -rw-r--r--
Initial documentation. Closes #4849313

import warnings
import logging

from pyramid.compat import pickle
from pyramid.session import SignedCookieSessionFactory

from cubicweb import Binary


log = logging.getLogger(__name__)


def logerrors(logger):
    def wrap(fn):
        def newfn(*args, **kw):
            try:
                return fn(*args, **kw)
            except:
                logger.exception("Error in %s" % fn.__name__)
        return newfn
    return wrap


def CWSessionFactory(
        secret,
        cookie_name='session',
        max_age=None,
        path='/',
        domain=None,
        secure=False,
        httponly=False,
        set_on_exception=True,
        timeout=1200,
        reissue_time=120,
        hashalg='sha512',
        salt='pyramid.session.',
        serializer=None):
    """ A pyramid session factory that store session data in the CubicWeb
    database.

    Storage is done with the 'CWSession' entity, which is provided by the
    'pyramid' cube.

    .. warning::

        Although it provides a sane default behavior, this session storage has
        a serious overhead because it uses RQL to access the database.

        Using pure SQL would improve a bit (it is roughly twice faster), but it
        is still pretty slow and thus not an immediate priority.

        It is recommended to use faster session factory
        (pyramid_redis_sessions_ for example) if you need speed.

    .. _pyramid_redis_sessions: http://pyramid-redis-sessions.readthedocs.org/
                                en/latest/index.html
    """

    SignedCookieSession = SignedCookieSessionFactory(
        secret,
        cookie_name=cookie_name,
        max_age=max_age,
        path=path,
        domain=domain,
        secure=secure,
        httponly=httponly,
        set_on_exception=set_on_exception,
        timeout=timeout,
        reissue_time=reissue_time,
        hashalg=hashalg,
        salt=salt,
        serializer=serializer)

    class CWSession(SignedCookieSession):
        def __init__(self, request):
            # _set_accessed will be called by the super __init__.
            # Setting _loaded to True inhibates it.
            self._loaded = True

            # the super __init__ will load a single value in the dictionnary,
            # the session id.
            super(CWSession, self).__init__(request)

            # Remove the session id from the dict
            self.sessioneid = self.pop('sessioneid', None)
            self.repo = request.registry['cubicweb.repository']

            # We need to lazy-load only for existing sessions
            self._loaded = self.sessioneid is None

        @logerrors(log)
        def _set_accessed(self, value):
            self._accessed = value

            if self._loaded:
                return

            with self.repo.internal_cnx() as cnx:
                session = cnx.find('CWSession', eid=self.sessioneid).one()
                value = session.cwsessiondata
                if value:
                    # Use directly dict.update to avoir _set_accessed to be
                    # recursively called
                    dict.update(self, pickle.load(value))

            self._loaded = True

        def _get_accessed(self):
            return self._accessed

        accessed = property(_get_accessed, _set_accessed)

        @logerrors(log)
        def _set_cookie(self, response):
            # Save the value in the database
            data = Binary(pickle.dumps(dict(self)))
            sessioneid = self.sessioneid

            with self.repo.internal_cnx() as cnx:
                if not sessioneid:
                    session = cnx.create_entity(
                        'CWSession', cwsessiondata=data)
                    sessioneid = session.eid
                else:
                    session = cnx.entity_from_eid(sessioneid)
                    session.cw_set(cwsessiondata=data)
                cnx.commit()

            # Only if needed actually set the cookie
            if self.new or self.accessed - self.renewed > self._reissue_time:
                dict.clear(self)
                dict.__setitem__(self, 'sessioneid', sessioneid)
                return super(CWSession, self)._set_cookie(response)

            return True

    return CWSession


def includeme(config):
    """ Activate the CubicWeb session factory.

    Usually called via ``config.include('pyramid_cubicweb.auth')``.

    See also :ref:`defaults_module`
    """
    secret = config.registry['cubicweb.config']['pyramid-session-secret']
    if not secret:
        secret = 'notsosecret'
        warnings.warn('''

            !! WARNING !! !! WARNING !!

            The session cookies are signed with a static secret key.
            To put your own secret key, edit your all-in-one.conf file
            and set the 'pyramid-session-secret' key.

            YOU SHOULD STOP THIS INSTANCE unless your really know what you
            are doing !!

        ''')
    session_factory = CWSessionFactory(secret)
    config.set_session_factory(session_factory)