9d1323fe97
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>
92 lines
3.1 KiB
TypeScript
92 lines
3.1 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
import { LdapConfigService } from './ldap-config.service';
|
|
import { LdapService } from './ldap.service';
|
|
|
|
/**
|
|
* LDAP Sync Scheduler (D-14 auto-sync).
|
|
*
|
|
* Runs every minute and checks each active LDAP config to determine
|
|
* if a sync is due based on syncIntervalMin.
|
|
*
|
|
* CRITICAL per Pitfall 2: Each tenant sync is an independent operation
|
|
* with its own tenant context. We do NOT share database connections
|
|
* across tenant syncs.
|
|
*/
|
|
@Injectable()
|
|
export class LdapSyncScheduler {
|
|
private readonly logger = new Logger(LdapSyncScheduler.name);
|
|
|
|
constructor(
|
|
private ldapService: LdapService,
|
|
private ldapConfigService: LdapConfigService,
|
|
) {}
|
|
|
|
/**
|
|
* Cron job running every minute. Checks all active LDAP configs
|
|
* and triggers sync for those whose interval has elapsed.
|
|
*/
|
|
@Cron(CronExpression.EVERY_MINUTE)
|
|
async handleCron() {
|
|
const configs = await this.ldapConfigService.getAllActiveConfigs();
|
|
|
|
for (const config of configs) {
|
|
try {
|
|
// Skip configs with auto-sync disabled (interval = 0)
|
|
if (config.syncIntervalMin <= 0) {
|
|
continue;
|
|
}
|
|
|
|
// Check if sync is due
|
|
const now = new Date();
|
|
if (config.lastSyncAt) {
|
|
const elapsed =
|
|
(now.getTime() - config.lastSyncAt.getTime()) / 1000 / 60;
|
|
if (elapsed < config.syncIntervalMin) {
|
|
continue; // Not yet time for next sync
|
|
}
|
|
}
|
|
// If lastSyncAt is null, sync has never run -- trigger now
|
|
|
|
this.logger.log(
|
|
`Starting LDAP sync for tenant ${config.tenantId} (interval: ${config.syncIntervalMin}min)`,
|
|
);
|
|
|
|
// CRITICAL per Pitfall 2: Each tenant sync gets its own context.
|
|
// The ldapService.syncUsersForTenant method receives tenantId explicitly
|
|
// and creates a forTenant scoped client for all DB operations.
|
|
const result = await this.ldapService.syncUsersForTenant(
|
|
{
|
|
id: config.id,
|
|
tenantId: config.tenantId,
|
|
serverUrl: config.serverUrl,
|
|
baseDn: config.baseDn,
|
|
bindDn: config.bindDn,
|
|
bindPassword: config.bindPassword,
|
|
searchFilter: config.searchFilter,
|
|
groupFilterDns: config.groupFilterDns,
|
|
userExcludeList: config.userExcludeList,
|
|
fieldMappings: config.fieldMappings,
|
|
},
|
|
config.tenantId,
|
|
);
|
|
|
|
this.logger.log(
|
|
`LDAP sync completed for tenant ${config.tenantId}: ` +
|
|
`created=${result.created}, updated=${result.updated}, deactivated=${result.deactivated}` +
|
|
(result.errors.length > 0
|
|
? `, errors=${result.errors.length}`
|
|
: ''),
|
|
);
|
|
} catch (error: unknown) {
|
|
// One tenant's failure must not block others
|
|
const message =
|
|
error instanceof Error ? error.message : 'Unknown error';
|
|
this.logger.error(
|
|
`LDAP sync failed for tenant ${config.tenantId}: ${message}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|