feat(16-01): tracer — select and import AD groups end-to-end
Task 1 checkpoint resolved: approve-both, granted 2026-08-06 by the
project owner (D-04 one-way schema extension: Group.internalName +
Group.ldapObjectGuid, both nullable, one versioned migration).
Adds the Phase 16 tracer slice through every layer:
- Prisma schema: Group.internalName, Group.ldapObjectGuid,
@@unique([tenantId, ldapObjectGuid]) (Prisma client regenerated;
the versioned migration itself is Task 3, separately blocking).
- LdapService: listGroups() now reads objectGUID via
explicitBufferAttributes and flags alreadyImported per tenant;
new importGroupsByDn() creates a Group per checked DN with
name/ldapDn/ldapObjectGuid, reject-with-report on name collision
(P2002 on name -> nameCollisions, P2002 on ldapObjectGuid ->
skipped), never aborts the batch on one DN's error; new static
escapeLdapFilterBuffer() for Plan 16-03's later existence sweep.
- DTO/controller: ImportGroupsDto, POST /ldap/groups/import
(ADMIN/SUPER_ADMIN), listGroups route now tenant-scoped.
- Frontend: new "AD-Gruppen importieren" section in /admin/ldap,
own discovery/import handlers with a visible error state
(Owner decision 2026-08-06 — no silent catch{} for these two
handlers), i18n keys in de.json/en.json.
- Tests: 8 new cases covering the full <behavior> list plus
listGroups sort order and alreadyImported.
Flagged assumption (RESEARCH.md A1/A2): objectGUID rename-stability
and the binary filter syntax are unverified against a real AD —
this plan only WRITES the GUID, Plan 16-03 reads it back live.
This commit is contained in:
@@ -16,6 +16,7 @@ import { Roles } from '../auth/decorators/roles.decorator';
|
||||
import {
|
||||
CreateFieldMappingDto,
|
||||
CreateLdapConfigDto,
|
||||
ImportGroupsDto,
|
||||
ImportUsersDto,
|
||||
TestConnectionDto,
|
||||
UpdateLdapConfigDto,
|
||||
@@ -168,13 +169,54 @@ export class LdapController {
|
||||
throw new NotFoundException('No LDAP config found for this tenant');
|
||||
}
|
||||
|
||||
return this.ldapService.listGroups({
|
||||
serverUrl: config.serverUrl,
|
||||
baseDn: config.baseDn,
|
||||
bindDn: config.bindDn,
|
||||
bindPassword: config.bindPassword,
|
||||
tlsRejectUnauthorized: config.tlsRejectUnauthorized,
|
||||
});
|
||||
return this.ldapService.listGroups(
|
||||
{
|
||||
serverUrl: config.serverUrl,
|
||||
baseDn: config.baseDn,
|
||||
bindDn: config.bindDn,
|
||||
bindPassword: config.bindPassword,
|
||||
tlsRejectUnauthorized: config.tlsRejectUnauthorized,
|
||||
},
|
||||
tenantId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /ldap/groups/import - Import specific AD groups by DN (from the
|
||||
* group discovery list, filtered to type: 'group') as Tessera groups
|
||||
* (SC-1/SC-2, D-01/D-02). Static route, placed before any future dynamic
|
||||
* `:id`-style route on this controller (project route-order convention).
|
||||
*/
|
||||
@Post('groups/import')
|
||||
@Roles(Role.ADMIN, Role.SUPER_ADMIN)
|
||||
async importGroups(@Req() req: any, @Body() dto: ImportGroupsDto) {
|
||||
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.importGroupsByDn(
|
||||
{
|
||||
id: config.id,
|
||||
tenantId: config.tenantId,
|
||||
serverUrl: config.serverUrl,
|
||||
baseDn: config.baseDn,
|
||||
bindDn: config.bindDn,
|
||||
bindPassword: config.bindPassword,
|
||||
tlsRejectUnauthorized: config.tlsRejectUnauthorized,
|
||||
searchFilter: config.searchFilter,
|
||||
groupFilterDns: config.groupFilterDns,
|
||||
userExcludeList: config.userExcludeList,
|
||||
fieldMappings: config.fieldMappings,
|
||||
},
|
||||
tenantId,
|
||||
dto.dns,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user