Change session timeout to an idle timeout value

Add a new config SESSION_REFRESH (default True) which
turns SESSION_TIMEOUT into an idle timeout rather than
a hard timeout.

The existing hard timeout is awful UX, and while
SESSION_TIMEOUT could be set to a higher value, it
still makes for a somewhat unpleasant experience.

Co-Authored-By: Akihiro Motoki <amotoki@gmail.com>
Change-Id: Icc6942e62c4e8d2fac57988b0a2233a8073b1944
This commit is contained in:
Adrian Turjak 2018-07-11 16:33:31 +12:00 committed by Akihiro Motoki
parent 06ab7a5047
commit dc0ffaf2d8
7 changed files with 128 additions and 6 deletions

View File

@ -798,6 +798,16 @@ in `AVAILABLE_THEMES`_, but a brander may wish to simply inherit from an
existing theme and not allow that parent theme to be selected by the user.
``SELECTABLE_THEMES`` takes the exact same format as ``AVAILABLE_THEMES``.
SESSION_REFRESH
---------------
.. versionadded:: 15.0.0(Stein)
Default: ``True``
Control whether the SESSION_TIMEOUT period is refreshed due to activity. If
False, SESSION_TIMEOUT acts as a hard limit.
SESSION_TIMEOUT
---------------
@ -805,9 +815,14 @@ SESSION_TIMEOUT
Default: ``"3600"``
This SESSION_TIMEOUT is a method to supercede the token timeout with a shorter
horizon session timeout (in seconds). So if your token expires in 60 minutes,
a value of 1800 will log users out after 30 minutes.
This SESSION_TIMEOUT is a method to supercede the token timeout with a
shorter horizon session timeout (in seconds). If SESSION_REFRESH is True (the
default) SESSION_TIMEOUT acts like an idle timeout rather than being a hard
limit, but will never exceed the token expiry. If your token expires in 60
minutes, a value of 1800 will log users out after 30 minutes of inactivity,
or 60 minutes with activity. Setting SESSION_REFRESH to False will make
SESSION_TIMEOUT act like a hard limit on session times.
MEMOIZED_MAX_SIZE_DEFAULT
-------------------------

View File

@ -19,9 +19,12 @@
Middleware provided and used by Horizon.
"""
import datetime
import json
import logging
import pytz
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import redirect_to_login
@ -65,6 +68,15 @@ class HorizonMiddleware(object):
# to avoid creating too many sessions
return None
# Since we know the user is present and authenticated, lets refresh the
# session expiry if configured to do so.
if getattr(settings, "SESSION_REFRESH", True):
timeout = getattr(settings, "SESSION_TIMEOUT", 3600)
token_life = request.user.token.expires - datetime.datetime.now(
pytz.utc)
session_time = min(timeout, int(token_life.total_seconds()))
request.session.set_expiry(session_time)
if request.is_ajax():
# if the request is Ajax we do not want to proceed, as clients can
# 1) create pages with constant polling, which can create race

View File

@ -13,11 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import mock
import pytz
from django.conf import settings
from django.http import HttpResponseRedirect
from django import test as django_test
from django.test.utils import override_settings
from django.utils import timezone
from horizon import exceptions
@ -65,11 +69,13 @@ class MiddlewareTests(django_test.TestCase):
self.assertEqual(200, resp.status_code)
self.assertEqual(url, resp['X-Horizon-Location'])
@override_settings(SESSION_REFRESH=False)
def test_timezone_awareness(self):
url = settings.LOGIN_REDIRECT_URL
mw = middleware.HorizonMiddleware(self.get_response)
request = self.factory.get(url)
request.session['django_timezone'] = 'America/Chicago'
mw._process_request(request)
self.assertEqual(
@ -80,3 +86,67 @@ class MiddlewareTests(django_test.TestCase):
request.session['django_timezone'] = 'UTC'
mw._process_request(request)
self.assertEqual(timezone.get_current_timezone_name(), 'UTC')
@override_settings(SESSION_TIMEOUT=600,
SESSION_REFRESH=True)
def test_refresh_session_expiry_enough_token_life(self):
url = settings.LOGIN_REDIRECT_URL
mw = middleware.HorizonMiddleware(self.get_response)
request = self.factory.get(url)
now = datetime.datetime.now(pytz.utc)
token_expiry = now + datetime.timedelta(seconds=1800)
request.user.token = mock.Mock(expires=token_expiry)
session_expiry_before = now + datetime.timedelta(seconds=300)
request.session.set_expiry(session_expiry_before)
mw._process_request(request)
session_expiry_after = request.session.get_expiry_date()
# Check if session_expiry has been updated.
self.assertGreater(session_expiry_after, session_expiry_before)
# Check session_expiry is before token expiry
self.assertLess(session_expiry_after, token_expiry)
@override_settings(SESSION_TIMEOUT=600,
SESSION_REFRESH=True)
def test_refresh_session_expiry_near_token_expiry(self):
url = settings.LOGIN_REDIRECT_URL
mw = middleware.HorizonMiddleware(self.get_response)
request = self.factory.get(url)
now = datetime.datetime.now(pytz.utc)
token_expiry = now + datetime.timedelta(seconds=10)
request.user.token = mock.Mock(expires=token_expiry)
mw._process_request(request)
session_expiry_after = request.session.get_expiry_date()
# Check if session_expiry_after is around token_expiry.
# We set some margin to avoid accidental test failure.
self.assertGreater(session_expiry_after,
token_expiry - datetime.timedelta(seconds=3))
self.assertLess(session_expiry_after,
token_expiry + datetime.timedelta(seconds=3))
@override_settings(SESSION_TIMEOUT=600,
SESSION_REFRESH=False)
def test_no_refresh_session_expiry(self):
url = settings.LOGIN_REDIRECT_URL
mw = middleware.HorizonMiddleware(self.get_response)
request = self.factory.get(url)
now = datetime.datetime.now(pytz.utc)
token_expiry = now + datetime.timedelta(seconds=1800)
request.user.token = mock.Mock(expires=token_expiry)
session_expiry_before = now + datetime.timedelta(seconds=300)
request.session.set_expiry(session_expiry_before)
mw._process_request(request)
session_expiry_after = request.session.get_expiry_date()
# Check if session_expiry has been updated.
self.assertEqual(session_expiry_after, session_expiry_before)

View File

@ -18,6 +18,7 @@ import copy
from django.conf import settings
from django import http
from django.test.utils import override_settings
import six
@ -348,6 +349,7 @@ class TabExceptionTests(test.TestCase):
super(TabExceptionTests, self).tearDown()
TabWithTableView.tab_group_class.tabs = self._original_tabs
@override_settings(SESSION_REFRESH=False)
def test_tab_view_exception(self):
TabWithTableView.tab_group_class.tabs.append(RecoverableErrorTab)
view = TabWithTableView.as_view()
@ -355,6 +357,7 @@ class TabExceptionTests(test.TestCase):
res = view(req)
self.assertMessageCount(res, error=1)
@override_settings(SESSION_REFRESH=False)
def test_tab_302_exception(self):
TabWithTableView.tab_group_class.tabs.append(RedirectExceptionTab)
view = TabWithTableView.as_view()

View File

@ -25,6 +25,7 @@ from six import moves
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ImproperlyConfigured
from django.test.utils import override_settings
from django import urls
import horizon
@ -268,6 +269,7 @@ class HorizonTests(BaseHorizonTests):
self.assertEqual(redirect_url,
resp["X-Horizon-Location"])
@override_settings(SESSION_REFRESH=False)
def test_required_permissions(self):
dash = horizon.get_dashboard("cats")
panel = dash.get_panel('tigers')
@ -427,6 +429,7 @@ class CustomPermissionsTests(BaseHorizonTests):
# refresh config
conf.HORIZON_CONFIG._setup()
@override_settings(SESSION_REFRESH=False)
def test_customized_permissions(self):
dogs = horizon.get_dashboard("dogs")
panel = dogs.get_panel('puppies')

View File

@ -203,9 +203,17 @@ SESSION_COOKIE_HTTPONLY = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_SECURE = False
# SESSION_TIMEOUT is a method to supersede the token timeout with a shorter
# horizon session timeout (in seconds). So if your token expires in 60
# minutes, a value of 1800 will log users out after 30 minutes
# Control whether the SESSION_TIMEOUT period is refreshed due to activity. If
# False, SESSION_TIMEOUT acts as a hard limit.
SESSION_REFRESH = True
# This SESSION_TIMEOUT is a method to supercede the token timeout with a
# shorter horizon session timeout (in seconds). If SESSION_REFRESH is True (the
# default) SESSION_TIMEOUT acts like an idle timeout rather than being a hard
# limit, but will never exceed the token expiry. If your token expires in 60
# minutes, a value of 1800 will log users out after 30 minutes of inactivity,
# or 60 minutes with activity. Setting SESSION_REFRESH to False will make
# SESSION_TIMEOUT act like a hard limit on session times.
SESSION_TIMEOUT = 3600
# When using cookie-based sessions, log error when the session cookie exceeds

View File

@ -0,0 +1,11 @@
---
features:
- |
New setting ``SESSION_REFRESH`` (defaults to ``True``) that allows the user
session expiry to be refreshed for every request until the token itself
expires. ``SESSION_TIMEOUT`` acts as an idle timeout value now.
upgrade:
- |
``SESSION_TIMEOUT`` now by default acts as an idle timeout rather than a
hard timeout limit. If you wish to retain the old hard timeout
functionality set ``SESSION_REFRESH`` to ``False``.