Merge pull request #2793 from edx/dcs/mat-17
Add an option to prevent multiple logins of the same user.
This commit is contained in:
@@ -91,6 +91,9 @@ FEATURES = {
|
||||
|
||||
# Allow creating courses with non-ascii characters in the course id
|
||||
'ALLOW_UNICODE_COURSE_ID': False,
|
||||
|
||||
# Prevent concurrent logins per user
|
||||
'PREVENT_CONCURRENT_LOGINS': False,
|
||||
}
|
||||
ENABLE_JASMINE = False
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ from django_countries import CountryField
|
||||
from track import contexts
|
||||
from track.views import server_track
|
||||
from eventtracking import tracker
|
||||
from importlib import import_module
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
import lms.lib.comment_client as cc
|
||||
@@ -42,6 +43,7 @@ from util.query import use_read_replica_if_available
|
||||
unenroll_done = Signal(providing_args=["course_enrollment"])
|
||||
log = logging.getLogger(__name__)
|
||||
AUDIT_LOG = logging.getLogger("audit")
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
class AnonymousUserId(models.Model):
|
||||
@@ -239,6 +241,20 @@ class UserProfile(models.Model):
|
||||
def set_meta(self, js):
|
||||
self.meta = json.dumps(js)
|
||||
|
||||
def set_login_session(self, session_id=None):
|
||||
"""
|
||||
Sets the current session id for the logged-in user.
|
||||
If session_id doesn't match the existing session,
|
||||
deletes the old session object.
|
||||
"""
|
||||
meta = self.get_meta()
|
||||
old_login = meta.get('session_id', None)
|
||||
if old_login:
|
||||
SessionStore(session_key=old_login).delete()
|
||||
meta['session_id'] = session_id
|
||||
self.set_meta(meta)
|
||||
self.save()
|
||||
|
||||
|
||||
def unique_id_for_user(user):
|
||||
"""
|
||||
@@ -292,7 +308,6 @@ class PendingEmailChange(models.Model):
|
||||
activation_key = models.CharField(('activation key'), max_length=32, unique=True, db_index=True)
|
||||
|
||||
|
||||
|
||||
EVENT_NAME_ENROLLMENT_ACTIVATED = 'edx.course.enrollment.activated'
|
||||
EVENT_NAME_ENROLLMENT_DEACTIVATED = 'edx.course.enrollment.deactivated'
|
||||
|
||||
@@ -383,7 +398,6 @@ class CourseEnrollment(models.Model):
|
||||
# list of possible values.
|
||||
mode = models.CharField(default="honor", max_length=100)
|
||||
|
||||
|
||||
class Meta:
|
||||
unique_together = (('user', 'course_id'),)
|
||||
ordering = ('user', 'course_id')
|
||||
@@ -866,3 +880,18 @@ def log_successful_logout(sender, request, user, **kwargs):
|
||||
AUDIT_LOG.info(u"Logout - user.id: {0}".format(request.user.id))
|
||||
else:
|
||||
AUDIT_LOG.info(u"Logout - {0}".format(request.user))
|
||||
|
||||
|
||||
@receiver(user_logged_in)
|
||||
@receiver(user_logged_out)
|
||||
def enforce_single_login(sender, request, user, signal, **kwargs):
|
||||
"""
|
||||
Sets the current session id in the user profile,
|
||||
to prevent concurrent logins.
|
||||
"""
|
||||
if settings.FEATURES.get('PREVENT_CONCURRENT_LOGINS', False):
|
||||
if signal == user_logged_in:
|
||||
key = request.session.session_key
|
||||
else:
|
||||
key = None
|
||||
user.profile.set_login_session(key)
|
||||
|
||||
@@ -179,6 +179,32 @@ class LoginTest(TestCase):
|
||||
response, _audit_log = self._login_response('test@edx.org', 'wrong_password')
|
||||
self._assert_response(response, success=False, value='Too many failed login attempts')
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'PREVENT_CONCURRENT_LOGINS': True})
|
||||
def test_single_session(self):
|
||||
creds = {'email': 'test@edx.org', 'password': 'test_password'}
|
||||
client1 = Client()
|
||||
client2 = Client()
|
||||
|
||||
response = client1.post(self.url, creds)
|
||||
self._assert_response(response, success=True)
|
||||
|
||||
self.assertEqual(self.user.profile.get_meta()['session_id'], self.client.session.session_key)
|
||||
|
||||
# second login should log out the first
|
||||
response = client2.post(self.url, creds)
|
||||
self._assert_response(response, success=True)
|
||||
|
||||
try:
|
||||
# this test can be run with either lms or studio settings
|
||||
# since studio does not have a dashboard url, we should
|
||||
# look for another url that is login_required, in that case
|
||||
url = reverse('dashboard')
|
||||
except NoReverseMatch:
|
||||
url = reverse('upload_transcripts')
|
||||
response = client1.get(url)
|
||||
# client1 will be logged out
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def _login_response(self, email, password, patched_audit_log='student.views.AUDIT_LOG'):
|
||||
''' Post the login info '''
|
||||
post_params = {'email': email, 'password': password}
|
||||
|
||||
@@ -239,6 +239,9 @@ FEATURES = {
|
||||
|
||||
# Toggle to enable alternate urls for marketing links
|
||||
'ENABLE_MKTG_SITE': False,
|
||||
|
||||
# Prevent concurrent logins per user
|
||||
'PREVENT_CONCURRENT_LOGINS': False,
|
||||
}
|
||||
|
||||
# Used for A/B testing
|
||||
|
||||
Reference in New Issue
Block a user