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.
- cherrypy_foundation/components/ColorModes.jinja +4 -5
- cherrypy_foundation/components/Datatable.jinja +2 -2
- cherrypy_foundation/components/Datatable.js +2 -2
- cherrypy_foundation/components/Field.jinja +6 -16
- cherrypy_foundation/components/Fields.jinja +2 -0
- cherrypy_foundation/components/Typeahead.css +1 -6
- cherrypy_foundation/components/Typeahead.jinja +2 -2
- cherrypy_foundation/components/__init__.py +2 -2
- cherrypy_foundation/components/tests/test_static.py +1 -1
- cherrypy_foundation/error_page.py +17 -20
- cherrypy_foundation/flash.py +15 -17
- cherrypy_foundation/form.py +2 -2
- cherrypy_foundation/logging.py +2 -2
- cherrypy_foundation/passwd.py +2 -2
- cherrypy_foundation/plugins/db.py +9 -35
- cherrypy_foundation/plugins/ldap.py +38 -46
- cherrypy_foundation/plugins/restapi.py +1 -1
- cherrypy_foundation/plugins/scheduler.py +84 -208
- cherrypy_foundation/plugins/smtp.py +46 -78
- cherrypy_foundation/plugins/tests/test_db.py +4 -4
- cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
- cherrypy_foundation/plugins/tests/test_scheduler.py +50 -58
- cherrypy_foundation/plugins/tests/test_smtp.py +7 -40
- cherrypy_foundation/tests/__init__.py +0 -72
- cherrypy_foundation/tests/test_error_page.py +1 -7
- cherrypy_foundation/tests/test_passwd.py +2 -2
- cherrypy_foundation/tools/auth.py +38 -59
- cherrypy_foundation/tools/auth_mfa.py +88 -89
- cherrypy_foundation/tools/errors.py +27 -0
- cherrypy_foundation/tools/i18n.py +153 -246
- cherrypy_foundation/tools/jinja2.py +13 -29
- cherrypy_foundation/tools/ratelimit.py +27 -37
- cherrypy_foundation/tools/secure_headers.py +5 -1
- cherrypy_foundation/tools/sessions_timeout.py +21 -23
- cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
- cherrypy_foundation/tools/tests/test_auth.py +4 -21
- cherrypy_foundation/tools/tests/test_i18n.py +6 -81
- cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
- cherrypy_foundation/url.py +25 -25
- cherrypy_foundation/widgets.py +2 -2
- cherrypy_foundation-1.0.0a1.dist-info/METADATA +42 -0
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/RECORD +46 -65
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/WHEEL +1 -1
- cherrypy_foundation/components/Flash.jinja +0 -13
- cherrypy_foundation/components/LocaleSelection.jinja +0 -13
- cherrypy_foundation/components/LocaleSelection.js +0 -26
- cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
- cherrypy_foundation/sessions.py +0 -93
- cherrypy_foundation/tests/templates/test_flash.html +0 -9
- cherrypy_foundation/tests/templates/test_form.html +0 -16
- cherrypy_foundation/tests/templates/test_url.html +0 -15
- cherrypy_foundation/tests/test_flash.py +0 -61
- cherrypy_foundation/tests/test_form.py +0 -148
- cherrypy_foundation/tests/test_logging.py +0 -78
- cherrypy_foundation/tests/test_sessions.py +0 -89
- cherrypy_foundation/tests/test_url.py +0 -161
- cherrypy_foundation/tools/tests/components/Button.jinja +0 -2
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/templates/test_jinja2.html +0 -11
- cherrypy_foundation/tools/tests/templates/test_jinjax.html +0 -9
- cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
- cherrypy_foundation/tools/tests/test_auth_mfa.py +0 -369
- cherrypy_foundation/tools/tests/test_jinja2.py +0 -153
- cherrypy_foundation/tools/tests/test_secure_headers.py +0 -200
- cherrypy_foundation-1.0.0.dist-info/METADATA +0 -71
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/licenses/LICENSE.md +0 -0
- {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-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
session = cherrypy.serving.session
|
|
102
|
+
if session.get(MFA_USERNAME) != cherrypy.request.login:
|
|
103
|
+
return False
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
session = cherrypy.serving.session
|
|
131
|
+
if session.get(MFA_USERNAME) != cherrypy.request.login:
|
|
132
|
+
return True
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
134
|
+
# Check if code is defined.
|
|
135
|
+
hash_ = session.get(MFA_CODE)
|
|
136
|
+
if not hash_:
|
|
137
|
+
return True
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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)
|