cherrypy-foundation 1.0.0__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 (136) hide show
  1. cherrypy_foundation/__init__.py +0 -0
  2. cherrypy_foundation/components/ColorModes.jinja +70 -0
  3. cherrypy_foundation/components/Datatable.css +47 -0
  4. cherrypy_foundation/components/Datatable.jinja +63 -0
  5. cherrypy_foundation/components/Datatable.js +358 -0
  6. cherrypy_foundation/components/Field.css +10 -0
  7. cherrypy_foundation/components/Field.jinja +66 -0
  8. cherrypy_foundation/components/Field.js +56 -0
  9. cherrypy_foundation/components/Fields.jinja +4 -0
  10. cherrypy_foundation/components/Flash.jinja +13 -0
  11. cherrypy_foundation/components/Icon.jinja +3 -0
  12. cherrypy_foundation/components/LocaleSelection.jinja +13 -0
  13. cherrypy_foundation/components/LocaleSelection.js +26 -0
  14. cherrypy_foundation/components/SideBySideMultiSelect.css +25 -0
  15. cherrypy_foundation/components/SideBySideMultiSelect.jinja +9 -0
  16. cherrypy_foundation/components/SideBySideMultiSelect.js +9 -0
  17. cherrypy_foundation/components/Typeahead.css +55 -0
  18. cherrypy_foundation/components/Typeahead.jinja +106 -0
  19. cherrypy_foundation/components/Typeahead.js +8 -0
  20. cherrypy_foundation/components/__init__.py +51 -0
  21. cherrypy_foundation/components/tests/__init__.py +0 -0
  22. cherrypy_foundation/components/tests/test_static.py +90 -0
  23. cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css +2106 -0
  24. cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.min.css +5 -0
  25. cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  26. cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  27. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css +9262 -0
  28. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css.map +95 -0
  29. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css +6 -0
  30. cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css.map +7 -0
  31. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js +4846 -0
  32. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js.map +1 -0
  33. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js +7 -0
  34. cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js.map +7 -0
  35. cherrypy_foundation/components/vendor/bootstrap5/js/color-modes.js +80 -0
  36. cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.css +849 -0
  37. cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.min.css +1 -0
  38. cherrypy_foundation/components/vendor/datatables/images/sort_asc.png +0 -0
  39. cherrypy_foundation/components/vendor/datatables/images/sort_asc_disabled.png +0 -0
  40. cherrypy_foundation/components/vendor/datatables/images/sort_both.png +0 -0
  41. cherrypy_foundation/components/vendor/datatables/images/sort_desc.png +0 -0
  42. cherrypy_foundation/components/vendor/datatables/images/sort_desc_disabled.png +0 -0
  43. cherrypy_foundation/components/vendor/datatables/js/dataTables.js +14073 -0
  44. cherrypy_foundation/components/vendor/datatables/js/dataTables.min.js +4 -0
  45. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.css +556 -0
  46. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css +1 -0
  47. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.js +1700 -0
  48. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.min.js +8 -0
  49. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.js +2944 -0
  50. cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js +4 -0
  51. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css +13 -0
  52. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.min.css +1 -0
  53. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.js +1202 -0
  54. cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js +4 -0
  55. cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.js +11577 -0
  56. cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.min.js +13 -0
  57. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.css +194 -0
  58. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css +1 -0
  59. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.js +1861 -0
  60. cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js +4 -0
  61. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.js +75023 -0
  62. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.min.js +3 -0
  63. cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/vfs_fonts.js +6 -0
  64. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.css +53 -0
  65. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css +1 -0
  66. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js +485 -0
  67. cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.min.js +4 -0
  68. cherrypy_foundation/components/vendor/jquery/jquery.min.js +2 -0
  69. cherrypy_foundation/components/vendor/multi/LICENSE +7 -0
  70. cherrypy_foundation/components/vendor/multi/README.md +109 -0
  71. cherrypy_foundation/components/vendor/multi/multi.css +95 -0
  72. cherrypy_foundation/components/vendor/multi/multi.js +328 -0
  73. cherrypy_foundation/components/vendor/popper/popper.js +1825 -0
  74. cherrypy_foundation/components/vendor/popper/popper.min.js +6 -0
  75. cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css +1 -0
  76. cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js +10 -0
  77. cherrypy_foundation/error_page.py +94 -0
  78. cherrypy_foundation/flash.py +50 -0
  79. cherrypy_foundation/form.py +119 -0
  80. cherrypy_foundation/logging.py +103 -0
  81. cherrypy_foundation/passwd.py +65 -0
  82. cherrypy_foundation/plugins/__init__.py +0 -0
  83. cherrypy_foundation/plugins/db.py +286 -0
  84. cherrypy_foundation/plugins/ldap.py +257 -0
  85. cherrypy_foundation/plugins/restapi.py +74 -0
  86. cherrypy_foundation/plugins/scheduler.py +287 -0
  87. cherrypy_foundation/plugins/smtp.py +223 -0
  88. cherrypy_foundation/plugins/tests/__init__.py +0 -0
  89. cherrypy_foundation/plugins/tests/test_db.py +118 -0
  90. cherrypy_foundation/plugins/tests/test_ldap.py +451 -0
  91. cherrypy_foundation/plugins/tests/test_scheduler.py +100 -0
  92. cherrypy_foundation/plugins/tests/test_scheduler_db.py +107 -0
  93. cherrypy_foundation/plugins/tests/test_smtp.py +140 -0
  94. cherrypy_foundation/sessions.py +93 -0
  95. cherrypy_foundation/tests/__init__.py +72 -0
  96. cherrypy_foundation/tests/templates/test_flash.html +9 -0
  97. cherrypy_foundation/tests/templates/test_form.html +16 -0
  98. cherrypy_foundation/tests/templates/test_url.html +15 -0
  99. cherrypy_foundation/tests/test_error_page.py +78 -0
  100. cherrypy_foundation/tests/test_flash.py +61 -0
  101. cherrypy_foundation/tests/test_form.py +148 -0
  102. cherrypy_foundation/tests/test_logging.py +78 -0
  103. cherrypy_foundation/tests/test_passwd.py +51 -0
  104. cherrypy_foundation/tests/test_sessions.py +89 -0
  105. cherrypy_foundation/tests/test_url.py +161 -0
  106. cherrypy_foundation/tools/__init__.py +0 -0
  107. cherrypy_foundation/tools/auth.py +263 -0
  108. cherrypy_foundation/tools/auth_mfa.py +249 -0
  109. cherrypy_foundation/tools/i18n.py +529 -0
  110. cherrypy_foundation/tools/jinja2.py +158 -0
  111. cherrypy_foundation/tools/ratelimit.py +265 -0
  112. cherrypy_foundation/tools/secure_headers.py +119 -0
  113. cherrypy_foundation/tools/sessions_timeout.py +167 -0
  114. cherrypy_foundation/tools/tests/__init__.py +0 -0
  115. cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
  116. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  117. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po +15 -0
  118. cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo +0 -0
  119. cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po +15 -0
  120. cherrypy_foundation/tools/tests/locales/messages.pot +2 -0
  121. cherrypy_foundation/tools/tests/templates/test_jinja2.html +11 -0
  122. cherrypy_foundation/tools/tests/templates/test_jinjax.html +9 -0
  123. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +22 -0
  124. cherrypy_foundation/tools/tests/test_auth.py +110 -0
  125. cherrypy_foundation/tools/tests/test_auth_mfa.py +369 -0
  126. cherrypy_foundation/tools/tests/test_i18n.py +247 -0
  127. cherrypy_foundation/tools/tests/test_jinja2.py +153 -0
  128. cherrypy_foundation/tools/tests/test_ratelimit.py +109 -0
  129. cherrypy_foundation/tools/tests/test_secure_headers.py +200 -0
  130. cherrypy_foundation/url.py +66 -0
  131. cherrypy_foundation/widgets.py +48 -0
  132. cherrypy_foundation-1.0.0.dist-info/METADATA +71 -0
  133. cherrypy_foundation-1.0.0.dist-info/RECORD +136 -0
  134. cherrypy_foundation-1.0.0.dist-info/WHEEL +5 -0
  135. cherrypy_foundation-1.0.0.dist-info/licenses/LICENSE.md +674 -0
  136. cherrypy_foundation-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,451 @@
1
+ # LDAP Plugins for 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
+ Created on Oct 17, 2015
18
+
19
+ @author: Patrik Dufresne <patrik@ikus-soft.com>
20
+ """
21
+ import os
22
+ from unittest import TestCase, mock, skipUnless
23
+
24
+ import cherrypy
25
+ import ldap3
26
+ from cherrypy.test import helper
27
+
28
+ from ..ldap import all_attribute, first_attribute # noqa
29
+
30
+ original_connection = ldap3.Connection
31
+
32
+
33
+ def mock_ldap_connection(*args, **kwargs):
34
+ kwargs.pop('client_strategy', None)
35
+ return original_connection(*args, client_strategy=ldap3.MOCK_ASYNC, **kwargs)
36
+
37
+
38
+ class LdapFirstAttributeTest(TestCase):
39
+
40
+ def test_no_keys_returns_default(self):
41
+ attributes = {"cn": ["John Doe"]}
42
+ self.assertIsNone(first_attribute(attributes, None))
43
+ self.assertEqual(first_attribute(attributes, [], default="fallback"), "fallback")
44
+
45
+ def test_single_key_with_scalar_value(self):
46
+ attributes = {"uid": "jdoe"}
47
+ self.assertEqual(first_attribute(attributes, "uid"), "jdoe")
48
+
49
+ def test_single_key_with_list_value(self):
50
+ attributes = {"mail": ["john@example.com", "alt@example.com"]}
51
+ self.assertEqual(first_attribute(attributes, "mail"), "john@example.com")
52
+
53
+ def test_empty_list_value_is_skipped(self):
54
+ attributes = {
55
+ "mail": [],
56
+ "uid": ["jdoe"],
57
+ }
58
+ self.assertEqual(first_attribute(attributes, ["mail", "uid"]), "jdoe")
59
+
60
+ def test_missing_first_key_uses_next_key(self):
61
+ attributes = {"cn": ["John Doe"]}
62
+ self.assertEqual(first_attribute(attributes, ["sn", "cn"]), "John Doe")
63
+
64
+ def test_all_keys_missing_returns_default(self):
65
+ attributes = {"cn": ["John Doe"]}
66
+ self.assertEqual(first_attribute(attributes, ["sn", "uid"], default="unknown"), "unknown")
67
+
68
+ def test_key_not_found_without_default_returns_none(self):
69
+ attributes = {"cn": ["John Doe"]}
70
+ self.assertIsNone(first_attribute(attributes, "uid"))
71
+
72
+
73
+ class LdapAllAttributeTest(TestCase):
74
+
75
+ def test_no_keys_returns_default(self):
76
+ attributes = {"cn": ["John Doe"]}
77
+ self.assertIsNone(all_attribute(attributes, None))
78
+ self.assertEqual(all_attribute(attributes, [], default="fallback"), "fallback")
79
+
80
+ def test_single_key_with_scalar_value(self):
81
+ attributes = {"uid": "jdoe"}
82
+ self.assertEqual(all_attribute(attributes, "uid"), ["jdoe"])
83
+
84
+ def test_single_key_with_list_value(self):
85
+ attributes = {"mail": ["john@example.com", "alt@example.com"]}
86
+ self.assertEqual(all_attribute(attributes, "mail"), ["john@example.com"])
87
+
88
+ def test_multiple_keys_collect_values(self):
89
+ attributes = {
90
+ "uid": "jdoe",
91
+ "cn": ["John Doe"],
92
+ }
93
+ self.assertEqual(all_attribute(attributes, ["uid", "cn"]), ["jdoe", "John Doe"])
94
+
95
+ def test_missing_keys_are_skipped(self):
96
+ attributes = {"cn": ["John Doe"]}
97
+ self.assertEqual(all_attribute(attributes, ["sn", "cn", "uid"]), ["John Doe"])
98
+
99
+ def test_all_keys_missing_returns_default(self):
100
+ attributes = {"cn": ["John Doe"]}
101
+ self.assertEqual(all_attribute(attributes, ["sn", "uid"], default=[]), [])
102
+
103
+ def test_empty_list_value_is_appended_as_empty_list(self):
104
+ attributes = {
105
+ "mail": [],
106
+ "uid": "jdoe",
107
+ }
108
+ self.assertEqual(all_attribute(attributes, ["mail", "uid"]), [[], "jdoe"])
109
+
110
+
111
+ class LdapPluginTest(helper.CPWebCase):
112
+
113
+ @classmethod
114
+ def setup_server(cls):
115
+ # Configure Mock server early.
116
+ cls.server = ldap3.Server('my_fake_server')
117
+ cls.patcher = mock.patch('ldap3.Connection', side_effect=mock_ldap_connection)
118
+ cls.patcher.start()
119
+ cherrypy.config.update(
120
+ {
121
+ 'ldap.uri': 'my_fake_server',
122
+ 'ldap.base_dn': 'dc=example,dc=org',
123
+ 'ldap.email_attribute': ['mail', 'email'],
124
+ 'ldap.fullname_attribute': ['displayName'],
125
+ 'ldap.firstname_attribute': ['givenName'],
126
+ 'ldap.lastname_attribute': ['sn'],
127
+ }
128
+ )
129
+
130
+ @classmethod
131
+ def teardown_class(cls):
132
+ cherrypy.ldap.uri = None
133
+ # Release mock server.
134
+ cls.patcher.stop()
135
+ return super().teardown_class()
136
+
137
+ def setUp(self) -> None:
138
+ # Clear LDAP entries before test.
139
+ for entry in list(cherrypy.ldap._pool.strategy.connection.server.dit):
140
+ if entry == 'cn=schema':
141
+ continue
142
+ cherrypy.ldap._pool.strategy.remove_entry(entry)
143
+ return super().setUp()
144
+
145
+ def test_authenticate(self):
146
+ # Given a user in LDAP
147
+ cherrypy.ldap._pool.strategy.add_entry(
148
+ 'cn=user01,dc=example,dc=org',
149
+ {
150
+ 'userPassword': 'password1',
151
+ 'uid': ['user01'],
152
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
153
+ },
154
+ )
155
+ # When authenticating with that user
156
+ authenticated = cherrypy.ldap.authenticate('user01', 'password1')
157
+ # Then user is authenticated
158
+ self.assertTrue(authenticated)
159
+ self.assertEqual(authenticated[0], 'user01')
160
+ self.assertEqual(
161
+ {
162
+ 'dn': 'cn=user01,dc=example,dc=org',
163
+ 'userPassword': ['password1'],
164
+ 'uid': ['user01'],
165
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
166
+ 'cn': ['user01'],
167
+ 'email': None,
168
+ 'fullname': '',
169
+ },
170
+ authenticated[1],
171
+ )
172
+
173
+ def test_authenticate_with_wildcard(self):
174
+ # Given a user in LDAP
175
+ cherrypy.ldap._pool.strategy.add_entry(
176
+ 'cn=user01,dc=example,dc=org',
177
+ {
178
+ 'userPassword': 'password1',
179
+ 'uid': ['user01'],
180
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
181
+ },
182
+ )
183
+ # When authenticating with a username containing wildcard
184
+ authenticated = cherrypy.ldap.authenticate('user*', 'password1')
185
+ # Then user is NOT authenticated
186
+ self.assertFalse(authenticated)
187
+
188
+ def test_authenticate_with_invalid_user(self):
189
+ # Given a user in LDAP
190
+ cherrypy.ldap._pool.strategy.add_entry(
191
+ 'cn=user01,dc=example,dc=org',
192
+ {
193
+ 'userPassword': 'password1',
194
+ 'uid': ['user01'],
195
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
196
+ },
197
+ )
198
+ # When authenticating with an invalid user
199
+ authenticated = cherrypy.ldap.authenticate('invalid', 'password1')
200
+ # Then user is authenticated
201
+ self.assertEqual(False, authenticated)
202
+
203
+ def test_authenticate_with_invalid_password(self):
204
+ # Given a user in LDAP
205
+ cherrypy.ldap._pool.strategy.add_entry(
206
+ 'cn=user01,dc=example,dc=org',
207
+ {
208
+ 'userPassword': 'password1',
209
+ 'uid': ['user01'],
210
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
211
+ },
212
+ )
213
+ # When authenticating with an invalid user
214
+ authenticated = cherrypy.ldap.authenticate('user01', 'invalid')
215
+ # Then user is not authenticated
216
+ self.assertEqual(False, authenticated)
217
+ # When authenticating with an valid password
218
+ authenticated = cherrypy.ldap.authenticate('user01', 'password1')
219
+ # Then user is not authenticated
220
+ self.assertTrue(authenticated)
221
+
222
+ def test_authenticate_with_email(self):
223
+ # Given a user in LDAP with firstname and lastname
224
+ cherrypy.ldap._pool.strategy.add_entry(
225
+ 'cn=user01,dc=example,dc=org',
226
+ {
227
+ 'cn': ['user01'],
228
+ 'userPassword': 'password1',
229
+ 'uid': ['user01'],
230
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
231
+ 'mail': ['john@test.com'],
232
+ },
233
+ )
234
+ # When authenticating with that user
235
+ authenticated = cherrypy.ldap.authenticate('user01', 'password1')
236
+ # Then user is authenticated and firstname is composed of firstname and lastname
237
+ self.assertTrue(authenticated)
238
+ self.assertEqual(authenticated[0], 'user01')
239
+ self.assertEqual(
240
+ {
241
+ 'dn': 'cn=user01,dc=example,dc=org',
242
+ 'cn': ['user01'],
243
+ 'userPassword': ['password1'],
244
+ 'uid': ['user01'],
245
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
246
+ 'mail': ['john@test.com'],
247
+ 'email': 'john@test.com',
248
+ 'fullname': '',
249
+ },
250
+ authenticated[1],
251
+ )
252
+
253
+ def test_authenticate_with_fullname(self):
254
+ # Given a user in LDAP with firstname and lastname
255
+ cherrypy.ldap._pool.strategy.add_entry(
256
+ 'cn=user01,dc=example,dc=org',
257
+ {
258
+ 'cn': ['user01'],
259
+ 'userPassword': 'password1',
260
+ 'uid': ['user01'],
261
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
262
+ 'displayName': ['John Kennedy'],
263
+ },
264
+ )
265
+ # When authenticating with that user
266
+ authenticated = cherrypy.ldap.authenticate('user01', 'password1')
267
+ # Then user is authenticated and firstname is composed of firstname and lastname
268
+ self.assertTrue(authenticated)
269
+ self.assertEqual(authenticated[0], 'user01')
270
+ self.assertEqual(
271
+ {
272
+ 'dn': 'cn=user01,dc=example,dc=org',
273
+ 'cn': ['user01'],
274
+ 'userPassword': ['password1'],
275
+ 'uid': ['user01'],
276
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
277
+ 'displayName': ['John Kennedy'],
278
+ 'email': None,
279
+ 'fullname': 'John Kennedy',
280
+ },
281
+ authenticated[1],
282
+ )
283
+
284
+ def test_authenticate_with_firstname_lastname(self):
285
+ # Given a user in LDAP with firstname and lastname
286
+ cherrypy.ldap._pool.strategy.add_entry(
287
+ 'cn=user01,dc=example,dc=org',
288
+ {
289
+ 'cn': ['user01'],
290
+ 'userPassword': 'password1',
291
+ 'uid': ['user01'],
292
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
293
+ 'givenName': ['John'],
294
+ 'sn': ['Kennedy'],
295
+ },
296
+ )
297
+ # When authenticating with that user
298
+ authenticated = cherrypy.ldap.authenticate('user01', 'password1')
299
+ # Then user is authenticated and firstname is composed of firstname and lastname
300
+ self.assertTrue(authenticated)
301
+ self.assertEqual(authenticated[0], 'user01')
302
+ self.assertEqual(
303
+ {
304
+ 'dn': 'cn=user01,dc=example,dc=org',
305
+ 'cn': ['user01'],
306
+ 'userPassword': ['password1'],
307
+ 'uid': ['user01'],
308
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
309
+ 'givenName': ['John'],
310
+ 'sn': ['Kennedy'],
311
+ 'email': None,
312
+ 'fullname': 'John Kennedy',
313
+ },
314
+ authenticated[1],
315
+ )
316
+
317
+
318
+ class LdapPluginTestWithRequiredGroup(helper.CPWebCase):
319
+
320
+ @classmethod
321
+ def setup_server(cls):
322
+ # Configure Mock server early.
323
+ cls.server = ldap3.Server('my_fake_server')
324
+ cls.conn = ldap3.Connection(cls.server, client_strategy=ldap3.MOCK_ASYNC, raise_exceptions=True)
325
+ cls.patcher = mock.patch('ldap3.Connection', return_value=cls.conn)
326
+ cls.patcher.start()
327
+ cherrypy.config.update(
328
+ {
329
+ 'ldap.uri': 'my_fake_server',
330
+ 'ldap.base_dn': 'dc=example,dc=org',
331
+ 'ldap.required_group': ['appgroup', 'secondgroup'],
332
+ 'ldap.group_attribute': 'memberUid',
333
+ 'ldap.group_attribute_is_dn': False,
334
+ }
335
+ )
336
+
337
+ @classmethod
338
+ def teardown_class(cls):
339
+ cherrypy.ldap.uri = None
340
+ # Release mock server.
341
+ cls.patcher.stop()
342
+ return super().teardown_class()
343
+
344
+ def setUp(self) -> None:
345
+ # Clear LDAP entries before test.
346
+ for entry in list(cherrypy.ldap._pool.strategy.connection.server.dit):
347
+ if entry == 'cn=schema':
348
+ continue
349
+ cherrypy.ldap._pool.strategy.remove_entry(entry)
350
+ return super().setUp()
351
+
352
+ def test_authenticate_with_valid_group(self):
353
+ # Given a user and a group in LDAP
354
+ cherrypy.ldap._pool.strategy.add_entry(
355
+ 'cn=user01,dc=example,dc=org',
356
+ {
357
+ 'userPassword': 'password1',
358
+ 'uid': ['user01'],
359
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
360
+ },
361
+ )
362
+ cherrypy.ldap._pool.strategy.add_entry(
363
+ 'cn=appgroup,ou=Groups,dc=example,dc=org',
364
+ {
365
+ 'cn': ['appgroup'],
366
+ 'memberUid': ['user01', 'user02'],
367
+ 'objectClass': ['posixGroup'],
368
+ },
369
+ )
370
+ # When authenticating with that user
371
+ authenticated = cherrypy.ldap.authenticate('user01', 'password1')
372
+ # Then user is authenticated
373
+ self.assertTrue(authenticated)
374
+ self.assertEqual(authenticated[0], 'user01')
375
+ self.assertEqual(
376
+ {
377
+ 'dn': 'cn=user01,dc=example,dc=org',
378
+ 'userPassword': ['password1'],
379
+ 'uid': ['user01'],
380
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
381
+ 'cn': ['user01'],
382
+ 'email': None,
383
+ 'fullname': '',
384
+ },
385
+ authenticated[1],
386
+ )
387
+
388
+ def test_authenticate_with_not_member(self):
389
+ # Given a user and a group in LDAP
390
+ cherrypy.ldap._pool.strategy.add_entry(
391
+ 'cn=user01,dc=example,dc=org',
392
+ {
393
+ 'userPassword': 'password1',
394
+ 'uid': ['user01'],
395
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
396
+ },
397
+ )
398
+ cherrypy.ldap._pool.strategy.add_entry(
399
+ 'cn=appgroup,ou=Groups,dc=nodomain', {'memberUid': ['invalid', 'user02'], 'objectClass': ['posixGroup']}
400
+ )
401
+ # When authenticating with that user
402
+ authenticated = cherrypy.ldap.authenticate('user01', 'password1')
403
+ # Then user is not authenticated
404
+ self.assertEqual(False, authenticated)
405
+
406
+ def test_authenticate_with_invalid_group(self):
407
+ # Given a user and a group in LDAP
408
+ cherrypy.ldap._pool.strategy.add_entry(
409
+ 'cn=user01,dc=example,dc=org',
410
+ {
411
+ 'userPassword': 'password1',
412
+ 'uid': ['user01'],
413
+ 'objectClass': ['person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount'],
414
+ },
415
+ )
416
+ cherrypy.ldap._pool.strategy.add_entry(
417
+ 'cn=invalid,ou=Groups,dc=nodomain', {'memberUid': ['user01', 'user02'], 'objectClass': ['posixGroup']}
418
+ )
419
+ # When authenticating with that user
420
+ authenticated = cherrypy.ldap.authenticate('user01', 'password1')
421
+ # Then user is not authenticated
422
+ self.assertEqual(False, authenticated)
423
+
424
+
425
+ @skipUnless(os.environ.get('TEST_LDAP_URI', None), "required TEST_LDAP_URI pointing to openldap server")
426
+ class LdapPluginTestWithOpenldap(helper.CPWebCase):
427
+ """
428
+ This test required a real openldap serer running with 'user01' and 'password1'.
429
+ """
430
+
431
+ @classmethod
432
+ def setup_server(cls):
433
+ cherrypy.config.update(
434
+ {
435
+ 'ldap.uri': os.environ.get('TEST_LDAP_URI'),
436
+ 'ldap.base_dn': os.environ.get('TEST_LDAP_BASE_DN', 'dc=example,dc=org'),
437
+ }
438
+ )
439
+
440
+ @classmethod
441
+ def teardown_class(cls):
442
+ cherrypy.ldap.uri = None
443
+ return super().teardown_class()
444
+
445
+ def test_authenticate(self):
446
+ user = cherrypy.ldap.authenticate('user01', 'password1')
447
+ self.assertTrue(user)
448
+ self.assertEqual(user[0], 'user01')
449
+
450
+ def test_authenticate_with_invalid_user(self):
451
+ self.assertEqual(False, cherrypy.ldap.authenticate('josh', 'password'))
@@ -0,0 +1,100 @@
1
+ # Scheduler plugins for 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
+ from datetime import datetime, timezone
18
+ from threading import Event
19
+
20
+ import cherrypy
21
+ from cherrypy.test import helper
22
+
23
+ from .. import scheduler # noqa
24
+
25
+ done = Event()
26
+
27
+
28
+ def a_task(*args, **kwargs):
29
+ done.set()
30
+
31
+
32
+ class SchedulerPluginTest(helper.CPWebCase):
33
+ def setUp(self) -> None:
34
+ done.clear()
35
+ cherrypy.scheduler.remove_all_jobs()
36
+ return super().setUp()
37
+
38
+ def tearDown(self):
39
+ return super().tearDown()
40
+
41
+ @classmethod
42
+ def setup_server(cls):
43
+ pass
44
+
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())
62
+
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
69
+ self.assertTrue(scheduled)
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()))
73
+
74
+ def test_add_job_now(self):
75
+ # Given a task
76
+ # When scheduling that task
77
+ scheduled = cherrypy.engine.publish('scheduler:add_job_now', a_task, 1, 2, 3, foo=1, bar=2)
78
+ self.assertTrue(scheduled)
79
+ # When waiting for all tasks
80
+ cherrypy.scheduler.wait_for_jobs()
81
+ # Then the task get called
82
+ self.assertTrue(done.is_set())
83
+
84
+ def test_remove_job(self):
85
+ # Given a scheduler with a specific number of jobs
86
+ count = len(cherrypy.scheduler.get_jobs())
87
+ # Given a job schedule every seconds
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):
97
+ # Given an unschedule job
98
+ # When unscheduling an invalid job
99
+ cherrypy.engine.publish('scheduler:remove_job', a_task)
100
+ # Then an error is not raised.
@@ -0,0 +1,107 @@
1
+ # Scheduler plugins for 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
+ import importlib
17
+ import tempfile
18
+ from threading import Event
19
+
20
+ import cherrypy
21
+ from cherrypy.test import helper
22
+
23
+ from .. import scheduler # noqa
24
+
25
+ done = Event()
26
+
27
+ HAS_SQLALCHEMY = importlib.util.find_spec("sqlalchemy") is not None
28
+ if not HAS_SQLALCHEMY:
29
+ pass
30
+ else:
31
+ from sqlalchemy import Column, Integer, String
32
+ from sqlalchemy.exc import IntegrityError
33
+
34
+ from .. import db # noqa
35
+
36
+ Base = cherrypy.db.get_base()
37
+
38
+ class User2(Base):
39
+ __tablename__ = 'users2'
40
+ id = Column(Integer, primary_key=True)
41
+ username = Column(String)
42
+
43
+ def __repr__(self):
44
+ return f"User2(id={self.id}, username='{self.username}')"
45
+
46
+ class Root:
47
+
48
+ @cherrypy.expose
49
+ def index(self):
50
+ return str(User2.query.all())
51
+
52
+ @cherrypy.expose
53
+ def add(self, username):
54
+ try:
55
+ User2(username=username).add().commit()
56
+ return "OK"
57
+ except IntegrityError as e:
58
+ return str(e)
59
+
60
+ def create_user(*args, **kwargs):
61
+ user = User2(*args, **kwargs).add()
62
+ user.commit()
63
+ done.set()
64
+
65
+
66
+ class DbSchedulerPluginTest(helper.CPWebCase):
67
+ interactive = False
68
+
69
+ @classmethod
70
+ def setup_class(cls):
71
+ cls.tempdir = tempfile.TemporaryDirectory(prefix='cherrypy-foundation-', suffix='-db-test')
72
+ super().setup_class()
73
+
74
+ @classmethod
75
+ def teardown_class(cls):
76
+ cls.tempdir.cleanup()
77
+ super().teardown_class()
78
+
79
+ @classmethod
80
+ def setup_server(cls):
81
+ cherrypy.config.update(
82
+ {
83
+ 'db.uri': f"sqlite:///{cls.tempdir.name}/data.db",
84
+ }
85
+ )
86
+ cherrypy.tree.mount(Root(), '/')
87
+
88
+ def setUp(self) -> None:
89
+ done.clear()
90
+ cherrypy.db.create_all()
91
+ return super().setUp()
92
+
93
+ def tearDown(self):
94
+ cherrypy.db.drop_all()
95
+ return super().tearDown()
96
+
97
+ def test_add_job_now(self):
98
+ # Given a task
99
+ # When scheduling that task
100
+ scheduled = cherrypy.engine.publish('scheduler:add_job_now', create_user, username='myuser')
101
+ self.assertTrue(scheduled)
102
+ # When waiting for all tasks
103
+ cherrypy.scheduler.wait_for_jobs()
104
+ # Then the task get called
105
+ self.assertTrue(done.is_set())
106
+ # Then database was updated
107
+ User2.query.filter(User2.username == 'myuser').one()