feat(ldap): per-user exclude/denylist filter for sync
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 43s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m46s

Add a per-username denylist so individual accounts (service accounts like
administrator, krbtgt, guest, ldap$) can be excluded from LDAP sync,
independent of the group/OU include-filter which only scopes the search.

- schema: LdapConfig.userExcludeList String[] (+ migration)
- sync: skip excluded usernames (case-insensitive) before recording the DN,
  so an already-imported user added to the list gets deactivated next sync
- DTO / config service / controller / scheduler: thread userExcludeList through
- web: exclude-list admin UI section (add/remove/save) + de/en translations
- tests: 3 specs covering empty list, case-insensitive skip, deactivation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 09:34:01 +02:00
parent ef3e41769e
commit 9d1323fe97
11 changed files with 283 additions and 1 deletions
+23 -1
View File
@@ -26,6 +26,7 @@ interface LdapConfigData {
bindPassword?: string | null;
searchFilter: string;
groupFilterDns: string[];
userExcludeList: string[];
fieldMappings: Array<{
ldapField: string;
tesseraField: string;
@@ -205,11 +206,21 @@ export class LdapService {
// Track all DNs found in this sync for deactivation logic
const syncedDns: string[] = [];
// Per-user exclude/denylist: individual usernames (sAMAccountName) the
// admin never wants imported, e.g. service accounts like administrator,
// krbtgt, guest, ldap$. Distinct from groupFilterDns, which only limits
// which OUs/groups are searched. Normalized to lowercase to match the
// case-insensitive username handling below.
const excludeSet = new Set(
(config.userExcludeList ?? [])
.map((u) => u.trim().toLowerCase())
.filter(Boolean),
);
// 4. Process each LDAP entry
for (const entry of searchEntries) {
try {
const dn = entry.dn;
syncedDns.push(dn);
// Map LDAP fields to Tessera fields.
// ldapts represents a missing/absent attribute as an empty array
@@ -231,6 +242,17 @@ export class LdapService {
// Require at minimum a username. Normalize to lowercase so
// logins stay case-insensitive regardless of AD casing.
const username = mappedData['username']?.toLowerCase();
// Skip excluded users before recording the DN as synced. Leaving an
// excluded entry out of syncedDns means that if the admin adds an
// already-imported user to the exclude list, the deactivation pass
// below will deactivate them on the next sync.
if (username && excludeSet.has(username)) {
continue;
}
syncedDns.push(dn);
if (!username) {
result.errors.push(
`Entry ${dn}: no username mapped (check sAMAccountName mapping)`,