diff --git a/srv/tesm/app.py b/srv/tesm/app.py index 82e2493..952e5a8 100644 --- a/srv/tesm/app.py +++ b/srv/tesm/app.py @@ -1688,6 +1688,16 @@ def _ldap_resolve_app_groups(service_conn, user_dn): return matched +def _normalize_share_unc(raw): + """Normalisiert einen vom Formular übergebenen Freigabe-Pfad auf das + von mount.cifs benötigte "//server/freigabe"-Format -- ein + Windows-Admin gibt naturgemäß "\\\\server\\freigabe" ein.""" + unc = raw.strip().replace("\\", "/") + if unc and not unc.startswith("//"): + unc = "//" + unc.lstrip("/") + return unc + + def _ldap_fileshare_mappings(): conn = get_db_connection() rows = conn.execute( @@ -1876,6 +1886,60 @@ if _IS_WEB_PROCESS: _fileshare_cleanup_all_on_startup() +def _resolve_host_preferring_ipv4(hostname): + """Löst EINEN Hostnamen auf -- IPv4-Adresse wenn vorhanden, sonst + IPv6, sonst None. IPv4 wird bevorzugt, weil sie in der Praxis + zuverlässiger durchgeroutet ist als in DNS eingetragene IPv6-Adressen + (siehe _resolve_unc_host_ip); eine funktionierende IPv6-Route wird + aber genutzt, wenn es keine IPv4-Adresse gibt, statt komplett + aufzugeben.""" + try: + infos = socket.getaddrinfo(hostname, None, socket.AF_UNSPEC, socket.SOCK_STREAM) + except socket.gaierror: + return None + if not infos: + return None + ipv4 = next((i for i in infos if i[0] == socket.AF_INET), None) + return (ipv4 or infos[0])[4][0] + + +def _resolve_unc_host_ip(unc): + """Löst den Host-Teil einer "//host/share"-UNC auf eine IP-Adresse + auf, oder gibt None zurück (Host ist bereits eine IP-Literal, oder es + lässt sich nichts ermitteln). Zwei Probleme werden hier abgefangen, + die beide dazu führen, dass eine Freigabe per IP klappt, per Hostname + aber nicht: + 1. Ein reiner Kurzname (z.B. "s2025" statt "s2025.ad.eertmoed.net") + löst über die konfigurierte DNS des Servers oft GAR NICHT auf, weil + hier (anders als bei einem domänenbeigetretenen Windows-Client) + keine DNS-Suffixsuche eingerichtet ist. Als Fallback wird deshalb + zusätzlich mit dem aus der LDAP-Servereinstellung abgeleiteten + AD-Domänensuffix versucht (die dortige AD-DNS-Zone enthält + erfahrungsgemäß auch die Datei-Server). + 2. Manche interne DNS-Zonen liefern für Server-Hostnamen NUR + AAAA-Einträge (siehe ad.eertmoed.net) -- IPv4 wird bevorzugt + verwendet, falls zusätzlich vorhanden, s.o. + Der Hostname bleibt in jedem Fall unverändert in der UNC stehen -- + nur die tatsächliche Verbindung wird per "ip="-Mount-Option gezielt + auf die ermittelte Adresse gelenkt.""" + host = unc[2:].split("/", 1)[0] if unc.startswith("//") else "" + if not host: + return None + try: + socket.inet_aton(host) + return None # Host ist bereits eine IPv4-Literal, nichts zu tun + except OSError: + pass + ip = _resolve_host_preferring_ipv4(host) + if ip: + return ip + if "." not in host: + domain = get_setting("ldap_server", "") + if domain and "." in domain: + ip = _resolve_host_preferring_ipv4(f"{host}.{domain}") + return ip + + def _mount_one_fileshare(mount_root, label, unc, username, password): """Mountet EINE Freigabe unter mount_root/label per mount.cifs. Das Passwort wird bewusst über die PASSWD-Umgebungsvariable übergeben statt @@ -1890,6 +1954,9 @@ def _mount_one_fileshare(mount_root, label, unc, username, password): except OSError as e: return False, str(e) options = f"username={username},vers=3.0,uid=0,gid=0,file_mode=0770,dir_mode=0770,iocharset=utf8" + resolved_ip = _resolve_unc_host_ip(unc) + if resolved_ip: + options += f",ip={resolved_ip}" env = dict(os.environ) env["PASSWD"] = password try: @@ -4769,6 +4836,27 @@ def settings_ldap(): log_action("settings.update", "LDAP-Gruppenzuordnung", f"{ad_group_name or ad_group_dn} → {app_group_id}") flash("Gruppenzuordnung gespeichert.", "success") + elif "edit_ldap_group_mapping" in request.form: + mapping_id = request.form.get("edit_ldap_group_mapping") + ad_group_dn = request.form.get("ad_group_dn", "").strip() + ad_group_name = request.form.get("ad_group_name", "").strip() + app_group_id = request.form.get("app_group_id", "").strip() + if not ad_group_dn or not app_group_id: + flash("AD-Gruppe und Rechtegruppe müssen ausgewählt werden.", "danger") + else: + conn = get_db_connection() + try: + conn.execute( + "UPDATE ldap_group_mappings SET ad_group_dn=?, ad_group_name=?, app_group_id=? WHERE id=?", + (ad_group_dn, ad_group_name or ad_group_dn, app_group_id, mapping_id), + ) + conn.commit() + log_action("settings.update", "LDAP-Gruppenzuordnung geändert", f"{ad_group_name or ad_group_dn} → {app_group_id}") + flash("Gruppenzuordnung aktualisiert.", "success") + except sqlite3.IntegrityError: + flash("Diese AD-Gruppe ist bereits einer anderen Rechtegruppe zugeordnet.", "danger") + conn.close() + elif "delete_ldap_group_mapping" in request.form: mapping_id = request.form.get("delete_ldap_group_mapping") conn = get_db_connection() @@ -4783,9 +4871,7 @@ def settings_ldap(): ad_group_dn = request.form.get("fs_ad_group_dn", "").strip() ad_group_name = request.form.get("fs_ad_group_name", "").strip() share_label = request.form.get("fs_share_label", "").strip() - share_unc = request.form.get("fs_share_unc", "").strip().replace("\\", "/") - if share_unc and not share_unc.startswith("//"): - share_unc = "//" + share_unc.lstrip("/") + share_unc = _normalize_share_unc(request.form.get("fs_share_unc", "")) if not ad_group_dn or not share_label or not share_unc: flash("AD-Gruppe, Bezeichnung und Freigabe-Pfad müssen angegeben werden.", "danger") else: @@ -4801,6 +4887,28 @@ def settings_ldap(): log_action("settings.update", "Fileshare-Gruppenzuordnung", f"{ad_group_name or ad_group_dn} → {share_label} ({share_unc})") flash("Fileshare-Zuordnung gespeichert.", "success") + elif "edit_fileshare_mapping" in request.form: + mapping_id = request.form.get("edit_fileshare_mapping") + ad_group_dn = request.form.get("fs_ad_group_dn", "").strip() + ad_group_name = request.form.get("fs_ad_group_name", "").strip() + share_label = request.form.get("fs_share_label", "").strip() + share_unc = _normalize_share_unc(request.form.get("fs_share_unc", "")) + if not ad_group_dn or not share_label or not share_unc: + flash("AD-Gruppe, Bezeichnung und Freigabe-Pfad müssen angegeben werden.", "danger") + else: + conn = get_db_connection() + try: + conn.execute( + "UPDATE ldap_fileshare_mappings SET ad_group_dn=?, ad_group_name=?, share_label=?, share_unc=? WHERE id=?", + (ad_group_dn, ad_group_name or ad_group_dn, share_label, share_unc, mapping_id), + ) + conn.commit() + log_action("settings.update", "Fileshare-Gruppenzuordnung geändert", f"{ad_group_name or ad_group_dn} → {share_label} ({share_unc})") + flash("Fileshare-Zuordnung aktualisiert.", "success") + except sqlite3.IntegrityError: + flash("Diese Kombination aus AD-Gruppe und Freigabe-Pfad existiert bereits.", "danger") + conn.close() + elif "delete_fileshare_mapping" in request.form: mapping_id = request.form.get("delete_fileshare_mapping") conn = get_db_connection() diff --git a/srv/tesm/templates/settings_ldap.html b/srv/tesm/templates/settings_ldap.html index aa730c8..f170455 100644 --- a/srv/tesm/templates/settings_ldap.html +++ b/srv/tesm/templates/settings_ldap.html @@ -135,12 +135,18 @@ {{ m.app_group_name or '—' }} {% if can_edit %} -
- - -
+
+ + +
+ {% endif %} @@ -186,12 +192,18 @@ {{ m.share_unc }} {% if can_edit %} -
- - -
+
+ + +
+ {% endif %} @@ -283,6 +295,77 @@ + + + + {% endif %} {% endblock %}