Zum Inhalt

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

c.search(baseDN, filter[, scope[, callback]])
  • 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 false zurü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ähren
  • auth.fail(code, meldung) — ablehnen
  • auth.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.success prüfen, ob eine erlaubte Gruppe in groups ist, sonst auth.fail(...).
  • Benutzer lokal spiegeln: mit db.update(...) einen Datensatz in der Tabelle users anlegen oder aktualisieren (für Formular-Zuordnungen). Für viele Benutzer ist ldap.sync der 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 aktiviertem memberof-Overlay.
  • Über die Gruppe (member): funktioniert immer — man liest die Mitglieder-DNs der Gruppe bzw. sucht die Gruppen, die den Benutzer als member fü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.