Zum Inhalt

Verzeichnis (LDAP)

Ein Verzeichnis (LDAP, im Firmenumfeld oft Active Directory) ist das zentrale Adressbuch einer Organisation: Benutzer, Gruppen, Telefonnummern, Abteilungen. Mit ldap liest ein Skript daraus und schreibt — mit den passenden Rechten — auch hinein.

Ausführliches dazu: LDAP-Zugriff.

Was du vorher wissen solltest

  • Jeder Eintrag hat eine eindeutige Adresse, den DN (Distinguished Name), z. B. uid=oliver,ou=Users,dc=kcforms,dc=test. Lies ihn von links nach rechts wie eine Postanschrift: Person oliver in der Abteilung Users der Organisation kcforms.test.
  • Ein Eintrag hat Attribute wie cn (voller Name), mail, title (Funktion) oder telephoneNumber. Manche Attribute haben mehrere Werte.
  • Gesucht wird mit einem Filter in Klammern: (uid=oliver), (&(sn=Meyer)(l=Hagen)) (beides muss passen), (|(l=Köln)(l=Bonn)) (eins von beiden).

Die Beispiele verwenden den LDAP-Konnektor kcads und den Test-Server der Entwicklungsumgebung (dc=kcforms,dc=test). Bei dir heißen Konnektor, Basis-DN und Attribute womöglich anders — das richtet der Admin unter LDAP-Konnektoren ein.

Einsteiger

1. Verbindung öffnen und wieder schließen

ldap.connect("name") meldet sich mit den Daten aus dem Konnektor an. close() beendet die Verbindung.

var c = ldap.connect("kcads");
log.info("Verbunden");
c.close();

Vergisst du close(), schließt KCForms die Verbindung am Ende des Skripts selbst.

2. Gibt es einen Benutzer?

exists(dn) antwortet mit true oder false.

var c = ldap.connect("kcads");
if (c.exists("uid=oliver,ou=Users,dc=kcforms,dc=test")) {
  log.info("Den Benutzer oliver gibt es.");
}
c.close();

3. Einen Benutzer auslesen

get(dn) holt einen Eintrag. e.get("attribut") liest einen Wert daraus.

var c = ldap.connect("kcads");
var e = c.get("uid=oliver,ou=Users,dc=kcforms,dc=test");
if (e) {
  log.info("Name: " + e.get("cn"));
  log.info("E-Mail: " + e.get("mail"));
  log.info("Funktion: " + e.get("title"));
}
c.close();

Gibt es den Eintrag nicht, liefert get den Wert null.

4. Einen Benutzer über seinen Anmeldenamen suchen

Meist kennst du nicht den ganzen DN, sondern nur den Anmeldenamen. search findet den Eintrag. Leere Such-Basis "" heißt: im ganzen Verzeichnis.

var c = ldap.connect("kcads");
var treffer = c.search("", "(uid=cbecker)");
if (treffer.length > 0) {
  log.info("Gefunden: " + treffer[0].dn);
}
c.close();

5. Wie viele Personen stehen im Verzeichnis?

search liefert eine Liste; length ist ihre Länge.

var c = ldap.connect("kcads");
var alle = c.search("ou=Users,dc=kcforms,dc=test", "(objectClass=inetOrgPerson)");
log.info(alle.length + " Personen im Verzeichnis");
c.close();

6. Alle Werte eines Attributs

values("attribut") liefert alle Werte als Liste — wichtig bei Attributen wie objectClass, die mehrere Werte haben.

var c = ldap.connect("kcads");
var e = c.get("uid=oliver,ou=Users,dc=kcforms,dc=test");
log.info("Objektklassen: " + e.values("objectClass").join(", "));
c.close();

7. Prüfen, ob ein Attribut gefüllt ist

var c = ldap.connect("kcads");
var e = c.get("uid=oliver,ou=Users,dc=kcforms,dc=test");
if (e.has("telephoneNumber")) {
  log.info("Telefon: " + e.get("telephoneNumber"));
} else {
  log.info("Keine Telefonnummer hinterlegt");
}
c.close();

8. Welche Attribute hat ein Eintrag?

attributes.ids listet alle Attributnamen — hilfreich, wenn du ein neues Verzeichnis erkundest.

var c = ldap.connect("kcads");
var e = c.search("", "(uid=abeck)")[0];
log.info("Attribute: " + e.attributes.ids.join(", "));
c.close();

9. Ein Passwort prüfen

ldap.authenticate(konnektor, dn, passwort) meldet sich testweise als dieser Benutzer an und sagt true oder false. Ein leeres Passwort musst du vorher abfangen — manche Server lassen eine Anmeldung ohne Passwort als „anonym" durch.

var dn = "uid=" + ctx.form.login + ",ou=Users,dc=kcforms,dc=test";
var passwort = "sicher-falsch";

if (!passwort) {
  log.info("Kein Passwort eingegeben");
} else if (ldap.authenticate("kcads", dn, passwort)) {
  log.info("Passwort stimmt");
} else {
  log.info("Passwort falsch");
}

10. Werte ins Formular übernehmen

Ein Klassiker im onLoad-Hook: Name und E-Mail des Benutzers vorbelegen.

var c = ldap.connect("kcads");
var e = c.get("uid=" + ctx.form.login + ",ou=Users,dc=kcforms,dc=test");
if (e) {
  ctx.form.vorname = e.get("givenName");
  ctx.form.name    = e.get("sn");
  ctx.form.email   = e.get("mail");
  log.info("Formular vorbelegt für " + e.get("cn"));
}
c.close();

Fortgeschritten

11. Suche mit mehreren Bedingungen

& verbindet Bedingungen („und"). Hier: alle Personen aus Hagen in der Abteilung „IT & Infrastruktur".

var c = ldap.connect("kcads");
var filter = "(&(objectClass=inetOrgPerson)(l=Hagen)(ou=IT & Infrastruktur))";
var leute = c.search("ou=Users,dc=kcforms,dc=test", filter);
log.info(leute.length + " IT-Mitarbeitende in Hagen");
leute.slice(0, 5).forEach(function (p) { log.info("- " + p.get("cn") + ", " + p.get("title")); });
c.close();

12. Treffer durchgehen und früh aufhören

Mit einer Funktion als viertem Wert wird jeder Treffer einzeln behandelt. Gibt die Funktion false zurück, endet die Suche.

var c = ldap.connect("kcads");
var gezaehlt = 0;
c.search("ou=Users,dc=kcforms,dc=test", "(title=*leiter*)", "subtree", function (e) {
  gezaehlt++;
  log.info("gefunden: " + e.get("cn") + " – " + e.get("title"));
  return gezaehlt < 3;   // nach drei Treffern aufhören
});
c.close();

13. Nur eine Ebene durchsuchen

"onelevel" sucht nur direkt unterhalb der Basis — hier: alle Gruppen, ohne in die Tiefe zu gehen.

var c = ldap.connect("kcads");
var gruppen = c.search("ou=Groups,dc=kcforms,dc=test", "(objectClass=groupOfNames)", "onelevel");
log.info(gruppen.length + " Gruppen: "
  + gruppen.slice(0, 6).map(function (g) { return g.get("cn"); }).join(", ") + " …");
c.close();

14. Die Mitglieder einer Gruppe lesen

Eine Gruppe speichert ihre Mitglieder im Attribut member — als Liste von DNs.

var c = ldap.connect("kcads");
var g = c.get("cn=Entwickler,ou=Groups,dc=kcforms,dc=test");
g.values("member").forEach(function (dn) {
  var person = c.get(dn);
  log.info("Mitglied: " + (person ? person.get("cn") : dn));
});
c.close();

15. In welchen Gruppen ist ein Benutzer?

Die „umgekehrte" Suche: alle Gruppen, in deren member der Benutzer steht. Das funktioniert mit jedem Verzeichnis.

var c = ldap.connect("kcads");
var userDn = "uid=oliver,ou=Users,dc=kcforms,dc=test";
var gruppen = c.search("", "(&(objectClass=groupOfNames)(member=" + userDn + "))");
log.info("Gruppen von oliver: " + gruppen.map(function (g) { return g.get("cn"); }).join(", "));
c.close();

In Active Directory steht dasselbe direkt am Benutzer im Attribut memberOf.

16. Eingaben im Filter absichern

Zeichen wie *, ( oder ) haben im Filter eine Bedeutung. Kommt der Suchbegriff vom Benutzer, müssen sie „entschärft" werden — sonst lässt sich die Suche manipulieren.

function ldapEscape(s) {
  return String(s || "").replace(/[\\*()\0]/g, function (z) {
    return "\\" + ("0" + z.charCodeAt(0).toString(16)).slice(-2);
  });
}

var c = ldap.connect("kcads");
var filter = "(sn=" + ldapEscape(ctx.form.suchbegriff) + ")";
log.info("Filter: " + filter);
log.info(c.search("", filter).length + " Treffer");   // der Trick-Versuch findet nichts
c.close();

17. Ein kleines Telefonbuch

Alle Personen, deren Nachname mit „Beck" beginnt, alphabetisch sortiert. Den * setzt hier das Skript, nicht der Benutzer.

var c = ldap.connect("kcads");
var leute = c.search("ou=Users,dc=kcforms,dc=test", "(sn=Beck*)");
leute.sort(function (a, b) { return a.get("cn").localeCompare(b.get("cn")); });
leute.forEach(function (p) {
  log.info(p.get("cn") + "  " + (p.get("telephoneNumber") || "–") + "  " + p.get("l"));
});
c.close();

18. Suchergebnis in eine Detail-Tabelle schreiben

Eine Detail-Tabelle erwartet eine Liste von Objekten — eins pro Zeile.

var c = ldap.connect("kcads");
var leute = c.search("ou=Users,dc=kcforms,dc=test", "(&(objectClass=inetOrgPerson)(l=Berlin))");
ctx.form.personen = leute.slice(0, 20).map(function (p) {
  return { name: p.get("cn"), email: p.get("mail"), telefon: p.get("telephoneNumber") };
});
log.info(ctx.form.personen.length + " Zeilen übernommen");
c.close();

19. Mitarbeitende je Standort zählen

Ein Objekt dient als „Strichliste": Für jeden Ort wird hochgezählt.

var c = ldap.connect("kcads");
var zaehler = {};
c.search("ou=Users,dc=kcforms,dc=test", "(objectClass=inetOrgPerson)", "subtree", function (p) {
  var ort = p.get("l") || "(ohne Ort)";
  zaehler[ort] = (zaehler[ort] || 0) + 1;
});
Object.keys(zaehler).sort().forEach(function (ort) {
  log.info("Standort " + ort + ": " + zaehler[ort]);
});
c.close();

20. Den Vorgesetzten nachschlagen

Das Attribut manager enthält den DN der Führungskraft. Den liest du mit einem zweiten get.

var c = ldap.connect("kcads");
var p = c.search("", "(uid=abeck)")[0];
if (p.has("manager")) {
  var chef = c.get(p.get("manager"));
  log.info(p.get("cn") + " – Vorgesetzte(r): " + (chef ? chef.get("cn") : "unbekannt"));
} else {
  log.info("Kein Vorgesetzter eingetragen");
}
c.close();

Profi

Für diese Beispiele braucht der Konnektor ein Konto mit Schreibrechten. Sie legen eigene Übungseinträge an (uid=bsp.muster, cn=Beispielgruppe) und löschen sie im letzten Beispiel wieder.

21. Einen Benutzer anlegen

add(dn, attribute) legt einen neuen Eintrag an. objectClass bestimmt, welche Attribute erlaubt sind; cn und sn sind bei Personen Pflicht.

var c = ldap.connect("kcads");
var dn = "uid=bsp.muster,ou=Users,dc=kcforms,dc=test";
if (!c.exists(dn)) {
  c.add(dn, {
    objectClass: ["top", "person", "organizationalPerson", "inetOrgPerson"],
    uid:       "bsp.muster",
    cn:        "Max Muster",
    givenName: "Max",
    sn:        "Muster",
    mail:      "max.muster@beispiel.example",
    title:     "Praktikant"
  });
  log.info("Benutzer angelegt: " + dn);
} else {
  log.info("Benutzer gibt es schon");
}
c.close();

22. Werte ändern

modify(dn, attribute) ersetzt die genannten Attribute. Alle anderen bleiben, wie sie sind.

var c = ldap.connect("kcads");
var dn = "uid=bsp.muster,ou=Users,dc=kcforms,dc=test";
c.modify(dn, { title: "Sachbearbeiter", telephoneNumber: "+49 221 555-0101" });
var e = c.get(dn);
log.info("Neu: " + e.get("title") + ", " + e.get("telephoneNumber"));
c.close();

23. Ein Attribut entfernen

Wird ein Attribut mit null (oder einer leeren Liste) gesetzt, verschwindet es ganz.

var c = ldap.connect("kcads");
var dn = "uid=bsp.muster,ou=Users,dc=kcforms,dc=test";
c.modify(dn, { telephoneNumber: null });
log.info(c.get(dn).has("telephoneNumber") ? "noch da" : "Telefon entfernt");
c.close();

24. Einen Wert zu einem mehrwertigen Attribut hinzufügen

Weil modify ersetzt, liest du zuerst die vorhandenen Werte, fügst den neuen hinzu und schreibst alle zurück.

var c = ldap.connect("kcads");
var dn = "uid=bsp.muster,ou=Users,dc=kcforms,dc=test";
var adressen = c.get(dn).values("mail").slice();   // Kopie der Liste
var neu = "m.muster@beispiel.example";
if (adressen.indexOf(neu) < 0) adressen.push(neu);
c.modify(dn, { mail: adressen });
log.info(c.get(dn).values("mail").length + " Adresse(n): " + c.get(dn).values("mail").join(", "));
c.close();

25. Eine Gruppe anlegen

Eine Gruppe vom Typ groupOfNames braucht mindestens ein Mitglied.

var c = ldap.connect("kcads");
var gruppe = "cn=Beispielgruppe,ou=Groups,dc=kcforms,dc=test";
if (!c.exists(gruppe)) {
  c.add(gruppe, {
    objectClass: ["top", "groupOfNames"],
    cn: "Beispielgruppe",
    description: "Übungsgruppe aus den Skript-Beispielen",
    member: ["uid=bsp.muster,ou=Users,dc=kcforms,dc=test"]
  });
}
log.info("Gruppe angelegt mit " + c.get(gruppe).values("member").length + " Mitglied(ern)");
c.close();

26. Mitglieder hinzufügen und entfernen

Wie bei Beispiel 24: lesen, Liste ändern, zurückschreiben.

var c = ldap.connect("kcads");
var gruppe = "cn=Beispielgruppe,ou=Groups,dc=kcforms,dc=test";

function mitglieder(dn) { return c.get(dn).values("member").slice(); }

// hinzufügen
var m = mitglieder(gruppe);
var neu = "uid=cbecker,ou=Users,dc=kcforms,dc=test";
if (m.indexOf(neu) < 0) { m.push(neu); c.modify(gruppe, { member: m }); }

// wieder entfernen – aber nie das letzte Mitglied
m = mitglieder(gruppe).filter(function (x) { return x !== neu; });
if (m.length > 0) c.modify(gruppe, { member: m });

// zur Kontrolle einmal hinzufügen und stehen lassen
m = mitglieder(gruppe); m.push(neu); c.modify(gruppe, { member: m });
log.info("Mitglieder jetzt: " + mitglieder(gruppe).length);
c.close();

27. Formularwerte ins Verzeichnis übernehmen

Aus einem Antrag heraus: Gibt es den Benutzer, werden seine Daten aktualisiert, sonst wird er angelegt. Fehler landen sauber im Formular.

var c = ldap.connect("kcads");
try {
  var dn = "uid=" + ctx.form.login + ",ou=Users,dc=kcforms,dc=test";
  var werte = {
    givenName: ctx.form.vorname,
    sn:        ctx.form.name,
    cn:        ctx.form.vorname + " " + ctx.form.name,
    mail:      ctx.form.email
  };
  if (c.exists(dn)) {
    c.modify(dn, werte);
    log.info("Verzeichnis aktualisiert: " + dn);
  } else {
    werte.objectClass = ["top", "person", "organizationalPerson", "inetOrgPerson"];
    werte.uid = ctx.form.login;
    c.add(dn, werte);
    log.info("Im Verzeichnis angelegt: " + dn);
  }
} catch (e) {
  ctx.addError("login", "Verzeichnis konnte nicht geändert werden: " + e.message);
} finally {
  c.close();
}

finally läuft immer — auch nach einem Fehler. So bleibt keine Verbindung offen.

28. Benutzerliste als CSV speichern

LDAP und CSV zusammen: alle Personen eines Standorts als Datei für Excel.

var c = ldap.connect("kcads");
var p = new csv.StandardPrinter({ separator: ";", bom: true, lineSeparator: "WINDOWS" });
p.println(["Name", "E-Mail", "Telefon", "Abteilung"]);
var n = 0;
c.search("ou=Users,dc=kcforms,dc=test", "(&(objectClass=inetOrgPerson)(l=Hagen))", "subtree", function (e) {
  p.println([e.get("cn"), e.get("mail"), e.get("telephoneNumber"), e.get("ou")]);
  n++;
});
c.close();
fs["/exports/telefonliste-hagen.csv"] = p;
log.info(n + " Zeilen geschrieben");

29. Nur bestimmte Gruppen einlassen

Zum Beispiel in onLoad: Wer nicht in einer der erlaubten LDAP-Gruppen ist, bekommt einen Hinweis und das Formular schließt sich.

var erlaubt = ["ADMIN", "MANAGER"];
var c = ldap.connect("kcads");
var userDn = "uid=" + ctx.form.login + ",ou=Users,dc=kcforms,dc=test";
var gruppen = c.search("ou=Groups,dc=kcforms,dc=test",
  "(&(objectClass=groupOfNames)(member=" + userDn + "))").map(function (g) { return g.get("cn"); });
c.close();

var darf = gruppen.some(function (g) { return erlaubt.indexOf(g) >= 0; });
if (darf) {
  log.info("Zugriff erlaubt (" + gruppen.join(", ") + ")");
} else {
  log.info("Zugriff verweigert");
  view.add(new view.Message("Dieses Formular ist nur für Führungskräfte.", "Kein Zugriff"));
  ctx.requestClose();
}

30. Aufräumen: Übungseinträge löschen

delete(dn) entfernt einen Eintrag. Erst die Gruppe, dann die Personen.

var c = ldap.connect("kcads");
[
  "cn=Beispielgruppe,ou=Groups,dc=kcforms,dc=test",
  "uid=bsp.muster,ou=Users,dc=kcforms,dc=test",
  "uid=bsp.antrag,ou=Users,dc=kcforms,dc=test"
].forEach(function (dn) {
  if (c.exists(dn)) { c.delete(dn); log.info("gelöscht: " + dn); }
});
c.close();

Für später: große Verzeichnisse schnell abfragen

Bei Tausenden von Benutzern sind Live-Abfragen langsam. ldap.sync spiegelt Benutzer und Gruppen in die Datenbank — typischerweise nachts als Job. Danach fragst du die Tabellen per SQL ab. Gemessen an einem Verzeichnis mit 1.625 Benutzern dauert ein Lauf unter einer Sekunde.

Prüfe immer r.status. Jeder Konnektor hat einen eigenen Schalter „Synchronisation aktiv". Steht er aus, macht ldap.sync gar nichts und meldet status: "disabled" — ohne Prüfung siehst du nur Nullen und suchst den Fehler an der falschen Stelle.

var r = ldap.sync("kcads");
if (r.status !== "ok") {
  log.warn("Kein Abgleich: " + r.status + " – " + r.message);
} else {
  log.info(r.usersScanned + " Benutzer, " + r.added + " neu, " + r.updated + " geändert");
}
var n = db.scalar("SELECT COUNT(*) FROM exported_user");
log.info(n + " Benutzer in der Datenbank");