feat(quick-260909-ipc): ldap.service.ts an forTenant() binden, Adress-Kollisionspruefung bewusst uebergreifend belassen
Aufgabe 3 der Etappe 2: elf Abfragen in sechs Methoden (listGroups, upsertMappedUser, searchUsers, importUsersByDn, importGroupsByDn, syncUsersForTenant) laufen jetzt ueber forTenant(), teils mit einem neu erzeugten, teils mit dem in derselben Methode bereits vorhandenen gebundenen Client. resolveEmailForWrite bleibt ausdruecklich ungebunden (Befund A, T-IPC-04): email/username sind plattformweit eindeutig, eine Bindung wuerde einen fremden Halter uebersehen und eine saubere Kollisionsmeldung in einen P2002-Abbruch verwandeln. Ein neuer Testblock biegt forTenant() auf ein zweites, unterscheidbares Client-Objekt um (der bisherige Identitaets-Mock haette die Umstellung nicht bemerkt, Befund F) und belegt damit, dass die Adressabfrage weiterhin am ungebundenen und der Rest am gebundenen Client landet. Alle 67 Bestandstests bleiben unveraendert gruen. docs/mandantentrennung-zugriffsklassifikation.md ist fuer den Bereich ldap geschlossen: gemessener Stand je Fundstelle, neu gerechnete Bereichsuebersicht (gebunden getrennt von ungebunden gezaehlt) und die Uebergabe des Standardgruppen-Punkts an den Bereich groups vor Etappe 4 dokumentiert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
@@ -295,6 +295,10 @@ export class LdapService {
|
||||
const client = new Client(
|
||||
this.buildClientOptions(config.serverUrl, config.tlsRejectUnauthorized),
|
||||
);
|
||||
// Mandantengescopter Lesepfad (WINDOWS #20 Etappe 2, 260909-ipc): die
|
||||
// "bereits importiert"-Markierung darf nur die Gruppen DIESES Mandanten
|
||||
// sehen.
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
|
||||
try {
|
||||
await this.bind(client, config.bindDn, config.bindPassword);
|
||||
@@ -343,7 +347,7 @@ export class LdapService {
|
||||
.filter((h): h is string => !!h);
|
||||
let importedSet = new Set<string>();
|
||||
if (guidHexes.length > 0) {
|
||||
const existing = await this.prisma.group.findMany({
|
||||
const existing = await tenantPrisma.group.findMany({
|
||||
where: { tenantId, ldapObjectGuid: { in: guidHexes } },
|
||||
select: { ldapObjectGuid: true },
|
||||
});
|
||||
@@ -412,6 +416,21 @@ export class LdapService {
|
||||
* NEVER handed from one account to another (T-Q3-01): a directory entry
|
||||
* could otherwise take over a real person's address and receive their
|
||||
* password-reset mail.
|
||||
*
|
||||
* BLEIBT bewusst UNGEBUNDEN (WINDOWS #20 Etappe 2, 260909-ipc, Befund A,
|
||||
* T-IPC-04): `email` und `username` sind in `prisma/schema.prisma`
|
||||
* plattformweit eindeutig (`@unique`), nicht je Mandant. Wuerde diese
|
||||
* Abfrage mit `forTenant()` an den eigenen Mandanten gebunden, saehe sie
|
||||
* einen fremden Halter der Adresse nicht mehr, meldete "Adresse frei", und
|
||||
* der anschliessende Schreibvorgang liefe in die plattformweite
|
||||
* Eindeutigkeitsbedingung der Datenbank — aus einer sauber berichteten
|
||||
* Kollision (WINDOWS #15/T-Q3-01) wuerde ein P2002-Abbruch des gesamten
|
||||
* Sync-Laufs. Nach dem Scharfschalten (Etappe 4) liefert diese Abfrage
|
||||
* fuer jeden Mandanten AUSSER dem der Adresse selbst 0 Zeilen und meldet
|
||||
* damit IMMER "frei" — ein bekannter, hier bewusst offen gelassener Punkt.
|
||||
* Die Loesung gehoert nach Etappe 3, vermutlich als vierte
|
||||
* SECURITY-DEFINER-Funktion nach dem Muster des Anmeldewegs
|
||||
* (siehe auth.service.ts).
|
||||
*/
|
||||
private async resolveEmailForWrite(
|
||||
desiredEmail: string,
|
||||
@@ -449,12 +468,16 @@ export class LdapService {
|
||||
status: 'created' | 'updated';
|
||||
emailConflict?: LdapEmailConflict;
|
||||
}> {
|
||||
const existingByDn = await this.prisma.user.findFirst({
|
||||
// Mandantengescopter Identitaets-/Schreibpfad (WINDOWS #20 Etappe 2,
|
||||
// 260909-ipc). Nicht zu verwechseln mit resolveEmailForWrite() oben, die
|
||||
// bewusst ungebunden bleibt.
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const existingByDn = await tenantPrisma.user.findFirst({
|
||||
where: { ldapDn: dn, tenantId },
|
||||
});
|
||||
const existingByUsername = existingByDn
|
||||
? null
|
||||
: await this.prisma.user.findFirst({ where: { username, tenantId } });
|
||||
: await tenantPrisma.user.findFirst({ where: { username, tenantId } });
|
||||
const existing = existingByDn || existingByUsername;
|
||||
|
||||
if (existing) {
|
||||
@@ -472,7 +495,7 @@ export class LdapService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.prisma.user.update({
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
...(mappedData['displayName'] && {
|
||||
@@ -540,6 +563,10 @@ export class LdapService {
|
||||
const client = new Client(
|
||||
this.buildClientOptions(config.serverUrl, config.tlsRejectUnauthorized),
|
||||
);
|
||||
// Mandantengescopter Lesepfad (WINDOWS #20 Etappe 2, 260909-ipc): die
|
||||
// "bereits importiert"-Markierung darf nur die Konten DIESES Mandanten
|
||||
// sehen.
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
const first = (v: unknown): string =>
|
||||
Array.isArray(v) ? String(v[0] ?? '') : v != null ? String(v) : '';
|
||||
|
||||
@@ -576,7 +603,7 @@ export class LdapService {
|
||||
const usernames = entries
|
||||
.map((e) => e.username.toLowerCase())
|
||||
.filter(Boolean);
|
||||
const existing = await this.prisma.user.findMany({
|
||||
const existing = await tenantPrisma.user.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
OR: [{ ldapDn: { in: dns } }, { username: { in: usernames } }],
|
||||
@@ -584,10 +611,12 @@ export class LdapService {
|
||||
select: { ldapDn: true, username: true },
|
||||
});
|
||||
const dnSet = new Set(
|
||||
existing.map((u) => u.ldapDn).filter((d): d is string => !!d),
|
||||
existing
|
||||
.map((u: { ldapDn: string | null }) => u.ldapDn)
|
||||
.filter((d: string | null): d is string => !!d),
|
||||
);
|
||||
const usernameSet = new Set(
|
||||
existing.map((u) => u.username.toLowerCase()),
|
||||
existing.map((u: { username: string }) => u.username.toLowerCase()),
|
||||
);
|
||||
|
||||
return entries.map((e) => ({
|
||||
@@ -632,6 +661,9 @@ export class LdapService {
|
||||
.map((u) => u.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
// Mandantengescopter Dedup-/Schreibpfad (WINDOWS #20 Etappe 2,
|
||||
// 260909-ipc).
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
|
||||
try {
|
||||
await this.bind(client, config.bindDn, config.bindPassword);
|
||||
@@ -672,12 +704,12 @@ export class LdapService {
|
||||
// Dedup: if a user already exists (by ldapDn or username) skip it,
|
||||
// but link the ldapDn so a later group/OU sync matches it and never
|
||||
// duplicates.
|
||||
const existing = await this.prisma.user.findFirst({
|
||||
const existing = await tenantPrisma.user.findFirst({
|
||||
where: { tenantId, OR: [{ ldapDn: dn }, { username }] },
|
||||
});
|
||||
if (existing) {
|
||||
if (existing.ldapDn !== dn) {
|
||||
await this.prisma.user.update({
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: existing.id },
|
||||
data: { ldapDn: dn },
|
||||
});
|
||||
@@ -797,7 +829,7 @@ export class LdapService {
|
||||
|
||||
// Idempotency: a second import of the same AD group is a skip, not
|
||||
// a duplicate row.
|
||||
const existingByGuid = await this.prisma.group.findFirst({
|
||||
const existingByGuid = await tenantPrisma.group.findFirst({
|
||||
where: { tenantId, ldapObjectGuid },
|
||||
});
|
||||
if (existingByGuid) {
|
||||
@@ -1000,7 +1032,7 @@ export class LdapService {
|
||||
}
|
||||
|
||||
// 5. Deactivation per D-15: Deactivate users removed from LDAP
|
||||
const localLdapUsers = await this.prisma.user.findMany({
|
||||
const localLdapUsers = await tenantPrisma.user.findMany({
|
||||
where: {
|
||||
tenantId,
|
||||
ldapDn: { not: null },
|
||||
@@ -1011,7 +1043,7 @@ export class LdapService {
|
||||
|
||||
for (const localUser of localLdapUsers) {
|
||||
if (localUser.ldapDn && !syncedDns.includes(localUser.ldapDn)) {
|
||||
await this.prisma.user.update({
|
||||
await tenantPrisma.user.update({
|
||||
where: { id: localUser.id },
|
||||
data: { isActive: false },
|
||||
});
|
||||
@@ -1047,7 +1079,7 @@ export class LdapService {
|
||||
);
|
||||
|
||||
// 6. Update lastSyncAt
|
||||
await this.prisma.ldapConfig.update({
|
||||
await tenantPrisma.ldapConfig.update({
|
||||
where: { id: config.id },
|
||||
data: { lastSyncAt: new Date() },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user