diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py index 7ac99e9a64..691239903e 100644 --- a/cms/djangoapps/contentstore/tests/tests.py +++ b/cms/djangoapps/contentstore/tests/tests.py @@ -1,6 +1,12 @@ +""" +This test file will test registration, login, activation, and session activity timeouts +""" +import time + from django.test.utils import override_settings from django.core.cache import cache from django.core.urlresolvers import reverse +from django.conf import settings from contentstore.tests.utils import parse_json, user, registration, AjaxEnabledTestClient from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase @@ -188,6 +194,29 @@ class AuthTestCase(ContentStoreTestCase): # Logged in should work. + @override_settings(SESSION_INACTIVITY_TIMEOUT_IN_SECONDS=1) + def test_inactive_session_timeout(self): + """ + Verify that an inactive session times out and redirects to the + login page + """ + self.create_account(self.username, self.email, self.pw) + self.activate_user(self.email) + + self.login(self.email, self.pw) + + # make sure we can access courseware immediately + resp = self.client.get_html('/course') + self.assertEquals(resp.status_code, 200) + + # then wait a bit and see if we get timed out + time.sleep(2) + + resp = self.client.get_html('/course') + + # re-request, and we should get a redirect to login page + self.assertRedirects(resp, settings.LOGIN_REDIRECT_URL + '?next=/course') + class ForumTestCase(CourseTestCase): def setUp(self): diff --git a/cms/envs/common.py b/cms/envs/common.py index 62e52d2eb5..c96b171959 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -174,6 +174,9 @@ MIDDLEWARE_CLASSES = ( # catches any uncaught RateLimitExceptions and returns a 403 instead of a 500 'ratelimitbackend.middleware.RateLimitMiddleware', + + # for expiring inactive sessions + 'session_inactivity_timeout.middleware.SessionInactivityTimeout', ) ############# XBlock Configuration ########## diff --git a/common/djangoapps/session_inactivity_timeout/__init__.py b/common/djangoapps/session_inactivity_timeout/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/session_inactivity_timeout/middleware.py b/common/djangoapps/session_inactivity_timeout/middleware.py new file mode 100644 index 0000000000..1473d3cef8 --- /dev/null +++ b/common/djangoapps/session_inactivity_timeout/middleware.py @@ -0,0 +1,53 @@ +""" +Middleware to auto-expire inactive sessions after N seconds, which is configurable in +settings. + +To enable this feature, set in a settings.py: + + SESSION_INACTIVITY_TIMEOUT_IN_SECS = 300 + +This was taken from StackOverflow (http://stackoverflow.com/questions/14830669/how-to-expire-django-session-in-5minutes) +""" +from datetime import datetime, timedelta +from django.conf import settings +from django.contrib import auth + +LAST_TOUCH_KEYNAME = 'SessionInactivityTimeout:last_touch' + + +class SessionInactivityTimeout(object): + """ + Middleware class to keep track of activity on a given session + """ + def process_request(self, request): + """ + Standard entry point for processing requests in Django + """ + if not hasattr(request, "user") or not request.user.is_authenticated(): + #Can't log out if not logged in + return + + timeout_in_seconds = getattr(settings, "SESSION_INACTIVITY_TIMEOUT_IN_SECONDS", None) + + # Do we have this feature enabled? + if timeout_in_seconds: + # what time is it now? + utc_now = datetime.utcnow() + + # Get the last time user made a request to server, which is stored in session data + last_touch = request.session.get(LAST_TOUCH_KEYNAME) + + # have we stored a 'last visited' in session? NOTE: first time access after login + # this key will not be present in the session data + if last_touch: + # compute the delta since last time user came to the server + time_since_last_activity = utc_now - last_touch + + # did we exceed the timeout limit? + if time_since_last_activity > timedelta(seconds=timeout_in_seconds): + # yes? Then log the user out + del request.session[LAST_TOUCH_KEYNAME] + auth.logout(request) + return + + request.session[LAST_TOUCH_KEYNAME] = utc_now diff --git a/lms/djangoapps/courseware/tests/test_navigation.py b/lms/djangoapps/courseware/tests/test_navigation.py index 2b416b16de..d9aaa8d9aa 100644 --- a/lms/djangoapps/courseware/tests/test_navigation.py +++ b/lms/djangoapps/courseware/tests/test_navigation.py @@ -1,3 +1,9 @@ +""" +This test file will run through some LMS test scenarios regarding access and navigation of the LMS +""" +import time +from django.conf import settings + from django.core.urlresolvers import reverse from django.test.utils import override_settings @@ -37,6 +43,28 @@ class TestNavigation(ModuleStoreTestCase, LoginEnrollmentTestCase): self.create_account(username, email, password) self.activate_user(email) + @override_settings(SESSION_INACTIVITY_TIMEOUT_IN_SECONDS=1) + def test_inactive_session_timeout(self): + """ + Verify that an inactive session times out and redirects to the + login page + """ + email, password = self.STUDENT_INFO[0] + self.login(email, password) + + # make sure we can access courseware immediately + resp = self.client.get(reverse('dashboard')) + self.assertEquals(resp.status_code, 200) + + # then wait a bit and see if we get timed out + time.sleep(2) + + resp = self.client.get(reverse('dashboard')) + + # re-request, and we should get a redirect to login page + self.assertRedirects(resp, settings.LOGIN_REDIRECT_URL + '?next=' + reverse('dashboard')) + + def test_redirects_first_time(self): """ Verify that the first time we click on the courseware tab we are diff --git a/lms/envs/common.py b/lms/envs/common.py index 44ccb1771e..36d7980684 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -654,6 +654,9 @@ MIDDLEWARE_CLASSES = ( # For A/B testing 'waffle.middleware.WaffleMiddleware', + + # for expiring inactive sessions + 'session_inactivity_timeout.middleware.SessionInactivityTimeout', ) ############################### Pipeline #######################################