Эх сурвалжийг харах

HUE-6238 [useradmin] Option to disable concurrent user sessions

jdesjean 8 жил өмнө
parent
commit
ac7e88e

+ 23 - 1
apps/useradmin/src/useradmin/middleware.py

@@ -20,11 +20,13 @@ from datetime import datetime
 
 from django.contrib import messages
 from django.contrib.auth.models import User
+from django.contrib.sessions.models import Session
 from django.db import DatabaseError
+from django.db.models import Q
 from django.utils.translation import ugettext as _
 
 from desktop.auth.views import dt_logout
-from desktop.conf import AUTH, LDAP
+from desktop.conf import AUTH, LDAP, SESSION
 
 from models import UserProfile, get_profile
 from views import import_ldap_users
@@ -101,3 +103,23 @@ class LastActivityMiddleware(object):
       return dt.total_seconds()
     else:
       return (dt.microseconds + (dt.seconds + dt.days * 24 * 3600) * 10**6) / 10**6
+
+class ConcurrentUserSessionMiddleware(object):
+  """
+  Middleware that remove concurrent user sessions when configured
+  """
+  def process_response(self, request, response):
+    if request.user.is_authenticated() and request.session.modified and request.user.id:
+      limit = SESSION.CONCURRENT_USER_SESSION_LIMIT.get()
+      if limit:
+        count = 1;
+        for session in Session.objects.filter(~Q(session_key=request.session.session_key), expire_date__gte=datetime.now()).order_by('-expire_date'):
+          data = session.get_decoded()
+          if data.get('_auth_user_id') == request.user.id:
+            if count >= limit:
+              LOG.info('Expiring concurrent user session %s' % request.user.username)
+              session.expire_date = datetime.now()
+              session.save()
+            else:
+              count += 1
+    return response

+ 54 - 1
apps/useradmin/src/useradmin/tests.py

@@ -25,8 +25,10 @@ import urllib
 
 from nose.plugins.skip import SkipTest
 from nose.tools import assert_true, assert_equal, assert_false, assert_not_equal
-
+from datetime import datetime
 from django.contrib.auth.models import User, Group
+from django.contrib.sessions.models import Session
+from django.db.models import Q
 from django.utils.encoding import smart_unicode
 from django.core.urlresolvers import reverse
 from django.test.client import Client
@@ -41,6 +43,7 @@ from hadoop.pseudo_hdfs4 import is_live_cluster
 import useradmin.conf
 from useradmin.forms import UserChangeForm
 import useradmin.ldap_access
+from useradmin.middleware import ConcurrentUserSessionMiddleware
 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
@@ -960,3 +963,53 @@ class LastActivityMiddlewareTests(object):
     finally:
       for f in reset:
         f()
+
+class MockRequest(dict):
+  pass
+
+class MockUser(dict):
+  def is_authenticated(self):
+    return True
+
+class MockSession(dict):
+  pass
+
+class ConcurrentUserSessionMiddlewareTests(object):
+  def setUp(self):
+    self.cm = ConcurrentUserSessionMiddleware()
+    self.reset = desktop.conf.SESSION.CONCURRENT_USER_SESSION_LIMIT.set_for_testing(1)
+
+  def tearDown(self):
+    self.reset()
+
+  def test_concurrent_session_logout(self):
+    c = make_logged_in_client(username="test_concurr", groupname="test_concurr", recreate=True, is_superuser=True)
+    session = MockSession()
+    session.session_key = c.session.session_key
+    session.modified = True
+    user = MockUser()
+    user.id = c.session.get('_auth_user_id')
+    user.username = 'test_concurr'
+    request = MockRequest()
+    request.user = user
+    request.session = session
+    response = MockRequest()
+
+    # Call middleware with test_concurr on session 1
+    self.cm.process_response(request, response)
+
+    c2 = make_logged_in_client(username="test_concurr", groupname="test_concurr", is_superuser=True)
+    session.session_key = c2.session.session_key
+    request.session = session
+
+    # Call middleware with test_concurr on session 2
+    self.cm.process_response(request, response)
+
+    now = datetime.now()
+    # Session 1 is expired
+    assert_true(list(Session.objects.filter(Q(session_key=c.session.session_key)))[0].expire_date <= now)
+    assert_equal(302, c.get('/editor', follow=False).status_code) # Redirect to login page
+
+    # Session 2 is still active
+    assert_true(list(Session.objects.filter(Q(session_key=c2.session.session_key)))[0].expire_date > now)
+    assert_equal(200, c2.get('/editor', follow=True).status_code)

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

@@ -623,6 +623,8 @@
     # Use session-length cookies. Logs out the user when she closes the browser window.
     ## expire_at_browser_close=false
 
+    # If set, limits the number of concurrent user sessions. 1 represents 1 session per user. Default: 0 (unlimited sessions per user)
+    ## concurrent_user_session_limit=0
 
   # Configuration options for connecting to an external SMTP server
   # ------------------------------------------------------------------------

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

@@ -625,6 +625,8 @@
     # Use session-length cookies. Logs out the user when she closes the browser window.
     ## expire_at_browser_close=false
 
+    # If set, limits the number of concurrent user sessions. 1 represents 1 session per user. Default: 0 (unlimited sessions per user)
+    ## concurrent_user_session_limit=0
 
   # Configuration options for connecting to an external SMTP server
   # ------------------------------------------------------------------------

+ 7 - 1
desktop/core/src/desktop/conf.py

@@ -732,6 +732,12 @@ SESSION = ConfigSection(
       help=_("Use session-length cookies. Logs out the user when she closes the browser window."),
       type=coerce_bool,
       default=False
+    ),
+    CONCURRENT_USER_SESSION_LIMIT = Config(
+      key="concurrent_user_session_limit",
+      help=_("If set, limits the number of concurrent user sessions. 1 represents 1 session per user. Default: 0 (unlimited sessions per user)"),
+      type=int,
+      default=0,
     )
   )
 )
@@ -957,7 +963,7 @@ AUTH = ConfigSection(
       help=_("If behind_reverse_proxy is True, it will look for the IP address from this header. Default: HTTP_X_FORWARDED_FOR"),
       type=str,
       default="HTTP_X_FORWARDED_FOR",
-    ),
+    )
 ))
 
 

+ 3 - 0
desktop/core/src/desktop/settings.py

@@ -454,6 +454,9 @@ if desktop.conf.SECURE_PROXY_SSL_HEADER.get():
 if 'useradmin' in [app.name for app in appmanager.DESKTOP_APPS]:
   MIDDLEWARE_CLASSES.append('useradmin.middleware.LastActivityMiddleware')
 
+if desktop.conf.SESSION.CONCURRENT_USER_SESSION_LIMIT.get():
+  MIDDLEWARE_CLASSES.append('useradmin.middleware.ConcurrentUserSessionMiddleware')
+
 LOAD_BALANCER_COOKIE = 'ROUTEID'
 
 ################################################################