feat(ldap): opt-in skip TLS verification for ldaps (internal CA)
Add a per-tenant "Skip TLS certificate verification" toggle to the LDAP admin page so admins can connect to an AD whose ldaps:// certificate is signed by an internal/self-signed CA (Node error: "unable to verify the first certificate"). When enabled, ldapts is given tlsOptions.rejectUnauthorized=false; the flag is ignored for plain ldap:// (no TLS). Defaults to full verification. New Boolean column LdapConfig.tlsRejectUnauthorized (@default(true)) + migration; wired through DTOs, config service, all Client creations (test/groups/user-search/import/sync) and the test-connection endpoint. UI checkbox with an insecure-network warning (de/en). 3 new service specs; API 218 green, web 131 green, both apps tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,9 @@ interface LdapConfigData {
|
||||
bindDn?: string | null;
|
||||
bindPassword?: string | null;
|
||||
searchFilter: string;
|
||||
// When false, skip TLS certificate verification for ldaps:// (internal CA
|
||||
// / self-signed AD certs). Ignored for plain ldap://. Default true.
|
||||
tlsRejectUnauthorized?: boolean | null;
|
||||
groupFilterDns: string[];
|
||||
userExcludeList: string[];
|
||||
fieldMappings: Array<{
|
||||
@@ -97,6 +100,27 @@ export class LdapService {
|
||||
await client.bind(bindDn || '', bindPassword || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ldapts Client options. For ldaps:// connections, honor an opt-in
|
||||
* "skip TLS verification" flag (tlsRejectUnauthorized === false) so admins
|
||||
* can connect to an AD whose certificate is signed by an internal/private
|
||||
* CA that Node doesn't trust ("unable to verify the first certificate").
|
||||
* Ignored for plain ldap:// (no TLS). Default is full verification.
|
||||
*/
|
||||
private buildClientOptions(
|
||||
serverUrl: string,
|
||||
tlsRejectUnauthorized?: boolean | null,
|
||||
): ConstructorParameters<typeof Client>[0] {
|
||||
const options: ConstructorParameters<typeof Client>[0] = { url: serverUrl };
|
||||
if (
|
||||
serverUrl.toLowerCase().startsWith('ldaps') &&
|
||||
tlsRejectUnauthorized === false
|
||||
) {
|
||||
options.tlsOptions = { rejectUnauthorized: false };
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test LDAP connection with given configuration.
|
||||
* Returns success/failure with optional error message.
|
||||
@@ -105,8 +129,11 @@ export class LdapService {
|
||||
serverUrl: string;
|
||||
bindDn?: string | null;
|
||||
bindPassword?: string | null;
|
||||
tlsRejectUnauthorized?: boolean | null;
|
||||
}): Promise<{ success: boolean; error?: string }> {
|
||||
const client = new Client({ url: config.serverUrl });
|
||||
const client = new Client(
|
||||
this.buildClientOptions(config.serverUrl, config.tlsRejectUnauthorized),
|
||||
);
|
||||
|
||||
try {
|
||||
await this.bind(client, config.bindDn, config.bindPassword);
|
||||
@@ -135,8 +162,11 @@ export class LdapService {
|
||||
baseDn: string;
|
||||
bindDn?: string | null;
|
||||
bindPassword?: string | null;
|
||||
tlsRejectUnauthorized?: boolean | null;
|
||||
}): Promise<LdapDirectoryEntry[]> {
|
||||
const client = new Client({ url: config.serverUrl });
|
||||
const client = new Client(
|
||||
this.buildClientOptions(config.serverUrl, config.tlsRejectUnauthorized),
|
||||
);
|
||||
|
||||
try {
|
||||
await this.bind(client, config.bindDn, config.bindPassword);
|
||||
@@ -263,6 +293,7 @@ export class LdapService {
|
||||
baseDn: string;
|
||||
bindDn?: string | null;
|
||||
bindPassword?: string | null;
|
||||
tlsRejectUnauthorized?: boolean | null;
|
||||
},
|
||||
tenantId: string,
|
||||
query: string,
|
||||
@@ -272,7 +303,9 @@ export class LdapService {
|
||||
return [];
|
||||
}
|
||||
|
||||
const client = new Client({ url: config.serverUrl });
|
||||
const client = new Client(
|
||||
this.buildClientOptions(config.serverUrl, config.tlsRejectUnauthorized),
|
||||
);
|
||||
const first = (v: unknown): string =>
|
||||
Array.isArray(v) ? String(v[0] ?? '') : v != null ? String(v) : '';
|
||||
|
||||
@@ -350,7 +383,9 @@ export class LdapService {
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const client = new Client({ url: config.serverUrl });
|
||||
const client = new Client(
|
||||
this.buildClientOptions(config.serverUrl, config.tlsRejectUnauthorized),
|
||||
);
|
||||
const excludeSet = new Set(
|
||||
(config.userExcludeList ?? [])
|
||||
.map((u) => u.trim().toLowerCase())
|
||||
@@ -464,7 +499,9 @@ export class LdapService {
|
||||
errors: [],
|
||||
};
|
||||
|
||||
const client = new Client({ url: config.serverUrl });
|
||||
const client = new Client(
|
||||
this.buildClientOptions(config.serverUrl, config.tlsRejectUnauthorized),
|
||||
);
|
||||
|
||||
// Create tenant-scoped Prisma client per Pitfall 2
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId) as any;
|
||||
|
||||
Reference in New Issue
Block a user