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.
Files changed (68) hide show
  1. cherrypy_foundation/components/ColorModes.jinja +4 -5
  2. cherrypy_foundation/components/Datatable.jinja +2 -2
  3. cherrypy_foundation/components/Datatable.js +2 -2
  4. cherrypy_foundation/components/Field.jinja +6 -16
  5. cherrypy_foundation/components/Fields.jinja +2 -0
  6. cherrypy_foundation/components/Typeahead.css +1 -6
  7. cherrypy_foundation/components/Typeahead.jinja +2 -2
  8. cherrypy_foundation/components/__init__.py +2 -2
  9. cherrypy_foundation/components/tests/test_static.py +1 -1
  10. cherrypy_foundation/error_page.py +17 -20
  11. cherrypy_foundation/flash.py +15 -17
  12. cherrypy_foundation/form.py +2 -2
  13. cherrypy_foundation/logging.py +2 -2
  14. cherrypy_foundation/passwd.py +2 -2
  15. cherrypy_foundation/plugins/db.py +9 -35
  16. cherrypy_foundation/plugins/ldap.py +38 -46
  17. cherrypy_foundation/plugins/restapi.py +1 -1
  18. cherrypy_foundation/plugins/scheduler.py +84 -208
  19. cherrypy_foundation/plugins/smtp.py +46 -78
  20. cherrypy_foundation/plugins/tests/test_db.py +4 -4
  21. cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
  22. cherrypy_foundation/plugins/tests/test_scheduler.py +50 -58
  23. cherrypy_foundation/plugins/tests/test_smtp.py +7 -40
  24. cherrypy_foundation/tests/__init__.py +0 -72
  25. cherrypy_foundation/tests/test_error_page.py +1 -7
  26. cherrypy_foundation/tests/test_passwd.py +2 -2
  27. cherrypy_foundation/tools/auth.py +38 -59
  28. cherrypy_foundation/tools/auth_mfa.py +88 -89
  29. cherrypy_foundation/tools/errors.py +27 -0
  30. cherrypy_foundation/tools/i18n.py +153 -246
  31. cherrypy_foundation/tools/jinja2.py +13 -29
  32. cherrypy_foundation/tools/ratelimit.py +27 -37
  33. cherrypy_foundation/tools/secure_headers.py +5 -1
  34. cherrypy_foundation/tools/sessions_timeout.py +21 -23
  35. cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
  36. cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
  37. cherrypy_foundation/tools/tests/test_auth.py +4 -21
  38. cherrypy_foundation/tools/tests/test_i18n.py +6 -81
  39. cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
  40. cherrypy_foundation/url.py +25 -25
  41. cherrypy_foundation/widgets.py +2 -2
  42. cherrypy_foundation-1.0.0a1.dist-info/METADATA +42 -0
  43. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/RECORD +46 -65
  44. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/WHEEL +1 -1
  45. cherrypy_foundation/components/Flash.jinja +0 -13
  46. cherrypy_foundation/components/LocaleSelection.jinja +0 -13
  47. cherrypy_foundation/components/LocaleSelection.js +0 -26
  48. cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
  49. cherrypy_foundation/sessions.py +0 -93
  50. cherrypy_foundation/tests/templates/test_flash.html +0 -9
  51. cherrypy_foundation/tests/templates/test_form.html +0 -16
  52. cherrypy_foundation/tests/templates/test_url.html +0 -15
  53. cherrypy_foundation/tests/test_flash.py +0 -61
  54. cherrypy_foundation/tests/test_form.py +0 -148
  55. cherrypy_foundation/tests/test_logging.py +0 -78
  56. cherrypy_foundation/tests/test_sessions.py +0 -89
  57. cherrypy_foundation/tests/test_url.py +0 -161
  58. cherrypy_foundation/tools/tests/components/Button.jinja +0 -2
  59. cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
  60. cherrypy_foundation/tools/tests/templates/test_jinja2.html +0 -11
  61. cherrypy_foundation/tools/tests/templates/test_jinjax.html +0 -9
  62. cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
  63. cherrypy_foundation/tools/tests/test_auth_mfa.py +0 -369
  64. cherrypy_foundation/tools/tests/test_jinja2.py +0 -153
  65. cherrypy_foundation/tools/tests/test_secure_headers.py +0 -200
  66. cherrypy_foundation-1.0.0.dist-info/METADATA +0 -71
  67. {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/licenses/LICENSE.md +0 -0
  68. {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">{{ header }}</span>
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
- {{ light_label }}
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
- {{ dark_label }}
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
- {{ auto_label }}
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-2 mb-sm-0",
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-2 mb-sm-0 w-100 text-start",
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
- * Cherrypy-foundation
3
- * Copyright (C) 2026 IKUS Software
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
- {# Build container & label specific attributes #}
39
- {% set container_attrs = attrs.__class__({'class': 'mb-2 form-field'}) %}
40
- {% set label_attrs = attrs.__class__({'class': label_class}) %}
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(**label_attrs.as_dict) }}{% endif %}
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(**label_attrs.as_dict) }}{% endif %}
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=False,
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=None,
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-foundation
2
- # Copyright (C) 2026 IKUS Software
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
1
  # CherryPy
2
- # Copyright (C) 2026 IKUS Software
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
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2020-2026 IKUS Software
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(status='', message='', traceback='', version=''):
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
- cherrypy.log(
62
- f'error page status={status} message={message}\n{traceback}', context='ERROR-PAGE', severity=logging.WARNING
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' and cherrypy.serving.request.path_info in message:
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', context=context)
84
+ return cherrypy.tools.jinja2.render_request(template='error_page.html', vars=kwargs)
83
85
  except Exception:
84
- cherrypy.log(
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 % context
88
+ return HTML_ERROR_TEMPLATE % kwargs
92
89
 
93
90
  # Fallback to raw error message.
94
- return message
91
+ return kwargs.get('message')
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2020-2026 IKUS Software
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
- from cherrypy_foundation.sessions import session_lock
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
- with session_lock() as session:
31
- if 'flash' not in session:
32
- session['flash'] = []
33
- # Support Markup and string
34
- if hasattr(message, '__html__'):
35
- flash_message = FlashMessage(message, level)
36
- else:
37
- flash_message = FlashMessage(str(message), level)
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
- with session_lock() as session:
46
- if 'flash' in session:
47
- messages = session['flash']
48
- del session['flash']
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 []
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2020-2026 IKUS Software
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
@@ -1,5 +1,5 @@
1
- # Cherrypy-foundation
2
- # Copyright (C) 2026 IKUS Software
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
- # Cherrypy-foundation
2
- # Copyright (C) 2026 IKUS Software
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-2026 IKUS Software
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("database session plugin started")
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("database session plugin stopped")
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
- cherrypy.log(
256
- 'database session dirty; uncommitted objects detected — potential application bug',
257
- context='DB',
258
- severity=logging.ERROR,
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) 2026 IKUS Software
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 values from LDAP attributes.
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(login, password)` return None if the credentials are not
81
- valid. Otherwise it return a tuple or login and extra attributes.
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
- login_attribute = ['uid']
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 graceful(self):
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 (<login>, <attributes>) if the credentials are valid.
134
+ Return tuple (<username>, <attributes>) if the credentials are valid.
142
135
  """
143
- assert isinstance(login, str)
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
- safe_login = _safe(login)
154
- attr_filter = ''.join([f'({_safe(attr)}={safe_login})' for attr in self.login_attribute])
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
- cherrypy.log(f"lookup failed login={login} reason=not_found", context='LDAP')
151
+ logger.info("user %s not found in LDAP", username)
159
152
  return False
160
- cherrypy.log(f"lookup successful login={login}", context='LDAP')
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
- cherrypy.log(
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 user's login
169
+ # Get username
181
170
  attrs = response[0]['attributes']
182
- new_login = first_attribute(attrs, self.login_attribute[0])
183
- if not new_login:
184
- cherrypy.log(
185
- f"object missing login attribute user_dn={user_dn} attribute={self.login_attribute[0]}",
186
- context='LDAP',
187
- severity=logging.WARNING,
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 new_login
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
- cherrypy.log(
204
- f"group check start login={user_value} required_groups={' '.join(self.required_group)}",
205
- context='LDAP',
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
- cherrypy.log(
210
- f"group check failed login={user_value} required_groups={' '.join(self.required_group)}",
211
- context='LDAP',
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 (new_login, attrs)
214
+ return (new_username, attrs)
224
215
  except LDAPInvalidCredentialsResult:
225
216
  return False
226
217
  except LDAPException:
227
- cherrypy.log(f"unexpected error login={login}", context='LDAP', severity=logging.ERROR, traceback=True)
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
- search_base = search_base or self.base_dn
241
- cherrypy.log(f"search {self.uri}/{search_base}?{search_scope}?{filter}", context='LDAP')
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,
@@ -1,5 +1,5 @@
1
1
  # RestAPI plugin for cherrypy
2
- # Copyright (C) 2024-2026 IKUS Software
2
+ # Copyright (C) 2024-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