feat(02-04): LdapModule with sync service, config service, scheduler, and controller
- LdapService uses ldapts for DIRECTORY SYNC ONLY (anti-pattern avoidance) - LdapConfigService creates default field mappings per D-16 (displayName, mail, sAMAccountName) - Custom field mappings can be added/removed per D-17 - Per-tenant LDAP config per D-18 - syncUsersForTenant deactivates users removed from LDAP per D-15 - LdapSyncScheduler sets tenant context explicitly per Pitfall 2 - Manual sync endpoint POST /ldap/sync per D-14 - Auto-sync cron checks syncIntervalMin per D-14 - Test connection endpoint for LDAP config validation - OpenLDAP + phpLDAPadmin added to docker-compose.dev.yml - LDAP search filter sanitization per T-02-16 - bindPassword never returned in API responses per T-02-17
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
CreateFieldMappingDto,
|
||||
CreateLdapConfigDto,
|
||||
UpdateLdapConfigDto,
|
||||
} from './dto/ldap-config.dto';
|
||||
|
||||
/**
|
||||
* Per-tenant LDAP configuration CRUD (D-18).
|
||||
* Manages LDAP connection settings and field mappings.
|
||||
*/
|
||||
@Injectable()
|
||||
export class LdapConfigService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Get LDAP config for a tenant, including field mappings.
|
||||
*/
|
||||
async getConfig(tenantId: string) {
|
||||
return this.prisma.ldapConfig.findUnique({
|
||||
where: { tenantId },
|
||||
include: { fieldMappings: true },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create LDAP config for a tenant with default field mappings (D-16).
|
||||
* Defaults: displayName -> displayName, mail -> email, sAMAccountName -> username
|
||||
*/
|
||||
async createConfig(tenantId: string, dto: CreateLdapConfigDto) {
|
||||
return this.prisma.ldapConfig.create({
|
||||
data: {
|
||||
tenantId,
|
||||
serverUrl: dto.serverUrl,
|
||||
baseDn: dto.baseDn,
|
||||
bindDn: dto.bindDn,
|
||||
bindPassword: dto.bindPassword,
|
||||
searchFilter: dto.searchFilter ?? '(objectClass=person)',
|
||||
syncIntervalMin: dto.syncIntervalMin ?? 60,
|
||||
isActive: dto.isActive ?? true,
|
||||
fieldMappings: {
|
||||
create: [
|
||||
{
|
||||
ldapField: 'displayName',
|
||||
tesseraField: 'displayName',
|
||||
isDefault: true,
|
||||
},
|
||||
{ ldapField: 'mail', tesseraField: 'email', isDefault: true },
|
||||
{
|
||||
ldapField: 'sAMAccountName',
|
||||
tesseraField: 'username',
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: { fieldMappings: true },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update LDAP config for a tenant.
|
||||
*/
|
||||
async updateConfig(tenantId: string, dto: UpdateLdapConfigDto) {
|
||||
return this.prisma.ldapConfig.update({
|
||||
where: { tenantId },
|
||||
data: {
|
||||
...(dto.serverUrl !== undefined && { serverUrl: dto.serverUrl }),
|
||||
...(dto.baseDn !== undefined && { baseDn: dto.baseDn }),
|
||||
...(dto.bindDn !== undefined && { bindDn: dto.bindDn }),
|
||||
...(dto.bindPassword !== undefined && {
|
||||
bindPassword: dto.bindPassword,
|
||||
}),
|
||||
...(dto.searchFilter !== undefined && {
|
||||
searchFilter: dto.searchFilter,
|
||||
}),
|
||||
...(dto.syncIntervalMin !== undefined && {
|
||||
syncIntervalMin: dto.syncIntervalMin,
|
||||
}),
|
||||
...(dto.isActive !== undefined && { isActive: dto.isActive }),
|
||||
},
|
||||
include: { fieldMappings: true },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom field mapping to an LDAP config (D-17).
|
||||
*/
|
||||
async addFieldMapping(configId: string, dto: CreateFieldMappingDto) {
|
||||
return this.prisma.ldapFieldMapping.create({
|
||||
data: {
|
||||
ldapConfigId: configId,
|
||||
ldapField: dto.ldapField,
|
||||
tesseraField: dto.tesseraField,
|
||||
isDefault: dto.isDefault ?? false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a field mapping. Only non-default mappings can be deleted.
|
||||
* System-provided defaults (isDefault=true) are protected.
|
||||
*/
|
||||
async removeFieldMapping(mappingId: string) {
|
||||
const mapping = await this.prisma.ldapFieldMapping.findUnique({
|
||||
where: { id: mappingId },
|
||||
});
|
||||
|
||||
if (!mapping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mapping.isDefault) {
|
||||
throw new Error('Cannot delete default field mappings');
|
||||
}
|
||||
|
||||
return this.prisma.ldapFieldMapping.delete({
|
||||
where: { id: mappingId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active LDAP configs. Used by the scheduler to determine which
|
||||
* tenants need auto-sync.
|
||||
*/
|
||||
async getAllActiveConfigs() {
|
||||
return this.prisma.ldapConfig.findMany({
|
||||
where: { isActive: true },
|
||||
include: { tenant: true, fieldMappings: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user