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