Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supporting custom 'EXPIRE_AFTER' value (use case: user-defined expiration value) #20

Merged
merged 12 commits into from
Oct 30, 2014
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ django_session_security.egg-info
dist
docs/build
docs/source/_static/
.idea
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ install:
- pip install pep8 --use-mirrors
- pip install https://github.com/dcramer/pyflakes/tarball/master
- pip install -q -e . --use-mirrors
- pip install selenium==2.37.2 unittest_data_provider six
- pip install selenium==2.42.1 unittest_data_provider six
- pip freeze
before_script:
- "pep8 --exclude=tests,migrations --ignore=E124,E128 session_security"
Expand Down
10 changes: 9 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ ping.

First, a warning should be shown after ``settings.SESSION_SECURITY_WARN_AFTER``
seconds. The warning displays a text like "Your session is about to expire,
move the mouse to extend it".
move the mouse to extend it". Alternatively, you could set
``settings.SESSION_SECURITY_WARN_BEFORE`` if you don't know the
``settings.SESSION_SECURITY_EXPIRE_AFTER`` value before hand.

Before displaying this warning, SessionSecurity will upload the time since the
last client-side activity was recorded. The middleware will take it if it is
Expand All @@ -53,6 +55,12 @@ Same goes to expire after ``settings.SESSION_SECURITY_EXPIRE_AFTER`` seconds.
Javascript will first make an ajax request to PingView to ensure that another
more recent activity was not detected anywhere else - in any other browser tab.

NOTE: To handle custom ``settings.SESSION_SECURITY_EXPIRE_AFTER`` values
(when there's a need for user-defined value), simply set
``settings.SESSION_SECURITY_CUSTOM_SESSION_KEY``. Middleware will take value
from ``request.session[settings.SESSION_SECURITY_CUSTOM_SESSION_KEY]`` first
before defaulting to ``settings.SESSION_SECURITY_EXPIRE_AFTER`` value.

Requirements
------------

Expand Down
9 changes: 9 additions & 0 deletions docs/requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Django==1.6.5
Markdown==2.4.1
Pycco==0.3.0
Pygments==1.6
pystache==0.5.4
selenium==2.42.1
six==1.7.2
smartypants==1.8.3
unittest-data-provider==1.0.1
28 changes: 15 additions & 13 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
# All configuration values have a default; values that are commented out
# serve to show the default.

import sys, os, os.path
import sys
import os
import os.path

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
Expand All @@ -20,7 +22,7 @@
sys.path.insert(0, os.path.abspath('../../../../lib/python2.7/site-packages/'))
from django.conf import settings
settings.configure()
settings.ROOT_URLCONF='session_security.urls'
settings.ROOT_URLCONF = 'session_security.urls'
settings.SESSION_EXPIRE_AT_BROWSER_CLOSE

autoclass_content = "both"
Expand Down Expand Up @@ -190,21 +192,21 @@
# -- Options for LaTeX output --------------------------------------------------

latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
#The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',

# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
#The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',

# Additional stuff for the LaTeX preamble.
#'preamble': '',
#Additional stuff for the LaTeX preamble.
#'preamble': '',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'django-session-security.tex', u'django-session-security Documentation',
u'James Pic', 'manual'),
('index', 'django-session-security.tex', u'django-session-security Documentation',
u'James Pic', 'manual'),
]

# The name of an image file (relative to this directory) to place at the top of
Expand Down Expand Up @@ -247,9 +249,9 @@
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'django-session-security', u'django-session-security Documentation',
u'James Pic', 'django-session-security', 'One line description of project.',
'Miscellaneous'),
('index', 'django-session-security', u'django-session-security Documentation',
u'James Pic', 'django-session-security', 'One line description of project.',
'Miscellaneous'),
]

# Documents to append as an appendix to all manuals.
Expand Down
5 changes: 3 additions & 2 deletions session_security/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from django.core.urlresolvers import reverse

from .utils import get_last_activity, set_last_activity
from .settings import EXPIRE_AFTER, PASSIVE_URLS
from .settings import PASSIVE_URLS, get_expire_after


class SessionSecurityMiddleware(object):
Expand All @@ -35,7 +35,8 @@ def process_request(self, request):
self.update_last_activity(request, now)

delta = now - get_last_activity(request.session)
if delta >= timedelta(seconds=EXPIRE_AFTER):

if delta >= timedelta(seconds=get_expire_after(request)):
logout(request)
elif request.path not in PASSIVE_URLS:
set_last_activity(request.session, now)
Expand Down
74 changes: 67 additions & 7 deletions session_security/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,30 @@
Settings for django-session-security.

WARN_AFTER
Time (in seconds) before the user should be warned that is session will
expire because of inactivity. Default 540. Overridable in
Time (in seconds) counting *forward* from when user session begins
until when user is warned that session will expire because of
inactivity. Default 540. Can be set in
``settings.SESSION_SECURITY_WARN_AFTER``.

WARN_BEFORE
Time (in seconds) counting *back* from session expiration when user
is warned that session will expire because of inactivity. Can be set
in ``settings.SESSION_SECURITY_WARN_BEFORE``.

EXPIRE_AFTER
Time (in seconds) before the user should be logged out if inactive. Default
is 600. Overridable in ``settings.SESSION_SECURITY_EXPIRE_AFTER``.
is 600. Can be set in ``settings.SESSION_SECURITY_EXPIRE_AFTER``.

EXPIRE_AFTER_CUSTOM_SESSION_KEY
Session key to set a custom EXPIRE_AFTER value. Can be set in
``settings.SESSION_SECURITY_CUSTOM_SESSION_KEY``
Use case: per-user EXPIRE_AFTER

PASSIVE_URLS
List of urls that should be ignored by the middleware. For example the ping
ajax request of session_security is made without user intervention, as such
it should not be used to update the user's last activity datetime.
Overridable in ``settings.SESSION_SECURITY_PASSIVE_URLS``.
it should not be used to update the user's last activity datetime. Can be
set in ``settings.SESSION_SECURITY_PASSIVE_URLS``.

Note that this module will raise a warning if
``settings.SESSION_EXPIRE_AT_BROWSER_CLOSE`` is not True, because it makes no
Expand All @@ -26,11 +37,22 @@
from django.core import urlresolvers
from django.conf import settings

__all__ = ['EXPIRE_AFTER', 'WARN_AFTER', 'PASSIVE_URLS']
__all__ = ['EXPIRE_AFTER', 'WARN_BEFORE', 'WARN_AFTER',
'PASSIVE_URLS', 'EXPIRE_AFTER_CUSTOM_SESSION_KEY']

EXPIRE_AFTER = getattr(settings, 'SESSION_SECURITY_EXPIRE_AFTER', 600)

WARN_AFTER = getattr(settings, 'SESSION_SECURITY_WARN_AFTER', 540)
EXPIRE_AFTER_CUSTOM_SESSION_KEY = getattr(
settings, 'SESSION_SECURITY_CUSTOM_SESSION_KEY', None)

WARN_BEFORE = getattr(
settings, 'SESSION_SECURITY_WARN_BEFORE', None)

WARN_AFTER = getattr(
settings,
'SESSION_SECURITY_WARN_AFTER',
(EXPIRE_AFTER - WARN_BEFORE if WARN_BEFORE else 540)
)

PASSIVE_URLS = getattr(settings, 'SESSION_SECURITY_PASSIVE_URLS', [])
PASSIVE_URLS += [
Expand All @@ -39,3 +61,41 @@

if not getattr(settings, 'SESSION_EXPIRE_AT_BROWSER_CLOSE', False):
warnings.warn('settings.SESSION_EXPIRE_AT_BROWSER_CLOSE is not True')


def get_expire_after(request):
"""
Calculate EXPIRE_AFTER value while accounting for
custom/user-defined value
"""

if EXPIRE_AFTER_CUSTOM_SESSION_KEY is None:
return EXPIRE_AFTER

expire_after_value = request.session.get(
EXPIRE_AFTER_CUSTOM_SESSION_KEY
)

if isinstance(expire_after_value, int) and expire_after_value > 0:
return expire_after_value
else:
return EXPIRE_AFTER


def get_warn_after(request):
"""
Calculate WARN_AFTER value while accounting for case
where EXPIRE_AFTER may be smaller
"""

expire_after_value = get_expire_after(request)
warn_after_value = WARN_AFTER

if WARN_BEFORE is not None:
warn_after_value = expire_after_value - WARN_BEFORE

if (warn_after_value < 0) or \
(expire_after_value - warn_after_value < 0):
warn_after_value = 1

return warn_after_value
7 changes: 4 additions & 3 deletions session_security/templatetags/session_security_tags.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from django import template

from session_security.settings import WARN_AFTER, EXPIRE_AFTER
from session_security.settings import (
get_expire_after, get_warn_after)

register = template.Library()


@register.filter
def expire_after(request):
return EXPIRE_AFTER
return get_expire_after(request)


@register.filter
def warn_after(request):
return WARN_AFTER
return get_warn_after(request)
2 changes: 1 addition & 1 deletion session_security/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .script import ScriptTestCase
from .views import ViewsTestCase
from .middleware import MiddlewareTestCase
from .middleware import MiddlewareTestCase, DynamicSessionLevelTestCase
6 changes: 3 additions & 3 deletions session_security/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ def do_admin_login(self, username, password):
self.browser.find_element_by_xpath('//input[@value="Log in"]').click()

def new_window(self, name='other'):
self.browser.execute_script('window.open("/admin/", "'+ name +'")')
self.browser.switch_to_window(self.browser.window_handles[1])
self.browser.execute_script('window.open("/admin/", "' + name + '")')
self.browser.switch_to.window(self.browser.window_handles[1])
while self.warning_element() is False:
time.sleep(0.1)
self.browser.switch_to_window(self.browser.window_handles[0])
self.browser.switch_to.window(self.browser.window_handles[0])

def press_space(self):
a = ActionChains(self.browser)
Expand Down
69 changes: 59 additions & 10 deletions session_security/tests/middleware.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import time
import unittest
from datetime import datetime, timedelta

from django.test import TestCase as DjangoTestCase
from django.test.client import Client

from session_security.utils import set_last_activity
from datetime import datetime, timedelta
from session_security import settings


class MiddlewareTestCase(unittest.TestCase):
Expand All @@ -12,36 +15,82 @@ def setUp(self):

def test_auto_logout(self):
self.client.login(username='test', password='test')
response = self.client.get('/admin/')
self.client.get('/admin/')
self.assertTrue('_auth_user_id' in self.client.session)
time.sleep(12)
response = self.client.get('/admin/')
self.client.get('/admin/')
self.assertFalse('_auth_user_id' in self.client.session)

def test_last_activity_in_future(self):
self.client.login(username='test', password='test')
now = datetime.now()
future = now + timedelta(0, 30)
set_last_activity(self.client.session, future)
response = self.client.get('/admin/')
self.client.get('/admin/')
self.assertTrue('_auth_user_id' in self.client.session)

def test_non_javascript_browse_no_logout(self):
self.client.login(username='test', password='test')
response = self.client.get('/admin/')
self.client.get('/admin/')
time.sleep(8)
response = self.client.get('/admin/')
self.client.get('/admin/')
self.assertTrue('_auth_user_id' in self.client.session)
time.sleep(4)
response = self.client.get('/admin/')
self.client.get('/admin/')
self.assertTrue('_auth_user_id' in self.client.session)

def test_javascript_activity_no_logout(self):
self.client.login(username='test', password='test')
response = self.client.get('/admin/')
self.client.get('/admin/')
time.sleep(8)
response = self.client.get('/session_security/ping/?idleFor=1')
self.client.get('/session_security/ping/?idleFor=1')
self.assertTrue('_auth_user_id' in self.client.session)
time.sleep(4)
self.client.get('/admin/')
self.assertTrue('_auth_user_id' in self.client.session)


class DynamicSessionLevelTestCase(DjangoTestCase):
def monkey_patch(self, saved_settings=None):
values = {'EXPIRE_AFTER': 3,
'WARN_BEFORE': 2,
'WARN_AFTER': 1,
'EXPIRE_AFTER_CUSTOM_SESSION_KEY': 'user-session-key'}
old_settings = {}
for k, v in values.items():
old_settings[k] = getattr(settings, k, None)
setattr(
settings, k,
v if saved_settings is None else saved_settings.get(k))
return old_settings

def setUp(self):
self.client = Client()
self.client.login(username='test', password='test')
self.saved_settings = self.monkey_patch()

def tearDown(self):
self.monkey_patch(saved_settings=self.saved_settings)

def set_custom_expire_after_value(self, value):
s = self.client.session
s['user-auto-logout'] = value
s.save()

def test_global_session_value_logout(self):
self.client.get('/admin/')
self.assertTrue('_auth_user_id' in self.client.session)
time.sleep(4)
response = self.client.get('/admin/')
self.client.get('/admin/')
self.assertFalse('_auth_user_id' in self.client.session)

def test_dynamic_session_value_logout(self):
self.set_custom_expire_after_value(2)

self.client.get('/admin/')
self.assertTrue('_auth_user_id' in self.client.session)
self.assertTrue('user-auto-logout' in self.client.session)
self.assertEqual(self.client.session.get('user-auto-logout'), 2)
time.sleep(3)
self.client.get('/admin/')
self.assertFalse('_auth_user_id' in self.client.session)
Loading