This is an automated email from the ASF dual-hosted git repository.

brondsem pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/allura.git


The following commit(s) were added to refs/heads/master by this push:
     new 965263a68 Track session ids on user records
965263a68 is described below

commit 965263a68d33a2cae73c92f11244fe7010d93de2
Author: Carlos Cruz <[email protected]>
AuthorDate: Fri Feb 28 19:46:08 2025 +0000

    Track session ids on user records
---
 Allura/allura/lib/plugin.py                 | 13 ++++++
 Allura/allura/model/auth.py                 | 20 +++++++++
 Allura/allura/tests/functional/test_auth.py | 66 +++++++++++++++++++++++++++++
 Allura/development.ini                      |  5 ++-
 Allura/test.ini                             |  3 ++
 5 files changed, 106 insertions(+), 1 deletion(-)

diff --git a/Allura/allura/lib/plugin.py b/Allura/allura/lib/plugin.py
index 036afa276..cbbb54404 100644
--- a/Allura/allura/lib/plugin.py
+++ b/Allura/allura/lib/plugin.py
@@ -136,6 +136,12 @@ def authenticate_request(self):
         if user.disabled or user.pending:
             self.logout()
             return M.User.anonymous()
+
+        if asbool(config.get('auth.reject_untracked_sessions', False)) and not 
user.validate_session(self.session.id):
+            log.info(f'Session ID is not tracked: {self.session.id}')
+            self.logout()
+            return M.User.anonymous()
+
         session_create_date = datetime.utcfromtimestamp(self.session.created)
         if user.is_anonymous():
             sessions_need_reauth = False
@@ -241,6 +247,7 @@ def login(self, user: M.User = None, multifactor_success: 
bool = False) -> M.Use
         g.statsUpdater.addUserLogin(user)
         user.add_login_detail(login_details)
         user.track_login(self.request)
+        user.track_session(self.session.id)
         return user
 
     def login_check_password_change_needed(self, user: M.User, password: str | 
None, login_details: M.UserLoginDetails) -> str | None:
@@ -293,6 +300,12 @@ def login_check_password_change_needed(self, user: M.User, 
password: str | None,
                                          'Please check your email to 
continue.')
 
     def logout(self):
+        try:
+            user = c.user
+        except AttributeError:
+            pass
+        else:
+            user.untrack_session(self.session.id)
         self.session.invalidate()
         self.session.save()
         response.set_cookie('memorable_forget', '/', 
secure=request.environ['beaker.session'].secure)
diff --git a/Allura/allura/model/auth.py b/Allura/allura/model/auth.py
index 40742060e..4088154b8 100644
--- a/Allura/allura/model/auth.py
+++ b/Allura/allura/model/auth.py
@@ -24,6 +24,7 @@
 from urllib.parse import urlparse
 from email import header
 from hashlib import sha256
+from collections import deque
 from datetime import timedelta, datetime, time
 import os
 import re
@@ -387,6 +388,25 @@ def track_active(self, req):
             self.last_access['session_ua'] = user_agent
             session(self).flush(self)
 
+    def track_session(self, session_id):
+        session_ids = deque(self.get_tool_data('web_session', 'ids', []), 
maxlen=100)
+        if session_id not in session_ids:
+            session_ids.appendleft(session_id)
+            self.set_tool_data('web_session', ids=list(session_ids))
+
+    def untrack_session(self, session_id):
+        session_ids = self.get_tool_data('web_session', 'ids', [])
+        if session_id in session_ids:
+            session_ids.remove(session_id)
+            self.set_tool_data('web_session', ids=session_ids)
+
+    def validate_session(self, session_id):
+        session_ids = self.get_tool_data('web_session', 'ids', [])
+        return session_id in session_ids
+
+    def has_active_sessions(self):
+        return len(self.get_tool_data('web_session', 'ids', [])) > 0
+
     def add_login_detail(self, detail):
         try:
             session(detail).flush(detail)
diff --git a/Allura/allura/tests/functional/test_auth.py 
b/Allura/allura/tests/functional/test_auth.py
index 8ead0e334..e280fed69 100644
--- a/Allura/allura/tests/functional/test_auth.py
+++ b/Allura/allura/tests/functional/test_auth.py
@@ -3457,3 +3457,69 @@ def test_input_expired_auth_code(self):
             r = r.form.submit()
 
             assert 'Invalid code' in r.text
+
+
+class TestTrackUserSessions(TestController):
+    def login(self, username='test-user', pwd='foo'):
+        self.app.get('/auth/preferences/')  # establish session_id cookie
+        r = self.app.get('/auth/')
+
+        f = r.forms[0]
+        encoded = self.app.antispam_field_names(f)
+        f[encoded['username']] = username
+        f[encoded['password']] = pwd
+        return f.submit()
+
+    @mock.patch.dict(config, {'auth.reject_untracked_sessions': True})
+    def test_validate_tracked_session(self):
+        r = self.login()
+        session_id = r.session.id
+        user = M.User.by_username('test-user')
+        session_ids = user.get_tool_data('web_session', 'ids')
+        assert session_id in session_ids
+
+    @mock.patch.dict(config, {'auth.reject_untracked_sessions': True})
+    def test_untrack_user_session(self):
+        r = self.login()
+        user = M.User.by_username('test-user')
+        session_ids = user.get_tool_data('web_session', 'ids')
+        assert len(session_ids) == 1
+
+        r = self.app.get('/auth/logout', extra_environ={'username': 
'test-user'})
+        user = M.User.by_username('test-user')
+        session_ids = user.get_tool_data('web_session', 'ids')
+        assert len(session_ids) ==  0
+
+
+    @mock.patch.dict(config, {'auth.reject_untracked_sessions': True})
+    def test_navigation(self):
+        r = self.login()
+        r = self.app.get('/auth/preferences/', extra_environ={'username': 
'test-user'})
+        assert 'User Preferences for test-user' in r.text
+
+        # Remove tracked session ids
+        user = M.User.by_username('test-user')
+        user.set_tool_data('web_session', ids=[])
+
+        # Without session ids the user should be redirected to the login page
+        r = self.app.get('/auth/preferences/', extra_environ={'username': 
'test-user'})
+        assert r.status_int == 302
+        r = r.follow()
+        assert 'Username:' in r.text
+        assert 'Password:' in r.text
+
+    @mock.patch.dict(config, {'auth.reject_untracked_sessions': True})
+    def test_sessions_max_limit(self):
+        user = M.User.by_username('test-user')
+        mock_session_ids = list(range(99, -1, -1))
+        user.set_tool_data('web_session', ids=mock_session_ids)
+
+        r = self.login()
+        session_id = r.session.id
+        user = M.User.by_username('test-user')
+        session_ids = user.get_tool_data('web_session', 'ids')
+
+        # Validate the session size is not exceeded upon adding a new session 
after login
+        assert len(session_ids) == 100
+        assert session_ids[0] == session_id
+        assert session_ids[-1] == 1
diff --git a/Allura/development.ini b/Allura/development.ini
index 92914e8ef..6339d95ba 100644
--- a/Allura/development.ini
+++ b/Allura/development.ini
@@ -267,9 +267,12 @@ auth.auth.trust_ip_3_octets_match = true
 ; Enable OAuth2 support
 auth.oauth2.enabled = true
 
-; Enable login by email authenticatin code
+; Enable login by email authentication code
 auth.email_auth_code.enabled = true
 
+; Reject requests for users who don't have tracked sessions
+auth.reject_untracked_sessions = true
+
 user_prefs_storage.method = local
 ; user_prefs_storage.method = ldap
 ; If using ldap, you can specify which fields to use for a preference.
diff --git a/Allura/test.ini b/Allura/test.ini
index 037bc924f..24770373d 100644
--- a/Allura/test.ini
+++ b/Allura/test.ini
@@ -90,6 +90,9 @@ forgemail.port = 8827
 ; Disable emailing authentication codes during tests
 auth.email_auth_code.enabled = false
 
+; Enable rejecting untracked user sessions
+auth.reject_untracked_sessions = false
+
 [app:task]
 use = main
 ; TurboGears will use controllers/task.py as root controller

Reply via email to