From 246dc89a987ae62a038c2316f717e5fc2508b2dd Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 9 Jul 2026 15:28:48 +0200 Subject: [PATCH] fix(ldap): treat ldapts empty-array attributes as absent, not "undefined" ldapts represents a missing/absent LDAP attribute as an empty array ([]), not undefined -- entry['mail'] is [] when an account has no mail set. The field-mapping loop did Array.isArray(value) ? String(value[0]) : ..., and String(undefined) is the literal string "undefined". Every synced entry without that attribute got mappedData['email'] = "undefined" (a truthy string, so the `|| fallback` never kicked in), and the second such entry onward crashed with a unique constraint violation on email since they all shared the exact same literal string. Found live: syncing against a real Zentyal/Samba AD directory failed on every entry after the first (Kevin Schaller, krbtgt, Guest, the DC computer object, etc.) with "Unique constraint failed on the fields: (email)". Fix: resolve array values to their first element (or use the raw value for non-arrays) and only keep it when actually present and non-empty, so a genuinely missing attribute falls through to the `${username}@ldap.local` fallback instead of the string "undefined". Co-Authored-By: Claude Sonnet 5 --- apps/api/src/ldap/ldap.service.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/api/src/ldap/ldap.service.ts b/apps/api/src/ldap/ldap.service.ts index f9ee53f..e0e6ea4 100644 --- a/apps/api/src/ldap/ldap.service.ts +++ b/apps/api/src/ldap/ldap.service.ts @@ -211,15 +211,20 @@ export class LdapService { const dn = entry.dn; syncedDns.push(dn); - // Map LDAP fields to Tessera fields + // Map LDAP fields to Tessera fields. + // ldapts represents a missing/absent attribute as an empty array + // ([]), not undefined -- naively doing String(value[0]) on that + // produces the literal string "undefined", identical across every + // entry lacking the attribute (e.g. no `mail` set), which then + // collides on unique constraints like email. Resolve to the first + // array element (or the raw value) and skip when it's actually + // missing/empty. const mappedData: Record = {}; for (const mapping of config.fieldMappings) { const value = entry[mapping.ldapField]; - if (value !== undefined && value !== null) { - // LDAP attributes can be arrays; take first value - mappedData[mapping.tesseraField] = Array.isArray(value) - ? String(value[0]) - : String(value); + const resolved = Array.isArray(value) ? value[0] : value; + if (resolved !== undefined && resolved !== null && resolved !== '') { + mappedData[mapping.tesseraField] = String(resolved); } }