From 906fd3fb15c0961e48240994984ecce5d3ffcd5a Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Mon, 29 Jun 2026 17:18:13 -0400 Subject: [PATCH 1/2] feat: allow exempting service accounts from PREVENT_CONCURRENT_LOGINS --- common/djangoapps/student/models/user.py | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/common/djangoapps/student/models/user.py b/common/djangoapps/student/models/user.py index 4789f0e47c38..e3db6cdd572e 100644 --- a/common/djangoapps/student/models/user.py +++ b/common/djangoapps/student/models/user.py @@ -1338,6 +1338,31 @@ def log_successful_logout(sender, request, user, **kwargs): # pylint: disable=u segment.track(request.user.id, 'edx.bi.user.account.logout') +def _is_single_login_exempt(user): + """ + Return True if ``user`` is exempt from single-login enforcement. + + ``PREVENT_CONCURRENT_LOGINS`` keeps a single active session per user and + deletes the previously registered session on each login. That is correct for + human accounts, but it breaks shared service/automation accounts (for + example the xqueue-watcher grader account) whose many concurrent workers all + authenticate as one user and would otherwise continually evict each other's + sessions. + + Exemptions are opt-in and default to empty, so behaviour is unchanged unless + configured: + + * ``SINGLE_LOGIN_EXEMPT_USERNAMES`` -- iterable of exact usernames. + * ``SINGLE_LOGIN_EXEMPT_GROUPS`` -- iterable of group names; a user in any of + these groups is exempt. + """ + exempt_usernames = getattr(settings, 'SINGLE_LOGIN_EXEMPT_USERNAMES', None) or () + if user.username in exempt_usernames: + return True + exempt_groups = getattr(settings, 'SINGLE_LOGIN_EXEMPT_GROUPS', None) or () + return bool(exempt_groups) and user.groups.filter(name__in=exempt_groups).exists() + + @receiver(user_logged_in) @receiver(user_logged_out) def enforce_single_login(sender, request, user, signal, **kwargs): # pylint: disable=unused-argument @@ -1346,6 +1371,9 @@ def enforce_single_login(sender, request, user, signal, **kwargs): # pylint: di to prevent concurrent logins. """ if settings.FEATURES.get('PREVENT_CONCURRENT_LOGINS', False): + if user and _is_single_login_exempt(user): + # Shared service/automation accounts may hold concurrent sessions. + return if signal == user_logged_in: key = request.session.session_key else: From b652f696560c8311f121aa63809744f360dd99dc Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Mon, 29 Jun 2026 17:18:16 -0400 Subject: [PATCH 2/2] test: cover SINGLE_LOGIN_EXEMPT_USERNAMES exemption --- .../user_authn/views/tests/test_login.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_login.py b/openedx/core/djangoapps/user_authn/views/tests/test_login.py index 4e4d0a9e8894..4a1c443c9ec1 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_login.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_login.py @@ -661,6 +661,30 @@ def test_single_session(self): # client1 will be logged out assert response.status_code == 302 + @patch.dict("django.conf.settings.FEATURES", {'PREVENT_CONCURRENT_LOGINS': True}) + def test_single_session_exempt_user(self): + """ + A user whose username is in SINGLE_LOGIN_EXEMPT_USERNAMES is not subject + to single-login enforcement: a concurrent login does not record the + single-session slot and therefore does not evict the first session. + """ + creds = {'email': self.user_email, 'password': self.password} + client1 = Client() + client2 = Client() + + with override_settings(SINGLE_LOGIN_EXEMPT_USERNAMES=[self.user.username]): + response = client1.post(self.url, creds) + self._assert_response(response, success=True) + + # A second login must NOT evict the exempt user's first session. + response = client2.post(self.url, creds) + self._assert_response(response, success=True) + + self.user = User.objects.get(pk=self.user.pk) + # No single-session slot is recorded for exempt users, so neither + # session is ever deleted. + assert 'session_id' not in self.user.profile.get_meta() + @patch.dict("django.conf.settings.FEATURES", {'PREVENT_CONCURRENT_LOGINS': True}) def test_single_session_with_no_user_profile(self): """