Add basic support for per-user session timeout
This commit is contained in:
parent
4659d3473b
commit
b2e21b8e74
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2016 Lance Edgar
|
# Copyright © 2010-2017 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -133,7 +133,7 @@ def make_pyramid_config(settings):
|
||||||
config.set_default_csrf_options(require_csrf=True, token='_csrf')
|
config.set_default_csrf_options(require_csrf=True, token='_csrf')
|
||||||
|
|
||||||
# Bring in some Pyramid goodies.
|
# Bring in some Pyramid goodies.
|
||||||
config.include('pyramid_beaker')
|
config.include('tailbone.beaker')
|
||||||
config.include('pyramid_mako')
|
config.include('pyramid_mako')
|
||||||
config.include('pyramid_tm')
|
config.include('pyramid_tm')
|
||||||
|
|
||||||
|
|
140
tailbone/beaker.py
Normal file
140
tailbone/beaker.py
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2017 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU Affero General Public License as published by the Free
|
||||||
|
# Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# Rattail 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 Affero General Public License for
|
||||||
|
# more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Custom sessions, based on Beaker
|
||||||
|
|
||||||
|
Note that most of the code for this module was copied from the beaker and
|
||||||
|
pyramid_beaker projects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from beaker.session import Session
|
||||||
|
from beaker.util import coerce_session_params
|
||||||
|
from pyramid.settings import asbool
|
||||||
|
from pyramid_beaker import BeakerSessionFactoryConfig, set_cache_regions_from_settings
|
||||||
|
|
||||||
|
|
||||||
|
class TailboneSession(Session):
|
||||||
|
"""
|
||||||
|
Custom session class for Beaker, which overrides load() to add per-request
|
||||||
|
session timeout support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
"Loads the data from this session from persistent storage"
|
||||||
|
self.namespace = self.namespace_class(self.id,
|
||||||
|
data_dir=self.data_dir,
|
||||||
|
digest_filenames=False,
|
||||||
|
**self.namespace_args)
|
||||||
|
now = time.time()
|
||||||
|
if self.use_cookies:
|
||||||
|
self.request['set_cookie'] = True
|
||||||
|
|
||||||
|
self.namespace.acquire_read_lock()
|
||||||
|
timed_out = False
|
||||||
|
try:
|
||||||
|
self.clear()
|
||||||
|
try:
|
||||||
|
session_data = self.namespace['session']
|
||||||
|
|
||||||
|
if (session_data is not None and self.encrypt_key):
|
||||||
|
session_data = self._decrypt_data(session_data)
|
||||||
|
|
||||||
|
# Memcached always returns a key, its None when its not
|
||||||
|
# present
|
||||||
|
if session_data is None:
|
||||||
|
session_data = {
|
||||||
|
'_creation_time': now,
|
||||||
|
'_accessed_time': now
|
||||||
|
}
|
||||||
|
self.is_new = True
|
||||||
|
except (KeyError, TypeError):
|
||||||
|
session_data = {
|
||||||
|
'_creation_time': now,
|
||||||
|
'_accessed_time': now
|
||||||
|
}
|
||||||
|
self.is_new = True
|
||||||
|
|
||||||
|
if session_data is None or len(session_data) == 0:
|
||||||
|
session_data = {
|
||||||
|
'_creation_time': now,
|
||||||
|
'_accessed_time': now
|
||||||
|
}
|
||||||
|
self.is_new = True
|
||||||
|
|
||||||
|
# TODO: sure would be nice if we could get this little bit of logic
|
||||||
|
# into the upstream Beaker package, as that would avoid the need
|
||||||
|
# for this module entirely...
|
||||||
|
timeout = session_data.get('_timeout', self.timeout)
|
||||||
|
if timeout is not None and \
|
||||||
|
now - session_data['_accessed_time'] > timeout:
|
||||||
|
timed_out = True
|
||||||
|
else:
|
||||||
|
# Properly set the last_accessed time, which is different
|
||||||
|
# than the *currently* _accessed_time
|
||||||
|
if self.is_new or '_accessed_time' not in session_data:
|
||||||
|
self.last_accessed = None
|
||||||
|
else:
|
||||||
|
self.last_accessed = session_data['_accessed_time']
|
||||||
|
|
||||||
|
# Update the current _accessed_time
|
||||||
|
session_data['_accessed_time'] = now
|
||||||
|
|
||||||
|
# Set the path if applicable
|
||||||
|
if '_path' in session_data:
|
||||||
|
self._path = session_data['_path']
|
||||||
|
self.update(session_data)
|
||||||
|
self.accessed_dict = session_data.copy()
|
||||||
|
finally:
|
||||||
|
self.namespace.release_read_lock()
|
||||||
|
if timed_out:
|
||||||
|
self.invalidate()
|
||||||
|
|
||||||
|
|
||||||
|
def session_factory_from_settings(settings):
|
||||||
|
""" Return a Pyramid session factory using Beaker session settings
|
||||||
|
supplied from a Paste configuration file"""
|
||||||
|
prefixes = ('session.', 'beaker.session.')
|
||||||
|
options = {}
|
||||||
|
|
||||||
|
# Pull out any config args meant for beaker session. if there are any
|
||||||
|
for k, v in settings.items():
|
||||||
|
for prefix in prefixes:
|
||||||
|
if k.startswith(prefix):
|
||||||
|
option_name = k[len(prefix):]
|
||||||
|
if option_name == 'cookie_on_exception':
|
||||||
|
v = asbool(v)
|
||||||
|
options[option_name] = v
|
||||||
|
|
||||||
|
options = coerce_session_params(options)
|
||||||
|
options['session_class'] = TailboneSession
|
||||||
|
return BeakerSessionFactoryConfig(**options)
|
||||||
|
|
||||||
|
|
||||||
|
def includeme(config):
|
||||||
|
session_factory = session_factory_from_settings(config.registry.settings)
|
||||||
|
config.set_session_factory(session_factory)
|
||||||
|
set_cache_regions_from_settings(config.registry.settings)
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2016 Lance Edgar
|
# Copyright © 2010-2017 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -28,7 +28,7 @@ from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import rattail
|
import rattail
|
||||||
from rattail.db import model
|
from rattail.db import model
|
||||||
from rattail.db.auth import has_permission, administrator_role
|
from rattail.db.auth import has_permission
|
||||||
|
|
||||||
from pyramid import threadlocal
|
from pyramid import threadlocal
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ def context_found(event):
|
||||||
if request.user:
|
if request.user:
|
||||||
Session().set_continuum_user(request.user)
|
Session().set_continuum_user(request.user)
|
||||||
|
|
||||||
request.is_admin = request.user and administrator_role(Session()) in request.user.roles
|
request.is_admin = bool(request.user) and request.user.is_admin()
|
||||||
request.is_root = request.is_admin and request.session.get('is_root', False)
|
request.is_root = request.is_admin and request.session.get('is_root', False)
|
||||||
|
|
||||||
def has_perm(name):
|
def has_perm(name):
|
||||||
|
|
|
@ -26,7 +26,9 @@ Auth Views
|
||||||
|
|
||||||
from __future__ import unicode_literals, absolute_import
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
from rattail.db.auth import authenticate_user, set_user_password
|
import logging
|
||||||
|
|
||||||
|
from rattail.db.auth import authenticate_user, set_user_password, has_permission
|
||||||
|
|
||||||
import formencode as fe
|
import formencode as fe
|
||||||
from pyramid.httpexceptions import HTTPForbidden
|
from pyramid.httpexceptions import HTTPForbidden
|
||||||
|
@ -39,6 +41,9 @@ from tailbone.db import Session
|
||||||
from tailbone.views import View
|
from tailbone.views import View
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class UserLogin(fe.Schema):
|
class UserLogin(fe.Schema):
|
||||||
allow_extra_fields = True
|
allow_extra_fields = True
|
||||||
filter_extra_fields = True
|
filter_extra_fields = True
|
||||||
|
@ -106,11 +111,17 @@ class AuthenticationView(View):
|
||||||
user = self.authenticate_user(form.data['username'],
|
user = self.authenticate_user(form.data['username'],
|
||||||
form.data['password'])
|
form.data['password'])
|
||||||
if user:
|
if user:
|
||||||
|
|
||||||
# okay now they're truly logged in
|
# okay now they're truly logged in
|
||||||
headers = remember(self.request, user.uuid)
|
headers = remember(self.request, user.uuid)
|
||||||
|
timeout = self.get_session_timeout_for_user(user) or None
|
||||||
|
log.debug("setting session timeout for '{}' to {}".format(user.username, timeout))
|
||||||
|
self.set_session_timeout(timeout)
|
||||||
|
|
||||||
# treat URL from session as referrer, if available
|
# treat URL from session as referrer, if available
|
||||||
referrer = self.request.session.pop('next_url', referrer)
|
referrer = self.request.session.pop('next_url', referrer)
|
||||||
return self.redirect(referrer, headers=headers)
|
return self.redirect(referrer, headers=headers)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.request.session.flash("Invalid username or password", 'error')
|
self.request.session.flash("Invalid username or password", 'error')
|
||||||
|
|
||||||
|
@ -123,6 +134,33 @@ class AuthenticationView(View):
|
||||||
def authenticate_user(self, username, password):
|
def authenticate_user(self, username, password):
|
||||||
return authenticate_user(Session(), username, password)
|
return authenticate_user(Session(), username, password)
|
||||||
|
|
||||||
|
def get_session_timeout_for_user(self, user):
|
||||||
|
"""
|
||||||
|
Must return a value to be used to set the session timeout for the given
|
||||||
|
user. By default this will return ``None`` if the user has the
|
||||||
|
"forever session" permission, otherwise will try to read a default
|
||||||
|
value from config:
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[tailbone]
|
||||||
|
|
||||||
|
# set session timeout to 10 minutes:
|
||||||
|
session.default_timeout = 600
|
||||||
|
|
||||||
|
# or, set to 0 to disable:
|
||||||
|
#session.default_timeout = 0
|
||||||
|
"""
|
||||||
|
if not has_permission(Session(), user, 'general.forever_session'):
|
||||||
|
return self.rattail_config.getint('tailbone', 'session.default_timeout',
|
||||||
|
default=300) # 5 minutes
|
||||||
|
|
||||||
|
def set_session_timeout(self, timeout):
|
||||||
|
"""
|
||||||
|
Set the server-side session timeout to the given value.
|
||||||
|
"""
|
||||||
|
self.request.session['_timeout'] = timeout or None
|
||||||
|
|
||||||
def mobile_login(self):
|
def mobile_login(self):
|
||||||
return self.login(mobile=True)
|
return self.login(mobile=True)
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,11 @@ class CommonView(View):
|
||||||
# auto-correct URLs which require trailing slash
|
# auto-correct URLs which require trailing slash
|
||||||
config.add_notfound_view(cls, attr='notfound', append_slash=True)
|
config.add_notfound_view(cls, attr='notfound', append_slash=True)
|
||||||
|
|
||||||
|
# general permissions
|
||||||
|
config.add_tailbone_permission_group('general', "(General)", overwrite=False)
|
||||||
|
config.add_tailbone_permission('general', 'general.forever_session',
|
||||||
|
"Never expire sessions on server")
|
||||||
|
|
||||||
# home
|
# home
|
||||||
config.add_route('home', '/')
|
config.add_route('home', '/')
|
||||||
config.add_view(cls, attr='home', route_name='home', renderer='/home.mako')
|
config.add_view(cls, attr='home', route_name='home', renderer='/home.mako')
|
||||||
|
|
Loading…
Reference in a new issue