cherrypy-foundation 1.0.0__py3-none-any.whl → 1.0.0a1__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 (68) hide show
  1. cherrypy_foundation/components/ColorModes.jinja +4 -5
  2. cherrypy_foundation/components/Datatable.jinja +2 -2
  3. cherrypy_foundation/components/Datatable.js +2 -2
  4. cherrypy_foundation/components/Field.jinja +6 -16
  5. cherrypy_foundation/components/Fields.jinja +2 -0
  6. cherrypy_foundation/components/Typeahead.css +1 -6
  7. cherrypy_foundation/components/Typeahead.jinja +2 -2
  8. cherrypy_foundation/components/__init__.py +2 -2
  9. cherrypy_foundation/components/tests/test_static.py +1 -1
  10. cherrypy_foundation/error_page.py +17 -20
  11. cherrypy_foundation/flash.py +15 -17
  12. cherrypy_foundation/form.py +2 -2
  13. cherrypy_foundation/logging.py +2 -2
  14. cherrypy_foundation/passwd.py +2 -2
  15. cherrypy_foundation/plugins/db.py +9 -35
  16. cherrypy_foundation/plugins/ldap.py +38 -46
  17. cherrypy_foundation/plugins/restapi.py +1 -1
  18. cherrypy_foundation/plugins/scheduler.py +84 -208
  19. cherrypy_foundation/plugins/smtp.py +46 -78
  20. cherrypy_foundation/plugins/tests/test_db.py +4 -4
  21. cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
  22. cherrypy_foundation/plugins/tests/test_scheduler.py +50 -58
  23. cherrypy_foundation/plugins/tests/test_smtp.py +7 -40
  24. cherrypy_foundation/tests/__init__.py +0 -72
  25. cherrypy_foundation/tests/test_error_page.py +1 -7
  26. cherrypy_foundation/tests/test_passwd.py +2 -2
  27. cherrypy_foundation/tools/auth.py +38 -59
  28. cherrypy_foundation/tools/auth_mfa.py +88 -89
  29. cherrypy_foundation/tools/errors.py +27 -0
  30. cherrypy_foundation/tools/i18n.py +153 -246
  31. cherrypy_foundation/tools/jinja2.py +13 -29
  32. cherrypy_foundation/tools/ratelimit.py +27 -37
  33. cherrypy_foundation/tools/secure_headers.py +5 -1
  34. cherrypy_foundation/tools/sessions_timeout.py +21 -23
  35. cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
  36. cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
  37. cherrypy_foundation/tools/tests/test_auth.py +4 -21
  38. cherrypy_foundation/tools/tests/test_i18n.py +6 -81
  39. cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
  40. cherrypy_foundation/url.py +25 -25
  41. cherrypy_foundation/widgets.py +2 -2
  42. cherrypy_foundation-1.0.0a1.dist-info/METADATA +42 -0
  43. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/RECORD +46 -65
  44. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/WHEEL +1 -1
  45. cherrypy_foundation/components/Flash.jinja +0 -13
  46. cherrypy_foundation/components/LocaleSelection.jinja +0 -13
  47. cherrypy_foundation/components/LocaleSelection.js +0 -26
  48. cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
  49. cherrypy_foundation/sessions.py +0 -93
  50. cherrypy_foundation/tests/templates/test_flash.html +0 -9
  51. cherrypy_foundation/tests/templates/test_form.html +0 -16
  52. cherrypy_foundation/tests/templates/test_url.html +0 -15
  53. cherrypy_foundation/tests/test_flash.py +0 -61
  54. cherrypy_foundation/tests/test_form.py +0 -148
  55. cherrypy_foundation/tests/test_logging.py +0 -78
  56. cherrypy_foundation/tests/test_sessions.py +0 -89
  57. cherrypy_foundation/tests/test_url.py +0 -161
  58. cherrypy_foundation/tools/tests/components/Button.jinja +0 -2
  59. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  60. cherrypy_foundation/tools/tests/templates/test_jinja2.html +0 -11
  61. cherrypy_foundation/tools/tests/templates/test_jinjax.html +0 -9
  62. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
  63. cherrypy_foundation/tools/tests/test_auth_mfa.py +0 -369
  64. cherrypy_foundation/tools/tests/test_jinja2.py +0 -153
  65. cherrypy_foundation/tools/tests/test_secure_headers.py +0 -200
  66. cherrypy_foundation-1.0.0.dist-info/METADATA +0 -71
  67. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/licenses/LICENSE.md +0 -0
  68. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  # MFA tool for cherrypy
2
- # Copyright (C) 2022-2026 IKUS Software
2
+ # Copyright (C) 2022-2025 Patrik Dufresne
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,16 +21,16 @@ from typing import Optional
21
21
 
22
22
  import cherrypy
23
23
 
24
- from cherrypy_foundation.sessions import session_lock
25
-
26
24
  from ..passwd import check_password, hash_password
27
25
 
26
+ logger = logging.getLogger(__name__)
27
+
28
28
  MFA_CODE = '_auth_mfa_code'
29
29
  MFA_CODE_TIME = '_auth_mfa_code_time'
30
30
  MFA_CODE_ATTEMPT = '_auth_mfa_code_attempt'
31
31
  MFA_REDIRECT_URL = '_auth_mfa_redirect_url'
32
32
  MFA_TRUSTED_IP_LIST = '_auth_mfa_trusted_ip_list'
33
- MFA_USER_KEY = '_auth_mfa_user_key'
33
+ MFA_USERNAME = '_auth_mfa_username'
34
34
  MFA_VERIFICATION_TIME = '_auth_mfa_time'
35
35
 
36
36
  MFA_DEFAULT_CODE_TIMEOUT = 10 # minutes
@@ -85,11 +85,11 @@ class CheckAuthMfa(cherrypy.Tool):
85
85
 
86
86
  length = self._code_length()
87
87
  code = ''.join(secrets.choice(string.digits) for _ in range(length))
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
88
+ session = cherrypy.serving.session
89
+ session[MFA_USERNAME] = cherrypy.request.login
90
+ session[MFA_CODE] = hash_password(code)
91
+ session[MFA_CODE_TIME] = session.now()
92
+ session[MFA_CODE_ATTEMPT] = 0
93
93
  return code
94
94
 
95
95
  def _is_verified(self) -> bool:
@@ -98,24 +98,24 @@ class CheckAuthMfa(cherrypy.Tool):
98
98
  return False
99
99
 
100
100
  # Check if our username match current login user.
101
- with session_lock() as session:
102
- if session.get(MFA_USER_KEY) != cherrypy.request.login:
103
- return False
101
+ session = cherrypy.serving.session
102
+ if session.get(MFA_USERNAME) != cherrypy.request.login:
103
+ return False
104
104
 
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
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
109
109
 
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
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
114
114
 
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
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
119
119
 
120
120
  return True
121
121
 
@@ -127,26 +127,24 @@ class CheckAuthMfa(cherrypy.Tool):
127
127
  if not hasattr(cherrypy.serving, 'session'):
128
128
  return True
129
129
 
130
- with session_lock() as session:
131
- if session.get(MFA_USER_KEY) != cherrypy.request.login:
132
- return True
130
+ session = cherrypy.serving.session
131
+ if session.get(MFA_USERNAME) != cherrypy.request.login:
132
+ return True
133
133
 
134
- # Check if code is defined.
135
- hash_ = session.get(MFA_CODE)
136
- if not hash_:
137
- return True
134
+ # Check if code is defined.
135
+ hash_ = session.get(MFA_CODE)
136
+ if not hash_:
137
+ return True
138
138
 
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
139
+ # Check issued time.
140
+ issued_at: Optional[datetime.datetime] = session.get(MFA_CODE_TIME)
141
+ if not issued_at or ((issued_at + datetime.timedelta(minutes=self._code_timeout_minutes())) < session.now()):
142
+ return True
145
143
 
146
- # Check number of attempt.
147
- attempts = int(session.get(MFA_CODE_ATTEMPT, 0))
148
- if attempts >= self._max_attempts():
149
- return True
144
+ # Check number of attempt.
145
+ attempts = int(session.get(MFA_CODE_ATTEMPT, 0))
146
+ if attempts >= self._max_attempts():
147
+ return True
150
148
 
151
149
  return False
152
150
 
@@ -194,55 +192,56 @@ class CheckAuthMfa(cherrypy.Tool):
194
192
  if not hasattr(cherrypy.serving, 'session'):
195
193
  return False
196
194
 
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()
243
- cherrypy.log(
244
- f'verification successful user={cherrypy.request.login} ip={cherrypy.request.remote.ip}', context='MFA'
195
+ session = cherrypy.serving.session
196
+ stored_hash = session.get(MFA_CODE)
197
+ is_expired = self.is_code_expired()
198
+ if self.is_code_expired():
199
+ return False
200
+
201
+ # Always perform the hash check regardless of expiration
202
+ # to prevent timing attacks
203
+ code_valid = False
204
+ if stored_hash:
205
+ code_valid = check_password(code, stored_hash)
206
+
207
+ # Check all conditions after hash verification
208
+ if is_expired or not code_valid:
209
+ if not is_expired: # Only increment if not expired
210
+ session[MFA_CODE_ATTEMPT] = attempts = int(session.get(MFA_CODE_ATTEMPT, 0)) + 1
211
+ logger.warning(
212
+ 'MFA code verification failed for user=%s from ip=%s, attempt=%d',
213
+ cherrypy.request.login,
214
+ cherrypy.request.remote.ip,
215
+ attempts,
245
216
  )
217
+ return False
218
+
219
+ # Success: trust this device/IP for the configured duration
220
+ session[MFA_VERIFICATION_TIME] = session.now()
221
+ ip_list = list({*(session.get(MFA_TRUSTED_IP_LIST) or []), cherrypy.request.remote.ip})
222
+ # Cap the list to avoid unbounded growth (keep most recent N)
223
+ max_ips = self._max_trusted_ips()
224
+ if len(ip_list) > max_ips:
225
+ ip_list = ip_list[-max_ips:]
226
+ session[MFA_TRUSTED_IP_LIST] = ip_list
227
+
228
+ # Clear the one-time code
229
+ session[MFA_CODE] = None
230
+ session[MFA_CODE_TIME] = None
231
+ session[MFA_CODE_ATTEMPT] = 0
232
+
233
+ # Honor “remember this device” option by making the session persistent
234
+ if hasattr(cherrypy.tools, 'sessions_timeout'):
235
+ try:
236
+ cherrypy.tools.sessions_timeout.set_persistent(bool(persistent))
237
+ except Exception:
238
+ pass
239
+
240
+ # Rotate session id to prevent fixation
241
+ session.regenerate()
242
+ logger.info(
243
+ 'Successful MFA verification for user=%s from ip=%s', cherrypy.request.login, cherrypy.request.remote.ip
244
+ )
246
245
  return True
247
246
 
248
247
 
@@ -0,0 +1,27 @@
1
+ # udb, A web interface to manage IT network
2
+ # Copyright (C) 2025 IKUS Software inc.
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ import sys
17
+
18
+ import cherrypy
19
+
20
+
21
+ def handle_exception(error_table):
22
+ t = sys.exc_info()[0]
23
+ code = error_table.get(t, 500)
24
+ cherrypy.serving.request.error_response = cherrypy.HTTPError(code).set_response
25
+
26
+
27
+ cherrypy.tools.errors = cherrypy.Tool('before_error_response', handle_exception, priority=90)