Files
duffyduckandClaude Opus 5 5011491067 Umbenennung ueber users.cfg statt Administration API
Der Test gegen einen echten Server (Kerio Connect 10.0.9 patch 2) hat
gezeigt, dass Users.set das Feld loginName stillschweigend ignoriert -
kein Fehler, keine Wirkung, waehrend fullName im selben Aufruf sauber
uebernommen wird. Vier Varianten geprueft (nur loginName, mit domainId,
mit leeren emailAddresses, vollstaendiges User-Objekt): alle wirkungslos.
Der bisherige Ansatz konnte also gar nicht funktionieren.

Der Login-Name steht in users.cfg, und zwar in zwei Listen: User und
UserAdditionalData. Beide verweisen ueber Name+Domain statt ueber die
Guid, beide muessen mit, sonst verliert der Benutzer seine
Passworthistorie. Geaendert wird der Rohtext, damit Formatierung und
unbekannte Felder unangetastet bleiben.

users.cfg und Store-Verzeichnis wandern jetzt in EINEM Stopp-Fenster.
Laeuft Kerio zwischendurch mit nur einer Haelfte, legt es die
vermeintlich fehlende Mailbox sofort neu an - beim Entwickeln genau so
passiert. Damit entfaellt auch der Admin-Zugang: das Script braucht
keine Zugangsdaten mehr, --list zeigt die Benutzer aus der Datei.

Ausserdem gefunden: itemSource heisst 'DSInternalSource', nicht
'Internal' wie angenommen - die alte Pruefung haette bei jedem lokalen
Benutzer faelschlich abgebrochen. Die Erkennung laeuft jetzt ueber
InternalDb in users.cfg.

Tests fuer beide Haelften unter tests/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:44:31 +02:00

150 lines
5.7 KiB
Python

"""Testet UsersConfig gegen eine realistische users.cfg."""
import shutil, sys, tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from kerio_users_config import UsersConfig, validate_login
from kerio_common import AbortedError
SAMPLE = '''<config>
<list name="User">
<listitem>
<variable name="Name">Admin</variable>
<variable name="Domain">local.lan</variable>
<variable name="Account_enabled">1</variable>
<variable name="Guid">CDE8D0A3-929B-4AC6-8197-4C39D58C8A64</variable>
<variable name="Password">D3S:0faed6fd</variable>
<variable name="Fullname">Administrator</variable>
</listitem>
<listitem>
<variable name="Name">max.mustermann</variable>
<variable name="Domain">local.lan</variable>
<variable name="Account_enabled">1</variable>
<variable name="Guid">94480090-4C04-4FE2-9126-0DBDAEC85F5F</variable>
<variable name="Password">D3S:b6a11602</variable>
<variable name="Fullname">Max Mustermann</variable>
</listitem>
<listitem>
<variable name="Name">max.mustermann</variable>
<variable name="Domain">andere.lan</variable>
<variable name="Account_enabled">1</variable>
<variable name="Guid">AAAAAAAA-0000-0000-0000-000000000000</variable>
<variable name="Password">D3S:anders</variable>
<variable name="Fullname">Max in anderer Domain</variable>
</listitem>
<listitem>
<variable name="Name">ldap.user</variable>
<variable name="Domain">local.lan</variable>
<variable name="Account_enabled">1</variable>
<variable name="Guid">BBBBBBBB-0000-0000-0000-000000000000</variable>
<variable name="Fullname">LDAP Benutzer</variable>
</listitem>
</list>
<list name="Group">
<listitem>
<variable name="Name">testgruppe</variable>
<variable name="Domain">local.lan</variable>
<variable name="Guid">E78FEFAB-2C65-4D08-946B-76E3989EEF9D</variable>
</listitem>
</list>
<list name="Alias">
</list>
<list name="UserAdditionalData">
<listitem>
<variable name="Name">max.mustermann</variable>
<variable name="Domain">local.lan</variable>
<variable name="InternalDb">1</variable>
<variable name="PasswordHistory">SHA:28108e32</variable>
</listitem>
<listitem>
<variable name="Name">ldap.user</variable>
<variable name="Domain">local.lan</variable>
<variable name="InternalDb">0</variable>
</listitem>
</list>
</config>
'''
tmp = Path(tempfile.mkdtemp())
cfg_path = tmp / "users.cfg"
cfg_path.write_text(SAMPLE, encoding="utf-8")
ok = True
def check(label, cond):
global ok
print(f" {'ok ' if cond else 'FEHLER'} {label}")
if not cond: ok = False
print("== 1. Lesen ==")
c = UsersConfig(cfg_path)
check("Domains gefunden", set(c.domains()) == {"local.lan", "andere.lan"})
check("4 Benutzer gesamt", len(c.users()) == 4)
check("3 in local.lan", len(c.users("local.lan")) == 3)
u = c.find_user("max.mustermann", "local.lan")
check("Benutzer gefunden", u is not None and u["guid"].startswith("94480090"))
check("fullName gelesen", u["fullName"] == "Max Mustermann")
check("Domain-Trennung: andere.lan hat eigenen Eintrag",
c.find_user("max.mustermann", "andere.lan")["guid"].startswith("AAAA"))
print("\n== 2. Verzeichnisdienst erkennen ==")
check("max.mustermann ist intern", c.is_internal("max.mustermann", "local.lan"))
check("ldap.user ist NICHT intern", not c.is_internal("ldap.user", "local.lan"))
print("\n== 3. Login-Namen validieren ==")
for good in ("anna.schmidt", "a", "a-b_c.d", "user123"):
try:
validate_login(good); check(f"{good!r} erlaubt", True)
except AbortedError:
check(f"{good!r} erlaubt", False)
for bad in ("", ".anfang", "ende.", "mit leerzeichen", "boes<xml>", "a"*100, "a&b"):
try:
validate_login(bad); check(f"{bad!r} abgelehnt", False)
except AbortedError:
check(f"{bad!r} abgelehnt", True)
print("\n== 4. Umbenennen ==")
c2 = UsersConfig(cfg_path)
n = c2.rename_user("max.mustermann", "maxi.musterfrau", "local.lan", "Maxi Musterfrau")
check("2 Bloecke geaendert (User + UserAdditionalData)", n == 2)
c2.save()
c3 = UsersConfig(cfg_path)
check("neuer Name da", c3.find_user("maxi.musterfrau", "local.lan") is not None)
check("alter Name weg in local.lan", c3.find_user("max.mustermann", "local.lan") is None)
check("fullName geaendert", c3.find_user("maxi.musterfrau","local.lan")["fullName"] == "Maxi Musterfrau")
check("Guid unveraendert", c3.find_user("maxi.musterfrau","local.lan")["guid"].startswith("94480090"))
check("ANDERE DOMAIN unberuehrt",
c3.find_user("max.mustermann", "andere.lan") is not None)
check("Passworthistorie mitgewandert", c3.is_internal("maxi.musterfrau", "local.lan"))
txt = cfg_path.read_text(encoding="utf-8")
check("Passwort-Hash erhalten", "D3S:b6a11602" in txt)
check("PasswordHistory erhalten", "SHA:28108e32" in txt)
check("Admin unberuehrt", "<variable name=\"Name\">Admin</variable>" in txt)
check("Gruppe unberuehrt", "testgruppe" in txt)
check("kein max.mustermann mehr in UserAdditionalData/local.lan",
txt.count("max.mustermann") == 1) # nur noch andere.lan
print("\n== 5. other_occurrences ==")
c4 = UsersConfig(cfg_path)
hits = c4.other_occurrences("testgruppe", "local.lan")
check("findet Treffer in Group-Liste", any("Group" in h for h in hits))
check("keine Treffer fuer unbekannten Namen",
c4.other_occurrences("gibtsnicht", "local.lan") == [])
print("\n== 6. Abgelehnter Login-Name aendert nichts ==")
c5 = UsersConfig(cfg_path)
before = c5.text
try:
c5.rename_user("maxi.musterfrau", "boes<xml>", "local.lan")
check("haette ablehnen muessen", False)
except AbortedError:
check("ungueltiger Zielname abgelehnt", True)
check("Text unveraendert", c5.text == before)
print("\n" + ("ALLE TESTS BESTANDEN" if ok else "FEHLGESCHLAGEN"))
sys.exit(0 if ok else 1)