瀏覽代碼

[useradmin] Add a session timeout that will automatically log out idle users

Jenny Kim 9 年之前
父節點
當前提交
6a956857a8

+ 25 - 9
apps/useradmin/src/useradmin/middleware.py

@@ -18,9 +18,13 @@
 import logging
 from datetime import datetime
 
+from django.contrib import messages
 from django.contrib.auth.models import User
+from django.db import DatabaseError
+from django.utils.translation import ugettext as _
 
-from desktop.conf import LDAP
+from desktop.auth.views import dt_logout
+from desktop.conf import AUTH, LDAP
 
 from models import UserProfile, get_profile
 from views import import_ldap_users
@@ -60,9 +64,9 @@ class LdapSynchronizationMiddleware(object):
       request.session.modified = True
 
 
-class UpdateLastActivityMiddleware(object):
+class LastActivityMiddleware(object):
   """
-  Middleware to track the last activity of a user.
+  Middleware to track the last activity of a user and automatically log out the user after a specified period of inactivity
   """
 
   def process_request(self, request):
@@ -72,9 +76,21 @@ class UpdateLastActivityMiddleware(object):
       return
 
     profile = get_profile(user)
-    profile.last_activity = datetime.now()
-
-    try:
-      profile.save()
-    except DatabaseError:
-      log.exception('Error saving profile information')
+    expires_after = AUTH.IDLE_SESSION_TIMEOUT.get()
+    now = datetime.now()
+    logout = False
+
+    if profile.last_activity and expires_after > 0 and (now - profile.last_activity).total_seconds() > expires_after:
+      messages.info(request, _('Your session has been timed out due to inactivity.'))
+      logout = True
+
+    # Save last activity for user except when polling jobbrowser
+    if not (request.path.strip('/') == 'jobbrowser' and request.GET.get('format') == 'json'):
+      try:
+        profile.last_activity = datetime.now()
+        profile.save()
+      except DatabaseError:
+        LOG.exception('Error saving profile information')
+
+    if logout:
+      dt_logout(request, next_page='/')

+ 1 - 0
apps/useradmin/src/useradmin/models.py

@@ -162,6 +162,7 @@ def group_permissions(group):
 def create_profile_for_user(user):
   p = UserProfile()
   p.user = user
+  p.last_activity = datetime.now()
   p.home_directory = "/user/%s" % p.user.username
   try:
     p.save()

+ 63 - 11
apps/useradmin/src/useradmin/tests.py

@@ -20,25 +20,26 @@ import json
 import ldap
 import re
 import sys
+import time
 import urllib
 
-from nose.plugins.attrib import attr
 from nose.plugins.skip import SkipTest
 from nose.tools import assert_true, assert_equal, assert_false, assert_not_equal
 
-import desktop.conf
-from desktop.lib.django_test_util import make_logged_in_client
 from django.contrib.auth.models import User, Group
 from django.utils.encoding import smart_unicode
 from django.core.urlresolvers import reverse
 from django.test.client import Client
 
-from useradmin.models import HuePermission, GroupPermission, UserProfile
-from useradmin.models import get_profile, get_default_user_group
+import desktop.conf
+from desktop.lib.django_test_util import make_logged_in_client
+from desktop.views import home
+from hadoop import pseudo_hdfs4
 
 import useradmin.conf
 import useradmin.ldap_access
-from hadoop import pseudo_hdfs4
+from useradmin.models import HuePermission, GroupPermission, UserProfile
+from useradmin.models import get_profile, get_default_user_group
 from useradmin.password_policy import reset_password_policy
 
 
@@ -47,6 +48,7 @@ def reset_all_users():
   for user in User.objects.all():
     user.delete()
 
+
 def reset_all_groups():
   """Reset to a clean state by deleting all groups"""
 
@@ -222,6 +224,7 @@ def test_invalid_username():
 
 
 class BaseUserAdminTests(object):
+
   @classmethod
   def setUpClass(cls):
     cls._class_resets = [
@@ -242,6 +245,7 @@ class BaseUserAdminTests(object):
 
 
 class TestUserAdmin(BaseUserAdminTests):
+
   def test_group_permissions(self):
     # Get ourselves set up with a user and a group
     c = make_logged_in_client(username="test", is_superuser=True)
@@ -294,6 +298,7 @@ class TestUserAdmin(BaseUserAdminTests):
     response = c1.get('/useradmin/users')
     assert_true('You do not have permission to access the Useradmin application.' in response.content)
 
+
   def test_default_group(self):
     resets = [
       useradmin.conf.DEFAULT_USER_GROUP.set_for_testing('test_default')
@@ -390,6 +395,7 @@ class TestUserAdmin(BaseUserAdminTests):
     response = c.post('/useradmin/groups/new', dict(name="with space"))
     assert_equal(len(Group.objects.all()), group_count + 1)
 
+
   def test_user_admin_password_policy(self):
     # Set up password policy
     password_hint = password_error_msg = ("The password must be at least 8 characters long, "
@@ -684,7 +690,6 @@ class TestUserAdmin(BaseUserAdminTests):
         reset()
 
 
-
   def test_list_for_autocomplete(self):
     # Now the autocomplete has access to all the users and groups
     c1 = make_logged_in_client('test_list_for_autocomplete', is_superuser=False, groupname='test_list_for_autocomplete')
@@ -726,6 +731,7 @@ class TestUserAdmin(BaseUserAdminTests):
 
 
 class TestUserAdminWithHadoop(BaseUserAdminTests):
+
   requires_hadoop = True
 
   def test_ensure_home_directory(self):
@@ -766,6 +772,7 @@ class TestUserAdminWithHadoop(BaseUserAdminTests):
       for reset in resets:
         reset()
 
+
 class MockLdapConnection(object):
   def __init__(self, ldap_config, ldap_url, username, password, ldap_cert):
     self.ldap_config = ldap_config
@@ -774,6 +781,7 @@ class MockLdapConnection(object):
     self.password = password
     self.ldap_cert = ldap_cert
 
+
 def test_get_connection_bind_password():
   # Unfortunately our tests leak a cached test ldap connection across functions, so we need to clear it out.
   useradmin.ldap_access.CACHED_LDAP_CONN = None
@@ -803,6 +811,7 @@ def test_get_connection_bind_password():
     for f in reset:
       f()
 
+
 def test_get_connection_bind_password_script():
   # Unfortunately our tests leak a cached test ldap connection across functions, so we need to clear it out.
   useradmin.ldap_access.CACHED_LDAP_CONN = None
@@ -837,7 +846,50 @@ def test_get_connection_bind_password_script():
     for f in reset:
       f()
 
-def test_last_activity():
-  c = make_logged_in_client(username="test", is_superuser=True)
-  profile = UserProfile.objects.get(user__username='test')
-  assert_not_equal(profile.last_activity, 0)
+
+class BaseUserAdminTests(object):
+
+  def test_last_activity(self):
+    c = make_logged_in_client(username="test", is_superuser=True)
+    profile = UserProfile.objects.get(user__username='test')
+    assert_not_equal(profile.last_activity, 0)
+
+
+  def test_idle_timeout(self):
+    timeout = 5
+    reset = [
+      desktop.conf.AUTH.IDLE_SESSION_TIMEOUT.set_for_testing(timeout)
+    ]
+    try:
+      c = make_logged_in_client(username="test", is_superuser=True)
+      response = c.get(reverse(home))
+      assert_equal(200, response.status_code)
+
+      # Assert after timeout that user is redirected to login
+      time.sleep(timeout)
+      response = c.get(reverse(home))
+      assert_equal(302, response.status_code)
+    finally:
+      for f in reset:
+        f()
+
+  def test_ignore_jobbrowser_polling(self):
+    timeout = 5
+    reset = [
+      desktop.conf.AUTH.IDLE_SESSION_TIMEOUT.set_for_testing(timeout)
+    ]
+    try:
+      c = make_logged_in_client(username="test", is_superuser=True)
+      response = c.get(reverse(home))
+      assert_equal(200, response.status_code)
+
+      # Assert that jobbrowser polling does not reset idle time
+      time.sleep(2)
+      c.get('jobbrowser/?format=json&state=running&user=%s' % "test")
+      time.sleep(3)
+
+      response = c.get(reverse(home))
+      assert_equal(302, response.status_code)
+    finally:
+      for f in reset:
+        f()

+ 4 - 0
desktop/conf.dist/hue.ini

@@ -267,6 +267,10 @@
     # Apply 'expires_after' to superusers.
     ## expire_superusers=true
 
+    # Users will automatically be logged out after 'n' seconds of inactivity.
+    # A negative number means that idle sessions will not be timed out.
+    idle_session_timeout=-1
+
     # Force users to change password on first login with desktop.auth.backend.AllowFirstUserDjangoBackend
     ## change_default_password=false
 

+ 4 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -271,6 +271,10 @@
     # Apply 'expires_after' to superusers.
     ## expire_superusers=true
 
+    # Users will automatically be logged out after 'n' seconds of inactivity.
+    # A negative number means that idle sessions will not be timed out.
+    idle_session_timeout=-1
+
     # Force users to change password on first login with desktop.auth.backend.AllowFirstUserDjangoBackend
     ## change_default_password=false
 

+ 7 - 4
desktop/core/src/desktop/auth/views.py

@@ -24,9 +24,10 @@ except:
 import cgi
 import logging
 import urllib
+from datetime import datetime
 
-import django.contrib.auth.views
 from axes.decorators import watch_login
+import django.contrib.auth.views
 from django.core import urlresolvers
 from django.core.exceptions import SuspiciousOperation
 from django.contrib.auth import login, get_backends, authenticate
@@ -34,9 +35,6 @@ from django.contrib.auth.models import User
 from django.contrib.sessions.models import Session
 from django.http import HttpResponseRedirect
 from django.utils.translation import ugettext as _
-from hadoop.fs.exceptions import WebHdfsException
-from useradmin.models import get_profile
-from useradmin.views import ensure_home_directory, require_change_password
 
 from desktop.auth import forms as auth_forms
 from desktop.lib.django_util import render
@@ -45,6 +43,10 @@ from desktop.lib.django_util import JsonResponse
 from desktop.log.access import access_warn, last_access_map
 from desktop.conf import LDAP, OAUTH, DEMO_ENABLED
 
+from hadoop.fs.exceptions import WebHdfsException
+from useradmin.models import get_profile
+from useradmin.views import ensure_home_directory, require_change_password
+
 
 LOG = logging.getLogger(__name__)
 
@@ -133,6 +135,7 @@ def dt_login(request, from_modal=False):
           return HttpResponseRedirect(urlresolvers.reverse('useradmin.views.edit_user', kwargs={'username': user.username}))
 
         userprofile.first_login = False
+        userprofile.last_activity = datetime.now()
         userprofile.save()
 
         msg = 'Successful login for user: %s' % user.username

+ 5 - 0
desktop/core/src/desktop/conf.py

@@ -689,6 +689,11 @@ AUTH = ConfigSection(
                                 help=_("Apply 'expires_after' to superusers."),
                                 type=coerce_bool,
                                 default=True),
+    IDLE_SESSION_TIMEOUT = Config("idle_session_timeout",
+                            help=_("Users will automatically be logged out after 'n' seconds of inactivity."
+                                   "A negative number means that idle sessions will not be timed out."),
+                            type=int,
+                            default=-1),
     CHANGE_DEFAULT_PASSWORD = Config(
                             key="change_default_password",
                             help=_("When set to true this will allow you to specify a password for "

+ 2 - 2
desktop/core/src/desktop/settings.py

@@ -422,9 +422,9 @@ USE_X_FORWARDED_HOST = desktop.conf.USE_X_FORWARDED_HOST.get()
 if desktop.conf.SECURE_PROXY_SSL_HEADER.get():
   SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https')
 
-# Add last activity tracking.
+# Add last activity tracking and idle session timeout
 if 'useradmin' in [app.name for app in appmanager.DESKTOP_APPS]:
-  MIDDLEWARE_CLASSES.append('useradmin.middleware.UpdateLastActivityMiddleware')
+  MIDDLEWARE_CLASSES.append('useradmin.middleware.LastActivityMiddleware')
 
 ############################################################