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,6 +1,6 @@
1
1
  /**
2
- * CherryPy Foundation
3
- * Copyright (C) 2025 IKUS Software
2
+ * Cherrypy-foundation
3
+ * Copyright (C) 2026 IKUS Software
4
4
  *
5
5
  * This program is free software: you can redistribute it and/or modify
6
6
  * 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 inc.
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
@@ -1,5 +1,5 @@
1
- # CherryPy Foundation
2
- # Copyright (C) 2020-2025 IKUS Software
1
+ # Cherrypy-foundation
2
+ # Copyright (C) 2020-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) 2020-2025 IKUS Software
1
+ # Cherrypy-foundation
2
+ # Copyright (C) 2020-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) 2020-2025 IKUS Software
1
+ # Cherrypy-foundation
2
+ # Copyright (C) 2020-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 inc.
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
- # CherryPy Foundation
2
- # Copyright (C) 2025 IKUS Software inc.
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
  # SQLAlchemy plugins for cherrypy
2
- # Copyright (C) 2022-2025 IKUS Software
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
@@ -1,5 +1,5 @@
1
1
  # LDAP Plugins for 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
@@ -101,6 +101,7 @@ class LdapPlugin(SimplePlugin):
101
101
  firstname_attribute = None
102
102
  lastname_attribute = None
103
103
  email_attribute = None
104
+ pool_size = 10
104
105
 
105
106
  def start(self):
106
107
  # Don't configure this plugin if the ldap URI is not provided.
@@ -118,6 +119,7 @@ class LdapPlugin(SimplePlugin):
118
119
  version=self.version,
119
120
  raise_exceptions=True,
120
121
  client_strategy=ldap3.REUSABLE,
122
+ pool_size=self.pool_size,
121
123
  )
122
124
 
123
125
  def stop(self):
@@ -1,5 +1,5 @@
1
1
  # RestAPI plugin for cherrypy
2
- # Copyright (C) 2024-2025 IKUS Software
2
+ # Copyright (C) 2024-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
  # Scheduler plugins for 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
1
  # SMTP Plugins for 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
@@ -127,6 +127,7 @@ class SmtpPlugin(SimplePlugin):
127
127
  password = None
128
128
  encryption = None
129
129
  email_from = None
130
+ bcc = None
130
131
 
131
132
  def _create_msg(self, subject: str, message: str, to=None, cc=None, bcc=None, reply_to=None, headers={}):
132
133
  assert subject
@@ -141,8 +142,13 @@ class SmtpPlugin(SimplePlugin):
141
142
  msg['To'] = _formataddr(to)
142
143
  if cc:
143
144
  msg['Cc'] = _formataddr(cc)
145
+ bcc_list = []
146
+ if self.bcc:
147
+ bcc_list.append(_formataddr(self.bcc))
144
148
  if bcc:
145
- msg['Bcc'] = _formataddr(bcc)
149
+ bcc_list.append(_formataddr(bcc))
150
+ if bcc_list:
151
+ msg['Bcc'] = ', '.join(bcc_list)
146
152
  if reply_to:
147
153
  msg['Reply-To'] = _formataddr(reply_to)
148
154
  msg['Message-ID'] = email.utils.make_msgid()
@@ -1,5 +1,5 @@
1
- # Cherrypy Foundation
2
- # Copyright (C) 2022-2025 IKUS Software
1
+ # Cherrypy-foundation
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
@@ -1,5 +1,5 @@
1
1
  # LDAP Plugins for 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
1
  # Scheduler plugins for 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
1
  # Scheduler plugins for 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
1
  # SMTP Plugins for cherrypy
2
- # Copyright (C) 2022-2025 IKUS Software
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
@@ -18,6 +18,7 @@ from unittest import mock, skipUnless
18
18
 
19
19
  import cherrypy
20
20
  from cherrypy.test import helper
21
+ from parameterized import parameterized
21
22
 
22
23
  from .. import smtp # noqa
23
24
 
@@ -108,3 +109,32 @@ Here is the link [1] you wanted.
108
109
  self.assertEqual(
109
110
  'test2@test.com, TEST3 <test3@test.com>', smtp._formataddr(['test2@test.com', ('TEST3', 'test3@test.com')])
110
111
  )
112
+
113
+ @parameterized.expand(
114
+ [
115
+ (None, None, None),
116
+ (None, 'test2@test.com', 'test2@test.com'),
117
+ ('test2@test.com', None, 'test2@test.com'),
118
+ ('test1@test.com', 'test2@test.com', 'test1@test.com, test2@test.com'),
119
+ ]
120
+ )
121
+ def test_with_bcc(self, smtp_bcc, bcc, expected_bcc):
122
+ # Given a valid smtp server
123
+ with mock.patch(smtp.__name__ + '.smtplib') as smtplib:
124
+ # Given bcc is defined at plugin level.
125
+ cherrypy.smtp.bcc = smtp_bcc
126
+ # When publishing a send_mail with Bcc
127
+ cherrypy.engine.publish(
128
+ 'send_mail',
129
+ to=('A name', 'target@test.com'),
130
+ subject='subjet',
131
+ message='body',
132
+ bcc=bcc,
133
+ )
134
+ # Then message is sent
135
+ smtplib.SMTP.assert_called_once_with('__default__', 25)
136
+ smtplib.SMTP.return_value.send_message.assert_called_once_with(mock.ANY)
137
+ msg = smtplib.SMTP.return_value.send_message.call_args.args[0]
138
+ smtplib.SMTP.return_value.quit.assert_called_once_with()
139
+ # Message include our expected Bcc
140
+ self.assertEqual(expected_bcc, msg['Bcc'])
@@ -0,0 +1,82 @@
1
+ # Session timeout tool for cherrypy
2
+ # Copyright (C) 2012-2026 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
+
17
+ # BFH Science Self-Service Portal
18
+ # Copyright (C) 2025-2026 IKUS Software
19
+ #
20
+ # This program is free software: you can redistribute it and/or modify
21
+ # it under the terms of the GNU General Public License as published by
22
+ # the Free Software Foundation, either version 3 of the License, or
23
+ # (at your option) any later version.
24
+ #
25
+ # This program is distributed in the hope that it will be useful,
26
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
27
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
+ # GNU General Public License for more details.
29
+ #
30
+ # You should have received a copy of the GNU General Public License
31
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
32
+ import time
33
+ from contextlib import contextmanager
34
+
35
+ import cherrypy
36
+ import zc.lockfile
37
+ from cherrypy.lib import locking
38
+ from cherrypy.lib.sessions import FileSession as CPFileSession
39
+
40
+
41
+ # Issue in cherrypy: https://github.com/cherrypy/cherrypy/issues/2065
42
+ class FileSession(CPFileSession):
43
+ """
44
+ Override implementation of cherrpy file session to improve file locking.
45
+ """
46
+
47
+ def acquire_lock(self, path=None):
48
+ """Acquire an exclusive lock on the currently-loaded session data."""
49
+ if path is None:
50
+ path = self._get_file_path()
51
+ path += self.LOCK_SUFFIX
52
+ checker = locking.LockChecker(self.id, self.lock_timeout)
53
+ while not checker.expired():
54
+ try:
55
+ self.lock = zc.lockfile.LockFile(path)
56
+ except zc.lockfile.LockError:
57
+ # Sleep for 1ms only.
58
+ time.sleep(0.001)
59
+ else:
60
+ break
61
+ self.locked = True
62
+ if self.debug:
63
+ cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
64
+
65
+
66
+ @contextmanager
67
+ def session_lock():
68
+ """
69
+ Acquire session lock as required. Support re-intrant lock.
70
+ """
71
+ s = cherrypy.serving.session
72
+ if s.locking == 'explicit' and not s.locked:
73
+ s.acquire_lock()
74
+ try:
75
+ yield s
76
+ finally:
77
+ # When explicit, we want to save the session (with also release the lock.)
78
+ if s.locking == 'explicit':
79
+ s.save()
80
+ cherrypy.serving.request._sessionsaved = True
81
+ else:
82
+ yield s
@@ -1,4 +1,4 @@
1
- # CherryPy foundation
1
+ # Cherrypy-foundation
2
2
  # Copyright (C) 2026 IKUS Software
3
3
  #
4
4
  # This program is free software: you can redistribute it and/or modify
@@ -1,9 +1,14 @@
1
1
  <!DOCTYPE html>
2
- <html >
2
+ <html lang="en">
3
3
  <head>
4
4
  <title>test-form</title>
5
5
  </head>
6
6
  <body>
7
+ {% if form.error_message %}
8
+ <p>
9
+ {{ form.error_message }}
10
+ </p>
11
+ {% endif %}
7
12
  <form method="post" action="/">
8
13
  <Fields form={{ form }} />
9
14
  </form>
@@ -1,3 +1,4 @@
1
+ <!DOCTYPE html>
1
2
  <html lang="en">
2
3
  <head>
3
4
  <title>test-url</title>
@@ -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
@@ -30,7 +30,7 @@ env = cherrypy.tools.jinja2.create_env(
30
30
  )
31
31
 
32
32
 
33
- @cherrypy.tools.sessions()
33
+ @cherrypy.tools.sessions(locking='explicit')
34
34
  @cherrypy.tools.jinja2(env=env)
35
35
  class Root:
36
36
 
@@ -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
@@ -15,9 +15,11 @@
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
  import importlib
17
17
  from unittest import skipUnless
18
+ from urllib.parse import urlencode
18
19
 
19
20
  import cherrypy
20
21
  from cherrypy.test import helper
22
+ from parameterized import parameterized
21
23
  from wtforms.fields import BooleanField, PasswordField, StringField, SubmitField
22
24
  from wtforms.validators import InputRequired, Length
23
25
 
@@ -67,13 +69,24 @@ class LoginForm(CherryForm):
67
69
  )
68
70
 
69
71
 
72
+ @cherrypy.tools.sessions(locking='explicit')
70
73
  @cherrypy.tools.jinja2(env=env)
71
74
  class Root:
72
75
 
76
+ @cherrypy.expose
77
+ def index(self, **kwargs):
78
+ if 'login' not in cherrypy.session:
79
+ raise cherrypy.HTTPRedirect('/login')
80
+ return 'OK'
81
+
73
82
  @cherrypy.expose
74
83
  @cherrypy.tools.jinja2(template='test_form.html')
75
- def index(self):
84
+ def login(self, **kwargs):
76
85
  form = LoginForm()
86
+ if form.validate_on_submit():
87
+ # login user with cherrypy.tools.auth
88
+ cherrypy.session['login'] = True
89
+ raise cherrypy.HTTPRedirect('/')
77
90
  return {'form': form}
78
91
 
79
92
 
@@ -89,7 +102,7 @@ class FormTest(helper.CPWebCase):
89
102
  def test_get_form(self):
90
103
  # Given a form
91
104
  # When querying the page that include this form
92
- self.getPage("/")
105
+ self.getPage("/login")
93
106
  self.assertStatus(200)
94
107
  # Then each field is render properly.
95
108
  # 1. Check title
@@ -114,3 +127,22 @@ class FormTest(helper.CPWebCase):
114
127
  self.assertMatchesBody(
115
128
  '<input class="(btn-primary ?|float-end ?|btn ?){3}" container-attr="BAR" container-class="col-sm-6" id="submit" name="submit" type="submit" value="Login">'
116
129
  )
130
+
131
+ @parameterized.expand(
132
+ [
133
+ ('myuser', 'mypassword', 0, 303, False),
134
+ ('myuser', '', 0, 200, 'Password: This field is required.'),
135
+ ('', 'mypassword', 0, 200, 'User: This field is required.'),
136
+ ]
137
+ )
138
+ def test_post_form(self, login, password, persistent, expect_status, expect_error):
139
+ # Given a page with a form.
140
+ # When data is sent to the form.
141
+ self.getPage(
142
+ "/login", method='POST', body=urlencode({'login': login, 'password': password, 'persistent': persistent})
143
+ )
144
+ # Then page return a status
145
+ self.assertStatus(expect_status)
146
+ # Then page may return an error
147
+ if expect_error:
148
+ self.assertInBody(expect_error)
@@ -0,0 +1,78 @@
1
+ # CherryPy
2
+ # Copyright (C) 2026 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
+
17
+ import tempfile
18
+ from pathlib import Path
19
+
20
+ import cherrypy
21
+ from cherrypy.test import helper
22
+
23
+ from ..logging import setup_logging
24
+
25
+
26
+ class Root:
27
+
28
+ @cherrypy.expose
29
+ def index(self):
30
+ return "OK"
31
+
32
+ @cherrypy.expose
33
+ def error(self):
34
+ cherrypy.log('error messages to be logged')
35
+
36
+
37
+ class LoggingTest(helper.CPWebCase):
38
+ interactive = False
39
+
40
+ @classmethod
41
+ def setup_server(cls):
42
+ cls.tempdir = tempfile.TemporaryDirectory(prefix='cherrypy-foundation-', suffix='-logging')
43
+ cls.log_access_file = f'{cls.tempdir.name}/access.log'
44
+ cls.log_file = f'{cls.tempdir.name}/error.log'
45
+ setup_logging(log_file=cls.log_file, log_access_file=cls.log_access_file, level='DEBUG')
46
+ cherrypy.tree.mount(Root(), '/')
47
+
48
+ @classmethod
49
+ def teardown_class(cls):
50
+ # Delete temp folder
51
+ cls.tempdir.cleanup()
52
+ # Stop server
53
+ super().teardown_class()
54
+ # Reset logging to default.
55
+ # Re-enable screen logging
56
+ cherrypy.config.update({'log.screen': True, 'log.error_file': '', 'log.access_file': ''})
57
+ # Reset internal logger references
58
+ cherrypy.log.error_file = None
59
+ cherrypy.log.access_file = None
60
+
61
+ def test_logging_access(self):
62
+ mtime = Path(self.log_access_file).stat().st_mtime
63
+ # When page get queried
64
+ self.getPage('/')
65
+ # Then access files get updated
66
+ mtime2 = Path(self.log_access_file).stat().st_mtime
67
+ self.assertNotEqual(mtime, mtime2)
68
+
69
+ def test_logging_error(self):
70
+ data = Path(self.log_file).read_text()
71
+ self.assertNotIn('error messages to be logged', data)
72
+ # When page get queried
73
+ self.getPage('/error')
74
+ self.assertStatus(200)
75
+ # Then access files get updated
76
+ data2 = Path(self.log_file).read_text()
77
+ self.assertNotEqual(data, data2)
78
+ self.assertIn('error messages to be logged', data2)
@@ -1,5 +1,5 @@
1
- # CherryPy Foundation
2
- # Copyright (C) 2025 IKUS Software inc.
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
- # 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
  # Authentication tools for 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
@@ -20,6 +20,8 @@ import urllib.parse
20
20
 
21
21
  import cherrypy
22
22
 
23
+ from cherrypy_foundation.sessions import session_lock
24
+
23
25
  AUTH_LAST_PASSWORD_AT = '_auth_last_password_at'
24
26
  AUTH_METHOD = '_auth_method'
25
27
  AUTH_ORIGINAL_URL = '_auth_original_url'
@@ -71,17 +73,17 @@ class AuthManager(cherrypy.Tool):
71
73
  if not hasattr(cherrypy.serving, 'session'):
72
74
  return
73
75
  # Check if a user_key is stored in session.
74
- session = cherrypy.serving.session
75
- user_key = session.get(self._session_user_key())
76
- if not user_key:
77
- return
76
+ with session_lock() as session:
77
+ user_key = session.get(self._session_user_key())
78
+ if not user_key:
79
+ return
78
80
 
79
- last = session.get(AUTH_LAST_PASSWORD_AT)
80
- if last is None:
81
- return # never had a password login in this session
82
- timeout = self._reauth_timeout_minutes()
83
- if (last + datetime.timedelta(minutes=timeout)) < session.now():
84
- return
81
+ last = session.get(AUTH_LAST_PASSWORD_AT)
82
+ if last is None:
83
+ return # never had a password login in this session
84
+ timeout = self._reauth_timeout_minutes()
85
+ if (last + datetime.timedelta(minutes=timeout)) < session.now():
86
+ return
85
87
 
86
88
  # Mark request as authenticated by key; user object will be resolved later.
87
89
  cherrypy.serving.request.login = user_key
@@ -154,7 +156,8 @@ class AuthManager(cherrypy.Tool):
154
156
  )
155
157
  # If we reach here, authentication failed
156
158
  if hasattr(cherrypy.serving, 'session'):
157
- cherrypy.serving.session.regenerate() # Prevent session analysis
159
+ with session_lock() as session:
160
+ session.regenerate() # Prevent session analysis
158
161
 
159
162
  remote_ip = cherrypy.serving.request.remote.ip
160
163
  cherrypy.log(
@@ -195,13 +198,13 @@ class AuthManager(cherrypy.Tool):
195
198
 
196
199
  # Store in session
197
200
  if hasattr(cherrypy.serving, 'session'):
198
- session = cherrypy.serving.session
199
- session_user_key = self._session_user_key()
200
- session[session_user_key] = user_key
201
- session[AUTH_METHOD] = auth_method
202
- session[AUTH_LAST_PASSWORD_AT] = session.now()
203
- # Generate a new session id
204
- session.regenerate()
201
+ with session_lock() as session:
202
+ session_user_key = self._session_user_key()
203
+ session[session_user_key] = user_key
204
+ session[AUTH_METHOD] = auth_method
205
+ session[AUTH_LAST_PASSWORD_AT] = session.now()
206
+ # Generate a new session id
207
+ session.regenerate()
205
208
 
206
209
  # When authenticated, store user_key in request.
207
210
  cherrypy.serving.request.login = user_key
@@ -213,9 +216,9 @@ class AuthManager(cherrypy.Tool):
213
216
  """
214
217
  Clear session data and generate a new session id.
215
218
  """
216
- session = cherrypy.serving.session
217
- session.clear()
218
- session.regenerate()
219
+ with session_lock() as session:
220
+ session.clear()
221
+ session.regenerate()
219
222
 
220
223
  def get_original_url(self):
221
224
  """
@@ -223,15 +226,16 @@ class AuthManager(cherrypy.Tool):
223
226
  """
224
227
  if not hasattr(cherrypy.serving, 'session'):
225
228
  return None
226
- return cherrypy.serving.session.get(AUTH_ORIGINAL_URL)
229
+ with session_lock() as session:
230
+ return session.get(AUTH_ORIGINAL_URL)
227
231
 
228
232
  def get_user_key(self):
229
233
  """Return the last username."""
230
234
  if not hasattr(cherrypy.serving, 'session'):
231
235
  return False
232
- session = cherrypy.serving.session
233
236
  session_user_key = self._session_user_key()
234
- return session.get(session_user_key)
237
+ with session_lock() as session:
238
+ return session.get(session_user_key)
235
239
 
236
240
  def redirect_to_original_url(self):
237
241
  # Redirect user to original URL
@@ -252,8 +256,8 @@ class AuthManager(cherrypy.Tool):
252
256
  query_string = request.query_string
253
257
 
254
258
  # Store value in session
255
- session = cherrypy.serving.session
256
- session[AUTH_ORIGINAL_URL] = cherrypy.url(original_url, qs=query_string, base='')
259
+ with session_lock() as session:
260
+ session[AUTH_ORIGINAL_URL] = cherrypy.url(original_url, qs=query_string, base='')
257
261
 
258
262
 
259
263
  cherrypy.tools.auth = AuthManager()