cherrypy-foundation 1.0.0__py3-none-any.whl → 1.0.0a1__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 +6 -16
- 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 +17 -20
- 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 +9 -35
- cherrypy_foundation/plugins/ldap.py +38 -46
- cherrypy_foundation/plugins/restapi.py +1 -1
- cherrypy_foundation/plugins/scheduler.py +84 -208
- cherrypy_foundation/plugins/smtp.py +46 -78
- cherrypy_foundation/plugins/tests/test_db.py +4 -4
- cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
- cherrypy_foundation/plugins/tests/test_scheduler.py +50 -58
- cherrypy_foundation/plugins/tests/test_smtp.py +7 -40
- cherrypy_foundation/tests/__init__.py +0 -72
- cherrypy_foundation/tests/test_error_page.py +1 -7
- cherrypy_foundation/tests/test_passwd.py +2 -2
- cherrypy_foundation/tools/auth.py +38 -59
- cherrypy_foundation/tools/auth_mfa.py +88 -89
- cherrypy_foundation/tools/errors.py +27 -0
- cherrypy_foundation/tools/i18n.py +153 -246
- cherrypy_foundation/tools/jinja2.py +13 -29
- cherrypy_foundation/tools/ratelimit.py +27 -37
- cherrypy_foundation/tools/secure_headers.py +5 -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/test_auth.py +4 -21
- cherrypy_foundation/tools/tests/test_i18n.py +6 -81
- 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.0a1.dist-info/METADATA +42 -0
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/RECORD +46 -65
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.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_form.html +0 -16
- cherrypy_foundation/tests/templates/test_url.html +0 -15
- cherrypy_foundation/tests/test_flash.py +0 -61
- cherrypy_foundation/tests/test_form.py +0 -148
- 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/components/Button.jinja +0 -2
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/templates/test_jinja2.html +0 -11
- cherrypy_foundation/tools/tests/templates/test_jinjax.html +0 -9
- cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
- cherrypy_foundation/tools/tests/test_auth_mfa.py +0 -369
- cherrypy_foundation/tools/tests/test_jinja2.py +0 -153
- 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.0a1.dist-info}/licenses/LICENSE.md +0 -0
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.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),
|
|
@@ -31,24 +29,16 @@
|
|
|
31
29
|
{% set input_class, label_class, floating_supported, label_always_last = bootstrap_class_table.get(field.widget.__class__.__name__) or ('', 'form-label', False, False) %}
|
|
32
30
|
{% set label_last = (floating and floating_supported) or label_always_last %}
|
|
33
31
|
{% do attrs.add_class(input_class) %}
|
|
32
|
+
{# Render label an widget #}
|
|
34
33
|
{# Used for input-group #}
|
|
35
34
|
{% set prepend = attrs.get('prepend', None) %}
|
|
36
35
|
{% set append = attrs.get('append', None) %}
|
|
37
36
|
{% set input_group = prepend or append %}
|
|
38
|
-
{
|
|
39
|
-
{%
|
|
40
|
-
|
|
41
|
-
{% for key, value in attrs.as_dict.items() %}
|
|
42
|
-
{% if key.startswith('container-') or key.startswith('container_') %}
|
|
43
|
-
{% do container_attrs.set(**{key[10:]: value|string}) %}
|
|
44
|
-
{% endif %}
|
|
45
|
-
{% if key.startswith('label-') or key.startswith('label_') %}
|
|
46
|
-
{% do label_attrs.set(**{key[6:]: value|string}) %}
|
|
47
|
-
{% endif %}
|
|
48
|
-
{% endfor %}
|
|
49
|
-
<div {{ container_attrs.as_dict | xmlattr }}>
|
|
37
|
+
{% set container_class = attrs.get('container_class', 'col-12') %}
|
|
38
|
+
{% do attrs.__delitem__('container_class') %}
|
|
39
|
+
<div class="mb-2 form-field {{ container_class }}">
|
|
50
40
|
{% if floating and floating_supported %}<div class="form-floating">{% endif %}
|
|
51
|
-
{% if not label_last %}{{ field.label(
|
|
41
|
+
{% if not label_last %}{{ field.label(class=label_class) }}{% endif %}
|
|
52
42
|
{% if input_group %}
|
|
53
43
|
<div class="input-group">
|
|
54
44
|
{% if prepend %}<span class="input-group-text">{{ prepend }}</span>{% endif %}
|
|
@@ -58,7 +48,7 @@
|
|
|
58
48
|
{% if append %}<span class="input-group-text">{{ append }}</span>{% endif %}
|
|
59
49
|
</div>
|
|
60
50
|
{% endif %}
|
|
61
|
-
{% if label_last %}{{ field.label(
|
|
51
|
+
{% if label_last %}{{ field.label(class=label_class) }}{% endif %}
|
|
62
52
|
{% for error in field.errors %}<div class="invalid-feedback">{{ error }}</div>{% endfor %}
|
|
63
53
|
{% if field.description %}<div class="form-text small test-secondary">{{ field.description }}</div>{% endif %}
|
|
64
54
|
{% if floating and floating_supported %}</div>{% endif %}
|
|
@@ -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
|
-
#
|
|
2
|
-
# Copyright (C)
|
|
1
|
+
# udb, A web interface to manage IT network
|
|
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
|
|
@@ -19,6 +19,9 @@ import logging
|
|
|
19
19
|
|
|
20
20
|
import cherrypy
|
|
21
21
|
|
|
22
|
+
# Define the logger
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
22
25
|
HTML_ERROR_TEMPLATE = '''<!DOCTYPE html>
|
|
23
26
|
<html>
|
|
24
27
|
<head>
|
|
@@ -50,45 +53,39 @@ HTML_ERROR_TEMPLATE = '''<!DOCTYPE html>
|
|
|
50
53
|
'''
|
|
51
54
|
|
|
52
55
|
|
|
53
|
-
def error_page(
|
|
56
|
+
def error_page(**kwargs):
|
|
54
57
|
"""
|
|
55
58
|
Error page handler to handle Plain Text (text/plain), Json (application/json) or HTML (text/html) output.
|
|
56
59
|
|
|
57
60
|
If available, uses Jina2 environment to generate error page using `error_page.html`.
|
|
58
61
|
"""
|
|
59
62
|
# Log server error exception
|
|
60
|
-
if status.startswith('500'):
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
if kwargs.get('status', '').startswith('500'):
|
|
64
|
+
logger.error(
|
|
65
|
+
'error page: %s %s\n%s' % (kwargs.get('status', ''), kwargs.get('message', ''), kwargs.get('traceback', ''))
|
|
63
66
|
)
|
|
64
67
|
|
|
65
68
|
# Replace message by generic one for 404. Default implementation leak path info.
|
|
66
|
-
if status == '404 Not Found'
|
|
67
|
-
message = 'Nothing matches the given URI'
|
|
69
|
+
if kwargs.get('status', '') == '404 Not Found':
|
|
70
|
+
kwargs['message'] = 'Nothing matches the given URI'
|
|
68
71
|
|
|
69
72
|
# Check expected response type.
|
|
70
73
|
mtype = cherrypy.serving.response.headers.get('Content-Type') or cherrypy.tools.accept.callable(
|
|
71
74
|
['text/html', 'text/plain', 'application/json']
|
|
72
75
|
)
|
|
73
76
|
if mtype == 'text/plain':
|
|
74
|
-
return message
|
|
77
|
+
return kwargs.get('message')
|
|
75
78
|
elif mtype == 'application/json':
|
|
76
|
-
return json.dumps({'message': message, 'status': status})
|
|
79
|
+
return json.dumps({'message': kwargs.get('message', ''), 'status': kwargs.get('status', '')})
|
|
77
80
|
elif mtype == 'text/html':
|
|
78
|
-
context = {'status': status, 'message': message, 'traceback': traceback, 'version': version}
|
|
79
81
|
if hasattr(cherrypy.tools, 'jinja2'):
|
|
80
82
|
# Try to build a nice error page with Jinja2 env
|
|
81
83
|
try:
|
|
82
|
-
return cherrypy.tools.jinja2.render_request(template='error_page.html',
|
|
84
|
+
return cherrypy.tools.jinja2.render_request(template='error_page.html', vars=kwargs)
|
|
83
85
|
except Exception:
|
|
84
|
-
|
|
85
|
-
'fail to render error page with jinja2',
|
|
86
|
-
context='ERROR-PAGE',
|
|
87
|
-
severity=logging.ERROR,
|
|
88
|
-
traceback=True,
|
|
89
|
-
)
|
|
86
|
+
logger.exception('fail to render error page')
|
|
90
87
|
# Fallback to built-int HTML error page
|
|
91
|
-
return HTML_ERROR_TEMPLATE %
|
|
88
|
+
return HTML_ERROR_TEMPLATE % kwargs
|
|
92
89
|
|
|
93
90
|
# Fallback to raw error message.
|
|
94
|
-
return message
|
|
91
|
+
return kwargs.get('message')
|
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
|
# SQLAlchemy plugins for cherrypy
|
|
2
|
-
# Copyright (C) 2022-
|
|
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
|
|
@@ -23,6 +23,8 @@ from sqlalchemy.engine import Engine
|
|
|
23
23
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
|
24
24
|
from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker
|
|
25
25
|
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
26
28
|
|
|
27
29
|
@event.listens_for(Engine, 'connect')
|
|
28
30
|
def _set_sqlite_journal_mode_wal(connection, connection_record):
|
|
@@ -166,10 +168,6 @@ class SQLA(SimplePlugin):
|
|
|
166
168
|
_session = None
|
|
167
169
|
_engine = None
|
|
168
170
|
|
|
169
|
-
@property
|
|
170
|
-
def engine(self):
|
|
171
|
-
return self._engine
|
|
172
|
-
|
|
173
171
|
def start(self):
|
|
174
172
|
if self.uri is None:
|
|
175
173
|
return
|
|
@@ -182,22 +180,14 @@ class SQLA(SimplePlugin):
|
|
|
182
180
|
self.clear_sessions()
|
|
183
181
|
# Associate our session to our engine
|
|
184
182
|
self.get_session().configure(bind=self._engine)
|
|
185
|
-
self.bus.log("
|
|
186
|
-
|
|
187
|
-
# This is slightly lower priority to get database started first.
|
|
188
|
-
start.priority = 45
|
|
183
|
+
self.bus.log("Database session plugin started.")
|
|
189
184
|
|
|
190
185
|
def stop(self):
|
|
191
186
|
if self._session:
|
|
192
187
|
self.clear_sessions()
|
|
193
188
|
if self._engine:
|
|
194
189
|
self._engine.dispose()
|
|
195
|
-
self.bus.log("
|
|
196
|
-
|
|
197
|
-
def graceful(self):
|
|
198
|
-
"""Reload of subscribers."""
|
|
199
|
-
self.stop()
|
|
200
|
-
self.start()
|
|
190
|
+
self.bus.log("Database session plugin stopped.")
|
|
201
191
|
|
|
202
192
|
def create_all(self):
|
|
203
193
|
try:
|
|
@@ -252,27 +242,11 @@ class SQLA(SimplePlugin):
|
|
|
252
242
|
try:
|
|
253
243
|
# When terminating, raise an error if objects are not commit.
|
|
254
244
|
if self._session.dirty or self._session.new or self._session.deleted:
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
245
|
+
changes = ', '.join([str(_get_model_changes(obj)) for obj in self._session.dirty])
|
|
246
|
+
logger.exception(
|
|
247
|
+
'session is dirty, some database object(s) are not commited, this indicate a bug in the application '
|
|
248
|
+
'dirty %s new %s deleted %s' % (changes, self._session.new, self._session.deleted)
|
|
259
249
|
)
|
|
260
|
-
if self._session.dirty:
|
|
261
|
-
changes = ', '.join([str(_get_model_changes(obj)) for obj in self._session.dirty])
|
|
262
|
-
cherrypy.log(
|
|
263
|
-
f'database session dirty_objects={self._session.dirty}', context='DB', severity=logging.ERROR
|
|
264
|
-
)
|
|
265
|
-
cherrypy.log(f'database session pending_changes={changes}', context='DB', severity=logging.ERROR)
|
|
266
|
-
if self._session.new:
|
|
267
|
-
cherrypy.log(
|
|
268
|
-
f'database session new_objects={self._session.new}', context='DB', severity=logging.ERROR
|
|
269
|
-
)
|
|
270
|
-
if self._session.deleted:
|
|
271
|
-
cherrypy.log(
|
|
272
|
-
f'database session deleted_objects={self._session.deleted}',
|
|
273
|
-
context='DB',
|
|
274
|
-
severity=logging.ERROR,
|
|
275
|
-
)
|
|
276
250
|
raise SQLAlchemyError('session is dirty')
|
|
277
251
|
finally:
|
|
278
252
|
self._session.rollback()
|
|
@@ -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
|
|
@@ -21,12 +21,14 @@ import ldap3
|
|
|
21
21
|
from cherrypy.process.plugins import SimplePlugin
|
|
22
22
|
from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult
|
|
23
23
|
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
24
26
|
_safe = ldap3.utils.conv.escape_filter_chars
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def all_attribute(attributes, keys, default=None):
|
|
28
30
|
"""
|
|
29
|
-
Extract all
|
|
31
|
+
Extract the all value from LDAP attributes.
|
|
30
32
|
"""
|
|
31
33
|
# Skip loopkup if key is not defined.
|
|
32
34
|
if not keys:
|
|
@@ -61,9 +63,7 @@ def first_attribute(attributes, keys, default=None):
|
|
|
61
63
|
for attr in keys:
|
|
62
64
|
try:
|
|
63
65
|
value = attributes[attr]
|
|
64
|
-
if isinstance(value, list):
|
|
65
|
-
if len(value) == 0:
|
|
66
|
-
continue
|
|
66
|
+
if isinstance(value, list) and len(value) > 0:
|
|
67
67
|
return value[0]
|
|
68
68
|
else:
|
|
69
69
|
return value
|
|
@@ -77,8 +77,8 @@ class LdapPlugin(SimplePlugin):
|
|
|
77
77
|
"""
|
|
78
78
|
Used this plugin to authenticate user against an LDAP server.
|
|
79
79
|
|
|
80
|
-
`authenticate(
|
|
81
|
-
valid. Otherwise it return a tuple or
|
|
80
|
+
`authenticate(username, password)` return None if the credentials are not
|
|
81
|
+
valid. Otherwise it return a tuple or username and extra attributes.
|
|
82
82
|
The extra attribute may contains `_fullname` and `_email`.
|
|
83
83
|
"""
|
|
84
84
|
|
|
@@ -89,7 +89,7 @@ class LdapPlugin(SimplePlugin):
|
|
|
89
89
|
scope = 'subtree'
|
|
90
90
|
tls = False
|
|
91
91
|
user_filter = '(objectClass=*)'
|
|
92
|
-
|
|
92
|
+
username_attribute = ['uid']
|
|
93
93
|
required_group = None
|
|
94
94
|
group_attribute = 'member'
|
|
95
95
|
group_attribute_is_dn = False
|
|
@@ -101,7 +101,6 @@ class LdapPlugin(SimplePlugin):
|
|
|
101
101
|
firstname_attribute = None
|
|
102
102
|
lastname_attribute = None
|
|
103
103
|
email_attribute = None
|
|
104
|
-
pool_size = 10
|
|
105
104
|
|
|
106
105
|
def start(self):
|
|
107
106
|
# Don't configure this plugin if the ldap URI is not provided.
|
|
@@ -119,7 +118,6 @@ class LdapPlugin(SimplePlugin):
|
|
|
119
118
|
version=self.version,
|
|
120
119
|
raise_exceptions=True,
|
|
121
120
|
client_strategy=ldap3.REUSABLE,
|
|
122
|
-
pool_size=self.pool_size,
|
|
123
121
|
)
|
|
124
122
|
|
|
125
123
|
def stop(self):
|
|
@@ -128,19 +126,14 @@ class LdapPlugin(SimplePlugin):
|
|
|
128
126
|
if hasattr(self, '_pool'):
|
|
129
127
|
self._pool.unbind()
|
|
130
128
|
|
|
131
|
-
def
|
|
132
|
-
"""Reload of subscribers."""
|
|
133
|
-
self.stop()
|
|
134
|
-
self.start()
|
|
135
|
-
|
|
136
|
-
def authenticate(self, login, password):
|
|
129
|
+
def authenticate(self, username, password):
|
|
137
130
|
"""
|
|
138
131
|
Check if the given credential as valid according to LDAP.
|
|
139
132
|
Return False if invalid.
|
|
140
133
|
Return None if the plugin is unavailable to validate credentials or if the plugin is disabled.
|
|
141
|
-
Return tuple (<
|
|
134
|
+
Return tuple (<username>, <attributes>) if the credentials are valid.
|
|
142
135
|
"""
|
|
143
|
-
assert isinstance(
|
|
136
|
+
assert isinstance(username, str)
|
|
144
137
|
assert isinstance(password, str)
|
|
145
138
|
|
|
146
139
|
if not hasattr(self, '_pool'):
|
|
@@ -150,14 +143,14 @@ class LdapPlugin(SimplePlugin):
|
|
|
150
143
|
with self._pool as conn:
|
|
151
144
|
try:
|
|
152
145
|
# Search the LDAP server for user's DN.
|
|
153
|
-
|
|
154
|
-
attr_filter = ''.join([f'({_safe(attr)}={
|
|
146
|
+
safe_username = _safe(username)
|
|
147
|
+
attr_filter = ''.join([f'({_safe(attr)}={safe_username})' for attr in self.username_attribute])
|
|
155
148
|
search_filter = f"(&{self.user_filter}(|{attr_filter}))"
|
|
156
149
|
response = self._search(conn, search_filter)
|
|
157
150
|
if not response:
|
|
158
|
-
|
|
151
|
+
logger.info("user %s not found in LDAP", username)
|
|
159
152
|
return False
|
|
160
|
-
|
|
153
|
+
logger.info("user %s found in LDAP", username)
|
|
161
154
|
user_dn = response[0]['dn']
|
|
162
155
|
|
|
163
156
|
# Use a separate connection to validate credentials
|
|
@@ -170,21 +163,17 @@ class LdapPlugin(SimplePlugin):
|
|
|
170
163
|
client_strategy=ldap3.ASYNC,
|
|
171
164
|
)
|
|
172
165
|
if not login_conn.bind():
|
|
173
|
-
|
|
174
|
-
f'ldap authentication failed login={login} reason=wrong_password',
|
|
175
|
-
context='LDAP',
|
|
176
|
-
severity=logging.WARNING,
|
|
177
|
-
)
|
|
166
|
+
logger.warning("LDAP authentication failed for user %s", username)
|
|
178
167
|
return False
|
|
179
168
|
|
|
180
|
-
# Get
|
|
169
|
+
# Get username
|
|
181
170
|
attrs = response[0]['attributes']
|
|
182
|
-
|
|
183
|
-
if not
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
171
|
+
new_username = first_attribute(attrs, self.username_attribute)
|
|
172
|
+
if not new_username:
|
|
173
|
+
logger.info(
|
|
174
|
+
"user object %s was found but the username attribute %s doesn't exists",
|
|
175
|
+
user_dn,
|
|
176
|
+
self.username_attribute,
|
|
188
177
|
)
|
|
189
178
|
return False
|
|
190
179
|
|
|
@@ -192,7 +181,7 @@ class LdapPlugin(SimplePlugin):
|
|
|
192
181
|
if self.required_group:
|
|
193
182
|
if not isinstance(self.required_group, list):
|
|
194
183
|
self.required_group = [self.required_group]
|
|
195
|
-
user_value = user_dn if self.group_attribute_is_dn else
|
|
184
|
+
user_value = user_dn if self.group_attribute_is_dn else new_username
|
|
196
185
|
group_filter = '(&(%s=%s)(|%s)%s)' % (
|
|
197
186
|
_safe(self.group_attribute),
|
|
198
187
|
_safe(user_value),
|
|
@@ -200,15 +189,17 @@ class LdapPlugin(SimplePlugin):
|
|
|
200
189
|
self.group_filter,
|
|
201
190
|
)
|
|
202
191
|
# Search LDAP Server for matching groups.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
192
|
+
logger.info(
|
|
193
|
+
"check if user %s is member of any group %s",
|
|
194
|
+
user_value,
|
|
195
|
+
' '.join(self.required_group),
|
|
206
196
|
)
|
|
207
197
|
response = self._search(conn, group_filter, attributes=['cn'])
|
|
208
198
|
if not response:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
199
|
+
logger.info(
|
|
200
|
+
"user %s was found but is not member of any group(s) %s",
|
|
201
|
+
user_value,
|
|
202
|
+
' '.join(self.required_group),
|
|
212
203
|
)
|
|
213
204
|
return False
|
|
214
205
|
|
|
@@ -220,11 +211,11 @@ class LdapPlugin(SimplePlugin):
|
|
|
220
211
|
firstname = first_attribute(attrs, self.firstname_attribute, '')
|
|
221
212
|
lastname = first_attribute(attrs, self.lastname_attribute, '')
|
|
222
213
|
attrs['fullname'] = ' '.join([name for name in [firstname, lastname] if name])
|
|
223
|
-
return (
|
|
214
|
+
return (new_username, attrs)
|
|
224
215
|
except LDAPInvalidCredentialsResult:
|
|
225
216
|
return False
|
|
226
217
|
except LDAPException:
|
|
227
|
-
|
|
218
|
+
logger.exception("can't validate user %s credentials", username)
|
|
228
219
|
return None
|
|
229
220
|
|
|
230
221
|
def search(self, filter, attributes=ldap3.ALL_ATTRIBUTES, search_base=None, paged_size=None):
|
|
@@ -237,10 +228,11 @@ class LdapPlugin(SimplePlugin):
|
|
|
237
228
|
search_scope = {'base': ldap3.BASE, 'onelevel': ldap3.LEVEL, 'subtree': ldap3.SUBTREE}.get(
|
|
238
229
|
self.scope, ldap3.SUBTREE
|
|
239
230
|
)
|
|
240
|
-
|
|
241
|
-
|
|
231
|
+
logger.debug(
|
|
232
|
+
"search ldap server: {}/{}?{}?{}".format(self.uri, search_base or self.base_dn, search_scope, filter)
|
|
233
|
+
)
|
|
242
234
|
msg_id = conn.search(
|
|
243
|
-
search_base=search_base,
|
|
235
|
+
search_base=search_base or self.base_dn,
|
|
244
236
|
search_filter=filter,
|
|
245
237
|
search_scope=search_scope,
|
|
246
238
|
time_limit=self.timeout,
|