goa/appobjects/sessions.py
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 15 Jun 2010 18:00:26 +0200
branchstable
changeset 5748 487ab6ac90cc
parent 5424 8ecbcbff9777
permissions -rw-r--r--
[auto-reload] we should never call load_module with use_sys=False, that may lead to inconsistency with module interdependancy (eg module get imported by another one, then is reimported by the vreg to load its appobjects. Cleanup of sys.modules done before reloading should be enough.

# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
"""persistent sessions stored in big table


XXX TODO:
* cleanup persistent session
* use user as ancestor?
"""
__docformat__ = "restructuredtext en"

from pickle import loads, dumps
from time import localtime, strftime

from logilab.common.decorators import cached, clear_cache

from cubicweb import BadConnectionId
from cubicweb.dbapi import Connection, ConnectionProperties, repo_connect
from cubicweb.selectors import none_rset, match_user_groups
from cubicweb.server.session import Session
from cubicweb.web import InvalidSession
from cubicweb.web.application import AbstractSessionManager
from cubicweb.web.application import AbstractAuthenticationManager

from google.appengine.api.datastore import Key, Entity, Get, Put, Delete, Query
from google.appengine.api.datastore_errors import EntityNotFoundError
from google.appengine.api.datastore_types import Blob

try:
    del Connection.__del__
except AttributeError:
    pass # already deleted


class GAEAuthenticationManager(AbstractAuthenticationManager):
    """authenticate user associated to a request and check session validity,
    using google authentication service
    """

    def __init__(self, *args, **kwargs):
        super(GAEAuthenticationManager, self).__init__(*args, **kwargs)
        self._repo = self.config.repository(vreg=self.vreg)

    def authenticate(self, req, _login=None, _password=None):
        """authenticate user and return an established connection for this user

        :raise ExplicitLogin: if authentication is required (no authentication
        info found or wrong user/password)
        """
        if _login is not None:
            login, password = _login, _password
        else:
            login, password = req.get_authorization()
        # remove possibly cached cursor coming from closed connection
        clear_cache(req, 'cursor')
        cnxprops = ConnectionProperties(self.vreg.config.repo_method,
                                        close=False, log=False)
        cnx = repo_connect(self._repo, login, password=password, cnxprops=cnxprops)
        self._init_cnx(cnx, login, password)
        # associate the connection to the current request
        req.set_connection(cnx)
        return cnx

    def _init_cnx(self, cnx, login, password):
        cnx.anonymous_connection = self.config.is_anonymous_user(login)
        cnx.vreg = self.vreg
        cnx.login = login
        cnx.password = password


class GAEPersistentSessionManager(AbstractSessionManager):
    """manage session data associated to a session identifier"""

    def __init__(self, vreg, *args, **kwargs):
        super(GAEPersistentSessionManager, self).__init__(vreg, *args, **kwargs)
        self._repo = self.config.repository(vreg=vreg)

    def get_session(self, req, sessionid):
        """return existing session for the given session identifier"""
        # search a record for the given session
        key = Key.from_path('CubicWebSession', 'key_' + sessionid, parent=None)
        try:
            record = Get(key)
        except EntityNotFoundError:
            raise InvalidSession()
        repo = self._repo
        if self.has_expired(record):
            repo._sessions.pop(sessionid, None)
            Delete(record)
            raise InvalidSession()
        # associate it with a repository session
        try:
            reposession = repo._get_session(sessionid)
            user = reposession.user
            # touch session to avoid closing our own session when sessions are
            # cleaned (touch is done on commit/rollback on the server side, too
            # late in that case)
            reposession._touch()
        except BadConnectionId:
            # can't found session in the repository, this probably mean the
            # session is not yet initialized on this server, hijack the repo
            # to create it
            # use an internal connection
            ssession = repo.internal_session()
            # try to get a user object
            try:
                user = repo.authenticate_user(ssession, record['login'],
                                              record['password'])
            finally:
                ssession.close()
            reposession = Session(user, self._repo, _id=sessionid)
            self._repo._sessions[sessionid] = reposession
        cnx = Connection(self._repo, sessionid)
        return self._get_proxy(req, record, cnx, user)

    def open_session(self, req):
        """open and return a new session for the given request"""
        cnx = self.authmanager.authenticate(req)
        # avoid rebuilding a user
        user = self._repo._get_session(cnx.sessionid).user
        # build persistent record for session data
        record = Entity('CubicWebSession', name='key_' + cnx.sessionid)
        record['login'] = cnx.login
        record['password'] = cnx.password
        record['anonymous_connection'] = cnx.anonymous_connection
        Put(record)
        return self._get_proxy(req, record, cnx, user)

    def close_session(self, proxy):
        """close session on logout or on invalid session detected (expired out,
        corrupted...)
        """
        proxy.close()

    def current_sessions(self):
        for record in Query('CubicWebSession').Run():
            yield ConnectionProxy(record)

    def _get_proxy(self, req, record, cnx, user):
        proxy = ConnectionProxy(record, cnx, user)
        user.req = req
        req.set_connection(proxy, user)
        return proxy


class ConnectionProxy(object):

    def __init__(self, record, cnx=None, user=None):
        self.__record = record
        self.__cnx = cnx
        self.__user = user
        self.__data = None
        self.__is_dirty = False
        self.sessionid = record.key().name()[4:] # remove 'key_' prefix

    def __repr__(self):
        sstr = '<ConnectionProxy %s' % self.sessionid
        if self.anonymous_connection:
            sstr += ' (anonymous)'
        elif self.__user:
            sstr += ' for %s' % self.__user.login
        sstr += ', last used %s>' % strftime('%T', localtime(self.last_usage_time))
        return sstr

    def __getattribute__(self, name):
        try:
            return super(ConnectionProxy, self).__getattribute__(name)
        except AttributeError:
            return getattr(self.__cnx, name)

    def _set_last_usage_time(self, value):
        self.__is_dirty = True
        self.__record['last_usage_time'] = value
    def _get_last_usage_time(self):
        return self.__record['last_usage_time']

    last_usage_time = property(_get_last_usage_time, _set_last_usage_time)

    @property
    def anonymous_connection(self):
        # use get() for bw compat if sessions without anonymous information are
        # found. Set default to True to limit lifetime of those sessions.
        return self.__record.get('anonymous_connection', True)

    @property
    @cached
    def data(self):
        if self.__record.get('data') is not None:
            try:
                return loads(self.__record['data'])
            except:
                self.__is_dirty = True
                self.exception('corrupted session data for session %s',
                               self.__cnx)
        return {}

    def get_session_data(self, key, default=None, pop=False):
        """return value associated to `key` in session data"""
        if pop:
            try:
                value = self.data.pop(key)
                self.__is_dirty = True
                return value
            except KeyError:
                return default
        else:
            return self.data.get(key, default)

    def set_session_data(self, key, value):
        """set value associated to `key` in session data"""
        self.data[key] = value
        self.__is_dirty = True

    def del_session_data(self, key):
        """remove value associated to `key` in session data"""
        try:
            del self.data[key]
            self.__is_dirty = True
        except KeyError:
            pass

    def commit(self):
        if self.__is_dirty:
            self.__save()
        self.__cnx.commit()

    def rollback(self):
        self.__save()
        self.__cnx.rollback()

    def close(self):
        if self.__cnx is not None:
            self.__cnx.close()
        Delete(self.__record)

    def __save(self):
        if self.__is_dirty:
            self.__record['data'] = Blob(dumps(self.data))
            Put(self.__record)
            self.__is_dirty = False

    def user(self, req=None, props=None):
        """return the User object associated to this connection"""
        return self.__user


import logging
from cubicweb import set_log_methods
set_log_methods(ConnectionProxy, logging.getLogger('cubicweb.web.goa.session'))


from cubicweb.view import StartupView
from cubicweb.web import application

class SessionsCleaner(StartupView):
    id = 'cleansessions'
    __select__ = none_rset() & match_user_groups('managers')

    def call(self):
        # clean web session
        session_manager = application.SESSION_MANAGER
        nbclosed, remaining = session_manager.clean_sessions()
        self.w(u'<div class="message">')
        self.w(u'%s web sessions closed<br/>\n' % nbclosed)
        # clean repository sessions
        repo = self.config.repository(vreg=self.vreg)
        nbclosed = repo.clean_sessions()
        self.w(u'%s repository sessions closed<br/>\n' % nbclosed)
        self.w(u'%s remaining sessions<br/>\n' % remaining)
        self.w(u'</div>')


def registration_callback(vreg):
    vreg.register(SessionsCleaner)
    vreg.register(GAEAuthenticationManager, clear=True)
    vreg.register(GAEPersistentSessionManager, clear=True)