cherrypy-foundation 1.0.0a1__py3-none-any.whl → 1.0.0a2__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 (31) hide show
  1. cherrypy_foundation/components/Field.jinja +14 -6
  2. cherrypy_foundation/components/__init__.py +1 -1
  3. cherrypy_foundation/error_page.py +18 -15
  4. cherrypy_foundation/plugins/db.py +34 -8
  5. cherrypy_foundation/plugins/ldap.py +28 -22
  6. cherrypy_foundation/plugins/scheduler.py +197 -84
  7. cherrypy_foundation/plugins/smtp.py +71 -45
  8. cherrypy_foundation/plugins/tests/test_db.py +2 -2
  9. cherrypy_foundation/plugins/tests/test_scheduler.py +58 -50
  10. cherrypy_foundation/plugins/tests/test_smtp.py +9 -6
  11. cherrypy_foundation/tests/templates/test_form.html +10 -0
  12. cherrypy_foundation/tests/test_form.py +119 -0
  13. cherrypy_foundation/tools/auth.py +28 -11
  14. cherrypy_foundation/tools/auth_mfa.py +10 -13
  15. cherrypy_foundation/tools/errors.py +1 -1
  16. cherrypy_foundation/tools/i18n.py +15 -6
  17. cherrypy_foundation/tools/jinja2.py +15 -12
  18. cherrypy_foundation/tools/ratelimit.py +5 -9
  19. cherrypy_foundation/tools/secure_headers.py +0 -4
  20. cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
  21. cherrypy_foundation/tools/tests/templates/test_jinja2.html +10 -0
  22. cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +11 -0
  23. cherrypy_foundation/tools/tests/templates/test_jinjax.html +8 -0
  24. cherrypy_foundation/tools/tests/test_auth.py +1 -1
  25. cherrypy_foundation/tools/tests/test_auth_mfa.py +367 -0
  26. cherrypy_foundation/tools/tests/test_jinja2.py +123 -0
  27. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/METADATA +1 -1
  28. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/RECORD +31 -23
  29. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/WHEEL +0 -0
  30. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/licenses/LICENSE.md +0 -0
  31. {cherrypy_foundation-1.0.0a1.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/top_level.txt +0 -0
@@ -100,6 +100,26 @@ def _formataddr(value):
100
100
  return ', '.join(map(_formataddr, value))
101
101
 
102
102
 
103
+ def _send_message(server, encryption, username, password, msg):
104
+ """
105
+ Send message using SMTP.
106
+ """
107
+ host, unused, port = server.partition(':')
108
+ if encryption == 'ssl':
109
+ conn = smtplib.SMTP_SSL(host, port or 465)
110
+ else:
111
+ conn = smtplib.SMTP(host, port or 25)
112
+ try:
113
+ if encryption == 'starttls':
114
+ conn.starttls()
115
+ # Authenticate if required.
116
+ if username:
117
+ conn.login(username, password)
118
+ conn.send_message(msg)
119
+ finally:
120
+ conn.quit()
121
+
122
+
103
123
  class SmtpPlugin(SimplePlugin):
104
124
 
105
125
  server = None
@@ -108,6 +128,32 @@ class SmtpPlugin(SimplePlugin):
108
128
  encryption = None
109
129
  email_from = None
110
130
 
131
+ def _create_msg(self, subject: str, message: str, to=None, cc=None, bcc=None, reply_to=None, headers={}):
132
+ assert subject
133
+ assert message
134
+ assert to or bcc
135
+
136
+ # Record the MIME types of both parts - text/plain and text/html.
137
+ msg = MIMEMultipart('alternative')
138
+ msg['Subject'] = str(subject)
139
+ msg['From'] = _formataddr(self.email_from)
140
+ if to:
141
+ msg['To'] = _formataddr(to)
142
+ if cc:
143
+ msg['Cc'] = _formataddr(cc)
144
+ if bcc:
145
+ msg['Bcc'] = _formataddr(bcc)
146
+ if reply_to:
147
+ msg['Reply-To'] = _formataddr(reply_to)
148
+ msg['Message-ID'] = email.utils.make_msgid()
149
+ if headers:
150
+ for key, value in headers.items():
151
+ msg[key] = value
152
+ text = _html2plaintext(message)
153
+ msg.attach(MIMEText(text, 'plain', 'utf8'))
154
+ msg.attach(MIMEText(message, 'html', 'utf8'))
155
+ return msg
156
+
111
157
  def start(self):
112
158
  self.bus.log('Start SMTP plugin')
113
159
  self.bus.subscribe("send_mail", self.send_mail)
@@ -118,70 +164,50 @@ class SmtpPlugin(SimplePlugin):
118
164
  self.bus.unsubscribe("send_mail", self.send_mail)
119
165
  self.bus.unsubscribe("queue_mail", self.queue_mail)
120
166
 
167
+ def graceful(self):
168
+ """Reload of subscribers."""
169
+ self.stop()
170
+ self.start()
171
+
121
172
  def queue_mail(self, *args, **kwargs):
122
173
  """
123
174
  Queue mail to be sent.
124
175
  """
125
176
  # Skip sending email if smtp server is not configured.
126
177
  if not self.server:
127
- self.bus.log('cannot send email because SMTP Server is not configured')
178
+ cherrypy.log('cannot send email because SMTP Server is not configured', context='SMTP')
128
179
  return
129
180
  if not self.email_from:
130
- self.bus.log('cannot send email because SMTP From is not configured')
181
+ cherrypy.log('cannot send email because SMTP From is not configured', context='SMTP')
131
182
  return
132
- self.bus.publish('schedule_task', self.send_mail, *args, **kwargs)
133
-
134
- def send_mail(self, subject: str, message: str, to=None, cc=None, bcc=None, reply_to=None, headers={}):
183
+ msg = self._create_msg(*args, **kwargs)
184
+ self.bus.publish(
185
+ 'scheduler:add_job_now',
186
+ _send_message,
187
+ server=self.server,
188
+ encryption=self.encryption,
189
+ username=self.username,
190
+ password=self.password,
191
+ msg=msg,
192
+ )
193
+
194
+ def send_mail(self, *args, **kwargs):
135
195
  """
136
196
  Reusable method to be called to send email to the user user.
137
197
  `user` user object where to send the email.
138
198
  """
139
- assert subject
140
- assert message
141
- assert to or bcc
142
-
143
199
  # Skip sending email if smtp server is not configured.
144
200
  if not self.server:
145
- self.bus.log('cannot send email because SMTP Server is not configured')
201
+ cherrypy.log('cannot send email because SMTP Server is not configured', context='SMTP')
146
202
  return
147
203
  if not self.email_from:
148
- self.bus.log('cannot send email because SMTP From is not configured')
204
+ cherrypy.log('cannot send email because SMTP From is not configured', context='SMTP')
149
205
  return
150
206
 
151
- # Record the MIME types of both parts - text/plain and text/html.
152
- msg = MIMEMultipart('alternative')
153
- msg['Subject'] = str(subject)
154
- msg['From'] = _formataddr(self.email_from)
155
- if to:
156
- msg['To'] = _formataddr(to)
157
- if cc:
158
- msg['Cc'] = _formataddr(cc)
159
- if bcc:
160
- msg['Bcc'] = _formataddr(bcc)
161
- if reply_to:
162
- msg['Reply-To'] = _formataddr(reply_to)
163
- msg['Message-ID'] = email.utils.make_msgid()
164
- if headers:
165
- for key, value in headers.items():
166
- msg[key] = value
167
- text = _html2plaintext(message)
168
- msg.attach(MIMEText(text, 'plain', 'utf8'))
169
- msg.attach(MIMEText(message, 'html', 'utf8'))
170
-
171
- host, unused, port = self.server.partition(':')
172
- if self.encryption == 'ssl':
173
- conn = smtplib.SMTP_SSL(host, port or 465)
174
- else:
175
- conn = smtplib.SMTP(host, port or 25)
176
- try:
177
- if self.encryption == 'starttls':
178
- conn.starttls()
179
- # Authenticate if required.
180
- if self.username:
181
- conn.login(self.username, self.password)
182
- conn.send_message(msg)
183
- finally:
184
- conn.quit()
207
+ msg = self._create_msg(*args, **kwargs)
208
+ _send_message(
209
+ server=self.server, encryption=self.encryption, username=self.username, password=self.password, msg=msg
210
+ )
185
211
 
186
212
 
187
213
  # Register SMTP plugin
@@ -93,7 +93,7 @@ else:
93
93
  # Given an empty database
94
94
  self.assertEqual(0, User.query.count())
95
95
  # When calling "/add/" to create a user
96
- self.getPage('/add', method='POST', body=urlencode([('username', 'myuser')]))
96
+ self.getPage('/add', method='POST', body=urlencode({'username': 'myuser'}))
97
97
  self.assertStatus(200)
98
98
  self.assertInBody('OK')
99
99
  # Then user get added to database
@@ -103,7 +103,7 @@ else:
103
103
  # Given a database with a users
104
104
  User(username='user1').add().commit()
105
105
  # When trying to add another user
106
- self.getPage('/add', method='POST', body=urlencode([('username', 'user1')]))
106
+ self.getPage('/add', method='POST', body=urlencode({'username': 'user1'}))
107
107
  # Then an error get raised
108
108
  self.assertStatus(200)
109
109
  self.assertInBody('user_username_unique_ix')
@@ -1,5 +1,5 @@
1
1
  # Scheduler plugins for Cherrypy
2
- # Copyright (C) 2022-2025 IKUS Software
2
+ # Copyright (C) 2025 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,79 +14,87 @@
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
- Created on Oct 17, 2015
19
-
20
- @author: Patrik Dufresne <patrik@ikus-soft.com>
21
- """
22
- import importlib.util
23
- import unittest
24
- from time import sleep
17
+ from datetime import datetime, timezone
18
+ from threading import Event
25
19
 
26
20
  import cherrypy
27
21
  from cherrypy.test import helper
28
22
 
29
- HAS_APSCHEDULER = importlib.util.find_spec("apscheduler") is not None
23
+ from .. import scheduler # noqa
24
+
25
+ done = Event()
30
26
 
31
- if HAS_APSCHEDULER:
32
- from .. import scheduler # noqa
27
+
28
+ def a_task(*args, **kwargs):
29
+ done.set()
33
30
 
34
31
 
35
- @unittest.skipUnless(HAS_APSCHEDULER, "apscheduler not installed")
36
32
  class SchedulerPluginTest(helper.CPWebCase):
37
33
  def setUp(self) -> None:
38
- self.called = False
34
+ done.clear()
35
+ cherrypy.scheduler.remove_all_jobs()
39
36
  return super().setUp()
40
37
 
38
+ def tearDown(self):
39
+ return super().tearDown()
40
+
41
41
  @classmethod
42
42
  def setup_server(cls):
43
43
  pass
44
44
 
45
- def test_schedule_job(self):
46
- # Given a scheduler with a specific number of jobs
47
- count = len(cherrypy.scheduler.list_jobs())
48
-
49
- # Given a job schedule every seconds
50
- def a_job(*args, **kwargs):
51
- self.called = True
45
+ def test_add_job(self):
46
+ # Given a scheduled job
47
+ scheduled = cherrypy.engine.publish(
48
+ 'scheduler:add_job',
49
+ a_task,
50
+ name='custom_name',
51
+ args=(1, 2, 3),
52
+ kwargs={'foo': 'bar'},
53
+ next_run_time=datetime.now(timezone.utc),
54
+ misfire_grace_time=None,
55
+ )
56
+ self.assertTrue(scheduled)
57
+ self.assertEqual('custom_name', scheduled[0].name)
58
+ # When waiting for all jobs
59
+ cherrypy.scheduler.wait_for_jobs()
60
+ # Then the job is done
61
+ self.assertTrue(done.is_set())
52
62
 
53
- scheduled = cherrypy.engine.publish('schedule_job', '23:00', a_job, 1, 2, 3, foo=1, bar=2)
63
+ def test_add_job_daily(self):
64
+ # Given a scheduler with a specific number of jobs
65
+ count = len(cherrypy.scheduler.get_jobs())
66
+ # When scheduling a daily job.
67
+ scheduled = cherrypy.engine.publish('scheduler:add_job_daily', '23:00', a_task, 1, 2, 3, foo=1, bar=2)
68
+ # Then the job is scheduled
54
69
  self.assertTrue(scheduled)
55
- self.assertEqual(count + 1, len(cherrypy.scheduler.list_jobs()))
70
+ self.assertEqual('a_task', scheduled[0].name)
71
+ # Then the number of jobs increase.
72
+ self.assertEqual(count + 1, len(cherrypy.scheduler.get_jobs()))
56
73
 
57
- def test_scheduler_task(self):
74
+ def test_add_job_now(self):
58
75
  # Given a task
59
-
60
- def a_task(*args, **kwargs):
61
- self.called = True
62
-
63
76
  # When scheduling that task
64
- scheduled = cherrypy.engine.publish('schedule_task', a_task, 1, 2, 3, foo=1, bar=2)
77
+ scheduled = cherrypy.engine.publish('scheduler:add_job_now', a_task, 1, 2, 3, foo=1, bar=2)
65
78
  self.assertTrue(scheduled)
66
- sleep(1)
67
- while len(cherrypy.scheduler.list_tasks()) >= 1:
68
- sleep(1)
79
+ # When waiting for all tasks
80
+ cherrypy.scheduler.wait_for_jobs()
69
81
  # Then the task get called
70
- self.assertTrue(self.called)
82
+ self.assertTrue(done.is_set())
71
83
 
72
- def test_unschedule_job(self):
84
+ def test_remove_job(self):
73
85
  # Given a scheduler with a specific number of jobs
74
- count = len(cherrypy.scheduler.list_jobs())
75
-
86
+ count = len(cherrypy.scheduler.get_jobs())
76
87
  # Given a job schedule every seconds
77
- def a_job(*args, **kwargs):
78
- self.called = True
79
-
80
- cherrypy.engine.publish('schedule_job', '23:00', a_job, 1, 2, 3, foo=1, bar=2)
81
- self.assertEqual(count + 1, len(cherrypy.scheduler.list_jobs()))
82
- cherrypy.engine.publish('unschedule_job', a_job)
83
- self.assertEqual(count, len(cherrypy.scheduler.list_jobs()))
84
-
85
- def test_unschedule_job_with_invalid_job(self):
88
+ cherrypy.engine.publish('scheduler:add_job_daily', '23:00', a_task, 1, 2, 3, foo=1, bar=2)
89
+ # Then number of job increase
90
+ self.assertEqual(count + 1, len(cherrypy.scheduler.get_jobs()))
91
+ # When the job is unscheduled.
92
+ cherrypy.engine.publish('scheduler:remove_job', a_task)
93
+ # Then the number of job decrease.
94
+ self.assertEqual(count, len(cherrypy.scheduler.get_jobs()))
95
+
96
+ def test_remove_job_with_invalid_job(self):
86
97
  # Given an unschedule job
87
- def a_job(*args, **kwargs):
88
- self.called = True
89
-
90
98
  # When unscheduling an invalid job
91
- cherrypy.engine.publish('unschedule_job', a_job)
92
- # Then no error are raised
99
+ cherrypy.engine.publish('scheduler:remove_job', a_task)
100
+ # Then an error is not raised.
@@ -63,12 +63,15 @@ class SmtpPluginTest(helper.CPWebCase):
63
63
 
64
64
  @skipUnless(hasattr(cherrypy, 'scheduler'), reason='Required scheduler')
65
65
  def test_queue_mail(self):
66
- # Given a paused scheduler plugin
67
- cherrypy.scheduler._scheduler.pause()
68
- # When queueing a email
69
- cherrypy.engine.publish('queue_mail', to='target@test.com', subject='subjet', message='body')
70
- # Then a new job get schedule
71
- self.assertEqual(1, len(cherrypy.scheduler.list_tasks()))
66
+ with mock.patch(smtp.__name__ + '.smtplib') as smtplib:
67
+ # Given a mail being queued.
68
+ cherrypy.engine.publish('queue_mail', to='target@test.com', subject='subjet', message='body')
69
+ # When waiting for all task to be processed
70
+ cherrypy.scheduler.wait_for_jobs()
71
+ # Then smtplib is called to send the mail.
72
+ smtplib.SMTP.assert_called_once_with('__default__', 25)
73
+ smtplib.SMTP.return_value.send_message.assert_called_once_with(mock.ANY)
74
+ smtplib.SMTP.return_value.quit.assert_called_once_with()
72
75
 
73
76
  def test_html2plaintext(self):
74
77
  """
@@ -0,0 +1,10 @@
1
+ <html>
2
+ <head>
3
+ <title>test-form</title>
4
+ </head>
5
+ <body>
6
+ <form method="post" action="/">
7
+ <Fields form={{ form }} />
8
+ </form>
9
+ </body>
10
+ </html>
@@ -0,0 +1,119 @@
1
+ # CherryPy Foundation
2
+ # Copyright (C) 2025 IKUS Software
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 importlib
17
+ from unittest import skipUnless
18
+
19
+ import cherrypy
20
+ from cherrypy.test import helper
21
+ from wtforms.fields import BooleanField, PasswordField, StringField, SubmitField
22
+ from wtforms.validators import InputRequired, Length
23
+
24
+ import cherrypy_foundation.tools.jinja2 # noqa
25
+ from cherrypy_foundation.form import CherryForm
26
+ from cherrypy_foundation.tools.i18n import gettext_lazy as _
27
+
28
+ HAS_JINJAX = importlib.util.find_spec("jinjax") is not None
29
+
30
+ env = cherrypy.tools.jinja2.create_env(
31
+ package_name=__package__,
32
+ globals={
33
+ 'const1': 'STATIC VALUE',
34
+ },
35
+ )
36
+
37
+
38
+ class LoginForm(CherryForm):
39
+ login = StringField(
40
+ _('User'),
41
+ validators=[
42
+ InputRequired(),
43
+ Length(max=256, message=_('User too long.')),
44
+ ],
45
+ render_kw={
46
+ "placeholder": _('User'),
47
+ "autocorrect": "off",
48
+ "autocapitalize": "none",
49
+ "autocomplete": "off",
50
+ "autofocus": "autofocus",
51
+ },
52
+ )
53
+ password = PasswordField(
54
+ _('Password'),
55
+ validators=[
56
+ InputRequired(),
57
+ Length(max=256, message=_('Password too long.')),
58
+ ],
59
+ render_kw={"placeholder": _("Password")},
60
+ )
61
+ persistent = BooleanField(
62
+ _('Remember me'),
63
+ # All `label-*` are assigned to the label tag.
64
+ render_kw={'container_class': 'col-sm-6', 'label-attr': 'FOO'},
65
+ )
66
+ submit = SubmitField(
67
+ _('Login'),
68
+ # All `container-*` are assigned to the container tag.
69
+ render_kw={"class": "btn-primary float-end", 'container_class': 'col-sm-6', 'container-attr': 'BAR'},
70
+ )
71
+
72
+
73
+ @cherrypy.tools.jinja2(env=env)
74
+ class Root:
75
+
76
+ @cherrypy.expose
77
+ @cherrypy.tools.jinja2(template='test_form.html')
78
+ def index(self):
79
+ form = LoginForm()
80
+ return {'form': form}
81
+
82
+
83
+ @skipUnless(HAS_JINJAX, reason='Required jinjax')
84
+ class FormTest(helper.CPWebCase):
85
+ default_lang = None
86
+ interactive = False
87
+
88
+ @classmethod
89
+ def setup_server(cls):
90
+ cherrypy.tree.mount(Root(), '/')
91
+
92
+ def test_get_form(self):
93
+ # Given a form
94
+ # When querying the page that include this form
95
+ self.getPage("/")
96
+ self.assertStatus(200)
97
+ # Then each field is render properly.
98
+ # 1. Check title
99
+ self.assertInBody('test-form')
100
+ # 2. Check user field
101
+ self.assertInBody('<label class="form-label" for="login">User</label>')
102
+ self.assertInBody(
103
+ '<input autocapitalize="none" autocomplete="off" autocorrect="off" autofocus="autofocus" class="form-control" id="login" maxlength="256" name="login" placeholder="User" required type="text" value="">'
104
+ )
105
+ # 3. Check password
106
+ self.assertInBody('<label class="form-label" for="password">Password</label>')
107
+ self.assertInBody(
108
+ '<input class="form-control" id="password" maxlength="256" name="password" placeholder="Password" required type="password" value="">'
109
+ )
110
+ # 4 Check remember me
111
+ self.assertInBody(
112
+ '<input class="form-check-input" container-class="col-sm-6" id="persistent" label-attr="FOO" name="persistent" type="checkbox" value="y">'
113
+ )
114
+ self.assertInBody('<label attr="FOO" class="form-check-label" for="persistent">Remember me</label>')
115
+ # 5. check submit button
116
+ self.assertInBody('<div attr="BAR" class="mb-2 form-field col-sm-6">')
117
+ self.assertInBody(
118
+ '<input class="btn-primary float-end btn" container-attr="BAR" container-class="col-sm-6" id="submit" name="submit" type="submit" value="Login">'
119
+ )
@@ -29,9 +29,6 @@ AUTH_DEFAULT_SESSION_KEY = "_auth_session_key"
29
29
  AUTH_DEFAULT_REAUTH_TIMEOUT = 60 # minutes
30
30
 
31
31
 
32
- logger = logging.getLogger(__name__)
33
-
34
-
35
32
  class AuthManager(cherrypy.Tool):
36
33
  """
37
34
  CherryPy tool handling authentication.
@@ -102,7 +99,12 @@ class AuthManager(cherrypy.Tool):
102
99
  try:
103
100
  currentuser = user_from_key_func(user_key)
104
101
  except Exception:
105
- logger.exception('error while resolving user from key')
102
+ cherrypy.log(
103
+ f'unexpected error searching for user_key={user_key}',
104
+ context='AUTH',
105
+ severity=logging.ERROR,
106
+ traceback=True,
107
+ )
106
108
  currentuser = None
107
109
 
108
110
  if currentuser:
@@ -122,7 +124,7 @@ class AuthManager(cherrypy.Tool):
122
124
  Validate credentials with configured checkers; on success, call login_with_result.
123
125
  """
124
126
  if not login or not password:
125
- logger.warning('empty login or password provided')
127
+ cherrypy.log('authentication failed reason=empty_credentials', context='AUTH', severity=logging.WARNING)
126
128
  return None
127
129
  # Validate credentials using checkpassword function(s).
128
130
  conf = self._merged_args()
@@ -143,15 +145,23 @@ class AuthManager(cherrypy.Tool):
143
145
  login, user_info = login, None
144
146
  # If authentication is successful, initiate login process.
145
147
  return self.login_with_result(login=login, user_info=user_info)
146
- except Exception as e:
147
- logger.exception(
148
- 'unexpected error during authentication for [%s] with [%s]: %s', login, func.__qualname__, e
148
+ except Exception:
149
+ cherrypy.log(
150
+ f'unexpected error checking password login={login} checkpassword={func.__qualname__} - continue with next function',
151
+ context='AUTH',
152
+ severity=logging.ERROR,
153
+ traceback=True,
149
154
  )
150
155
  # If we reach here, authentication failed
151
156
  if hasattr(cherrypy.serving, 'session'):
152
157
  cherrypy.serving.session.regenerate() # Prevent session analysis
153
158
 
154
- logger.warning('Failed login attempt for user=%s from ip=%s', login, cherrypy.serving.request.remote.ip)
159
+ remote_ip = cherrypy.serving.request.remote.ip
160
+ cherrypy.log(
161
+ f'authentication failed login={login} ip={remote_ip} reason=wrong_credentials',
162
+ context='AUTH',
163
+ severity=logging.WARNING,
164
+ )
155
165
 
156
166
  return None
157
167
 
@@ -166,11 +176,18 @@ class AuthManager(cherrypy.Tool):
166
176
  try:
167
177
  user_key, userobj = user_lookup_func(login=login, user_info=user_info or {})
168
178
  except Exception:
169
- logger.exception('failed to lookup user object for login=%s', login)
179
+ cherrypy.log(
180
+ f"unexpected error searching user login={login} user_info={user_info}",
181
+ context='AUTH',
182
+ severity=logging.ERROR,
183
+ traceback=True,
184
+ )
170
185
  return None
171
186
 
172
187
  if not userobj:
173
- logger.warning('failed to lookup user object for login=%s', login)
188
+ cherrypy.log(
189
+ f"authentication failed login={login} reason=not_found", context='AUTH', severity=logging.WARNING
190
+ )
174
191
  return None
175
192
 
176
193
  # Notify plugins about user login
@@ -23,14 +23,12 @@ import cherrypy
23
23
 
24
24
  from ..passwd import check_password, hash_password
25
25
 
26
- logger = logging.getLogger(__name__)
27
-
28
26
  MFA_CODE = '_auth_mfa_code'
29
27
  MFA_CODE_TIME = '_auth_mfa_code_time'
30
28
  MFA_CODE_ATTEMPT = '_auth_mfa_code_attempt'
31
29
  MFA_REDIRECT_URL = '_auth_mfa_redirect_url'
32
30
  MFA_TRUSTED_IP_LIST = '_auth_mfa_trusted_ip_list'
33
- MFA_USERNAME = '_auth_mfa_username'
31
+ MFA_USER_KEY = '_auth_mfa_user_key'
34
32
  MFA_VERIFICATION_TIME = '_auth_mfa_time'
35
33
 
36
34
  MFA_DEFAULT_CODE_TIMEOUT = 10 # minutes
@@ -86,7 +84,7 @@ class CheckAuthMfa(cherrypy.Tool):
86
84
  length = self._code_length()
87
85
  code = ''.join(secrets.choice(string.digits) for _ in range(length))
88
86
  session = cherrypy.serving.session
89
- session[MFA_USERNAME] = cherrypy.request.login
87
+ session[MFA_USER_KEY] = cherrypy.request.login
90
88
  session[MFA_CODE] = hash_password(code)
91
89
  session[MFA_CODE_TIME] = session.now()
92
90
  session[MFA_CODE_ATTEMPT] = 0
@@ -99,7 +97,7 @@ class CheckAuthMfa(cherrypy.Tool):
99
97
 
100
98
  # Check if our username match current login user.
101
99
  session = cherrypy.serving.session
102
- if session.get(MFA_USERNAME) != cherrypy.request.login:
100
+ if session.get(MFA_USER_KEY) != cherrypy.request.login:
103
101
  return False
104
102
 
105
103
  # Check if the current IP is trusted
@@ -128,7 +126,7 @@ class CheckAuthMfa(cherrypy.Tool):
128
126
  return True
129
127
 
130
128
  session = cherrypy.serving.session
131
- if session.get(MFA_USERNAME) != cherrypy.request.login:
129
+ if session.get(MFA_USER_KEY) != cherrypy.request.login:
132
130
  return True
133
131
 
134
132
  # Check if code is defined.
@@ -208,11 +206,10 @@ class CheckAuthMfa(cherrypy.Tool):
208
206
  if is_expired or not code_valid:
209
207
  if not is_expired: # Only increment if not expired
210
208
  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,
209
+ cherrypy.log(
210
+ f'verification failed user={cherrypy.request.login} ip={cherrypy.request.remote.ip} attempts={attempts}',
211
+ context='MFA',
212
+ severity=logging.WARNING,
216
213
  )
217
214
  return False
218
215
 
@@ -239,8 +236,8 @@ class CheckAuthMfa(cherrypy.Tool):
239
236
 
240
237
  # Rotate session id to prevent fixation
241
238
  session.regenerate()
242
- logger.info(
243
- 'Successful MFA verification for user=%s from ip=%s', cherrypy.request.login, cherrypy.request.remote.ip
239
+ cherrypy.log(
240
+ f'verification successful user={cherrypy.request.login} ip={cherrypy.request.remote.ip}', context='MFA'
244
241
  )
245
242
  return True
246
243
 
@@ -1,4 +1,4 @@
1
- # udb, A web interface to manage IT network
1
+ # Cherrypy Foundation
2
2
  # Copyright (C) 2025 IKUS Software inc.
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify