"""persistent sessions stored in big table:organization: Logilab:copyright: 2008-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.frXXX TODO:* cleanup persistent session* use user as ancestor?:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"frompickleimportloads,dumpsfromtimeimportlocaltime,strftimefromlogilab.common.decoratorsimportcached,clear_cachefromcubicwebimportBadConnectionIdfromcubicweb.dbapiimportConnection,ConnectionProperties,repo_connectfromcubicweb.selectorsimportnone_rset,match_user_groupsfromcubicweb.server.sessionimportSessionfromcubicweb.webimportInvalidSessionfromcubicweb.web.applicationimportAbstractSessionManagerfromcubicweb.web.applicationimportAbstractAuthenticationManagerfromgoogle.appengine.api.datastoreimportKey,Entity,Get,Put,Delete,Queryfromgoogle.appengine.api.datastore_errorsimportEntityNotFoundErrorfromgoogle.appengine.api.datastore_typesimportBlobtry:delConnection.__del__exceptAttributeError:pass# already deletedclassGAEAuthenticationManager(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)defauthenticate(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_loginisnotNone:login,password=_login,_passwordelse:login,password=req.get_authorization()# remove possibly cached cursor coming from closed connectionclear_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 requestreq.set_connection(cnx)returncnxdef_init_cnx(self,cnx,login,password):cnx.anonymous_connection=self.config.is_anonymous_user(login)cnx.vreg=self.vregcnx.login=logincnx.password=passwordclassGAEPersistentSessionManager(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)defget_session(self,req,sessionid):"""return existing session for the given session identifier"""# search a record for the given sessionkey=Key.from_path('CubicWebSession','key_'+sessionid,parent=None)try:record=Get(key)exceptEntityNotFoundError:raiseInvalidSession()repo=self._repoifself.has_expired(record):repo._sessions.pop(sessionid,None)Delete(record)raiseInvalidSession()# associate it with a repository sessiontry: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()exceptBadConnectionId:# 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 connectionssession=repo.internal_session()# try to get a user objecttry:user=repo.authenticate_user(ssession,record['login'],record['password'])finally:ssession.close()reposession=Session(user,self._repo,_id=sessionid)self._repo._sessions[sessionid]=reposessioncnx=Connection(self._repo,sessionid)returnself._get_proxy(req,record,cnx,user)defopen_session(self,req):"""open and return a new session for the given request"""cnx=self.authmanager.authenticate(req)# avoid rebuilding a useruser=self._repo._get_session(cnx.sessionid).user# build persistent record for session datarecord=Entity('CubicWebSession',name='key_'+cnx.sessionid)record['login']=cnx.loginrecord['password']=cnx.passwordrecord['anonymous_connection']=cnx.anonymous_connectionPut(record)returnself._get_proxy(req,record,cnx,user)defclose_session(self,proxy):"""close session on logout or on invalid session detected (expired out, corrupted...) """proxy.close()defcurrent_sessions(self):forrecordinQuery('CubicWebSession').Run():yieldConnectionProxy(record)def_get_proxy(self,req,record,cnx,user):proxy=ConnectionProxy(record,cnx,user)user.req=reqreq.set_connection(proxy,user)returnproxyclassConnectionProxy(object):def__init__(self,record,cnx=None,user=None):self.__record=recordself.__cnx=cnxself.__user=userself.__data=Noneself.__is_dirty=Falseself.sessionid=record.key().name()[4:]# remove 'key_' prefixdef__repr__(self):sstr='<ConnectionProxy %s'%self.sessionidifself.anonymous_connection:sstr+=' (anonymous)'elifself.__user:sstr+=' for %s'%self.__user.loginsstr+=', last used %s>'%strftime('%T',localtime(self.last_usage_time))returnsstrdef__getattribute__(self,name):try:returnsuper(ConnectionProxy,self).__getattribute__(name)exceptAttributeError:returngetattr(self.__cnx,name)def_set_last_usage_time(self,value):self.__is_dirty=Trueself.__record['last_usage_time']=valuedef_get_last_usage_time(self):returnself.__record['last_usage_time']last_usage_time=property(_get_last_usage_time,_set_last_usage_time)@propertydefanonymous_connection(self):# use get() for bw compat if sessions without anonymous information are# found. Set default to True to limit lifetime of those sessions.returnself.__record.get('anonymous_connection',True)@property@cacheddefdata(self):ifself.__record.get('data')isnotNone:try:returnloads(self.__record['data'])except:self.__is_dirty=Trueself.exception('corrupted session data for session %s',self.__cnx)return{}defget_session_data(self,key,default=None,pop=False):"""return value associated to `key` in session data"""ifpop:try:value=self.data.pop(key)self.__is_dirty=TruereturnvalueexceptKeyError:returndefaultelse:returnself.data.get(key,default)defset_session_data(self,key,value):"""set value associated to `key` in session data"""self.data[key]=valueself.__is_dirty=Truedefdel_session_data(self,key):"""remove value associated to `key` in session data"""try:delself.data[key]self.__is_dirty=TrueexceptKeyError:passdefcommit(self):ifself.__is_dirty:self.__save()self.__cnx.commit()defrollback(self):self.__save()self.__cnx.rollback()defclose(self):ifself.__cnxisnotNone:self.__cnx.close()Delete(self.__record)def__save(self):ifself.__is_dirty:self.__record['data']=Blob(dumps(self.data))Put(self.__record)self.__is_dirty=Falsedefuser(self,req=None,props=None):"""return the User object associated to this connection"""returnself.__userimportloggingfromcubicwebimportset_log_methodsset_log_methods(ConnectionProxy,logging.getLogger('cubicweb.web.goa.session'))fromcubicweb.viewimportStartupViewfromcubicweb.webimportapplicationclassSessionsCleaner(StartupView):id='cleansessions'__select__=none_rset()&match_user_groups('managers')defcall(self):# clean web sessionsession_manager=application.SESSION_MANAGERnbclosed,remaining=session_manager.clean_sessions()self.w(u'<div class="message">')self.w(u'%s web sessions closed<br/>\n'%nbclosed)# clean repository sessionsrepo=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>')defregistration_callback(vreg):vreg.register(SessionsCleaner)vreg.register(GAEAuthenticationManager,clear=True)vreg.register(GAEPersistentSessionManager,clear=True)