feat(verzeichnis): Entra ID als Quelle ueber Microsoft Graph (#48)
Ein Haus ohne lokales Active Directory hat kein LDAP, auf das sich zeigen liesse. Eine Verzeichnisquelle hat jetzt eine **Art** — LDAP oder GRAPH —, und beide laufen durch **denselben** Abgleich: anlegen, aktualisieren, deaktivieren, nie loeschen. Nur das Holen der Eintraege unterscheidet sich; zwei Abgleiche nebeneinander wuerden auseinanderdriften, und die Zusage „es wird nichts geloescht“ gaelte dann fuer eine Art und fuer die andere nicht mehr ganz. Gelesen wird mit dem Client-Credentials-Flow, seitenweise ueber @odata.nextLink, der Vorgesetzte ueber $expand=manager($select=id). Das Client-Secret wird verschluesselt gespeichert wie das Bind-Passwort und erreicht keine Antwort und keinen Protokolleintrag. Die Art ist **unveraenderlich**, und das steht nicht im Issue: an ihr haengt, was `externeId` bedeutet. Im AD eine objectGUID, in Entra ID eine andere GUID — nach einem Wechsel wuerde kein einziger Mitarbeiter wiedererkannt, der naechste Abgleich legte die ganze Belegschaft neu an und setzte die alten Zeilen auf aktiv = false, mit allen Laeufen und Geraeten daran. Der teuerste Fund beim Bauen: „in Entra ID deaktiviert“ als *uebersprungen* zu melden haette die Leute **aktiv** gelassen. Uebersprungen heisst im Abgleich „nicht zuordenbar“, und solche Eintraege werden seit #016 absichtlich nicht abgeschaltet. Gefiltert wird deshalb serverseitig mit `$filter=accountEnabled eq true`, als Gegenstueck zum LDAP-Standardfilter. Gefunden hat es der Test — aber nur, weil der Doppelgaenger den Filter wirklich auswertet statt ihn bloss zu empfangen. {{ou}} bleibt bei einer Graph-Quelle leer: Entra ID hat keine OU, und etwas hineinzudeuten waere ein Wert, den kein PowerShell-Befehl brauchen kann — er saehe nur richtig aus. Steht an der Abteilung eine Ziel-OU gepflegt, greift die. Gegen einen echten Entra-Mandanten ist das **nicht** gelaufen. Geprueft ist es gegen einen HTTP-Doppelgaenger mit aufgezeichneten Antworten; die Pruefliste fuer den ersten echten Lauf steht am Ende von docs/betrieb/entra-id.md. Closes #48 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0158d0292f
commit
76a61ec771
@@ -1,17 +1,27 @@
|
||||
import type { Prisma, Verzeichnisquelle } from "@/generated/prisma/client";
|
||||
import { Betriebsmodus } from "@/generated/prisma/enums";
|
||||
import { QuellenArt, Betriebsmodus } from "@/generated/prisma/enums";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { entschluesseln, verschluesseln } from "@/lib/geheimnisse";
|
||||
import { verschluesseln } from "@/lib/geheimnisse";
|
||||
import { darf } from "@/server/auth/rollen";
|
||||
import { betriebsmodus, eigeneOrganisation, kundeErmitteln, type HandelnderBenutzer } from "@/server/mandant";
|
||||
import { VerzeichnisFehler } from "@/server/ldap/fehler";
|
||||
import { INTERVALL_HOECHSTENS, INTERVALL_MINDESTENS, intervallMeldung } from "@/lib/verzeichnis";
|
||||
import { eintraegeLesen } from "@/server/ldap/leser";
|
||||
import { type LdapZugang, urlPruefen } from "@/server/ldap/verbindung";
|
||||
// Die Zugangs-Helfer liegen seit #028 in einem eigenen Modul – sonst waere die
|
||||
// Weiche in `quellenleser.ts` ein Import-Zyklus. Hier weitergereicht, damit
|
||||
// bestehende Aufrufer ihre Anlaufstelle behalten.
|
||||
import { ldapVorgabeAus, zugangAus, zuordnungsArt } from "@/server/verzeichnis/zugang";
|
||||
import {
|
||||
ZUORDNUNGS_VORGABE,
|
||||
INTERVALL_HOECHSTENS,
|
||||
INTERVALL_MINDESTENS,
|
||||
intervallMeldung,
|
||||
} from "@/lib/verzeichnis";
|
||||
import { eintraegeDerQuelleStroemen } from "@/server/verzeichnis/quellenleser";
|
||||
import { urlPruefen } from "@/server/ldap/verbindung";
|
||||
import {
|
||||
AD_ZUORDNUNG,
|
||||
type AttributZuordnung,
|
||||
type GemappterEintrag,
|
||||
type RoherEintrag,
|
||||
STANDARD_FILTER,
|
||||
type Uebersprungen,
|
||||
vorgesetzteAufloesen,
|
||||
@@ -48,16 +58,24 @@ export { AD_ZUORDNUNG, STANDARD_FILTER };
|
||||
* Aufrufer sie von hier kennt.
|
||||
*/
|
||||
export { VerzeichnisFehler };
|
||||
export { ldapVorgabeAus, zugangAus, zuordnungsArt };
|
||||
|
||||
/** Ohne Passwort – diese Form geht an die Oberflaeche. */
|
||||
export type QuellenSicht = {
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
readonly aktiv: boolean;
|
||||
readonly url: string;
|
||||
readonly bindDn: string;
|
||||
readonly basisDn: string;
|
||||
readonly filter: string;
|
||||
/** Woher gelesen wird (#028). Bis dahin war jede Quelle LDAP. */
|
||||
readonly art: QuellenArt;
|
||||
/** Nur bei `art = LDAP` belegt. */
|
||||
readonly url: string | null;
|
||||
readonly bindDn: string | null;
|
||||
readonly basisDn: string | null;
|
||||
readonly filter: string | null;
|
||||
/** Nur bei `art = GRAPH` belegt – das Geheimnis **nie**, nur ob eines da ist. */
|
||||
readonly tenantId: string | null;
|
||||
readonly clientId: string | null;
|
||||
readonly clientSecretVorhanden: boolean;
|
||||
readonly attributZuordnung: AttributZuordnung;
|
||||
readonly caZertifikatVorhanden: boolean;
|
||||
readonly abgleichIntervallMinuten: number;
|
||||
@@ -68,11 +86,18 @@ export type QuellenSicht = {
|
||||
|
||||
export type NeueQuelle = {
|
||||
name: string;
|
||||
url: string;
|
||||
bindDn: string;
|
||||
bindPasswort: string;
|
||||
basisDn: string;
|
||||
/** Ohne Angabe `LDAP` – so verhalten sich Aufrufer von vor #028 unveraendert. */
|
||||
art?: QuellenArt;
|
||||
/** Pflicht bei `art = LDAP`, bei Graph nicht erlaubt. */
|
||||
url?: string;
|
||||
bindDn?: string;
|
||||
bindPasswort?: string;
|
||||
basisDn?: string;
|
||||
filter?: string;
|
||||
/** Pflicht bei `art = GRAPH`. */
|
||||
tenantId?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
attributZuordnung?: Partial<AttributZuordnung>;
|
||||
caZertifikatPem?: string | null;
|
||||
abgleichIntervallMinuten?: number;
|
||||
@@ -150,11 +175,19 @@ function alsSicht(zeile: Verzeichnisquelle): QuellenSicht {
|
||||
id: zeile.id,
|
||||
name: zeile.name,
|
||||
aktiv: zeile.aktiv,
|
||||
art: zeile.art,
|
||||
url: zeile.url,
|
||||
bindDn: zeile.bindDn,
|
||||
basisDn: zeile.basisDn,
|
||||
filter: zeile.filter,
|
||||
attributZuordnung: zuordnungLesen(zeile.attributZuordnung),
|
||||
tenantId: zeile.tenantId,
|
||||
clientId: zeile.clientId,
|
||||
/*
|
||||
* Wie beim Bind-Passwort: nur **ob** eines hinterlegt ist. Ein Feld, das es
|
||||
* nicht in eine Antwort schafft, kann auch nicht versehentlich hinausgehen.
|
||||
*/
|
||||
clientSecretVorhanden: Boolean(zeile.clientSecretVerschluesselt?.trim()),
|
||||
attributZuordnung: zuordnungLesen(zeile.attributZuordnung, zuordnungsArt(zeile.art)),
|
||||
// Der Inhalt bleibt hier: die Oberflaeche muss nur wissen, ob eines
|
||||
// hinterlegt ist, und braucht dafuer keinen Zertifikatstext.
|
||||
caZertifikatVorhanden: Boolean(zeile.caZertifikatPem?.trim()),
|
||||
@@ -168,6 +201,64 @@ function alsSicht(zeile: Verzeichnisquelle): QuellenSicht {
|
||||
/** Auch fuer die Lesemodelle: die eine Stelle, die aus einer Zeile eine Sicht macht. */
|
||||
export { alsSicht as quellenSichtBauen };
|
||||
|
||||
/**
|
||||
* Die artabhaengigen Felder einer neuen Quelle, geprueft.
|
||||
*
|
||||
* Eine eigene Funktion, weil beide Richtungen gelten muessen: was eine Art
|
||||
* **braucht**, und was sie **nicht haben darf**. Ohne den zweiten Teil liesse
|
||||
* sich eine Graph-Quelle mit `basisDn` anlegen — eine Zeile, die zwei Dinge
|
||||
* gleichzeitig behauptet, und bei der niemand mehr sagen kann, was gilt. Die
|
||||
* Datenbank weist das ohnehin ab; hier entsteht die Meldung, die weiterhilft.
|
||||
*/
|
||||
function feldernJeArt(art: QuellenArt, eingabe: NeueQuelle) {
|
||||
if (art === "LDAP") {
|
||||
const url = pflichtText(eingabe.url, "Die Adresse des Verzeichnisses");
|
||||
urlPruefen(url);
|
||||
const bindPasswort = eingabe.bindPasswort?.trim();
|
||||
if (!bindPasswort) {
|
||||
throw new VerzeichnisFehler("Das Bind-Passwort fehlt. Ein anonymer Bind wird nicht unterstützt.");
|
||||
}
|
||||
if (eingabe.tenantId?.trim() || eingabe.clientId?.trim() || eingabe.clientSecret?.trim()) {
|
||||
throw new VerzeichnisFehler(
|
||||
"Eine LDAP-Quelle hat keine Graph-Zugangsdaten. Für Entra ID gibt es die Art „Graph“.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
// Gespeichert wird die getippte Adresse samt Schema. `ldap+starttls://`
|
||||
// ist Teil der Angabe und wird erst beim Verbinden abgetrennt — sonst
|
||||
// waere nach dem Speichern nicht mehr zu erkennen, dass StartTLS
|
||||
// gewuenscht war.
|
||||
url,
|
||||
bindDn: pflichtText(eingabe.bindDn, "Der Bind-DN"),
|
||||
bindPasswortVerschluesselt: verschluesseln(bindPasswort),
|
||||
basisDn: pflichtText(eingabe.basisDn, "Der Basis-DN"),
|
||||
filter: eingabe.filter?.trim() || STANDARD_FILTER,
|
||||
};
|
||||
}
|
||||
|
||||
if (art === "GRAPH") {
|
||||
const clientSecret = eingabe.clientSecret?.trim();
|
||||
if (!clientSecret) {
|
||||
throw new VerzeichnisFehler("Das Client-Secret der App-Registrierung fehlt.");
|
||||
}
|
||||
if (eingabe.url?.trim() || eingabe.bindDn?.trim() || eingabe.basisDn?.trim()) {
|
||||
throw new VerzeichnisFehler(
|
||||
"Eine Graph-Quelle hat keine Adresse, keinen Bind-DN und keinen Basis-DN — Entra ID kennt " +
|
||||
"beides nicht. Für ein lokales AD gibt es die Art „LDAP“.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
tenantId: pflichtText(eingabe.tenantId, "Die Mandanten-ID"),
|
||||
clientId: pflichtText(eingabe.clientId, "Die Anwendungs-ID"),
|
||||
clientSecretVerschluesselt: verschluesseln(clientSecret),
|
||||
};
|
||||
}
|
||||
|
||||
throw new VerzeichnisFehler(
|
||||
`Quellen der Art ${art} lassen sich noch nicht anlegen. Der SCIM-Endpunkt kommt mit #029.`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function quelleAnlegen(
|
||||
eingabe: NeueQuelle,
|
||||
benutzer: HandelnderBenutzer,
|
||||
@@ -177,31 +268,28 @@ export async function quelleAnlegen(
|
||||
const kundeId = await kundeErmitteln(null, benutzer);
|
||||
|
||||
const name = pflichtText(eingabe.name, "Der Name der Quelle");
|
||||
const url = pflichtText(eingabe.url, "Die Adresse des Verzeichnisses");
|
||||
urlPruefen(url);
|
||||
const bindDn = pflichtText(eingabe.bindDn, "Der Bind-DN");
|
||||
const bindPasswort = eingabe.bindPasswort?.trim();
|
||||
if (!bindPasswort) {
|
||||
throw new VerzeichnisFehler("Das Bind-Passwort fehlt. Ein anonymer Bind wird nicht unterstützt.");
|
||||
}
|
||||
const basisDn = pflichtText(eingabe.basisDn, "Der Basis-DN");
|
||||
const art = eingabe.art ?? "LDAP";
|
||||
const intervall = intervallPruefen(eingabe.abgleichIntervallMinuten);
|
||||
|
||||
/*
|
||||
* **Welche Felder gelten, haengt an der Art** (#028). Geprueft wird hier mit
|
||||
* einer brauchbaren Meldung; dieselbe Regel haelt die Datenbank fest (CHECK
|
||||
* `Verzeichnisquelle_felder_je_art`), damit sie auch gilt, wenn jemand an
|
||||
* dieser Schicht vorbei schreibt.
|
||||
*/
|
||||
const felder = feldernJeArt(art, eingabe);
|
||||
|
||||
const zeile = await prisma.$transaction(async (tx) => {
|
||||
const angelegt = await tx.verzeichnisquelle.create({
|
||||
data: {
|
||||
kundeId,
|
||||
name,
|
||||
// Gespeichert wird die getippte Adresse samt Schema. `ldap+starttls://`
|
||||
// ist Teil der Angabe und wird erst beim Verbinden abgetrennt — sonst
|
||||
// waere nach dem Speichern nicht mehr zu erkennen, dass StartTLS
|
||||
// gewuenscht war.
|
||||
url,
|
||||
bindDn,
|
||||
bindPasswortVerschluesselt: verschluesseln(bindPasswort),
|
||||
basisDn,
|
||||
filter: eingabe.filter?.trim() || STANDARD_FILTER,
|
||||
attributZuordnung: zuordnungLesen({ ...AD_ZUORDNUNG, ...eingabe.attributZuordnung }),
|
||||
art,
|
||||
...felder,
|
||||
attributZuordnung: zuordnungLesen(
|
||||
{ ...ZUORDNUNGS_VORGABE[zuordnungsArt(art)], ...eingabe.attributZuordnung },
|
||||
zuordnungsArt(art),
|
||||
),
|
||||
caZertifikatPem: zertifikatPruefen(eingabe.caZertifikatPem),
|
||||
// Ohne Angabe bleibt der Standardwert des Schemas (60 Minuten) stehen.
|
||||
...(intervall === undefined ? {} : { abgleichIntervallMinuten: intervall }),
|
||||
@@ -215,8 +303,20 @@ export async function quelleAnlegen(
|
||||
entitaetId: angelegt.id,
|
||||
aktion: "Verzeichnisquelle angelegt",
|
||||
benutzerId: benutzer.id,
|
||||
// Kein Passwort, auch nicht verschluesselt: das Protokoll wird gelesen.
|
||||
nachher: { name: angelegt.name, url: angelegt.url, basisDn: angelegt.basisDn },
|
||||
/*
|
||||
* Kein Passwort und kein Client-Secret, auch nicht verschluesselt: das
|
||||
* Protokoll wird gelesen (#023). Der Mandant und die Anwendungs-ID sind
|
||||
* dagegen keine Geheimnisse — ohne sie liesse sich hinterher nicht
|
||||
* sagen, welche Registrierung gemeint war.
|
||||
*/
|
||||
nachher: {
|
||||
name: angelegt.name,
|
||||
art: angelegt.art,
|
||||
...(angelegt.url ? { url: angelegt.url } : {}),
|
||||
...(angelegt.basisDn ? { basisDn: angelegt.basisDn } : {}),
|
||||
...(angelegt.tenantId ? { tenantId: angelegt.tenantId } : {}),
|
||||
...(angelegt.clientId ? { clientId: angelegt.clientId } : {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
return angelegt;
|
||||
@@ -236,6 +336,35 @@ export async function quelleBearbeiten(
|
||||
const daten: Prisma.VerzeichnisquelleUpdateInput = {};
|
||||
const geaendert: string[] = [];
|
||||
|
||||
/*
|
||||
* **Die Art laesst sich nicht aendern** (#028).
|
||||
*
|
||||
* Nicht aus Bequemlichkeit: an der Art haengt, was `externeId` bedeutet. Im
|
||||
* AD ist es eine `objectGUID`, in Entra ID eine andere GUID — nach einem
|
||||
* Wechsel wuerde kein einziger Mitarbeiter wiedererkannt. Der naechste
|
||||
* Abgleich legte die ganze Belegschaft neu an und setzte die alten Zeilen auf
|
||||
* `aktiv = false`: Laeufe und Geraete haengen dann an den inaktiven.
|
||||
*
|
||||
* Wer wechselt, legt eine neue Quelle an und laesst die alte stehen, bis die
|
||||
* Leute uebernommen sind — dafuer gibt es die Uebernahme von Hand angelegter
|
||||
* Personen aus #016.
|
||||
*/
|
||||
if (aenderung.art !== undefined && aenderung.art !== vorher.art) {
|
||||
throw new VerzeichnisFehler(
|
||||
`Die Art einer Quelle lässt sich nicht ändern (${vorher.art} → ${aenderung.art}). ` +
|
||||
"Daran hängt, was die dauerhafte Kennung bedeutet — nach einem Wechsel würde kein " +
|
||||
"Mitarbeiter wiedererkannt. Leg stattdessen eine neue Quelle an.",
|
||||
);
|
||||
}
|
||||
|
||||
const nurBei = (art: QuellenArt, feld: string): void => {
|
||||
if (vorher.art !== art) {
|
||||
throw new VerzeichnisFehler(
|
||||
`„${feld}“ gilt nur für eine Quelle der Art ${art}; „${vorher.name}“ ist ${vorher.art}.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (aenderung.name !== undefined) {
|
||||
const name = pflichtText(aenderung.name, "Der Name der Quelle");
|
||||
if (name !== vorher.name) {
|
||||
@@ -243,7 +372,40 @@ export async function quelleBearbeiten(
|
||||
geaendert.push("Name");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Graph-Felder ---------------------------------------------------------
|
||||
if (aenderung.tenantId !== undefined) {
|
||||
nurBei("GRAPH", "Mandanten-ID");
|
||||
const wert = pflichtText(aenderung.tenantId, "Die Mandanten-ID");
|
||||
if (wert !== vorher.tenantId) {
|
||||
daten.tenantId = wert;
|
||||
geaendert.push("Mandant");
|
||||
}
|
||||
}
|
||||
if (aenderung.clientId !== undefined) {
|
||||
nurBei("GRAPH", "Anwendungs-ID");
|
||||
const wert = pflichtText(aenderung.clientId, "Die Anwendungs-ID");
|
||||
if (wert !== vorher.clientId) {
|
||||
daten.clientId = wert;
|
||||
geaendert.push("Anwendung");
|
||||
}
|
||||
}
|
||||
if (aenderung.clientSecret !== undefined) {
|
||||
nurBei("GRAPH", "Client-Secret");
|
||||
/*
|
||||
* Leer heisst **unveraendert**, wie beim Bind-Passwort: das Formular zeigt
|
||||
* ein gesetztes Geheimnis nie an, also kommt es leer zurueck. Wuerde leer
|
||||
* als „loeschen“ gelesen, machte jedes Umbenennen die Quelle unbrauchbar.
|
||||
*/
|
||||
const wert = aenderung.clientSecret.trim();
|
||||
if (wert) {
|
||||
daten.clientSecretVerschluesselt = verschluesseln(wert);
|
||||
geaendert.push("Client-Secret");
|
||||
}
|
||||
}
|
||||
|
||||
if (aenderung.url !== undefined) {
|
||||
nurBei("LDAP", "Adresse");
|
||||
const url = pflichtText(aenderung.url, "Die Adresse des Verzeichnisses");
|
||||
urlPruefen(url);
|
||||
if (url !== vorher.url) {
|
||||
@@ -252,6 +414,7 @@ export async function quelleBearbeiten(
|
||||
}
|
||||
}
|
||||
if (aenderung.bindDn !== undefined) {
|
||||
nurBei("LDAP", "Bind-DN");
|
||||
const bindDn = pflichtText(aenderung.bindDn, "Der Bind-DN");
|
||||
if (bindDn !== vorher.bindDn) {
|
||||
daten.bindDn = bindDn;
|
||||
@@ -259,6 +422,7 @@ export async function quelleBearbeiten(
|
||||
}
|
||||
}
|
||||
if (aenderung.bindPasswort !== undefined) {
|
||||
nurBei("LDAP", "Bind-Passwort");
|
||||
// Ein leeres Feld heisst „unveraendert“. Ein Formular, das das Passwort nicht
|
||||
// anzeigen kann (und das darf es nicht), schickt sonst bei jedem Speichern
|
||||
// eine leere Zeichenkette und loeschte das Geheimnis.
|
||||
@@ -269,6 +433,7 @@ export async function quelleBearbeiten(
|
||||
}
|
||||
}
|
||||
if (aenderung.basisDn !== undefined) {
|
||||
nurBei("LDAP", "Basis-DN");
|
||||
const basisDn = pflichtText(aenderung.basisDn, "Der Basis-DN");
|
||||
if (basisDn !== vorher.basisDn) {
|
||||
daten.basisDn = basisDn;
|
||||
@@ -276,6 +441,7 @@ export async function quelleBearbeiten(
|
||||
}
|
||||
}
|
||||
if (aenderung.filter !== undefined) {
|
||||
nurBei("LDAP", "Filter");
|
||||
const filter = aenderung.filter.trim() || STANDARD_FILTER;
|
||||
if (filter !== vorher.filter) {
|
||||
daten.filter = filter;
|
||||
@@ -400,13 +566,15 @@ export async function verbindungTesten(
|
||||
const quelle = await quelleZeileHolen(quelleId);
|
||||
const wieViele = Math.min(Math.max(Math.trunc(grenze), 1), PROBE_GRENZE_HOECHSTENS);
|
||||
|
||||
const { eintraege, gesamtGelesen, uebersprungen } = await eintraegeLesen(
|
||||
zugangAus(quelle),
|
||||
{
|
||||
basisDn: quelle.basisDn,
|
||||
filter: quelle.filter,
|
||||
zuordnung: zuordnungLesen(quelle.attributZuordnung),
|
||||
},
|
||||
/*
|
||||
* **Dieselbe Weiche wie der Abgleich** (#028). Eine eigene Verzweigung hier
|
||||
* waere eine zweite Stelle, die sich an eine neue Quellenart erinnern muss —
|
||||
* und die vergisst man.
|
||||
*/
|
||||
const eintraege: RoherEintrag[] = [];
|
||||
const { gesamtGelesen, uebersprungen } = await eintraegeDerQuelleStroemen(
|
||||
quelle,
|
||||
(eintrag) => void eintraege.push(eintrag),
|
||||
wieViele,
|
||||
);
|
||||
return { treffer: vorgesetzteAufloesen(eintraege), gesamtGelesen, uebersprungen };
|
||||
@@ -433,13 +601,3 @@ export async function quelleZeileHolen(id: string): Promise<Verzeichnisquelle> {
|
||||
}
|
||||
return zeile;
|
||||
}
|
||||
|
||||
/** Entschluesselt das Bind-Passwort. Das Ergebnis bleibt in der Aufrufkette. */
|
||||
export function zugangAus(quelle: Verzeichnisquelle): LdapZugang {
|
||||
return {
|
||||
url: quelle.url,
|
||||
bindDn: quelle.bindDn,
|
||||
bindPasswort: entschluesseln(quelle.bindPasswortVerschluesselt),
|
||||
caZertifikatPem: quelle.caZertifikatPem,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user