Translate repository documentation and configuration added for ldap logging
This commit is contained in:
+119
-83
@@ -1053,12 +1053,17 @@ def _action_status(db: Session, action: str) -> StatusOption | None:
|
||||
return db.query(StatusOption).filter(StatusOption.active.is_(True), column.is_(True)).first()
|
||||
|
||||
|
||||
def _ldap_debug_enabled(auth_config: dict) -> bool:
|
||||
return bool(auth_config.get("ldap", {}).get("debug_logging", False))
|
||||
|
||||
|
||||
def _ldap_authenticate(username: str, password: str, auth_config: dict) -> dict | None:
|
||||
from ldap3 import ALL, Connection, Server, SUBTREE
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
|
||||
ldap = auth_config.get("ldap", {})
|
||||
debug_logging = _ldap_debug_enabled(auth_config)
|
||||
server_name = str(ldap.get("server", "")).strip()
|
||||
base_dn = str(ldap.get("user_base_dn", "")).strip()
|
||||
use_ssl = bool(ldap.get("use_ssl", False))
|
||||
@@ -1070,22 +1075,26 @@ def _ldap_authenticate(username: str, password: str, auth_config: dict) -> dict
|
||||
if not bind_password:
|
||||
bind_password = str(ldap.get("bind_password", ""))
|
||||
|
||||
ldap_logger.info(
|
||||
"Anmeldeversuch Benutzer=%r Server=%s Port=%s SSL=%s StartTLS=%s BaseDN=%r BindDN=%r Passwortvariable=%r gesetzt=%s",
|
||||
username, server_name, port, use_ssl, start_tls, base_dn, bind_dn, env_name, bool(bind_password),
|
||||
)
|
||||
ldap_logger.info("LDAP login lookup started: username=%r", username)
|
||||
if debug_logging:
|
||||
ldap_logger.debug(
|
||||
"LDAP connection settings: server=%s port=%s ssl=%s start_tls=%s base_dn=%r bind_dn=%r password_env=%r configured=%s",
|
||||
server_name, port, use_ssl, start_tls, base_dn, bind_dn, env_name, bool(bind_password),
|
||||
)
|
||||
|
||||
if not server_name:
|
||||
ldap_logger.error("LDAP-Server ist nicht konfiguriert")
|
||||
ldap_logger.error("LDAP authentication unavailable: server is not configured")
|
||||
return None
|
||||
if not base_dn:
|
||||
ldap_logger.error("Benutzer-Basis-DN ist nicht konfiguriert")
|
||||
ldap_logger.error("LDAP authentication unavailable: user base DN is not configured")
|
||||
return None
|
||||
if not password:
|
||||
ldap_logger.warning("Leeres Benutzerpasswort für Benutzer=%r", username)
|
||||
ldap_logger.warning("LDAP login rejected: username=%r reason=empty-password", username)
|
||||
return None
|
||||
if bind_dn and not bind_password:
|
||||
ldap_logger.error("Bind-DN ist gesetzt, aber das Bind-Passwort fehlt. Erwartete Umgebungsvariable=%r", env_name)
|
||||
ldap_logger.error("LDAP authentication unavailable: service-account password is missing")
|
||||
if debug_logging:
|
||||
ldap_logger.debug("Expected LDAP bind-password environment variable: %r", env_name)
|
||||
return None
|
||||
|
||||
server = Server(server_name, port=port, use_ssl=use_ssl, get_info=ALL, connect_timeout=10)
|
||||
@@ -1102,16 +1111,23 @@ def _ldap_authenticate(username: str, password: str, auth_config: dict) -> dict
|
||||
)
|
||||
if start_tls and not use_ssl:
|
||||
search_connection.open()
|
||||
ldap_logger.debug("TCP/LDAP-Verbindung geöffnet")
|
||||
if debug_logging:
|
||||
ldap_logger.debug("LDAP TCP connection opened")
|
||||
if not search_connection.start_tls():
|
||||
ldap_logger.error("StartTLS fehlgeschlagen: result=%r last_error=%r", search_connection.result, search_connection.last_error)
|
||||
ldap_logger.error("LDAP authentication unavailable: StartTLS failed")
|
||||
if debug_logging:
|
||||
ldap_logger.debug("StartTLS details: result=%r last_error=%r", search_connection.result, search_connection.last_error)
|
||||
return None
|
||||
ldap_logger.debug("StartTLS erfolgreich")
|
||||
if debug_logging:
|
||||
ldap_logger.debug("LDAP StartTLS completed")
|
||||
|
||||
if not search_connection.bind():
|
||||
ldap_logger.error("Dienstkonto-Bind fehlgeschlagen: result=%r last_error=%r", search_connection.result, search_connection.last_error)
|
||||
ldap_logger.error("LDAP authentication unavailable: service-account bind failed")
|
||||
if debug_logging:
|
||||
ldap_logger.debug("Service-account bind details: result=%r last_error=%r", search_connection.result, search_connection.last_error)
|
||||
return None
|
||||
ldap_logger.info("Dienstkonto-Bind erfolgreich")
|
||||
if debug_logging:
|
||||
ldap_logger.debug("LDAP service-account bind completed")
|
||||
|
||||
raw_filter = str(ldap.get("user_filter", "(sAMAccountName={username})"))
|
||||
escaped_username = escape_filter_chars(username)
|
||||
@@ -1119,7 +1135,8 @@ def _ldap_authenticate(username: str, password: str, auth_config: dict) -> dict
|
||||
display_attr = str(ldap.get("display_name_attribute", "displayName"))
|
||||
email_attr = str(ldap.get("email_attribute", "mail"))
|
||||
attributes = list(dict.fromkeys([display_attr, email_attr, "distinguishedName"]))
|
||||
ldap_logger.info("Benutzersuche BaseDN=%r Filter=%r Attribute=%r", base_dn, user_filter, attributes)
|
||||
if debug_logging:
|
||||
ldap_logger.debug("LDAP user search: base_dn=%r filter=%r attributes=%r", base_dn, user_filter, attributes)
|
||||
|
||||
search_ok = search_connection.search(
|
||||
base_dn,
|
||||
@@ -1127,22 +1144,25 @@ def _ldap_authenticate(username: str, password: str, auth_config: dict) -> dict
|
||||
search_scope=SUBTREE,
|
||||
attributes=attributes,
|
||||
)
|
||||
ldap_logger.info(
|
||||
"Benutzersuche beendet: success=%s Treffer=%s result=%r last_error=%r",
|
||||
search_ok, len(search_connection.entries), search_connection.result, search_connection.last_error,
|
||||
)
|
||||
if len(search_connection.entries) != 1:
|
||||
if len(search_connection.entries) == 0:
|
||||
ldap_logger.warning("Kein LDAP-Benutzer für Benutzer=%r gefunden", username)
|
||||
else:
|
||||
ldap_logger.warning("Mehrere LDAP-Benutzer für Benutzer=%r gefunden: %s", username, len(search_connection.entries))
|
||||
entry_count = len(search_connection.entries)
|
||||
if debug_logging:
|
||||
ldap_logger.debug(
|
||||
"LDAP user search details: success=%s entries=%s result=%r last_error=%r",
|
||||
search_ok, entry_count, search_connection.result, search_connection.last_error,
|
||||
)
|
||||
if entry_count != 1:
|
||||
ldap_logger.info("LDAP user lookup completed: username=%r found=no", username)
|
||||
if debug_logging and entry_count > 1:
|
||||
ldap_logger.debug("LDAP user lookup returned multiple entries: username=%r entries=%s", username, entry_count)
|
||||
return None
|
||||
|
||||
ldap_logger.info("LDAP user lookup completed: username=%r found=yes", username)
|
||||
entry = search_connection.entries[0]
|
||||
user_dn = entry.entry_dn
|
||||
display_name = str(getattr(entry, display_attr, "") or username)
|
||||
email = str(getattr(entry, email_attr, "") or "")
|
||||
ldap_logger.info("Benutzer gefunden: DN=%r Anzeigename=%r E-Mail=%r", user_dn, display_name, email)
|
||||
if debug_logging:
|
||||
ldap_logger.debug("LDAP user details: dn=%r display_name=%r email=%r", user_dn, display_name, email)
|
||||
|
||||
user_connection = Connection(
|
||||
server,
|
||||
@@ -1155,19 +1175,27 @@ def _ldap_authenticate(username: str, password: str, auth_config: dict) -> dict
|
||||
if start_tls and not use_ssl:
|
||||
user_connection.open()
|
||||
if not user_connection.start_tls():
|
||||
ldap_logger.error("StartTLS beim Benutzer-Bind fehlgeschlagen: result=%r last_error=%r", user_connection.result, user_connection.last_error)
|
||||
ldap_logger.info("LDAP login completed: username=%r success=no", username)
|
||||
if debug_logging:
|
||||
ldap_logger.debug("User StartTLS details: result=%r last_error=%r", user_connection.result, user_connection.last_error)
|
||||
return None
|
||||
if not user_connection.bind():
|
||||
ldap_logger.warning("Benutzer-Bind fehlgeschlagen für DN=%r: result=%r last_error=%r", user_dn, user_connection.result, user_connection.last_error)
|
||||
ldap_logger.info("LDAP login completed: username=%r success=no", username)
|
||||
if debug_logging:
|
||||
ldap_logger.debug("User bind details: dn=%r result=%r last_error=%r", user_dn, user_connection.result, user_connection.last_error)
|
||||
return None
|
||||
|
||||
ldap_logger.info("LDAP-Anmeldung erfolgreich für Benutzer=%r DN=%r", username, user_dn)
|
||||
ldap_logger.info("LDAP login completed: username=%r success=yes", username)
|
||||
return {"username": username, "display_name": display_name, "email": email}
|
||||
except LDAPException as exc:
|
||||
ldap_logger.exception("LDAP-Ausnahme für Benutzer=%r: %s", username, exc)
|
||||
ldap_logger.error("LDAP login failed because of an LDAP error: username=%r", username)
|
||||
if debug_logging:
|
||||
ldap_logger.exception("LDAP exception during login for username=%r: %s", username, exc)
|
||||
return None
|
||||
except Exception as exc:
|
||||
ldap_logger.exception("Unerwarteter LDAP-Fehler für Benutzer=%r: %s", username, exc)
|
||||
ldap_logger.error("LDAP login failed because of an unexpected error: username=%r", username)
|
||||
if debug_logging:
|
||||
ldap_logger.exception("Unexpected LDAP login error for username=%r: %s", username, exc)
|
||||
return None
|
||||
finally:
|
||||
for connection in (user_connection, search_connection):
|
||||
@@ -1690,10 +1718,10 @@ def startup():
|
||||
db.commit()
|
||||
_seed_field_definitions(db)
|
||||
|
||||
# Frühere Excel-Importe konnten Datumswerte als
|
||||
# "YYYY-MM-DD 00:00:00" speichern. Browser zeigen solche Werte in
|
||||
# <input type="date"> leer an. Bestehende System-Datumsfelder werden
|
||||
# deshalb einmalig auf YYYY-MM-DD normalisiert.
|
||||
# Earlier Excel imports could store date values as
|
||||
# "YYYY-MM-DD 00:00:00". Browsers render such values as empty in
|
||||
# <input type="date">. Existing system date fields are therefore
|
||||
# normalized to YYYY-MM-DD once.
|
||||
date_repairs = 0
|
||||
for asset in db.query(Asset).filter(
|
||||
(Asset.purchase_date.like("% %")) |
|
||||
@@ -1756,7 +1784,7 @@ def stop_mesh_presence_service():
|
||||
|
||||
|
||||
def delete_asset_image(image_path: str | None) -> None:
|
||||
"""Löscht nur individuell hochgeladene Asset-Bilder aus dem Upload-Ordner."""
|
||||
"""Delete only individually uploaded asset images from the upload directory."""
|
||||
if not image_path or not image_path.startswith("/static/uploads/"):
|
||||
return
|
||||
filename = Path(image_path).name
|
||||
@@ -1796,9 +1824,9 @@ def _record_asset_history(db: Session, asset: Asset, changes: dict, source: str
|
||||
|
||||
|
||||
def _xlsx_response(filename: str, headers: list[str], rows: list[list]) -> Response:
|
||||
"""Erzeugt eine echte XLSX-Datei und liefert vollständige Bytes statt eines Streams.
|
||||
"""Create a real XLSX file and return complete bytes instead of a stream.
|
||||
|
||||
Das verhindert beschädigte Downloads bei einzelnen Reverse-Proxies und Browsern.
|
||||
This prevents corrupted downloads with certain reverse proxies and browsers.
|
||||
"""
|
||||
workbook = Workbook()
|
||||
sheet = workbook.active
|
||||
@@ -1854,7 +1882,7 @@ def _excel_cell_value(value) -> str | None:
|
||||
|
||||
|
||||
def _normalize_date_value(value: Any) -> str | None:
|
||||
"""Normalisiert Excel-/Datenbankwerte für HTML-Datumsfelder auf YYYY-MM-DD."""
|
||||
"""Normalize Excel and database values for HTML date fields to YYYY-MM-DD."""
|
||||
if value in (None, ""):
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
@@ -1864,10 +1892,10 @@ def _normalize_date_value(value: Any) -> str | None:
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
return None
|
||||
# ISO-Datum oder ISO-Zeitstempel: Der Datumsteil sind die ersten 10 Zeichen.
|
||||
# ISO date or timestamp: the date portion is the first 10 characters.
|
||||
if re.match(r"^\d{4}-\d{2}-\d{2}(?:[ T].*)?$", text):
|
||||
return text[:10]
|
||||
# Deutsche Schreibweise aus manuellen Excel-Dateien.
|
||||
# German date notation from manually maintained Excel files.
|
||||
match = re.match(r"^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+.*)?$", text)
|
||||
if match:
|
||||
day, month, year = match.groups()
|
||||
@@ -1876,7 +1904,7 @@ def _normalize_date_value(value: Any) -> str | None:
|
||||
|
||||
|
||||
def _normalize_datetime_value(value: Any) -> str | None:
|
||||
"""Normalisiert Werte für <input type=datetime-local>."""
|
||||
"""Normalize values for <input type=datetime-local>."""
|
||||
if value in (None, ""):
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
@@ -1990,7 +2018,7 @@ def _would_create_parent_cycle(db: Session, asset_id: int, parent_id: int | None
|
||||
|
||||
|
||||
def _list_fields_for_categories(categories: list[Category]) -> list[CategoryField]:
|
||||
"""Bildet eine eindeutige, sortierte Vereinigungsmenge der konfigurierten Listenspalten."""
|
||||
"""Build a unique, sorted union of the configured list columns."""
|
||||
result: list[CategoryField] = []
|
||||
seen: set[str] = set()
|
||||
for category in categories:
|
||||
@@ -2025,7 +2053,7 @@ CHART_TYPES = {
|
||||
|
||||
|
||||
def _parse_value_mappings(text_value: str) -> list[dict[str, str]]:
|
||||
"""Liest Regeln im Format Suchtext => Anzeigename (eine Regel je Zeile)."""
|
||||
"""Read rules in the format search text => display name, one rule per line."""
|
||||
mappings: list[dict[str, str]] = []
|
||||
for line_number, raw_line in enumerate((text_value or "").splitlines(), start=1):
|
||||
line = raw_line.strip()
|
||||
@@ -2052,7 +2080,7 @@ def _value_mappings_text(chart: ChartDefinition | None) -> str:
|
||||
|
||||
|
||||
def _automatic_chart_value(field: str, value: str) -> str:
|
||||
"""Fasst technische Rohwerte für Diagramme zusammen, ohne die Assetdaten zu verändern."""
|
||||
"""Summarize technical raw values for charts without modifying asset data."""
|
||||
text = value.strip()
|
||||
lower = text.casefold()
|
||||
if field == "operating_system":
|
||||
@@ -2090,7 +2118,7 @@ def _automatic_chart_value(field: str, value: str) -> str:
|
||||
|
||||
|
||||
def _transform_chart_value(chart: ChartDefinition, field: str, value: str) -> str:
|
||||
# Eigene Regeln haben Vorrang; gesucht wird ohne Beachtung der Groß-/Kleinschreibung.
|
||||
# Custom rules take precedence; matching is case-insensitive.
|
||||
lower = value.casefold()
|
||||
for entry in chart.value_mappings or []:
|
||||
if not isinstance(entry, dict):
|
||||
@@ -2914,7 +2942,7 @@ async def assets_import(
|
||||
else:
|
||||
allowed_fields = set(definitions_by_name) | {"name"}
|
||||
|
||||
# Leere Zellen werden bewusst nicht in Updates übernommen.
|
||||
# Empty cells are intentionally not applied to updates.
|
||||
data = {
|
||||
name: value
|
||||
for name, value in raw_data.items()
|
||||
@@ -3005,7 +3033,7 @@ async def assets_import(
|
||||
})
|
||||
continue
|
||||
|
||||
# Ohne Asset-ID wird immer ein neuer Datensatz erzeugt.
|
||||
# Without an asset ID, always create a new record.
|
||||
if not system_data.get("name"):
|
||||
message = "Für ein neues Asset fehlt die Bezeichnung"
|
||||
errors.append(f"Zeile {excel_row_number}: {message}")
|
||||
@@ -3083,12 +3111,12 @@ def asset_tree(
|
||||
hide_without_parent: str | None = None,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Zeigt die gefilterte Gerätehierarchie.
|
||||
"""Display the filtered device hierarchy.
|
||||
|
||||
Mehrere Kategorien werden als wiederholte Query-Parameter ``category_id``
|
||||
übergeben. Filter werden auf jedes Gerät angewendet, nicht nur auf die
|
||||
Wurzelknoten. Wenn der direkte Elternknoten ausgefiltert ist, wird das
|
||||
passende Gerät in der Darstellung als eigener Wurzelknoten angezeigt.
|
||||
Multiple categories are passed as repeated ``category_id`` query parameters.
|
||||
Filters apply to every device, not only to root nodes. If the immediate parent
|
||||
node is filtered out, the device is rendered as a temporary root so it remains
|
||||
visible in the hierarchy.
|
||||
"""
|
||||
try:
|
||||
parsed_root_id = int(root_id) if root_id not in (None, "") else None
|
||||
@@ -3104,10 +3132,10 @@ def asset_tree(
|
||||
if category_id not in selected_category_ids:
|
||||
selected_category_ids.append(category_id)
|
||||
|
||||
# Zuerst alle für den angemeldeten Benutzer sichtbaren Assets laden. Die
|
||||
# Filterung erfolgt anschließend in Python, damit Treffer in beliebiger
|
||||
# Hierarchietiefe nicht durch eine SQL-Abfrage von ihren Eltern getrennt
|
||||
# oder versehentlich nur auf Wurzelknoten beschränkt werden.
|
||||
# First load all assets visible to the signed-in user. Filtering
|
||||
# is then performed in Python so matches at any hierarchy
|
||||
# depth are not separated from their parents by a SQL query
|
||||
# or accidentally restricted to root nodes only.
|
||||
all_assets = (
|
||||
_apply_asset_access(db.query(Asset), request)
|
||||
.options(joinedload(Asset.category))
|
||||
@@ -3120,7 +3148,7 @@ def asset_tree(
|
||||
parent_key = asset.parent_asset_id if asset.parent_asset_id in all_by_id else None
|
||||
all_children.setdefault(parent_key, []).append(asset)
|
||||
|
||||
# Bei ausgewähltem obersten Knoten nur dessen Teilbaum berücksichtigen.
|
||||
# When a top-level node is selected, include only its subtree.
|
||||
scope_ids: set[int]
|
||||
selected_root = all_by_id.get(parsed_root_id) if parsed_root_id else None
|
||||
if selected_root:
|
||||
@@ -3146,8 +3174,8 @@ def asset_tree(
|
||||
if category_matches and name_matches:
|
||||
visible_assets.append(asset)
|
||||
|
||||
# Der explizit gewählte oberste Knoten bleibt als Orientierung sichtbar,
|
||||
# auch wenn er selbst den Namens- oder Kategorienfilter nicht erfüllt.
|
||||
# Keep the explicitly selected top-level node visible for orientation,
|
||||
# even when it does not itself match the name or category filter.
|
||||
if selected_root and selected_root not in visible_assets:
|
||||
visible_assets.insert(0, selected_root)
|
||||
|
||||
@@ -3162,8 +3190,8 @@ def asset_tree(
|
||||
hide_unassigned = str(hide_without_parent or "").lower() in {"1", "true", "on", "yes"}
|
||||
if selected_root:
|
||||
roots = [selected_root]
|
||||
# Treffer, deren Zwischenknoten weggefiltert wurden, zusätzlich als
|
||||
# eigene Wurzeln darstellen, damit kein Treffer verloren geht.
|
||||
# Also render matches whose intermediate nodes were filtered out as
|
||||
# separate roots so no match is lost.
|
||||
roots.extend(
|
||||
asset for asset in by_parent.get(None, [])
|
||||
if asset.id != selected_root.id
|
||||
@@ -3172,9 +3200,9 @@ def asset_tree(
|
||||
roots = list(by_parent.get(None, []))
|
||||
|
||||
if hide_unassigned:
|
||||
# Nur echte Einzelgeräte ohne hinterlegtes übergeordnetes Gerät
|
||||
# ausblenden. Geräte, die lediglich wegen eines Filters als Wurzel
|
||||
# erscheinen, bleiben sichtbar. Ein gewählter oberster Knoten bleibt.
|
||||
# Hide only genuine standalone devices without a stored parent device.
|
||||
# Devices that appear as roots only because of filtering
|
||||
# remain visible. A selected top-level node also remains visible.
|
||||
roots = [
|
||||
asset for asset in roots
|
||||
if asset.id == parsed_root_id
|
||||
@@ -4505,8 +4533,8 @@ async def category_update(category_id: int, request: Request, name: str = Form(.
|
||||
new_image = save_upload(image)
|
||||
if new_image:
|
||||
category.image_path = new_image
|
||||
# Vorhandene Definitionen explizit löschen und flushen. Das verhindert
|
||||
# UniqueConstraint-Verletzungen bei anschließend identischen Feldnamen.
|
||||
# Explicitly delete and flush existing definitions. This prevents
|
||||
# unique-constraint violations when identical field names are inserted afterwards.
|
||||
db.query(CategoryField).filter(CategoryField.category_id == category.id).delete(synchronize_session=False)
|
||||
db.flush()
|
||||
definitions = db.query(FieldDefinition).filter(FieldDefinition.is_active.is_(True)).order_by(FieldDefinition.sort_order).all()
|
||||
@@ -6346,9 +6374,9 @@ def login_submit(request: Request, username: str = Form(...), password: str = Fo
|
||||
user = db.query(User).filter(func.lower(User.username) == username.casefold()).first()
|
||||
valid = False
|
||||
|
||||
# Ein geschütztes lokales Konto wird auch im LDAP-Modus ausschließlich
|
||||
# gegen sein lokales Passwort geprüft. Bei falschem Passwort gibt es
|
||||
# bewusst keinen LDAP-Fallback für denselben Benutzernamen.
|
||||
# A protected local account is authenticated only against its local password,
|
||||
# even in LDAP mode. If the password is incorrect, there is deliberately
|
||||
# no LDAP fallback for the same username.
|
||||
protected_local = bool(
|
||||
user
|
||||
and getattr(user, "is_protected", False)
|
||||
@@ -6581,10 +6609,10 @@ def user_delete(user_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
def _ldap_search_users(query_text: str, auth_config: dict) -> list[dict]:
|
||||
"""Listet AD-Benutzer über das konfigurierte LDAP-Dienstkonto auf.
|
||||
"""List AD users through the configured LDAP service account.
|
||||
|
||||
Eine leere Suche liefert alle aktiven Benutzerkonten. Bei einem Suchtext
|
||||
werden Benutzername, Anzeigename und E-Mail durchsucht.
|
||||
An empty query returns all active user accounts. A search term matches
|
||||
usernames, display names, and email addresses.
|
||||
"""
|
||||
from ldap3 import ALL, Connection, Server, SUBTREE
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
@@ -6610,8 +6638,8 @@ def _ldap_search_users(query_text: str, auth_config: dict) -> list[dict]:
|
||||
raw_query = query_text.strip()
|
||||
|
||||
def ldap_wildcard_pattern(value: str) -> str:
|
||||
# Benutzer-Wildcards zulassen, sonstige LDAP-Sonderzeichen aber sicher maskieren.
|
||||
# * = beliebig viele Zeichen.
|
||||
# Allow user wildcards while safely escaping all other LDAP special characters.
|
||||
# * matches any number of characters.
|
||||
parts = []
|
||||
literal = []
|
||||
for char in value:
|
||||
@@ -6625,7 +6653,7 @@ def _ldap_search_users(query_text: str, auth_config: dict) -> list[dict]:
|
||||
if literal:
|
||||
parts.append(escape_filter_chars("".join(literal)))
|
||||
pattern = "".join(parts)
|
||||
# Ohne explizite Wildcard bleibt die bisherige Teilstringsuche erhalten.
|
||||
# Without an explicit wildcard, retain the existing substring search.
|
||||
if "*" not in value:
|
||||
pattern = f"*{pattern}*"
|
||||
return pattern
|
||||
@@ -6651,8 +6679,8 @@ def _ldap_search_users(query_text: str, auth_config: dict) -> list[dict]:
|
||||
raise_exceptions=False,
|
||||
)
|
||||
try:
|
||||
# ldap3.Connection.open() liefert je nach Version keinen verlässlichen
|
||||
# booleschen Rückgabewert. Daher nur öffnen und anschließend binden.
|
||||
# Depending on the version, ldap3.Connection.open() does not return a reliable
|
||||
# Boolean result. Open the connection and then bind explicitly.
|
||||
if start_tls and not use_ssl:
|
||||
conn.open()
|
||||
if not conn.start_tls():
|
||||
@@ -6684,16 +6712,22 @@ def _ldap_search_users(query_text: str, auth_config: dict) -> list[dict]:
|
||||
"dn": str(entry.get("dn") or attrs.get("distinguishedName") or ""),
|
||||
})
|
||||
result.sort(key=lambda item: ((item.get("display_name") or item["username"]).casefold(), item["username"].casefold()))
|
||||
ldap_logger.info("LDAP-Benutzerabfrage erfolgreich: Filter=%r Treffer=%s", search_filter, len(result))
|
||||
ldap_logger.info("LDAP directory search completed: query=%r matches=%s", raw_query, len(result))
|
||||
if _ldap_debug_enabled(auth_config):
|
||||
ldap_logger.debug("LDAP directory search details: filter=%r base_dn=%r", search_filter, base_dn)
|
||||
return result
|
||||
except HTTPException:
|
||||
raise
|
||||
except LDAPException as exc:
|
||||
ldap_logger.exception("LDAP-Ausnahme bei Benutzerabfrage: %s", exc)
|
||||
raise HTTPException(400, f"LDAP-Abfrage fehlgeschlagen: {exc}") from exc
|
||||
ldap_logger.error("LDAP directory search failed because of an LDAP error")
|
||||
if _ldap_debug_enabled(auth_config):
|
||||
ldap_logger.exception("LDAP exception during directory search: %s", exc)
|
||||
raise HTTPException(400, "LDAP query failed") from exc
|
||||
except Exception as exc:
|
||||
ldap_logger.exception("Unerwarteter LDAP-Fehler bei Benutzerabfrage: %s", exc)
|
||||
raise HTTPException(400, f"LDAP-Abfrage fehlgeschlagen: {exc}") from exc
|
||||
ldap_logger.error("LDAP directory search failed because of an unexpected error")
|
||||
if _ldap_debug_enabled(auth_config):
|
||||
ldap_logger.exception("Unexpected LDAP directory-search error: %s", exc)
|
||||
raise HTTPException(400, "LDAP query failed") from exc
|
||||
finally:
|
||||
try:
|
||||
conn.unbind()
|
||||
@@ -6764,7 +6798,7 @@ def ldap_users_import(
|
||||
continue
|
||||
user = db.query(User).filter(func.lower(User.username) == item["username"].casefold()).first()
|
||||
if user:
|
||||
# Lokale Konten werden nicht automatisch in LDAP-Konten umgewandelt.
|
||||
# Local accounts are not converted to LDAP accounts automatically.
|
||||
if user.auth_source != "ldap":
|
||||
skipped += 1
|
||||
continue
|
||||
@@ -7247,6 +7281,7 @@ async def settings_save(
|
||||
ldap_user_filter: str = Form("(sAMAccountName={username})"),
|
||||
ldap_display_name_attribute: str = Form("displayName"),
|
||||
ldap_email_attribute: str = Form("mail"),
|
||||
ldap_debug_logging: str | None = Form(None),
|
||||
remove_logo: str | None = Form(None),
|
||||
remove_favicon: str | None = Form(None),
|
||||
logo: UploadFile | None = File(None),
|
||||
@@ -7325,6 +7360,7 @@ async def settings_save(
|
||||
"user_filter": ldap_user_filter.strip() or "(sAMAccountName={username})",
|
||||
"display_name_attribute": ldap_display_name_attribute.strip() or "displayName",
|
||||
"email_attribute": ldap_email_attribute.strip() or "mail",
|
||||
"debug_logging": ldap_debug_logging == "on",
|
||||
})
|
||||
authentication["ldap"] = ldap
|
||||
meshcentral = dict(current.get("meshcentral", {}))
|
||||
@@ -7497,7 +7533,7 @@ async def meshcentral_settings_save(request: Request, db: Session = Depends(get_
|
||||
'store_source_json': form.get('store_source_json') == 'on',
|
||||
})
|
||||
mesh['match_order'] = form.getlist('match_order') or ['node_id', 'serial_number']
|
||||
# Klartextpasswort aus Altversionen nicht weiter speichern.
|
||||
# Do not retain plaintext passwords from older versions.
|
||||
mesh.pop('password', None)
|
||||
mesh.pop('field_mappings', None)
|
||||
mesh.pop('field_rules', None)
|
||||
|
||||
Reference in New Issue
Block a user