LDAP-Zugriff (ldap)¶
Das Objekt ldap greift auf ein LDAP-/Active-Directory-Verzeichnis zu:
Benutzer suchen, Einträge lesen, anlegen, ändern und löschen, Passwörter per
Bind prüfen. Es steht in Formular-Skripten, der Skript-Konsole, Jobs,
REST-Services und Anmelde-Skripten bereit.
Voraussetzung ist ein LDAP-Konnektor unter System-Administration →
Schnittstellen & Verteilung → LDAP-Konnektoren (URL, Basis-DN,
Bind-Benutzer, SSL). Skripte sprechen ihn über seinen Namen an:
ldap.connect("kcads").
Verbindung¶
var c = ldap.connect("kcads"); // Name des Konnektors; ldap.bind(...) ist gleichbedeutend
try {
// … c.search / c.get / c.add / …
} finally {
c.close(); // gute Praxis; wird sonst nach dem Skript automatisch geschlossen
}
Die Verbindung bindet mit den im Konnektor hinterlegten Zugangsdaten. Am Ende
des Skript-Laufs schließt KCForms alle geöffneten Verbindungen automatisch — ein
explizites close() (gleichbedeutend unbind()) ist trotzdem empfehlenswert.
Ein unbekannter oder deaktivierter Konnektor wirft beim connect einen Fehler.
Suchen¶
- baseDN — Such-Basis (leer = der Basis-DN des Konnektors).
- filter — LDAP-Filter, z. B.
"(&(objectClass=inetOrgPerson)(uid=oliver))"(leer ="(objectClass=*)"). - scope —
"subtree"(Standard),"onelevel"oder"object". - callback (optional) — Funktion, die pro Treffer aufgerufen wird. Gibt sie
falsezurück, endet die Suche vorzeitig.
Rückgabe ist immer die Liste aller gefundenen Einträge — der Callback ist nur für den zeilenweisen Durchlauf da.
var c = ldap.connect("default");
// Variante mit Callback
var n = 0;
c.search("ou=Users,dc=kcforms,dc=test", "(objectClass=inetOrgPerson)", "subtree", function(e){
log.debug(e.dn + " " + e.get("mail"));
n++;
return true; // weiter; false = abbrechen
});
// Variante mit Rückgabeliste
var users = c.search("ou=Users,dc=kcforms,dc=test", "(sn=Meyer)", "subtree");
log.debug(users.length + " Treffer");
Der Eintrag (entry)¶
Jeder Treffer ist ein entry mit lesbaren Properties und Hilfsmethoden:
| Zugriff | Bedeutung |
|---|---|
entry.dn |
Distinguished Name (String) |
entry.get("mail") |
erster Wert des Attributs (leerer String, wenn keiner) |
entry.values("memberOf") |
alle Werte als Liste (leer, wenn keiner) |
entry.has("title") |
hat das Attribut einen Wert? |
entry.isA("inetOrgPerson") |
Mitglied der objectClass? (case-insensitive) |
entry.attributes.ids |
Array aller Attributnamen |
entry.attributes.all |
Array von { id, values, value } |
var c = ldap.connect("default");
var users = c.search("ou=Users,dc=kcforms,dc=test",
"(objectClass=inetOrgPerson)", "subtree");
var e = users[0];
log.debug("Name: " + e.get("cn"));
log.debug("E-Mail: " + e.get("mail"));
log.debug("Gruppen:" + e.values("memberOf").join(", "));
Einzelnes Objekt lesen / prüfen¶
var o = c.get("uid=oliver,ou=Users,dc=kcforms,dc=test"); // entry oder null
if (o && o.isA("inetOrgPerson")) { … }
if (c.exists("uid=oliver,ou=Users,dc=kcforms,dc=test")) { … } // boolean
Anlegen / Ändern / Löschen¶
Attribute werden als einfaches JS-Objekt übergeben ({ name: wert | [werte] }) —
mehrwertige Attribute als Array.
var dn = "uid=mmustermann,ou=Users,dc=kcforms,dc=test";
// Anlegen
c.add(dn, {
objectClass: ["top", "person", "organizationalPerson", "inetOrgPerson"],
uid: "mmustermann",
cn: "Max Mustermann",
givenName: "Max",
sn: "Mustermann",
mail: "max.mustermann@example.com",
userPassword: "start123"
});
// Ändern (ersetzt die genannten Attribute)
c.modify(dn, { mail: "m.mustermann@example.com", title: "Sachbearbeiter" });
// Löschen
c.delete(dn);
Schreibzugriffe erfordern, dass der Konnektor mit einem schreibberechtigten Bind-DN konfiguriert ist.
Passwort prüfen (Login)¶
var eingegebenesPasswort = "falsches-passwort"; // im Betrieb aus dem Formular
var ok = ldap.authenticate("default", "uid=oliver,ou=Users,dc=kcforms,dc=test", eingegebenesPasswort);
authenticate bindet als der angegebene Benutzer und gibt true/false zurück
(wirft nicht). Praktisch, um in einem onValidate- oder Schaltflächen-Hook ein
Passwort gegen das Verzeichnis zu prüfen.
Leere Passwörter werden abgelehnt
Ist das Passwort leer oder fehlt es, liefert authenticate immer false –
ohne überhaupt zu binden. Grund: Ein Bind mit Benutzer-DN, aber leerem Passwort
ist ein sogenannter unauthentifizierter Bind (RFC 4513, Abschnitt 5.1.2).
Active Directory und andere Server nehmen ihn als anonymen Bind an – er würde
also „gelingen", obwohl kein Passwort geprüft wurde. Eine eigene Leer-Prüfung
im Skript ist deshalb nicht mehr nötig.
Dasselbe gilt sinngemäß für Konnektoren: Ein Konnektor mit Bind-DN, aber ohne Bind-Passwort läuft je nach Server stillschweigend anonym. KCForms schreibt dafür eine Warnung ins Log; für bewusst anonymen Zugriff den Auth-Typ „none" wählen.
Anmeldung gegen LDAP (Login-Skript)¶
Die Anmeldung an KCForms ist per Skript frei programmierbar: System-Administration
→ Benutzer & Zugang → Anmeldung, Strategie Skript-Login (JavaScript-basiert)
(siehe Anmeldung). Dort stehen — neben
auth, db, log — auch ldap zur Verfügung. Ein Login-Skript muss am Ende
genau eines aufrufen:
auth.success(benutzer, gruppen[], rechte[])— Zugang gewährenauth.fail(code, meldung)— ablehnenauth.fallbackToDb()— auf den DB-Login zurückfallen (Pattern „erst LDAP, sonst DB")
auth.username / auth.password tragen die Eingabe des Anmeldeformulars.
// LDAP-Login: Benutzer im Verzeichnis suchen, Passwort per Bind prüfen,
// Gruppen als Rollen übernehmen. An die eigene Struktur anpassen.
var CONNECTOR = "kcads";
function esc(s) { // LDAP-Filter escapen (gegen Injection)
return String(s || "").replace(/[\\*() ]/g, function (c) {
return "\\" + c.charCodeAt(0).toString(16).padStart(2, "0");
});
}
var user = (auth.username || "").trim();
if (!user || !auth.password) {
auth.fail("empty", "Benutzername und Passwort erforderlich.");
} else {
var c = ldap.connect(CONNECTOR); // bindet als Service-Account (Konnektor)
var userDn = null, groups = [];
try {
var hits = c.search("", "(&(objectClass=inetOrgPerson)(uid=" + esc(user) + "))", "subtree");
if (hits.length > 0) {
userDn = hits[0].dn;
// Gruppen per Reverse-Suche (funktioniert ohne memberof-Overlay).
var gs = c.search("", "(&(objectClass=groupOfNames)(member=" + userDn + "))", "subtree");
for (var i = 0; i < gs.length; i++) groups.push(gs[i].get("cn"));
}
} finally {
c.close();
}
// Passwort prüfen: als gefundener Benutzer binden.
if (userDn && ldap.authenticate(CONNECTOR, userDn, auth.password)) {
// Berechtigungen wie beim DB-Login ableiten: Mitglied von "ADMIN" bekommt
// ALLE Permissions, sonst die seiner Gruppen (Tabelle group_permissions).
var perms = [];
function addPerm(code) { if (code && perms.indexOf(code) < 0) perms.push(code); }
if (groups.indexOf("ADMIN") >= 0) {
var all = db.query("SELECT code FROM permissions");
for (var a = 0; a < all.length; a++) addPerm(all[a].code);
} else {
for (var g = 0; g < groups.length; g++) {
var rows = db.query("SELECT permission_code FROM group_permissions WHERE group_id = ?", groups[g]);
for (var r = 0; r < rows.length; r++) addPerm(rows[r].permission_code);
}
}
log.info("LDAP-Login ok: " + user + " groups=[" + groups.join(",") + "] perms=" + perms.length);
auth.success(user, groups, perms);
} else {
// Unbekannt im LDAP oder falsches Passwort → DB-Login versuchen, damit
// lokale Konten (z.B. admin/kcforms) weiter funktionieren. Alternativ
// auth.fail(...) für „ausschließlich LDAP".
auth.fallbackToDb();
}
}
Gruppen-cn = KCForms-Gruppenname
Authorities entstehen als ROLE_<gruppe> je Eintrag in groups (plus
PERM_<code> je Berechtigung). Damit ROLE_ADMIN & Co. greifen, muss der
cn der LDAP-Gruppe dem Namen der KCForms-Gruppe entsprechen — im
Test-LDAP ist das 1:1 (ADMIN, Entwickler, …). Bei abweichenden Namen im
Skript umsetzen (z. B. {'Domänen-Admins': 'ADMIN'}).
Vor dem Aktivieren testen: Unter Test-Login auf der Anmelde-Seite
Benutzer und Passwort eingeben und Test ausführen — das prüft den Inhalt
des Editors, ohne die aktive Strategie umzustellen. Für Active Directory statt
uid auf sAMAccountName und objectClass user wechseln.
Zurückschalten: Wer sich ausgesperrt hat, stellt auf der Anmelde-Seite
wieder DB-Login ein — oder meldet sich über den Notfall-Login an
(Umgebungsvariablen KCFORMS_AUTH_EMERGENCY=true,
KCFORMS_AUTH_EMERGENCY_USER, KCFORMS_AUTH_EMERGENCY_PASSWORD).
Notfall-Login immer mit eigenem Passwort
Ohne eigene Werte gelten für den Notfall-Login admin/admin. Beim
Einschalten deshalb immer ein eigenes Passwort setzen und den
Notfall-Login danach wieder abschalten.
Varianten, die du „von Hand nachlegen" kannst:
- Nur bestimmte Gruppen zulassen: vor
auth.successprüfen, ob eine erlaubte Gruppe ingroupsist, sonstauth.fail(...). - Benutzer lokal spiegeln: mit
db.update(...)einen Datensatz in der Tabelleusersanlegen oder aktualisieren (für Formular-Zuordnungen). Für viele Benutzer istldap.syncder bessere Weg. - Hybrid: zuerst LDAP versuchen, bei Nichttreffer
auth.fallbackToDb().
Gruppen-Mitgliedschaft¶
Zwei Wege, je nach Verzeichnis:
- Über den Benutzer (
memberOf): Active Directory füllt das automatisch; OpenLDAP nur mit aktiviertemmemberof-Overlay. - Über die Gruppe (
member): funktioniert immer — man liest die Mitglieder-DNs der Gruppe bzw. sucht die Gruppen, die den Benutzer alsmemberführen (Reverse-Suche).
Ein fertiges, gegen beide Welten robustes Beispiel liegt als
Datei-Bibliothek helpers/ldap-tools.js
bei (ldapFindUser, ldapGroupMembers, ldapUserInGroup, ldapEscape) — im
Formular-Editor einem Formular als Bibliothek zuweisen oder als Vorlage kopieren.
var mitglieder = ldapGroupMembers("kcads", "Entwickler"); // Array der member-DNs
var istDrin = ldapUserInGroup("kcads", "swagner", "Entwickler"); // true
Active Directory¶
Die API funktioniert identisch gegen Active Directory — nur die Attribute/Filter unterscheiden sich:
var c = ldap.connect("firmen-ad");
c.search("DC=firma,DC=local", "(&(objectClass=user)(sAMAccountName=oliver))", "subtree", function(e){
log.debug(e.get("displayName") + " / " + e.get("mail"));
log.debug("Gruppen: " + e.values("memberOf").join(", "));
return false; // ersten Treffer reicht
});
Hinweise:
- AD nutzt
sAMAccountName(Login),displayName,memberOf(Gruppen-DNs). - Die primäre Gruppe (Domänen-Benutzer) steht technisch oft nicht in
memberOf. - Referrals werden vom Konnektor ignoriert, sonst wirft AD bei Teilbäumen.
Test-Server (Docker)¶
Die Entwicklungsumgebung bringt einen OpenLDAP-Testserver mit (Dienst ldap):
| URL (aus dem Backend) | ldap://ldap:389 |
| Basis-DN | dc=kcforms,dc=test |
| Admin-Bind | cn=admin,dc=kcforms,dc=test / adminpw |
| Testbenutzer | uid=admin, uid=oliver, uid=cbecker, uid=swagner … (uid = Anmeldename in KCForms), Passwort kcforms; dazu rund 1.600 erzeugte Mitarbeitende |
| Gruppen | ADMIN, Buchhaltung, Entwickler, MANAGER, Sachbearbeiter, USERADMIN (= KCForms-Gruppen) und Abteilungsgruppen („Abteilung Vertrieb" …) |
| Web-UI (optional) | phpLDAPadmin unter http://localhost:8389, zu starten mit docker compose --profile ldap-ui up -d ldap-admin |
Einen Konnektor legt KCForms dafür nicht automatisch an. Lege unter
LDAP-Konnektoren einen mit diesen Werten an — in den Beispielen dieser Seite
heißt er kcads.
Schnell statt live: ldap.sync(...)¶
Bei vielen Nutzern sind Live-LDAP-Abfragen langsam. ldap.sync("<konnektor>")
spiegelt Benutzer + Gruppen periodisch in DB-Tabellen (exported_user …),
sodass Abfragen in Millisekunden über die Datenbank laufen — ideal für einen
Zeitplaner-Job:
var r = ldap.sync("kcads");
// → { status, usersScanned, added, updated, unchanged, removed, groups,
// memberships, errors, skipped, durationMs, sweepSkipped, message }
log.info("Export: " + r.usersScanned + " Benutzer in " + r.durationMs + " ms");
ldap.sync steht in Formular-Skripten und Jobs zur Verfügung (ldap.exportUsers
ist gleichbedeutend). Details, Zieltabellen und Konfiguration:
LDAP-Benutzer-Export.
Sicherheit¶
- Bind-Passwörter liegen im Konnektor (Klartext in der DB, in der UI maskiert) — wie bei der Mail-Konfiguration. Für Produktion einen eigenen, möglichst lese-beschränkten Service-Account verwenden.
- „Allen Zertifikaten vertrauen" (SSL) ist nur für Testserver mit
selbstsigniertem
ldaps://-Zertifikat gedacht; produktiv gehört der JVM-Standard-Truststore genutzt.