# -*- test-case-name: xmantissa.test.test_websession -*-
# Copyright 2005 Divmod, Inc. See LICENSE file for details
"""Sessions that persist in the database.
Every SESSION_CLEAN_FREQUENCY seconds, a pass is made over all persistant
sessions, and those that are more than PERSISTENT_SESSION_LIFETIME seconds old
are deleted. Transient sessions die after TRANSIENT_SESSION_LIFETIME seconds.
These three globals can be overridden by passing appropriate values to the
PersistentSessionWrapper constructor: sessionCleanFrequency, persistentSessionLifetime,
and transientSessionLifetime.
"""
from twisted.cred import credentials
from epsilon import extime
from axiom import attributes, item, userbase
from nevow import guard
SESSION_CLEAN_FREQUENCY = 60 * 60 * 25 # 1 day, almost
PERSISTENT_SESSION_LIFETIME = 60 * 60 * 24 * 7 * 2 # 2 weeks
TRANSIENT_SESSION_LIFETIME = 60 * 12 + 32 # 12 minutes, 32 seconds.
def usernameFromRequest(request):
"""
Take a HTTP request and return a username of the form <user>@<domain>.
@type request: L{inevow.IRequest}
@param request: A HTTP request
@return: A C{str}
"""
username = request.args.get('username', [''])[0]
if '@' not in username:
username = '%s@%s' % (username, request.getHeader('host').split(':')[0])
return username
class PersistentSession(item.Item):
"""A session that persists on the database.
These sessions should not store any state, but are used only to determine
that the user has previously authenticated and should be given a transient
session (a regular guard session, not database persistant) without
providing credentials again.
"""
typeName = 'persistent_session'
schemaVersion = 1
sessionKey = attributes.bytes()
lastUsed = attributes.timestamp()
authenticatedAs = attributes.bytes() # The username and domain
# that this session was
# authenticated as.
def __init__(self, **kw):
assert kw.get('sessionKey') is not None, "None cookie propogated to PersistentSession"
kw['lastUsed'] = extime.Time()
super(PersistentSession, self).__init__(**kw)
def __repr__(self):
return "PersistentSession(%r)" % (self.sessionKey, )
def renew(self):
"""Renew the lifetime of this object.
Call this when the user logs in so this session does not expire.
"""
self.lastUsed = extime.Time()
class DBPassthrough(object):
"""A dictionaryish thing that manages sessions and interfaces with guard.
This is set as the sessions attribute on a nevow.guard.SessionWrapper
instance, or in this case, a subclass. Guard uses a vanilla dict by
default; here we pretend to be a dict and introduce presistant-session
behaviour.
"""
def __init__(self, wrapper):
self.wrapper = wrapper
self._transientSessions = {}
def __contains__(self, key):
# we use __get__ here so that transient sessions are always created.
# Otherwise, sometimes guard will call __contains__ and assume the
# transient session is there, without creating it.
try:
self[key]
except KeyError:
return False
return True
has_key = __contains__
def __getitem__(self, key):
if key is None:
raise KeyError("None is not a valid session key")
try:
return self._transientSessions[key]
except KeyError:
if self.wrapper.authenticatedUserForKey(key):
session = self.wrapper.sessionFactory(self.wrapper, key)
self._transientSessions[key] = session
session.setLifetime(self.wrapper.sessionLifetime) # screw you guard!
session.checkExpired()
return session
raise
def __setitem__(self, key, value):
self._transientSessions[key] = value
def __delitem__(self, key):
del self._transientSessions[key]
def __repr__(self):
return 'DBPassthrough at %i; %r, with embelishments' % (id(self), self._transientSessions)
class PersistentSessionWrapper(guard.SessionWrapper):
"""
Extends nevow.guard.SessionWrapper to reauthenticate previously
authenticated users.
There are 4 possible states:
1) new user, no persistent session, no transient session
2) anonymous user, no persistent session, transient session
3) returning user, persistent session, no transient session
4) active user, persistent session, transient session
Guard will look it the sessions dict, and if it finds a key
matching a cookie sent by the client, will return the value as the
session. However, if a user has a persistent session cookie, but
no transient session, one is created here.
"""
def __init__(
self,
store,
portal,
transientSessionLifetime=TRANSIENT_SESSION_LIFETIME,
persistentSessionLifetime=PERSISTENT_SESSION_LIFETIME,
sessionCleanFrequency=SESSION_CLEAN_FREQUENCY,
**kw):
"""Initialize the PersistentSessionWrapper
"""
guard.SessionWrapper.__init__(self, portal, **kw)
self.store = store
self.sessions = DBPassthrough(self)
self.cookieKey = 'divmod-user-cookie'
self.sessionLifetime = transientSessionLifetime
self.persistentSessionLifetime = persistentSessionLifetime
self.sessionCleanFrequency = sessionCleanFrequency
def createSessionForKey(self, key, user):
PersistentSession(
store=self.store,
sessionKey=key,
authenticatedAs=user)
def authenticatedUserForKey(self, key):
for session in self.store.query(PersistentSession, PersistentSession.sessionKey == key):
session.renew()
return session.authenticatedAs
return None
def removeSessionWithKey(self, key):
for session in self.store.query(PersistentSession, PersistentSession.sessionKey == key):
session.deleteFromStore()
break
# if the session doesn't exist, we ignore that fact here.
def savorSessionCookie(self, request):
"""Make the session cookie last as long as the persistant session."""
cookieValue = request.getSession().uid
request.addCookie(self.cookieKey, cookieValue, path='/', max_age=PERSISTENT_SESSION_LIFETIME)
def login(self, request, session, creds, segments):
"""Called to check the credentials of a user.
Here we extend guard's implementation to preauthenticate users
if they have a valid persistant session.
"""
if isinstance(creds, credentials.Anonymous):
preauth = self.authenticatedUserForKey(session.uid)
if preauth is not None:
self.savorSessionCookie(request)
creds = userbase.Preauthenticated(preauth)
def cbLoginSuccess(input):
"""User authenticated successfully.
Create the persistent session, and associate it with the
username. (XXX it doesn't work like this now)
"""
user = request.args.get('username')
if user is not None:
# create a database session and associate it with this user
cookieValue = session.uid
if request.args.get('rememberMe'):
self.createSessionForKey(cookieValue, creds.username)
self.savorSessionCookie(request)
return input
return guard.SessionWrapper.login(self, request, session, creds, segments
).addCallback(cbLoginSuccess)
def explicitLogout(self, session):
"""
Here we override guard's behaviour for the logout action to
delete the persistent session. In this case the user has
explicitly requested a logout, so the persistent session must
be deleted to require the user to log in on the next request.
"""
guard.SessionWrapper.explicitLogout(self, session)
self.removeSessionWithKey(session.uid)
def getCredentials(self, request):
"""
Override SessionWrapper.getCredentials to add the Host: header
to the credentials. This will make web-based virtual hosting
work.
"""
username = usernameFromRequest(request)
password = request.args.get('password', [''])[0]
return credentials.UsernamePassword(username, password)
syntax highlighted by Code2HTML, v. 0.9.1