cherrypy-foundation 1.0.0a15__py3-none-any.whl → 1.0.0a17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. cherrypy_foundation/components/Datatable.js +2 -2
  2. cherrypy_foundation/components/__init__.py +2 -2
  3. cherrypy_foundation/components/tests/test_static.py +1 -1
  4. cherrypy_foundation/error_page.py +2 -2
  5. cherrypy_foundation/flash.py +2 -2
  6. cherrypy_foundation/form.py +2 -2
  7. cherrypy_foundation/logging.py +2 -2
  8. cherrypy_foundation/passwd.py +2 -2
  9. cherrypy_foundation/plugins/db.py +1 -1
  10. cherrypy_foundation/plugins/ldap.py +3 -1
  11. cherrypy_foundation/plugins/restapi.py +1 -1
  12. cherrypy_foundation/plugins/scheduler.py +1 -1
  13. cherrypy_foundation/plugins/smtp.py +8 -2
  14. cherrypy_foundation/plugins/tests/test_db.py +2 -2
  15. cherrypy_foundation/plugins/tests/test_ldap.py +1 -1
  16. cherrypy_foundation/plugins/tests/test_scheduler.py +1 -1
  17. cherrypy_foundation/plugins/tests/test_scheduler_db.py +1 -1
  18. cherrypy_foundation/plugins/tests/test_smtp.py +31 -1
  19. cherrypy_foundation/sessions.py +82 -0
  20. cherrypy_foundation/tests/__init__.py +1 -1
  21. cherrypy_foundation/tests/templates/test_form.html +6 -1
  22. cherrypy_foundation/tests/templates/test_url.html +1 -0
  23. cherrypy_foundation/tests/test_error_page.py +1 -1
  24. cherrypy_foundation/tests/test_flash.py +3 -3
  25. cherrypy_foundation/tests/test_form.py +36 -4
  26. cherrypy_foundation/tests/test_logging.py +78 -0
  27. cherrypy_foundation/tests/test_passwd.py +2 -2
  28. cherrypy_foundation/tests/test_url.py +2 -2
  29. cherrypy_foundation/tools/auth.py +31 -27
  30. cherrypy_foundation/tools/auth_mfa.py +87 -83
  31. cherrypy_foundation/tools/i18n.py +1 -1
  32. cherrypy_foundation/tools/jinja2.py +1 -1
  33. cherrypy_foundation/tools/ratelimit.py +33 -19
  34. cherrypy_foundation/tools/secure_headers.py +1 -1
  35. cherrypy_foundation/tools/sessions_timeout.py +23 -21
  36. cherrypy_foundation/tools/tests/test_auth.py +20 -3
  37. cherrypy_foundation/tools/tests/test_auth_mfa.py +6 -4
  38. cherrypy_foundation/tools/tests/test_i18n.py +1 -1
  39. cherrypy_foundation/tools/tests/test_jinja2.py +2 -2
  40. cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
  41. cherrypy_foundation/tools/tests/test_secure_headers.py +200 -0
  42. cherrypy_foundation/url.py +2 -2
  43. cherrypy_foundation/widgets.py +2 -2
  44. cherrypy_foundation-1.0.0a17.dist-info/METADATA +71 -0
  45. {cherrypy_foundation-1.0.0a15.dist-info → cherrypy_foundation-1.0.0a17.dist-info}/RECORD +48 -46
  46. {cherrypy_foundation-1.0.0a15.dist-info → cherrypy_foundation-1.0.0a17.dist-info}/WHEEL +1 -1
  47. cherrypy_foundation/tools/errors.py +0 -27
  48. cherrypy_foundation-1.0.0a15.dist-info/METADATA +0 -42
  49. {cherrypy_foundation-1.0.0a15.dist-info → cherrypy_foundation-1.0.0a17.dist-info}/licenses/LICENSE.md +0 -0
  50. {cherrypy_foundation-1.0.0a15.dist-info → cherrypy_foundation-1.0.0a17.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  # MFA tool for cherrypy
2
- # Copyright (C) 2022-2025 Patrik Dufresne
2
+ # Copyright (C) 2022-2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -21,6 +21,8 @@ from typing import Optional
21
21
 
22
22
  import cherrypy
23
23
 
24
+ from cherrypy_foundation.sessions import session_lock
25
+
24
26
  from ..passwd import check_password, hash_password
25
27
 
26
28
  MFA_CODE = '_auth_mfa_code'
@@ -83,11 +85,11 @@ class CheckAuthMfa(cherrypy.Tool):
83
85
 
84
86
  length = self._code_length()
85
87
  code = ''.join(secrets.choice(string.digits) for _ in range(length))
86
- session = cherrypy.serving.session
87
- session[MFA_USER_KEY] = cherrypy.request.login
88
- session[MFA_CODE] = hash_password(code)
89
- session[MFA_CODE_TIME] = session.now()
90
- session[MFA_CODE_ATTEMPT] = 0
88
+ with session_lock() as session:
89
+ session[MFA_USER_KEY] = cherrypy.request.login
90
+ session[MFA_CODE] = hash_password(code)
91
+ session[MFA_CODE_TIME] = session.now()
92
+ session[MFA_CODE_ATTEMPT] = 0
91
93
  return code
92
94
 
93
95
  def _is_verified(self) -> bool:
@@ -96,24 +98,24 @@ class CheckAuthMfa(cherrypy.Tool):
96
98
  return False
97
99
 
98
100
  # Check if our username match current login user.
99
- session = cherrypy.serving.session
100
- if session.get(MFA_USER_KEY) != cherrypy.request.login:
101
- return False
101
+ with session_lock() as session:
102
+ if session.get(MFA_USER_KEY) != cherrypy.request.login:
103
+ return False
102
104
 
103
- # Check if the current IP is trusted
104
- ip_list = session.get(MFA_TRUSTED_IP_LIST) or []
105
- if cherrypy.request.remote.ip not in ip_list:
106
- return False
105
+ # Check if the current IP is trusted
106
+ ip_list = session.get(MFA_TRUSTED_IP_LIST) or []
107
+ if cherrypy.request.remote.ip not in ip_list:
108
+ return False
107
109
 
108
- # Check if user ever pass the verification.
109
- verified_at: Optional[datetime.datetime] = session.get(MFA_VERIFICATION_TIME)
110
- if not verified_at:
111
- return False
110
+ # Check if user ever pass the verification.
111
+ verified_at: Optional[datetime.datetime] = session.get(MFA_VERIFICATION_TIME)
112
+ if not verified_at:
113
+ return False
112
114
 
113
- # Check trusted duration time
114
- trust_minutes = self._trust_duration_minutes()
115
- if (verified_at + datetime.timedelta(minutes=trust_minutes)) <= session.now():
116
- return False
115
+ # Check trusted duration time
116
+ trust_minutes = self._trust_duration_minutes()
117
+ if (verified_at + datetime.timedelta(minutes=trust_minutes)) <= session.now():
118
+ return False
117
119
 
118
120
  return True
119
121
 
@@ -125,24 +127,26 @@ class CheckAuthMfa(cherrypy.Tool):
125
127
  if not hasattr(cherrypy.serving, 'session'):
126
128
  return True
127
129
 
128
- session = cherrypy.serving.session
129
- if session.get(MFA_USER_KEY) != cherrypy.request.login:
130
- return True
130
+ with session_lock() as session:
131
+ if session.get(MFA_USER_KEY) != cherrypy.request.login:
132
+ return True
131
133
 
132
- # Check if code is defined.
133
- hash_ = session.get(MFA_CODE)
134
- if not hash_:
135
- return True
134
+ # Check if code is defined.
135
+ hash_ = session.get(MFA_CODE)
136
+ if not hash_:
137
+ return True
136
138
 
137
- # Check issued time.
138
- issued_at: Optional[datetime.datetime] = session.get(MFA_CODE_TIME)
139
- if not issued_at or ((issued_at + datetime.timedelta(minutes=self._code_timeout_minutes())) < session.now()):
140
- return True
139
+ # Check issued time.
140
+ issued_at: Optional[datetime.datetime] = session.get(MFA_CODE_TIME)
141
+ if not issued_at or (
142
+ (issued_at + datetime.timedelta(minutes=self._code_timeout_minutes())) < session.now()
143
+ ):
144
+ return True
141
145
 
142
- # Check number of attempt.
143
- attempts = int(session.get(MFA_CODE_ATTEMPT, 0))
144
- if attempts >= self._max_attempts():
145
- return True
146
+ # Check number of attempt.
147
+ attempts = int(session.get(MFA_CODE_ATTEMPT, 0))
148
+ if attempts >= self._max_attempts():
149
+ return True
146
150
 
147
151
  return False
148
152
 
@@ -190,55 +194,55 @@ class CheckAuthMfa(cherrypy.Tool):
190
194
  if not hasattr(cherrypy.serving, 'session'):
191
195
  return False
192
196
 
193
- session = cherrypy.serving.session
194
- stored_hash = session.get(MFA_CODE)
195
- is_expired = self.is_code_expired()
196
- if self.is_code_expired():
197
- return False
198
-
199
- # Always perform the hash check regardless of expiration
200
- # to prevent timing attacks
201
- code_valid = False
202
- if stored_hash:
203
- code_valid = check_password(code, stored_hash)
204
-
205
- # Check all conditions after hash verification
206
- if is_expired or not code_valid:
207
- if not is_expired: # Only increment if not expired
208
- session[MFA_CODE_ATTEMPT] = attempts = int(session.get(MFA_CODE_ATTEMPT, 0)) + 1
197
+ with session_lock() as session:
198
+ stored_hash = session.get(MFA_CODE)
199
+ is_expired = self.is_code_expired()
200
+ if self.is_code_expired():
201
+ return False
202
+
203
+ # Always perform the hash check regardless of expiration
204
+ # to prevent timing attacks
205
+ code_valid = False
206
+ if stored_hash:
207
+ code_valid = check_password(code, stored_hash)
208
+
209
+ # Check all conditions after hash verification
210
+ if is_expired or not code_valid:
211
+ if not is_expired: # Only increment if not expired
212
+ session[MFA_CODE_ATTEMPT] = attempts = int(session.get(MFA_CODE_ATTEMPT, 0)) + 1
213
+ cherrypy.log(
214
+ f'verification failed user={cherrypy.request.login} ip={cherrypy.request.remote.ip} attempts={attempts}',
215
+ context='MFA',
216
+ severity=logging.WARNING,
217
+ )
218
+ return False
219
+
220
+ # Success: trust this device/IP for the configured duration
221
+ session[MFA_VERIFICATION_TIME] = session.now()
222
+ ip_list = list({*(session.get(MFA_TRUSTED_IP_LIST) or []), cherrypy.request.remote.ip})
223
+ # Cap the list to avoid unbounded growth (keep most recent N)
224
+ max_ips = self._max_trusted_ips()
225
+ if len(ip_list) > max_ips:
226
+ ip_list = ip_list[-max_ips:]
227
+ session[MFA_TRUSTED_IP_LIST] = ip_list
228
+
229
+ # Clear the one-time code
230
+ session[MFA_CODE] = None
231
+ session[MFA_CODE_TIME] = None
232
+ session[MFA_CODE_ATTEMPT] = 0
233
+
234
+ # Honor “remember this device” option by making the session persistent
235
+ if hasattr(cherrypy.tools, 'sessions_timeout'):
236
+ try:
237
+ cherrypy.tools.sessions_timeout.set_persistent(bool(persistent))
238
+ except Exception:
239
+ pass
240
+
241
+ # Rotate session id to prevent fixation
242
+ session.regenerate()
209
243
  cherrypy.log(
210
- f'verification failed user={cherrypy.request.login} ip={cherrypy.request.remote.ip} attempts={attempts}',
211
- context='MFA',
212
- severity=logging.WARNING,
244
+ f'verification successful user={cherrypy.request.login} ip={cherrypy.request.remote.ip}', context='MFA'
213
245
  )
214
- return False
215
-
216
- # Success: trust this device/IP for the configured duration
217
- session[MFA_VERIFICATION_TIME] = session.now()
218
- ip_list = list({*(session.get(MFA_TRUSTED_IP_LIST) or []), cherrypy.request.remote.ip})
219
- # Cap the list to avoid unbounded growth (keep most recent N)
220
- max_ips = self._max_trusted_ips()
221
- if len(ip_list) > max_ips:
222
- ip_list = ip_list[-max_ips:]
223
- session[MFA_TRUSTED_IP_LIST] = ip_list
224
-
225
- # Clear the one-time code
226
- session[MFA_CODE] = None
227
- session[MFA_CODE_TIME] = None
228
- session[MFA_CODE_ATTEMPT] = 0
229
-
230
- # Honor “remember this device” option by making the session persistent
231
- if hasattr(cherrypy.tools, 'sessions_timeout'):
232
- try:
233
- cherrypy.tools.sessions_timeout.set_persistent(bool(persistent))
234
- except Exception:
235
- pass
236
-
237
- # Rotate session id to prevent fixation
238
- session.regenerate()
239
- cherrypy.log(
240
- f'verification successful user={cherrypy.request.login} ip={cherrypy.request.remote.ip}', context='MFA'
241
- )
242
246
  return True
243
247
 
244
248
 
@@ -1,5 +1,5 @@
1
1
  # Internationalisation tool for cherrypy
2
- # Copyright (C) 2012-2025 Patrik Dufresne
2
+ # Copyright (C) 2012-2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
1
  # Jinja2 tools for cherrypy
2
- # Copyright (C) 2021-2025 Patrik Dufresne
2
+ # Copyright (C) 2021-2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
1
  # Ratelimit tools for cherrypy
2
- # Copyright (C) 2022-2025 Patrik Dufresne
2
+ # Copyright (C) 2022-2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -22,6 +22,8 @@ from collections import namedtuple
22
22
 
23
23
  import cherrypy
24
24
 
25
+ from cherrypy_foundation.sessions import session_lock
26
+
25
27
  Tracker = namedtuple('Tracker', ['token', 'hits', 'timeout'])
26
28
 
27
29
 
@@ -148,9 +150,27 @@ class FileRateLimit(_DataStore):
148
150
  class Ratelimit(cherrypy.Tool):
149
151
  CONTEXT = 'TOOLS.RATELIMIT'
150
152
 
153
+ _datastores = {}
154
+
151
155
  def __init__(self, priority=60):
152
156
  super().__init__('before_handler', self.check_ratelimit, 'ratelimit', priority)
153
157
 
158
+ def _get_datastore(self, storage_class, **kwargs):
159
+ """
160
+ Create new datastore or return existing datastore.
161
+ """
162
+ # Default to RAM if storage class is not defined.
163
+ if storage_class is None:
164
+ kwargs = {}
165
+ storage_class = RamRateLimit
166
+ # Lookup for matching storage.
167
+ key = (storage_class, str(kwargs))
168
+ datastore = self._datastores.get(key)
169
+ if datastore is None:
170
+ # Create new storage if not found.
171
+ self._datastores[key] = datastore = storage_class(**kwargs)
172
+ return datastore
173
+
154
174
  def check_ratelimit(
155
175
  self,
156
176
  delay=3600,
@@ -161,7 +181,8 @@ class Ratelimit(cherrypy.Tool):
161
181
  methods=None,
162
182
  debug=False,
163
183
  hit=1,
164
- **conf,
184
+ storage_class=None,
185
+ **storage_kwargs,
165
186
  ):
166
187
  """
167
188
  Verify the ratelimit. By default return a 429 HTTP error code (Too Many Request). After 25 request within the same hour.
@@ -174,11 +195,10 @@ class Ratelimit(cherrypy.Tool):
174
195
  scope: if specify, define the scope of rate limit. Default to path_info.
175
196
  methods: if specify, only the methods in the list will be rate limited.
176
197
  """
177
- assert delay > 0, 'invalid delay'
178
-
179
- # Check if limit is enabled
198
+ if delay <= 0:
199
+ raise ValueError('Invalid delay: %s' % delay)
180
200
  if limit <= 0:
181
- return
201
+ raise ValueError('Invalid limit: %s' % limit)
182
202
 
183
203
  # Check if this 'method' should be rate limited
184
204
  request = cherrypy.request
@@ -187,13 +207,8 @@ class Ratelimit(cherrypy.Tool):
187
207
  cherrypy.log(f'skip rate limit for HTTP method {request.method}', context=self.CONTEXT)
188
208
  return
189
209
 
190
- # If datastore is not pass as configuration, create it for the first time.
191
- datastore = getattr(self, '_ratelimit_datastore', None)
192
- if datastore is None:
193
- # Create storage using storage class
194
- storage_class = conf.get('storage_class', RamRateLimit)
195
- datastore = storage_class(**conf)
196
- self._ratelimit_datastore = datastore
210
+ # Create storage using storage class
211
+ datastore = self._get_datastore(storage_class, **storage_kwargs)
197
212
 
198
213
  # Identifier: prefer authenticated user; else client IP
199
214
  identifier = getattr(cherrypy.serving.request, 'login', None) or cherrypy.request.remote.ip
@@ -226,7 +241,8 @@ class Ratelimit(cherrypy.Tool):
226
241
  cherrypy.log(f'block access to path_info={request.path_info}', context=self.CONTEXT)
227
242
  if logout:
228
243
  if hasattr(cherrypy.serving, 'session'):
229
- cherrypy.serving.session.clear()
244
+ with session_lock() as session:
245
+ session.clear()
230
246
  raise cherrypy.HTTPRedirect("/")
231
247
  raise cherrypy.HTTPError(return_status)
232
248
 
@@ -240,12 +256,10 @@ class Ratelimit(cherrypy.Tool):
240
256
 
241
257
  def reset(self):
242
258
  """
243
- Used to reset the ratelimit.
259
+ Used to reset the ratelimit. (for unit test)
244
260
  """
245
- datastore = getattr(self, '_ratelimit_datastore', False)
246
- if not datastore:
247
- return
248
- datastore.reset()
261
+ for datastore in self._datastores.values():
262
+ datastore.reset()
249
263
 
250
264
 
251
265
  cherrypy.tools.ratelimit = Ratelimit()
@@ -1,5 +1,5 @@
1
1
  # Secure Headers tool for cherrypy
2
- # Copyright (C) 2012-2025 IKUS Software
2
+ # Copyright (C) 2012-2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
1
  # Session timeout tool for cherrypy
2
- # Copyright (C) 2012-2025 IKUS Software
2
+ # Copyright (C) 2012-2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -21,6 +21,8 @@ import time
21
21
  import cherrypy
22
22
  from cherrypy.lib import httputil
23
23
 
24
+ from cherrypy_foundation.sessions import session_lock
25
+
24
26
  SESSION_PERSISTENT = '_session_persistent'
25
27
  SESSION_START_TIME = '_session_start_time'
26
28
 
@@ -57,13 +59,13 @@ class SessionsTimeout(cherrypy.Tool):
57
59
  absolute_timeout = int(cfg.get('tools.sessions_timeout.absolute_timeout', SESSION_DEFAULT_ABSOLUTE_TIMEOUT))
58
60
  return idle_timeout, persistent_timeout, absolute_timeout
59
61
 
60
- def _set_cookie_max_age(self, max_age: int) -> None:
62
+ def _set_cookie_max_age(self, session, max_age: int) -> None:
61
63
  """Adjust only Max-Age and Expires for the session cookie, keep other flags intact."""
62
64
  cookie_name = self._cookie_name()
63
65
  cookie = cherrypy.serving.response.cookie
64
66
  # Ensure the cookie key exists (CherryPy sets it when session id is issued/regenerated).
65
67
  if cookie_name not in cookie:
66
- cookie[cookie_name] = cherrypy.serving.session.id
68
+ cookie[cookie_name] = session.id
67
69
  cookie[cookie_name]['max-age'] = str(max(0, max_age))
68
70
  cookie[cookie_name]['expires'] = httputil.HTTPDate(time.time() + max(0, max_age))
69
71
 
@@ -73,24 +75,21 @@ class SessionsTimeout(cherrypy.Tool):
73
75
  return 0
74
76
  return int(math.ceil(seconds / 60.0))
75
77
 
76
- def _ensure_start_time(self) -> None:
78
+ def _ensure_start_time(self, session) -> None:
77
79
  """Ensure the session has a stable start time anchor for absolute timeout."""
78
- session = cherrypy.serving.session
79
80
  if SESSION_START_TIME not in session or not isinstance(session.get(SESSION_START_TIME), datetime.datetime):
80
81
  session[SESSION_START_TIME] = session.now()
81
82
 
82
- def _expire_and_restart(self, make_persistent: bool | None = None) -> None:
83
+ def _expire_and_restart(self, session, make_persistent: bool | None = None) -> None:
83
84
  """Expire current session and start a fresh one. Optionally set persistent flag."""
84
- session = cherrypy.serving.session
85
85
  session.clear()
86
86
  session.regenerate()
87
87
  session[SESSION_START_TIME] = session.now()
88
88
  if make_persistent is not None:
89
89
  session[SESSION_PERSISTENT] = bool(make_persistent)
90
90
 
91
- def _update_session_timeout(self) -> None:
92
- session = cherrypy.serving.session
93
- self._ensure_start_time()
91
+ def _update_session_timeout(self, session) -> None:
92
+ self._ensure_start_time(session)
94
93
 
95
94
  now: datetime.datetime = session.now()
96
95
  start: datetime.datetime = session[SESSION_START_TIME]
@@ -120,9 +119,9 @@ class SessionsTimeout(cherrypy.Tool):
120
119
  # Expired due to either sliding window or absolute cap
121
120
  # Start a fresh session; do NOT automatically re-mark persistent here.
122
121
  # Authentication/Remember me logic elsewhere can call set_persistent(True) again after login.
123
- self._expire_and_restart()
122
+ self._expire_and_restart(session)
124
123
  # After regeneration, set a sensible session.timeout baseline:
125
- cherrypy.serving.session.timeout = idle_timeout_min
124
+ session.timeout = idle_timeout_min
126
125
  return
127
126
 
128
127
  # Apply per-request remaining minutes to CherryPy's session timeout
@@ -130,7 +129,7 @@ class SessionsTimeout(cherrypy.Tool):
130
129
 
131
130
  if is_persistent:
132
131
  # Keep cookie lifetime aligned with remaining time so it can survive browser restarts.
133
- self._set_cookie_max_age(remaining_sec)
132
+ self._set_cookie_max_age(session, remaining_sec)
134
133
  else:
135
134
  # For non-persistent, let CherryPy manage the session cookie (session or transient cookie).
136
135
  # If you want strict enforcement client-side too, uncomment the next line:
@@ -143,23 +142,26 @@ class SessionsTimeout(cherrypy.Tool):
143
142
  return
144
143
 
145
144
  # Ensure configured values are honored (method signature kept for CherryPy Tool interface)
146
- self._update_session_timeout()
145
+ with session_lock() as session:
146
+ self._update_session_timeout(session)
147
147
 
148
148
  def set_persistent(self, value: bool = True) -> None:
149
149
  """Mark current session as persistent (or not) and reset the start window."""
150
- session = cherrypy.serving.session
151
- session[SESSION_PERSISTENT] = bool(value)
152
- # Reset absolute window anchor when the persistence policy changes
153
- session[SESSION_START_TIME] = session.now()
154
- self._update_session_timeout()
150
+ with session_lock() as session:
151
+ session[SESSION_PERSISTENT] = bool(value)
152
+ # Reset absolute window anchor when the persistence policy changes
153
+ session[SESSION_START_TIME] = session.now()
154
+ self._update_session_timeout(session)
155
155
 
156
156
  def is_persistent(self) -> bool:
157
157
  """Return True if the session is marked persistent."""
158
- return bool(cherrypy.serving.session.get(SESSION_PERSISTENT, False))
158
+ with session_lock() as session:
159
+ return bool(session.get(SESSION_PERSISTENT, False))
159
160
 
160
161
  def get_session_start_time(self) -> datetime.datetime | None:
161
162
  """Return the session start time if any."""
162
- return cherrypy.serving.session.get(SESSION_START_TIME, None)
163
+ with session_lock() as session:
164
+ return session.get(SESSION_START_TIME, None)
163
165
 
164
166
 
165
167
  cherrypy.tools.sessions_timeout = SessionsTimeout()
@@ -1,5 +1,5 @@
1
1
  # CherryPy
2
- # Copyright (C) 2025 IKUS Software
2
+ # Copyright (C) 2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -14,11 +14,12 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
-
17
+ import tempfile
18
18
  from collections import namedtuple
19
19
  from urllib.parse import urlencode
20
20
 
21
21
  import cherrypy
22
+ from cherrypy.lib.sessions import FileSession
22
23
  from cherrypy.test import helper
23
24
 
24
25
  from .. import auth # noqa
@@ -42,7 +43,7 @@ def user_from_key_func(userkey):
42
43
  return None
43
44
 
44
45
 
45
- @cherrypy.tools.sessions()
46
+ @cherrypy.tools.sessions(locking='explicit', storage_class=FileSession)
46
47
  @cherrypy.tools.auth(
47
48
  user_lookup_func=user_lookup_func,
48
49
  user_from_key_func=user_from_key_func,
@@ -68,8 +69,24 @@ class Root:
68
69
  class AuthManagerTest(helper.CPWebCase):
69
70
  interactive = False
70
71
 
72
+ @classmethod
73
+ def setup_class(cls):
74
+ cls.tempdir = tempfile.TemporaryDirectory(prefix='cherrypy-foundation-', suffix='-session')
75
+ cls.session_dir = cls.tempdir.name
76
+ super().setup_class()
77
+
78
+ @classmethod
79
+ def teardown_class(cls):
80
+ cls.tempdir.cleanup()
81
+ super().teardown_class()
82
+
71
83
  @classmethod
72
84
  def setup_server(cls):
85
+ cherrypy.config.update(
86
+ {
87
+ 'tools.sessions.storage_path': cls.session_dir,
88
+ }
89
+ )
73
90
  cherrypy.tree.mount(Root(), '/')
74
91
 
75
92
  def test_auth_redirect(self):
@@ -1,5 +1,5 @@
1
1
  # CherryPy
2
- # Copyright (C) 2025 IKUS Software
2
+ # Copyright (C) 2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -14,7 +14,6 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
-
18
17
  import datetime
19
18
  from collections import namedtuple
20
19
  from urllib.parse import urlencode
@@ -23,6 +22,8 @@ import cherrypy
23
22
  from cherrypy.lib.sessions import RamSession
24
23
  from cherrypy.test import helper
25
24
 
25
+ from cherrypy_foundation.sessions import session_lock
26
+
26
27
  from ..auth import AUTH_LAST_PASSWORD_AT
27
28
  from ..auth_mfa import (
28
29
  MFA_CODE_TIME,
@@ -64,7 +65,7 @@ def user_from_key_func(userkey):
64
65
  return None
65
66
 
66
67
 
67
- @cherrypy.tools.sessions()
68
+ @cherrypy.tools.sessions(locking='explicit')
68
69
  @cherrypy.tools.auth(
69
70
  user_lookup_func=user_lookup_func,
70
71
  user_from_key_func=user_from_key_func,
@@ -103,7 +104,8 @@ class Root:
103
104
  code = cherrypy.tools.auth_mfa.generate_code()
104
105
  # Here the code should be send by email, SMS or any other means.
105
106
  # For our test we store it in the session.
106
- cherrypy.serving.session['code'] = code
107
+ with session_lock() as session:
108
+ session['code'] = code
107
109
  html += "<p>new verification code sent to your email</p>\n"
108
110
  return html
109
111
 
@@ -1,5 +1,5 @@
1
1
  # CherryPy
2
- # Copyright (C) 2025 IKUS Software
2
+ # Copyright (C) 2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
- # CherryPy Foundation
2
- # Copyright (C) 2025 IKUS Software
1
+ # Cherrypy-foundation
2
+ # Copyright (C) 2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -1,5 +1,5 @@
1
1
  # CherryPy
2
- # Copyright (C) 2025 IKUS Software
2
+ # Copyright (C) 2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
5
5
  # it under the terms of the GNU General Public License as published by
@@ -49,7 +49,7 @@ class RateLimitTest(helper.CPWebCase):
49
49
 
50
50
  @classmethod
51
51
  def setup_server(cls):
52
- rate_limit_storage_class = ratelimit.RamRateLimit
52
+ rate_limit_storage_class = None
53
53
  if cls.rate_limit_dir:
54
54
  rate_limit_storage_class = ratelimit.FileRateLimit
55
55
  cherrypy.config.update(