/** * Was ueber eine Verzeichnisquelle auf beiden Seiten gilt (#016). * * Hier stehen die Attributzuordnung und die Grenzen des Abgleich-Intervalls: Zahlen * und Namen, die das Formular genauso braucht wie der Dienst. Sie ein zweites Mal * im Formular zu schreiben ging eine Weile gut und lief dann auseinander — die * Oberflaeche liess hoechstens 1440 Minuten zu, der Dienst 10080, und wer das * Gegenteil glaubte, stand vor einer Grenze, die nirgends stand (Reviewbefund zu * #016). * * Die Zuordnung steht je Quelle in der Datenbank (`Verzeichnisquelle.attributZuordnung`, * ein Json-Feld), weil nicht jedes Verzeichnis ein Active Directory ist. Die * Vorgaben sind die AD-Namen; wer ein OpenLDAP anbindet, aendert einzelne Zeilen. * * Diese Datei liegt in `src/lib` und nicht bei den uebrigen LDAP-Modulen, weil das * Formular dieselben Feldnamen und dieselben Vorgaben braucht — und eine * Client-Komponente nichts aus `src/server/*` importieren darf. Hier steht * deshalb nur, was ohne Netz und ohne Datenbank gilt. */ /** * Die Grenzen des Abgleich-Intervalls, in Minuten. * * Unten fuenf Minuten: haeufiger ist kein Abgleich, sondern Last auf dem * Domaenencontroller. Oben eine Woche: seltener gelesen heisst, dass ein * ausgetretener Mitarbeiter tagelang aktiv bleibt. */ export const INTERVALL_MINDESTENS = 5; export const INTERVALL_HOECHSTENS = 7 * 24 * 60; /** Dieselbe Regel als Satz, damit Dienst und Formular dasselbe sagen. */ export function intervallMeldung(): string { return `Das Abgleich-Intervall muss zwischen ${INTERVALL_MINDESTENS} und ${INTERVALL_HOECHSTENS} Minuten liegen (fünf Minuten bis eine Woche).`; } export type AttributZuordnung = { vorname: string; nachname: string; anzeigename: string; mail: string; upn: string; sam: string; abteilung: string; titel: string; vorgesetzter: string; externeId: string; }; /** Die Vorgaben aus dem Auftrag: die Attributnamen eines Active Directory. */ export const AD_ZUORDNUNG: AttributZuordnung = { vorname: "givenName", nachname: "sn", anzeigename: "displayName", mail: "mail", upn: "userPrincipalName", sam: "sAMAccountName", abteilung: "department", titel: "title", vorgesetzter: "manager", externeId: "objectGUID", }; /** * Dieselben Felder mit den Namen, die **Microsoft Graph** benutzt (#028). * * Vier Unterschiede zum AD, und jeder hat einen Grund: * * - `surname` statt `sn` — Graph schreibt die Namen aus. * - `jobTitle` statt `title` — `title` gibt es bei Graph auch, es ist aber die * Anrede („Dr.“), nicht die Funktion. * - `onPremisesSamAccountName` statt `sAMAccountName` — der Wert kommt aus dem * lokalen AD und ist **leer**, wenn der Mandant kein AD synchronisiert. Eine * reine Cloud-Umgebung hat keinen Anmeldenamen in diesem Sinn; `{{sam}}` * bleibt dort leer, statt dass etwas hineingedeutet wird. * - `id` statt `objectGUID` — dieselbe GUID, nur schon als Text. Das Umsortieren * der Bytes (`src/lib/ldap-guid.ts`) braucht es hier nicht. */ export const GRAPH_ZUORDNUNG: AttributZuordnung = { vorname: "givenName", nachname: "surname", anzeigename: "displayName", mail: "mail", upn: "userPrincipalName", sam: "onPremisesSamAccountName", abteilung: "department", titel: "jobTitle", vorgesetzter: "manager", externeId: "id", }; /** Die Vorgaben je Quellenart. `SCIM` hat noch keine (#029). */ export const ZUORDNUNGS_VORGABE: Record<"LDAP" | "GRAPH", AttributZuordnung> = { LDAP: AD_ZUORDNUNG, GRAPH: GRAPH_ZUORDNUNG, }; /** Die Felder in der Reihenfolge, in der das Formular sie zeigt. */ export const ZUORDNUNGS_FELDER = Object.keys(AD_ZUORDNUNG) as (keyof AttributZuordnung)[]; /** Wie das Feld in der Oberflaeche heisst. Menschenlesbar, also mit Umlauten. */ export const ZUORDNUNGS_BESCHRIFTUNG: Record = { vorname: "Vorname", nachname: "Nachname", anzeigename: "Anzeigename", mail: "E-Mail", upn: "UPN", sam: "Anmeldename (sam)", abteilung: "Abteilung", titel: "Titel", vorgesetzter: "Vorgesetzter", externeId: "Kennung (dauerhaft)", }; /** * Ein Attributname, wie ein Verzeichnis ihn kennt: Buchstabe voran, dann * Buchstaben, Ziffern und Bindestriche. Mehr braucht es nicht, und mehr * zuzulassen hiesse, Klammern und Sternchen in eine LDAP-Anfrage zu lassen. */ export const ATTRIBUT_MUSTER = /^[A-Za-z][A-Za-z0-9-]*$/u; /** * Liest eine gespeicherte Zuordnung aus dem Json-Feld. * * Fehlende oder leere Felder bekommen die AD-Vorgabe. Das ist Absicht: eine * Quelle, die nur `anzeigename` abweichend belegt, soll nicht alle zehn Zeilen * mitschreiben muessen, und eine von Hand geleerte Zeile darf kein Attribut mit * leerem Namen anfragen. */ export function zuordnungLesen(roh: unknown, art: "LDAP" | "GRAPH" = "LDAP"): AttributZuordnung { const felder = (typeof roh === "object" && roh !== null ? roh : {}) as Record; /* * Die Vorgabe haengt an der **Art** (#028). Ohne diesen Parameter bekaeme eine * Graph-Quelle mit leerer Zuordnung die AD-Namen — und fragte dann bei Graph * nach `sn` und `objectGUID`, die es dort nicht gibt. Der Standardwert `LDAP` * haelt jeden Aufrufer von vor #028 bei seinem bisherigen Verhalten. */ const gelesen = { ...ZUORDNUNGS_VORGABE[art] }; for (const schluessel of ZUORDNUNGS_FELDER) { const wert = felder[schluessel]; if (typeof wert === "string" && wert.trim()) gelesen[schluessel] = wert.trim(); } return gelesen; } /** * Die Attributzuordnung aus den Formularfeldern, geprueft. * * Steht hier und nicht in der Server Action, weil es reine Textarbeit ist und * jeden Sonderfall einzeln geprueft gehoert: ein leeres Feld faellt heraus (der * Dienst legt dann die Vorgabe darueber), und ein Name, der kein Attributname * ist, wird abgewiesen statt in eine LDAP-Anfrage geschrieben. */ export function zuordnungAusEingabe( werte: Partial>, ): { readonly zuordnung: Partial } | { readonly fehler: string } { const zuordnung: Partial = {}; for (const feld of ZUORDNUNGS_FELDER) { const wert = werte[feld]?.trim(); if (!wert) continue; if (!ATTRIBUT_MUSTER.test(wert)) { return { fehler: `„${wert}“ ist kein Attributname (${ZUORDNUNGS_BESCHRIFTUNG[feld]}). ` + "Erwartet werden Buchstaben, Ziffern und Bindestriche, beginnend mit einem Buchstaben.", }; } zuordnung[feld] = wert; } return { zuordnung }; } /** * Was mit dem CA-Zertifikat geschehen soll: drei Faelle aus zwei Feldern. * * `undefined` heisst **unveraendert** — das hinterlegte bleibt. `""` heisst * entfernen. Alles andere ist der neue Text. Beides zugleich ist keine Eingabe, * die sich ausfuehren liesse, sondern ein Missverstaendnis, und wird als solches * gemeldet. */ export function zertifikatWahl( text: string, entfernen: boolean, ): { readonly wert: string | undefined } | { readonly fehler: string } { const neu = text.trim(); if (neu && entfernen) { return { fehler: "Entweder ein neues CA-Zertifikat eintragen oder das hinterlegte entfernen — beides zugleich " + "ergibt keinen Sinn.", }; } if (entfernen) return { wert: "" }; return { wert: neu || undefined }; }