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.
- cherrypy_foundation/__init__.py +0 -0
- cherrypy_foundation/components/ColorModes.jinja +70 -0
- cherrypy_foundation/components/Datatable.css +47 -0
- cherrypy_foundation/components/Datatable.jinja +63 -0
- cherrypy_foundation/components/Datatable.js +358 -0
- cherrypy_foundation/components/Field.css +10 -0
- cherrypy_foundation/components/Field.jinja +66 -0
- cherrypy_foundation/components/Field.js +56 -0
- cherrypy_foundation/components/Fields.jinja +4 -0
- cherrypy_foundation/components/Flash.jinja +13 -0
- cherrypy_foundation/components/Icon.jinja +3 -0
- cherrypy_foundation/components/LocaleSelection.jinja +13 -0
- cherrypy_foundation/components/LocaleSelection.js +26 -0
- cherrypy_foundation/components/SideBySideMultiSelect.css +25 -0
- cherrypy_foundation/components/SideBySideMultiSelect.jinja +9 -0
- cherrypy_foundation/components/SideBySideMultiSelect.js +9 -0
- cherrypy_foundation/components/Typeahead.css +55 -0
- cherrypy_foundation/components/Typeahead.jinja +106 -0
- cherrypy_foundation/components/Typeahead.js +8 -0
- cherrypy_foundation/components/__init__.py +51 -0
- cherrypy_foundation/components/tests/__init__.py +0 -0
- cherrypy_foundation/components/tests/test_static.py +90 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css +2106 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.min.css +5 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css +9262 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css.map +95 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css +6 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css.map +7 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js +4846 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js.map +1 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js +7 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js.map +7 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/color-modes.js +80 -0
- cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.css +849 -0
- cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_asc.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_asc_disabled.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_both.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_desc.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_desc_disabled.png +0 -0
- cherrypy_foundation/components/vendor/datatables/js/dataTables.js +14073 -0
- cherrypy_foundation/components/vendor/datatables/js/dataTables.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.css +556 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.js +1700 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.min.js +8 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.js +2944 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css +13 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.js +1202 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.js +11577 -0
- cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.min.js +13 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.css +194 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.js +1861 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.js +75023 -0
- cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.min.js +3 -0
- cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/vfs_fonts.js +6 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.css +53 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js +485 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.min.js +4 -0
- cherrypy_foundation/components/vendor/jquery/jquery.min.js +2 -0
- cherrypy_foundation/components/vendor/multi/LICENSE +7 -0
- cherrypy_foundation/components/vendor/multi/README.md +109 -0
- cherrypy_foundation/components/vendor/multi/multi.css +95 -0
- cherrypy_foundation/components/vendor/multi/multi.js +328 -0
- cherrypy_foundation/components/vendor/popper/popper.js +1825 -0
- cherrypy_foundation/components/vendor/popper/popper.min.js +6 -0
- cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css +1 -0
- cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js +10 -0
- cherrypy_foundation/error_page.py +94 -0
- cherrypy_foundation/flash.py +50 -0
- cherrypy_foundation/form.py +119 -0
- cherrypy_foundation/logging.py +103 -0
- cherrypy_foundation/passwd.py +65 -0
- cherrypy_foundation/plugins/__init__.py +0 -0
- cherrypy_foundation/plugins/db.py +286 -0
- cherrypy_foundation/plugins/ldap.py +257 -0
- cherrypy_foundation/plugins/restapi.py +74 -0
- cherrypy_foundation/plugins/scheduler.py +287 -0
- cherrypy_foundation/plugins/smtp.py +223 -0
- cherrypy_foundation/plugins/tests/__init__.py +0 -0
- cherrypy_foundation/plugins/tests/test_db.py +118 -0
- cherrypy_foundation/plugins/tests/test_ldap.py +451 -0
- cherrypy_foundation/plugins/tests/test_scheduler.py +100 -0
- cherrypy_foundation/plugins/tests/test_scheduler_db.py +107 -0
- cherrypy_foundation/plugins/tests/test_smtp.py +140 -0
- cherrypy_foundation/sessions.py +93 -0
- cherrypy_foundation/tests/__init__.py +72 -0
- cherrypy_foundation/tests/templates/test_flash.html +9 -0
- cherrypy_foundation/tests/templates/test_form.html +16 -0
- cherrypy_foundation/tests/templates/test_url.html +15 -0
- cherrypy_foundation/tests/test_error_page.py +78 -0
- cherrypy_foundation/tests/test_flash.py +61 -0
- cherrypy_foundation/tests/test_form.py +148 -0
- cherrypy_foundation/tests/test_logging.py +78 -0
- cherrypy_foundation/tests/test_passwd.py +51 -0
- cherrypy_foundation/tests/test_sessions.py +89 -0
- cherrypy_foundation/tests/test_url.py +161 -0
- cherrypy_foundation/tools/__init__.py +0 -0
- cherrypy_foundation/tools/auth.py +263 -0
- cherrypy_foundation/tools/auth_mfa.py +249 -0
- cherrypy_foundation/tools/i18n.py +529 -0
- cherrypy_foundation/tools/jinja2.py +158 -0
- cherrypy_foundation/tools/ratelimit.py +265 -0
- cherrypy_foundation/tools/secure_headers.py +119 -0
- cherrypy_foundation/tools/sessions_timeout.py +167 -0
- cherrypy_foundation/tools/tests/__init__.py +0 -0
- cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po +15 -0
- cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po +15 -0
- cherrypy_foundation/tools/tests/locales/messages.pot +2 -0
- cherrypy_foundation/tools/tests/templates/test_jinja2.html +11 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax.html +9 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +22 -0
- cherrypy_foundation/tools/tests/test_auth.py +110 -0
- cherrypy_foundation/tools/tests/test_auth_mfa.py +369 -0
- cherrypy_foundation/tools/tests/test_i18n.py +247 -0
- cherrypy_foundation/tools/tests/test_jinja2.py +153 -0
- cherrypy_foundation/tools/tests/test_ratelimit.py +109 -0
- cherrypy_foundation/tools/tests/test_secure_headers.py +200 -0
- cherrypy_foundation/url.py +66 -0
- cherrypy_foundation/widgets.py +48 -0
- cherrypy_foundation-1.0.0.dist-info/METADATA +71 -0
- cherrypy_foundation-1.0.0.dist-info/RECORD +136 -0
- cherrypy_foundation-1.0.0.dist-info/WHEEL +5 -0
- cherrypy_foundation-1.0.0.dist-info/licenses/LICENSE.md +674 -0
- 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()
|