cherrypy-foundation 1.0.0__py3-none-any.whl → 1.0.0a2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cherrypy_foundation/components/ColorModes.jinja +4 -5
- cherrypy_foundation/components/Datatable.jinja +2 -2
- cherrypy_foundation/components/Datatable.js +2 -2
- cherrypy_foundation/components/Field.jinja +2 -4
- cherrypy_foundation/components/Fields.jinja +2 -0
- cherrypy_foundation/components/Typeahead.css +1 -6
- cherrypy_foundation/components/Typeahead.jinja +2 -2
- cherrypy_foundation/components/__init__.py +2 -2
- cherrypy_foundation/components/tests/test_static.py +1 -1
- cherrypy_foundation/error_page.py +3 -3
- cherrypy_foundation/flash.py +15 -17
- cherrypy_foundation/form.py +2 -2
- cherrypy_foundation/logging.py +2 -2
- cherrypy_foundation/passwd.py +2 -2
- cherrypy_foundation/plugins/db.py +1 -1
- cherrypy_foundation/plugins/ldap.py +25 -27
- cherrypy_foundation/plugins/restapi.py +1 -1
- cherrypy_foundation/plugins/scheduler.py +3 -14
- cherrypy_foundation/plugins/smtp.py +2 -8
- cherrypy_foundation/plugins/tests/test_db.py +2 -2
- cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
- cherrypy_foundation/plugins/tests/test_scheduler.py +1 -1
- cherrypy_foundation/plugins/tests/test_smtp.py +1 -31
- cherrypy_foundation/tests/__init__.py +0 -72
- cherrypy_foundation/tests/templates/test_form.html +1 -7
- cherrypy_foundation/tests/test_error_page.py +1 -7
- cherrypy_foundation/tests/test_form.py +11 -40
- cherrypy_foundation/tests/test_passwd.py +2 -2
- cherrypy_foundation/tools/auth.py +27 -31
- cherrypy_foundation/tools/auth_mfa.py +83 -87
- cherrypy_foundation/tools/errors.py +27 -0
- cherrypy_foundation/tools/i18n.py +151 -235
- cherrypy_foundation/tools/jinja2.py +2 -15
- cherrypy_foundation/tools/ratelimit.py +18 -32
- cherrypy_foundation/tools/secure_headers.py +1 -1
- cherrypy_foundation/tools/sessions_timeout.py +21 -23
- cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
- cherrypy_foundation/tools/tests/templates/test_jinja2.html +1 -2
- cherrypy_foundation/tools/tests/templates/test_jinja2_i18n.html +11 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax.html +2 -3
- cherrypy_foundation/tools/tests/test_auth.py +3 -20
- cherrypy_foundation/tools/tests/test_auth_mfa.py +4 -6
- cherrypy_foundation/tools/tests/test_i18n.py +6 -81
- cherrypy_foundation/tools/tests/test_jinja2.py +5 -35
- cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
- cherrypy_foundation/url.py +25 -25
- cherrypy_foundation/widgets.py +2 -2
- cherrypy_foundation-1.0.0a2.dist-info/METADATA +42 -0
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/RECORD +53 -64
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/WHEEL +1 -1
- cherrypy_foundation/components/Flash.jinja +0 -13
- cherrypy_foundation/components/LocaleSelection.jinja +0 -13
- cherrypy_foundation/components/LocaleSelection.js +0 -26
- cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
- cherrypy_foundation/sessions.py +0 -93
- cherrypy_foundation/tests/templates/test_flash.html +0 -9
- cherrypy_foundation/tests/templates/test_url.html +0 -15
- cherrypy_foundation/tests/test_flash.py +0 -61
- cherrypy_foundation/tests/test_logging.py +0 -78
- cherrypy_foundation/tests/test_sessions.py +0 -89
- cherrypy_foundation/tests/test_url.py +0 -161
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
- cherrypy_foundation/tools/tests/test_secure_headers.py +0 -200
- cherrypy_foundation-1.0.0.dist-info/METADATA +0 -71
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/licenses/LICENSE.md +0 -0
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a2.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
{# def header="Toggle theme", light_label="Light", dark_label="Dark", auto_label="Auto" #}
|
|
2
1
|
{#css vendor/bootstrap5/css/bootstrap.min.css #}
|
|
3
2
|
{#js vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js, vendor/bootstrap5/js/color-modes.js #}
|
|
4
3
|
<svg class="d-none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -22,7 +21,7 @@
|
|
|
22
21
|
</svg>
|
|
23
22
|
<li>
|
|
24
23
|
<span id="bd-theme" class="dropdown-item disabled">
|
|
25
|
-
<span id="bd-theme-text">{{
|
|
24
|
+
<span id="bd-theme-text">{% trans %}Toggle theme{% endtrans %}</span>
|
|
26
25
|
<svg aria-hidden="true"
|
|
27
26
|
class="theme-icon-active visually-hidden"
|
|
28
27
|
width="16"
|
|
@@ -41,7 +40,7 @@
|
|
|
41
40
|
<use href="#sun-fill">
|
|
42
41
|
</use>
|
|
43
42
|
</svg>
|
|
44
|
-
{{
|
|
43
|
+
{% trans %}Light{% endtrans %}
|
|
45
44
|
</button>
|
|
46
45
|
</li>
|
|
47
46
|
<li>
|
|
@@ -53,7 +52,7 @@
|
|
|
53
52
|
<use href="#moon-stars-fill">
|
|
54
53
|
</use>
|
|
55
54
|
</svg>
|
|
56
|
-
{{
|
|
55
|
+
{% trans %}Dark{% endtrans %}
|
|
57
56
|
</button>
|
|
58
57
|
</li>
|
|
59
58
|
<li>
|
|
@@ -65,6 +64,6 @@
|
|
|
65
64
|
<use href="#circle-half">
|
|
66
65
|
</use>
|
|
67
66
|
</svg>
|
|
68
|
-
{{
|
|
67
|
+
{% trans %}Auto{% endtrans %}
|
|
69
68
|
</button>
|
|
70
69
|
</li>
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
{% set buttons_cfg_default = {
|
|
16
16
|
"dom": {
|
|
17
17
|
"button": {
|
|
18
|
-
"className": "btn btn-sm ms-1 mb-
|
|
18
|
+
"className": "btn btn-sm ms-1 mb-1",
|
|
19
19
|
"active": "active"
|
|
20
20
|
},
|
|
21
21
|
"collection": {
|
|
22
22
|
"tag": "div",
|
|
23
23
|
"button": {
|
|
24
24
|
"tag": "a",
|
|
25
|
-
"className": "btn btn-sm btn-link mb-
|
|
25
|
+
"className": "btn btn-sm btn-link mb-1 w-100 text-start",
|
|
26
26
|
"active": "active",
|
|
27
27
|
"disabled": "disabled"
|
|
28
28
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Copyright (C)
|
|
2
|
+
* CherryPy Foundation
|
|
3
|
+
* Copyright (C) 2025 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,6 +1,4 @@
|
|
|
1
1
|
{# def field, floating=False #}
|
|
2
|
-
{#css vendor/bootstrap5/css/bootstrap.min.css #}
|
|
3
|
-
{#js vendor/jquery/jquery.min.js, vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js #}
|
|
4
2
|
{# djlint:off #}
|
|
5
3
|
{% set bootstrap_class_table = {
|
|
6
4
|
"CheckboxInput": ("form-check-input", "form-check-label", False, True),
|
|
@@ -40,10 +38,10 @@
|
|
|
40
38
|
{% set label_attrs = attrs.__class__({'class': label_class}) %}
|
|
41
39
|
{% for key, value in attrs.as_dict.items() %}
|
|
42
40
|
{% if key.startswith('container-') or key.startswith('container_') %}
|
|
43
|
-
{% do container_attrs.set(**{key[10:]: value
|
|
41
|
+
{% do container_attrs.set(**{key[10:]: value}) %}
|
|
44
42
|
{% endif %}
|
|
45
43
|
{% if key.startswith('label-') or key.startswith('label_') %}
|
|
46
|
-
{% do label_attrs.set(**{key[6:]: value
|
|
44
|
+
{% do label_attrs.set(**{key[6:]: value}) %}
|
|
47
45
|
{% endif %}
|
|
48
46
|
{% endfor %}
|
|
49
47
|
<div {{ container_attrs.as_dict | xmlattr }}>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
{# def form, floating=False #}
|
|
2
|
+
{#css vendor/bootstrap5/css/bootstrap.min.css #}
|
|
3
|
+
{#js vendor/jquery/jquery.min.js, vendor/popper/popper.min.js, vendor/bootstrap5/js/bootstrap.min.js #}
|
|
2
4
|
<div class="row">
|
|
3
5
|
{% for id, field in form._fields.items() %}<Field field={{ field }} floating={{ floating }} />{% endfor %}
|
|
4
6
|
</div>
|
|
@@ -10,11 +10,6 @@
|
|
|
10
10
|
top: 0.5rem;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/* Fix lense position */
|
|
14
|
-
.typeahead__button button {
|
|
15
|
-
height: 100%;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
/* Fix color for DarkMode */
|
|
19
14
|
.typeahead__container button,
|
|
20
15
|
.typeahead__container button.disabled,
|
|
@@ -52,4 +47,4 @@
|
|
|
52
47
|
.typeahead__list .typeahead__item:not([disabled])>a:hover {
|
|
53
48
|
background-color: var(--bs-tertiary-bg);
|
|
54
49
|
color: var(--bs-body-color);
|
|
55
|
-
}
|
|
50
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{#def
|
|
2
|
-
accent=
|
|
2
|
+
accent=True,
|
|
3
3
|
async_result=False,
|
|
4
4
|
autofocus=False,
|
|
5
5
|
backdrop=False,
|
|
@@ -27,7 +27,7 @@ href=None,
|
|
|
27
27
|
loading_animation=True,
|
|
28
28
|
matcher=None,
|
|
29
29
|
max_item=8,
|
|
30
|
-
max_item_per_group=
|
|
30
|
+
max_item_per_group=10,
|
|
31
31
|
max_length=None,
|
|
32
32
|
min_length=2,
|
|
33
33
|
multiselect=None,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Cherrypy
|
|
2
|
-
# Copyright (C)
|
|
1
|
+
# Cherrypy Foundation
|
|
2
|
+
# Copyright (C) 2025 IKUS Software inc.
|
|
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
|
-
#
|
|
2
|
-
# Copyright (C) 2020-
|
|
1
|
+
# CherryPy Foundation
|
|
2
|
+
# Copyright (C) 2020-2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -63,7 +63,7 @@ def error_page(status='', message='', traceback='', version=''):
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
# Replace message by generic one for 404. Default implementation leak path info.
|
|
66
|
-
if status == '404 Not Found'
|
|
66
|
+
if status == '404 Not Found':
|
|
67
67
|
message = 'Nothing matches the given URI'
|
|
68
68
|
|
|
69
69
|
# Check expected response type.
|
cherrypy_foundation/flash.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Copyright (C) 2020-
|
|
1
|
+
# CherryPy Foundation
|
|
2
|
+
# Copyright (C) 2020-2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
from collections import namedtuple
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
import cherrypy
|
|
20
20
|
|
|
21
21
|
FlashMessage = namedtuple('FlashMessage', ['message', 'level'])
|
|
22
22
|
|
|
@@ -27,24 +27,22 @@ def flash(message, level='info'):
|
|
|
27
27
|
"""
|
|
28
28
|
assert message
|
|
29
29
|
assert level in ['info', 'error', 'warning', 'success']
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
session['flash'].append(flash_message)
|
|
30
|
+
if 'flash' not in cherrypy.session:
|
|
31
|
+
cherrypy.session['flash'] = []
|
|
32
|
+
# Support Markup and string
|
|
33
|
+
if hasattr(message, '__html__'):
|
|
34
|
+
flash_message = FlashMessage(message, level)
|
|
35
|
+
else:
|
|
36
|
+
flash_message = FlashMessage(str(message), level)
|
|
37
|
+
cherrypy.session['flash'].append(flash_message)
|
|
39
38
|
|
|
40
39
|
|
|
41
40
|
def get_flashed_messages():
|
|
42
41
|
"""
|
|
43
42
|
Return all flash message.
|
|
44
43
|
"""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return messages
|
|
44
|
+
if 'flash' in cherrypy.session:
|
|
45
|
+
messages = cherrypy.session['flash']
|
|
46
|
+
del cherrypy.session['flash']
|
|
47
|
+
return messages
|
|
50
48
|
return []
|
cherrypy_foundation/form.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Copyright (C) 2020-
|
|
1
|
+
# CherryPy Foundation
|
|
2
|
+
# Copyright (C) 2020-2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
cherrypy_foundation/logging.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Copyright (C)
|
|
1
|
+
# CherryPy Foundation
|
|
2
|
+
# Copyright (C) 2025 IKUS Software inc.
|
|
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
|
cherrypy_foundation/passwd.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
#
|
|
2
|
-
# Copyright (C)
|
|
1
|
+
# CherryPy Foundation
|
|
2
|
+
# Copyright (C) 2025 IKUS Software inc.
|
|
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)
|
|
2
|
+
# Copyright (C) 2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -26,7 +26,7 @@ _safe = ldap3.utils.conv.escape_filter_chars
|
|
|
26
26
|
|
|
27
27
|
def all_attribute(attributes, keys, default=None):
|
|
28
28
|
"""
|
|
29
|
-
Extract all
|
|
29
|
+
Extract the all value from LDAP attributes.
|
|
30
30
|
"""
|
|
31
31
|
# Skip loopkup if key is not defined.
|
|
32
32
|
if not keys:
|
|
@@ -61,9 +61,7 @@ def first_attribute(attributes, keys, default=None):
|
|
|
61
61
|
for attr in keys:
|
|
62
62
|
try:
|
|
63
63
|
value = attributes[attr]
|
|
64
|
-
if isinstance(value, list):
|
|
65
|
-
if len(value) == 0:
|
|
66
|
-
continue
|
|
64
|
+
if isinstance(value, list) and len(value) > 0:
|
|
67
65
|
return value[0]
|
|
68
66
|
else:
|
|
69
67
|
return value
|
|
@@ -77,8 +75,8 @@ class LdapPlugin(SimplePlugin):
|
|
|
77
75
|
"""
|
|
78
76
|
Used this plugin to authenticate user against an LDAP server.
|
|
79
77
|
|
|
80
|
-
`authenticate(
|
|
81
|
-
valid. Otherwise it return a tuple or
|
|
78
|
+
`authenticate(username, password)` return None if the credentials are not
|
|
79
|
+
valid. Otherwise it return a tuple or username and extra attributes.
|
|
82
80
|
The extra attribute may contains `_fullname` and `_email`.
|
|
83
81
|
"""
|
|
84
82
|
|
|
@@ -89,7 +87,7 @@ class LdapPlugin(SimplePlugin):
|
|
|
89
87
|
scope = 'subtree'
|
|
90
88
|
tls = False
|
|
91
89
|
user_filter = '(objectClass=*)'
|
|
92
|
-
|
|
90
|
+
username_attribute = ['uid']
|
|
93
91
|
required_group = None
|
|
94
92
|
group_attribute = 'member'
|
|
95
93
|
group_attribute_is_dn = False
|
|
@@ -101,7 +99,6 @@ class LdapPlugin(SimplePlugin):
|
|
|
101
99
|
firstname_attribute = None
|
|
102
100
|
lastname_attribute = None
|
|
103
101
|
email_attribute = None
|
|
104
|
-
pool_size = 10
|
|
105
102
|
|
|
106
103
|
def start(self):
|
|
107
104
|
# Don't configure this plugin if the ldap URI is not provided.
|
|
@@ -119,7 +116,6 @@ class LdapPlugin(SimplePlugin):
|
|
|
119
116
|
version=self.version,
|
|
120
117
|
raise_exceptions=True,
|
|
121
118
|
client_strategy=ldap3.REUSABLE,
|
|
122
|
-
pool_size=self.pool_size,
|
|
123
119
|
)
|
|
124
120
|
|
|
125
121
|
def stop(self):
|
|
@@ -133,14 +129,14 @@ class LdapPlugin(SimplePlugin):
|
|
|
133
129
|
self.stop()
|
|
134
130
|
self.start()
|
|
135
131
|
|
|
136
|
-
def authenticate(self,
|
|
132
|
+
def authenticate(self, username, password):
|
|
137
133
|
"""
|
|
138
134
|
Check if the given credential as valid according to LDAP.
|
|
139
135
|
Return False if invalid.
|
|
140
136
|
Return None if the plugin is unavailable to validate credentials or if the plugin is disabled.
|
|
141
|
-
Return tuple (<
|
|
137
|
+
Return tuple (<username>, <attributes>) if the credentials are valid.
|
|
142
138
|
"""
|
|
143
|
-
assert isinstance(
|
|
139
|
+
assert isinstance(username, str)
|
|
144
140
|
assert isinstance(password, str)
|
|
145
141
|
|
|
146
142
|
if not hasattr(self, '_pool'):
|
|
@@ -150,14 +146,14 @@ class LdapPlugin(SimplePlugin):
|
|
|
150
146
|
with self._pool as conn:
|
|
151
147
|
try:
|
|
152
148
|
# Search the LDAP server for user's DN.
|
|
153
|
-
|
|
154
|
-
attr_filter = ''.join([f'({_safe(attr)}={
|
|
149
|
+
safe_username = _safe(username)
|
|
150
|
+
attr_filter = ''.join([f'({_safe(attr)}={safe_username})' for attr in self.username_attribute])
|
|
155
151
|
search_filter = f"(&{self.user_filter}(|{attr_filter}))"
|
|
156
152
|
response = self._search(conn, search_filter)
|
|
157
153
|
if not response:
|
|
158
|
-
cherrypy.log(f"lookup failed
|
|
154
|
+
cherrypy.log(f"lookup failed username={username} reason=not_found", context='LDAP')
|
|
159
155
|
return False
|
|
160
|
-
cherrypy.log(f"lookup successful
|
|
156
|
+
cherrypy.log(f"lookup successful username={username}", context='LDAP')
|
|
161
157
|
user_dn = response[0]['dn']
|
|
162
158
|
|
|
163
159
|
# Use a separate connection to validate credentials
|
|
@@ -171,18 +167,18 @@ class LdapPlugin(SimplePlugin):
|
|
|
171
167
|
)
|
|
172
168
|
if not login_conn.bind():
|
|
173
169
|
cherrypy.log(
|
|
174
|
-
f'ldap authentication failed
|
|
170
|
+
f'ldap authentication failed username={username} reason=wrong_password',
|
|
175
171
|
context='LDAP',
|
|
176
172
|
severity=logging.WARNING,
|
|
177
173
|
)
|
|
178
174
|
return False
|
|
179
175
|
|
|
180
|
-
# Get
|
|
176
|
+
# Get username
|
|
181
177
|
attrs = response[0]['attributes']
|
|
182
|
-
|
|
183
|
-
if not
|
|
178
|
+
new_username = first_attribute(attrs, self.username_attribute)
|
|
179
|
+
if not new_username:
|
|
184
180
|
cherrypy.log(
|
|
185
|
-
f"object missing
|
|
181
|
+
f"object missing username attribute user_dn={user_dn} attribute={self.username_attribute}",
|
|
186
182
|
context='LDAP',
|
|
187
183
|
severity=logging.WARNING,
|
|
188
184
|
)
|
|
@@ -192,7 +188,7 @@ class LdapPlugin(SimplePlugin):
|
|
|
192
188
|
if self.required_group:
|
|
193
189
|
if not isinstance(self.required_group, list):
|
|
194
190
|
self.required_group = [self.required_group]
|
|
195
|
-
user_value = user_dn if self.group_attribute_is_dn else
|
|
191
|
+
user_value = user_dn if self.group_attribute_is_dn else new_username
|
|
196
192
|
group_filter = '(&(%s=%s)(|%s)%s)' % (
|
|
197
193
|
_safe(self.group_attribute),
|
|
198
194
|
_safe(user_value),
|
|
@@ -201,13 +197,13 @@ class LdapPlugin(SimplePlugin):
|
|
|
201
197
|
)
|
|
202
198
|
# Search LDAP Server for matching groups.
|
|
203
199
|
cherrypy.log(
|
|
204
|
-
f"group check start
|
|
200
|
+
f"group check start username={user_value} required_groups={' '.join(self.required_group)}",
|
|
205
201
|
context='LDAP',
|
|
206
202
|
)
|
|
207
203
|
response = self._search(conn, group_filter, attributes=['cn'])
|
|
208
204
|
if not response:
|
|
209
205
|
cherrypy.log(
|
|
210
|
-
f"group check failed
|
|
206
|
+
f"group check failed username={user_value} required_groups={' '.join(self.required_group)}",
|
|
211
207
|
context='LDAP',
|
|
212
208
|
)
|
|
213
209
|
return False
|
|
@@ -220,11 +216,13 @@ class LdapPlugin(SimplePlugin):
|
|
|
220
216
|
firstname = first_attribute(attrs, self.firstname_attribute, '')
|
|
221
217
|
lastname = first_attribute(attrs, self.lastname_attribute, '')
|
|
222
218
|
attrs['fullname'] = ' '.join([name for name in [firstname, lastname] if name])
|
|
223
|
-
return (
|
|
219
|
+
return (new_username, attrs)
|
|
224
220
|
except LDAPInvalidCredentialsResult:
|
|
225
221
|
return False
|
|
226
222
|
except LDAPException:
|
|
227
|
-
cherrypy.log(
|
|
223
|
+
cherrypy.log(
|
|
224
|
+
f"unexpected error username={username}", context='LDAP', severity=logging.ERROR, traceback=True
|
|
225
|
+
)
|
|
228
226
|
return None
|
|
229
227
|
|
|
230
228
|
def search(self, filter, attributes=ldap3.ALL_ATTRIBUTES, search_base=None, paged_size=None):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Scheduler plugins for Cherrypy
|
|
2
|
-
# Copyright (C)
|
|
2
|
+
# Copyright (C) 2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -35,6 +35,7 @@ def clear_db_sessions(func):
|
|
|
35
35
|
|
|
36
36
|
@wraps(func)
|
|
37
37
|
def func_wrapper(*args, **kwargs):
|
|
38
|
+
cherrypy.db.clear_sessions()
|
|
38
39
|
try:
|
|
39
40
|
result = func(*args, **kwargs)
|
|
40
41
|
finally:
|
|
@@ -205,7 +206,6 @@ class Scheduler(SimplePlugin):
|
|
|
205
206
|
"""
|
|
206
207
|
hour, minute = execution_time.split(':', 2)
|
|
207
208
|
return self.add_job(
|
|
208
|
-
id=getattr(func, '__name__', str(func)),
|
|
209
209
|
func=func,
|
|
210
210
|
name=getattr(func, '__name__', str(func)),
|
|
211
211
|
args=args,
|
|
@@ -214,8 +214,6 @@ class Scheduler(SimplePlugin):
|
|
|
214
214
|
hour=hour,
|
|
215
215
|
minute=minute,
|
|
216
216
|
misfire_grace_time=None,
|
|
217
|
-
coalesce=True,
|
|
218
|
-
replace_existing=True,
|
|
219
217
|
)
|
|
220
218
|
|
|
221
219
|
def add_job_now(self, func, *args, **kwargs):
|
|
@@ -245,29 +243,20 @@ class Scheduler(SimplePlugin):
|
|
|
245
243
|
Remove the given job from scheduler.
|
|
246
244
|
"""
|
|
247
245
|
# Search for a matching job
|
|
248
|
-
return_value = False
|
|
249
|
-
if self._scheduler is None:
|
|
250
|
-
return return_value
|
|
251
246
|
for j in self._scheduler.get_jobs(jobstore=jobstore):
|
|
252
|
-
if j.func == job
|
|
247
|
+
if j.func == job:
|
|
253
248
|
self._scheduler.remove_job(job_id=j.id, jobstore=jobstore)
|
|
254
|
-
return_value = True
|
|
255
|
-
return return_value
|
|
256
249
|
|
|
257
250
|
def remove_all_jobs(self, jobstore=None):
|
|
258
251
|
"""
|
|
259
252
|
Remove all jobs from scheduler.
|
|
260
253
|
"""
|
|
261
|
-
if self._scheduler is None:
|
|
262
|
-
return
|
|
263
254
|
self._scheduler.remove_all_jobs(jobstore=jobstore)
|
|
264
255
|
|
|
265
256
|
def wait_for_jobs(self, jobstore=None):
|
|
266
257
|
"""
|
|
267
258
|
Used to wait for all running jobs to complete.
|
|
268
259
|
"""
|
|
269
|
-
if self._scheduler is None:
|
|
270
|
-
return
|
|
271
260
|
# Wait until the queue is empty.
|
|
272
261
|
while any(
|
|
273
262
|
job for job in self._scheduler.get_jobs(jobstore=jobstore) if job.next_run_time < datetime.now(timezone.utc)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# SMTP Plugins for cherrypy
|
|
2
|
-
# Copyright (C)
|
|
2
|
+
# Copyright (C) 2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -127,7 +127,6 @@ class SmtpPlugin(SimplePlugin):
|
|
|
127
127
|
password = None
|
|
128
128
|
encryption = None
|
|
129
129
|
email_from = None
|
|
130
|
-
bcc = None
|
|
131
130
|
|
|
132
131
|
def _create_msg(self, subject: str, message: str, to=None, cc=None, bcc=None, reply_to=None, headers={}):
|
|
133
132
|
assert subject
|
|
@@ -142,13 +141,8 @@ class SmtpPlugin(SimplePlugin):
|
|
|
142
141
|
msg['To'] = _formataddr(to)
|
|
143
142
|
if cc:
|
|
144
143
|
msg['Cc'] = _formataddr(cc)
|
|
145
|
-
bcc_list = []
|
|
146
|
-
if self.bcc:
|
|
147
|
-
bcc_list.append(_formataddr(self.bcc))
|
|
148
144
|
if bcc:
|
|
149
|
-
|
|
150
|
-
if bcc_list:
|
|
151
|
-
msg['Bcc'] = ', '.join(bcc_list)
|
|
145
|
+
msg['Bcc'] = _formataddr(bcc)
|
|
152
146
|
if reply_to:
|
|
153
147
|
msg['Reply-To'] = _formataddr(reply_to)
|
|
154
148
|
msg['Message-ID'] = email.utils.make_msgid()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Cherrypy
|
|
2
|
-
# Copyright (C) 2022-
|
|
1
|
+
# Cherrypy Foundation
|
|
2
|
+
# Copyright (C) 2022-2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# LDAP Plugins for cherrypy
|
|
2
|
-
# Copyright (C)
|
|
2
|
+
# Copyright (C) 2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -19,13 +19,13 @@ Created on Oct 17, 2015
|
|
|
19
19
|
@author: Patrik Dufresne <patrik@ikus-soft.com>
|
|
20
20
|
"""
|
|
21
21
|
import os
|
|
22
|
-
from unittest import
|
|
22
|
+
from unittest import mock, skipUnless
|
|
23
23
|
|
|
24
24
|
import cherrypy
|
|
25
25
|
import ldap3
|
|
26
26
|
from cherrypy.test import helper
|
|
27
27
|
|
|
28
|
-
from ..
|
|
28
|
+
from .. import ldap # noqa
|
|
29
29
|
|
|
30
30
|
original_connection = ldap3.Connection
|
|
31
31
|
|
|
@@ -35,79 +35,6 @@ def mock_ldap_connection(*args, **kwargs):
|
|
|
35
35
|
return original_connection(*args, client_strategy=ldap3.MOCK_ASYNC, **kwargs)
|
|
36
36
|
|
|
37
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
38
|
class LdapPluginTest(helper.CPWebCase):
|
|
112
39
|
|
|
113
40
|
@classmethod
|