goa/appobjects/sessions.py
changeset 6366 1806148d6ce8
parent 6333 e3994fcc21c3
parent 6365 a15cc5e16178
child 6367 d4c485ec1ca1
equal deleted inserted replaced
6333:e3994fcc21c3 6366:1806148d6ce8
     1 # copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """persistent sessions stored in big table
       
    19 
       
    20 
       
    21 XXX TODO:
       
    22 * cleanup persistent session
       
    23 * use user as ancestor?
       
    24 """
       
    25 __docformat__ = "restructuredtext en"
       
    26 
       
    27 from pickle import loads, dumps
       
    28 from time import localtime, strftime
       
    29 
       
    30 from logilab.common.decorators import cached, clear_cache
       
    31 
       
    32 from cubicweb import BadConnectionId
       
    33 from cubicweb.dbapi import Connection, ConnectionProperties, repo_connect
       
    34 from cubicweb.selectors import none_rset, match_user_groups
       
    35 from cubicweb.server.session import Session
       
    36 from cubicweb.web import InvalidSession
       
    37 from cubicweb.web.application import AbstractSessionManager
       
    38 from cubicweb.web.application import AbstractAuthenticationManager
       
    39 
       
    40 from google.appengine.api.datastore import Key, Entity, Get, Put, Delete, Query
       
    41 from google.appengine.api.datastore_errors import EntityNotFoundError
       
    42 from google.appengine.api.datastore_types import Blob
       
    43 
       
    44 try:
       
    45     del Connection.__del__
       
    46 except AttributeError:
       
    47     pass # already deleted
       
    48 
       
    49 
       
    50 class GAEAuthenticationManager(AbstractAuthenticationManager):
       
    51     """authenticate user associated to a request and check session validity,
       
    52     using google authentication service
       
    53     """
       
    54 
       
    55     def __init__(self, *args, **kwargs):
       
    56         super(GAEAuthenticationManager, self).__init__(*args, **kwargs)
       
    57         self._repo = self.config.repository(vreg=self.vreg)
       
    58 
       
    59     def authenticate(self, req, _login=None, _password=None):
       
    60         """authenticate user and return an established connection for this user
       
    61 
       
    62         :raise ExplicitLogin: if authentication is required (no authentication
       
    63         info found or wrong user/password)
       
    64         """
       
    65         if _login is not None:
       
    66             login, password = _login, _password
       
    67         else:
       
    68             login, password = req.get_authorization()
       
    69         # remove possibly cached cursor coming from closed connection
       
    70         clear_cache(req, 'cursor')
       
    71         cnxprops = ConnectionProperties(self.vreg.config.repo_method,
       
    72                                         close=False, log=False)
       
    73         cnx = repo_connect(self._repo, login, password=password, cnxprops=cnxprops)
       
    74         self._init_cnx(cnx, login, password)
       
    75         # associate the connection to the current request
       
    76         req.set_connection(cnx)
       
    77         return cnx
       
    78 
       
    79     def _init_cnx(self, cnx, login, password):
       
    80         cnx.anonymous_connection = self.config.is_anonymous_user(login)
       
    81         cnx.vreg = self.vreg
       
    82         cnx.login = login
       
    83         cnx.password = password
       
    84 
       
    85 
       
    86 class GAEPersistentSessionManager(AbstractSessionManager):
       
    87     """manage session data associated to a session identifier"""
       
    88 
       
    89     def __init__(self, vreg, *args, **kwargs):
       
    90         super(GAEPersistentSessionManager, self).__init__(vreg, *args, **kwargs)
       
    91         self._repo = self.config.repository(vreg=vreg)
       
    92 
       
    93     def get_session(self, req, sessionid):
       
    94         """return existing session for the given session identifier"""
       
    95         # search a record for the given session
       
    96         key = Key.from_path('CubicWebSession', 'key_' + sessionid, parent=None)
       
    97         try:
       
    98             record = Get(key)
       
    99         except EntityNotFoundError:
       
   100             raise InvalidSession()
       
   101         repo = self._repo
       
   102         if self.has_expired(record):
       
   103             repo._sessions.pop(sessionid, None)
       
   104             Delete(record)
       
   105             raise InvalidSession()
       
   106         # associate it with a repository session
       
   107         try:
       
   108             reposession = repo._get_session(sessionid)
       
   109             user = reposession.user
       
   110             # touch session to avoid closing our own session when sessions are
       
   111             # cleaned (touch is done on commit/rollback on the server side, too
       
   112             # late in that case)
       
   113             reposession._touch()
       
   114         except BadConnectionId:
       
   115             # can't found session in the repository, this probably mean the
       
   116             # session is not yet initialized on this server, hijack the repo
       
   117             # to create it
       
   118             # use an internal connection
       
   119             ssession = repo.internal_session()
       
   120             # try to get a user object
       
   121             try:
       
   122                 user = repo.authenticate_user(ssession, record['login'],
       
   123                                               record['password'])
       
   124             finally:
       
   125                 ssession.close()
       
   126             reposession = Session(user, self._repo, _id=sessionid)
       
   127             self._repo._sessions[sessionid] = reposession
       
   128         cnx = Connection(self._repo, sessionid)
       
   129         return self._get_proxy(req, record, cnx, user)
       
   130 
       
   131     def open_session(self, req):
       
   132         """open and return a new session for the given request"""
       
   133         cnx = self.authmanager.authenticate(req)
       
   134         # avoid rebuilding a user
       
   135         user = self._repo._get_session(cnx.sessionid).user
       
   136         # build persistent record for session data
       
   137         record = Entity('CubicWebSession', name='key_' + cnx.sessionid)
       
   138         record['login'] = cnx.login
       
   139         record['password'] = cnx.password
       
   140         record['anonymous_connection'] = cnx.anonymous_connection
       
   141         Put(record)
       
   142         return self._get_proxy(req, record, cnx, user)
       
   143 
       
   144     def close_session(self, proxy):
       
   145         """close session on logout or on invalid session detected (expired out,
       
   146         corrupted...)
       
   147         """
       
   148         proxy.close()
       
   149 
       
   150     def current_sessions(self):
       
   151         for record in Query('CubicWebSession').Run():
       
   152             yield ConnectionProxy(record)
       
   153 
       
   154     def _get_proxy(self, req, record, cnx, user):
       
   155         proxy = ConnectionProxy(record, cnx, user)
       
   156         user.req = req
       
   157         req.set_connection(proxy, user)
       
   158         return proxy
       
   159 
       
   160 
       
   161 class ConnectionProxy(object):
       
   162 
       
   163     def __init__(self, record, cnx=None, user=None):
       
   164         self.__record = record
       
   165         self.__cnx = cnx
       
   166         self.__user = user
       
   167         self.__data = None
       
   168         self.__is_dirty = False
       
   169         self.sessionid = record.key().name()[4:] # remove 'key_' prefix
       
   170 
       
   171     def __repr__(self):
       
   172         sstr = '<ConnectionProxy %s' % self.sessionid
       
   173         if self.anonymous_connection:
       
   174             sstr += ' (anonymous)'
       
   175         elif self.__user:
       
   176             sstr += ' for %s' % self.__user.login
       
   177         sstr += ', last used %s>' % strftime('%T', localtime(self.last_usage_time))
       
   178         return sstr
       
   179 
       
   180     def __getattribute__(self, name):
       
   181         try:
       
   182             return super(ConnectionProxy, self).__getattribute__(name)
       
   183         except AttributeError:
       
   184             return getattr(self.__cnx, name)
       
   185 
       
   186     def _set_last_usage_time(self, value):
       
   187         self.__is_dirty = True
       
   188         self.__record['last_usage_time'] = value
       
   189     def _get_last_usage_time(self):
       
   190         return self.__record['last_usage_time']
       
   191 
       
   192     last_usage_time = property(_get_last_usage_time, _set_last_usage_time)
       
   193 
       
   194     @property
       
   195     def anonymous_connection(self):
       
   196         # use get() for bw compat if sessions without anonymous information are
       
   197         # found. Set default to True to limit lifetime of those sessions.
       
   198         return self.__record.get('anonymous_connection', True)
       
   199 
       
   200     @property
       
   201     @cached
       
   202     def data(self):
       
   203         if self.__record.get('data') is not None:
       
   204             try:
       
   205                 return loads(self.__record['data'])
       
   206             except:
       
   207                 self.__is_dirty = True
       
   208                 self.exception('corrupted session data for session %s',
       
   209                                self.__cnx)
       
   210         return {}
       
   211 
       
   212     def get_session_data(self, key, default=None, pop=False):
       
   213         """return value associated to `key` in session data"""
       
   214         if pop:
       
   215             try:
       
   216                 value = self.data.pop(key)
       
   217                 self.__is_dirty = True
       
   218                 return value
       
   219             except KeyError:
       
   220                 return default
       
   221         else:
       
   222             return self.data.get(key, default)
       
   223 
       
   224     def set_session_data(self, key, value):
       
   225         """set value associated to `key` in session data"""
       
   226         self.data[key] = value
       
   227         self.__is_dirty = True
       
   228 
       
   229     def del_session_data(self, key):
       
   230         """remove value associated to `key` in session data"""
       
   231         try:
       
   232             del self.data[key]
       
   233             self.__is_dirty = True
       
   234         except KeyError:
       
   235             pass
       
   236 
       
   237     def commit(self):
       
   238         if self.__is_dirty:
       
   239             self.__save()
       
   240         self.__cnx.commit()
       
   241 
       
   242     def rollback(self):
       
   243         self.__save()
       
   244         self.__cnx.rollback()
       
   245 
       
   246     def close(self):
       
   247         if self.__cnx is not None:
       
   248             self.__cnx.close()
       
   249         Delete(self.__record)
       
   250 
       
   251     def __save(self):
       
   252         if self.__is_dirty:
       
   253             self.__record['data'] = Blob(dumps(self.data))
       
   254             Put(self.__record)
       
   255             self.__is_dirty = False
       
   256 
       
   257     def user(self, req=None, props=None):
       
   258         """return the User object associated to this connection"""
       
   259         return self.__user
       
   260 
       
   261 
       
   262 import logging
       
   263 from cubicweb import set_log_methods
       
   264 set_log_methods(ConnectionProxy, logging.getLogger('cubicweb.web.goa.session'))
       
   265 
       
   266 
       
   267 from cubicweb.view import StartupView
       
   268 from cubicweb.web import application
       
   269 
       
   270 class SessionsCleaner(StartupView):
       
   271     id = 'cleansessions'
       
   272     __select__ = none_rset() & match_user_groups('managers')
       
   273 
       
   274     def call(self):
       
   275         # clean web session
       
   276         session_manager = application.SESSION_MANAGER
       
   277         nbclosed, remaining = session_manager.clean_sessions()
       
   278         self.w(u'<div class="message">')
       
   279         self.w(u'%s web sessions closed<br/>\n' % nbclosed)
       
   280         # clean repository sessions
       
   281         repo = self.config.repository(vreg=self.vreg)
       
   282         nbclosed = repo.clean_sessions()
       
   283         self.w(u'%s repository sessions closed<br/>\n' % nbclosed)
       
   284         self.w(u'%s remaining sessions<br/>\n' % remaining)
       
   285         self.w(u'</div>')
       
   286 
       
   287 
       
   288 def registration_callback(vreg):
       
   289     vreg.register(SessionsCleaner)
       
   290     vreg.register(GAEAuthenticationManager, clear=True)
       
   291     vreg.register(GAEPersistentSessionManager, clear=True)