feat(ldap): opt-in skip TLS verification for ldaps (internal CA)
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 49s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m45s

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:
2026-07-22 14:18:55 +02:00
parent 38face43b4
commit af9e968c6f
10 changed files with 144 additions and 7 deletions
+43
View File
@@ -20,6 +20,7 @@ vi.mock('../prisma/prisma-tenant.extension', () => ({
forTenant: vi.fn((p: unknown) => p),
}));
import { Client } from 'ldapts';
import { LdapService } from './ldap.service';
describe('LdapService.syncUsersForTenant — per-user exclude list', () => {
@@ -265,3 +266,45 @@ describe('LdapService — individual user search & import (dedup)', () => {
expect(userService.create).not.toHaveBeenCalled();
});
});
describe('LdapService.testConnection — TLS verification opt-out (ldaps)', () => {
let service: LdapService;
beforeEach(() => {
vi.clearAllMocks();
mockBind.mockResolvedValue(undefined);
mockUnbind.mockResolvedValue(undefined);
service = new LdapService({} as any, {} as any);
});
it('passes tlsOptions.rejectUnauthorized=false for ldaps when opted out', async () => {
await service.testConnection({
serverUrl: 'ldaps://ad:636',
tlsRejectUnauthorized: false,
});
expect(Client).toHaveBeenCalledWith(
expect.objectContaining({
url: 'ldaps://ad:636',
tlsOptions: { rejectUnauthorized: false },
}),
);
});
it('keeps verification on for ldaps when tlsRejectUnauthorized is true', async () => {
await service.testConnection({
serverUrl: 'ldaps://ad:636',
tlsRejectUnauthorized: true,
});
const opts = (Client as any).mock.calls.at(-1)[0];
expect(opts.tlsOptions).toBeUndefined();
});
it('ignores the flag for plain ldap:// (no TLS)', async () => {
await service.testConnection({
serverUrl: 'ldap://ad:389',
tlsRejectUnauthorized: false,
});
const opts = (Client as any).mock.calls.at(-1)[0];
expect(opts.tlsOptions).toBeUndefined();
});
});