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