feat(ldap): individual user search + selective import with dedup
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m45s

Add an AD single-user search (by cn/sAMAccountName/displayName/mail) and a
selective import to the LDAP admin page, alongside the existing group/OU
filter. Imported users are deduped against existing ones by (ldapDn, then
username): a manually-imported user carries its ldapDn, so a later
department/group sync matches and updates it in place instead of creating a
duplicate. Search results flag alreadyImported; import skips existing users
and links a missing ldapDn. Extracted shared mapEntry/upsertMappedUser
helpers so sync and manual import resolve identity identically.

Backend: GET /ldap/users/search, POST /ldap/users/import (RFC-4515 escaped
query, ADMIN-guarded). 6 new service specs (search flags, create, skip,
ldapDn-link, denylist). Full API suite 215 green, both apps tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-22 13:52:50 +02:00
parent 0dd104054b
commit 38face43b4
7 changed files with 744 additions and 59 deletions
+67
View File
@@ -8,6 +8,7 @@ import {
Param,
Patch,
Post,
Query,
Req,
} from '@nestjs/common';
import { Role } from '@prisma/client';
@@ -15,6 +16,7 @@ import { Roles } from '../auth/decorators/roles.decorator';
import {
CreateFieldMappingDto,
CreateLdapConfigDto,
ImportUsersDto,
TestConnectionDto,
UpdateLdapConfigDto,
} from './dto/ldap-config.dto';
@@ -166,6 +168,71 @@ export class LdapController {
});
}
/**
* GET /ldap/users/search?q=... - Search AD for individual users by a
* free-text query. Read-only. Each result is flagged `alreadyImported`.
*/
@Get('users/search')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async searchUsers(@Req() req: any, @Query('q') q: string) {
const tenantId = req.tenantId;
if (!tenantId) {
throw new BadRequestException('No tenant context');
}
const config = await this.ldapConfigService.getConfig(tenantId);
if (!config) {
throw new NotFoundException('No LDAP config found for this tenant');
}
return this.ldapService.searchUsers(
{
serverUrl: config.serverUrl,
baseDn: config.baseDn,
bindDn: config.bindDn,
bindPassword: config.bindPassword,
},
tenantId,
q ?? '',
);
}
/**
* POST /ldap/users/import - Import specific AD users by DN (from the user
* search). Idempotent and non-deactivating: existing users are skipped, so
* a later department/group sync never creates a duplicate.
*/
@Post('users/import')
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
async importUsers(@Req() req: any, @Body() dto: ImportUsersDto) {
const tenantId = req.tenantId;
if (!tenantId) {
throw new BadRequestException('No tenant context');
}
const config = await this.ldapConfigService.getConfig(tenantId);
if (!config) {
throw new NotFoundException('No LDAP config found for this tenant');
}
return this.ldapService.importUsersByDn(
{
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,
},
tenantId,
dto.dns,
);
}
/**
* POST /ldap/sync - Trigger manual sync (D-14 "LDAP synchronisieren" button).
* Returns sync results with created/updated/deactivated counts.