refactor: update LDAP password rotation functions to use executeWithPotentialGateway for connection handling and improve error management

This commit is contained in:
Victor Santos
2025-11-06 20:17:18 -03:00
parent e2e0c374b0
commit 8fd516d78e
3 changed files with 136 additions and 188 deletions
@@ -10,7 +10,7 @@ import {
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { DistinguishedNameRegex } from "@app/lib/regex"; import { DistinguishedNameRegex } from "@app/lib/regex";
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns"; import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
import { getLdapConnectionClient, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap"; import { executeWithPotentialGateway, LdapProvider, TLdapConnection } from "@app/services/app-connection/ldap";
import { generatePassword } from "../shared/utils"; import { generatePassword } from "../shared/utils";
import { import {
@@ -71,17 +71,19 @@ export const ldapPasswordRotationFactory: TRotationFactory<
TLdapPasswordRotationWithConnection, TLdapPasswordRotationWithConnection,
TLdapPasswordRotationGeneratedCredentials, TLdapPasswordRotationGeneratedCredentials,
TLdapPasswordRotationInput["temporaryParameters"] TLdapPasswordRotationInput["temporaryParameters"]
> = (secretRotation, appConnectionDAL, kmsService) => { > = (secretRotation, appConnectionDAL, kmsService, gatewayService, gatewayV2Service) => {
const { connection, parameters, secretsMapping, activeIndex } = secretRotation; const { connection, parameters, secretsMapping, activeIndex } = secretRotation;
const { dn, passwordRequirements } = parameters; const { dn, passwordRequirements } = parameters;
const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => { const $verifyCredentials = async (credentials: Pick<TLdapConnection["credentials"], "dn" | "password">) => {
try { try {
const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials }); await executeWithPotentialGateway(
{ ...connection, credentials: { ...connection.credentials, ...credentials } },
client.unbind(); gatewayService,
client.destroy(); gatewayV2Service,
async () => {}
);
} catch (error) { } catch (error) {
throw new Error(`Failed to verify credentials - ${(error as Error).message}`); throw new Error(`Failed to verify credentials - ${(error as Error).message}`);
} }
@@ -92,17 +94,7 @@ export const ldapPasswordRotationFactory: TRotationFactory<
if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection"); if (!credentials.url.startsWith("ldaps")) throw new Error("Password Rotation requires an LDAPS connection");
const client = await getLdapConnectionClient(
currentPassword
? {
...credentials,
password: currentPassword,
dn
}
: credentials
);
const isConnectionRotation = credentials.dn === dn; const isConnectionRotation = credentials.dn === dn;
const password = generatePassword(passwordRequirements); const password = generatePassword(passwordRequirements);
let changes: ldap.Change[] | ldap.Change; let changes: ldap.Change[] | ldap.Change;
@@ -147,22 +139,33 @@ export const ldapPasswordRotationFactory: TRotationFactory<
throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`); throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`);
} }
try { await executeWithPotentialGateway(
const userDn = await getDN(dn, client); {
await new Promise((resolve, reject) => { ...connection,
client.modify(userDn, changes, (err) => { credentials: currentPassword
if (err) { ? {
logger.error(err, "LDAP Password Rotation Failed"); ...credentials,
reject(new Error(`Provider Modify Error: ${err.message}`)); password: currentPassword,
} else { dn
resolve(true); }
} : credentials
},
gatewayService,
gatewayV2Service,
async (client) => {
const userDn = await getDN(dn, client);
await new Promise<void>((resolve, reject) => {
client.modify(userDn, changes, (err) => {
if (err) {
logger.error(err, "LDAP Password Rotation Failed");
reject(new Error(`Provider Modify Error: ${err.message}`));
} else {
resolve();
}
});
}); });
}); }
} finally { );
client.unbind();
client.destroy();
}
await $verifyCredentials({ dn, password }); await $verifyCredentials({ dn, password });
+2 -2
View File
@@ -8,10 +8,10 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "../errors"; import { BadRequestError } from "../errors";
import { isPrivateIp } from "../ip/ipRange"; import { isPrivateIp } from "../ip/ipRange";
export const blockLocalAndPrivateIpAddresses = async (url: string) => { export const blockLocalAndPrivateIpAddresses = async (url: string, isGateway = false) => {
const appCfg = getConfig(); const appCfg = getConfig();
if (appCfg.isDevelopmentMode) return; if (appCfg.isDevelopmentMode || isGateway) return;
const validUrl = new URL(url); const validUrl = new URL(url);
@@ -2,6 +2,7 @@ import ldap from "ldapjs";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
@@ -12,6 +13,8 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
import { LdapConnectionMethod } from "./ldap-connection-enums"; import { LdapConnectionMethod } from "./ldap-connection-enums";
import { TLdapConnectionConfig } from "./ldap-connection-types"; import { TLdapConnectionConfig } from "./ldap-connection-types";
const LDAP_TIMEOUT = 15_000;
const parseLdapUrl = (url: string): { protocol: string; host: string; port: number } => { const parseLdapUrl = (url: string): { protocol: string; host: string; port: number } => {
const urlObj = new URL(url); const urlObj = new URL(url);
const isSSL = urlObj.protocol === "ldaps:"; const isSSL = urlObj.protocol === "ldaps:";
@@ -28,6 +31,48 @@ const constructLdapUrl = (protocol: string, host: string, port: number): string
return `${protocol}://${host}:${port}`; return `${protocol}://${host}:${port}`;
}; };
const setupLdapClientHandlers = <T>(
client: ldap.Client,
dn: string,
password: string,
onSuccess: (client: ldap.Client) => T | Promise<T>
): Promise<T> => {
return new Promise<T>((resolve, reject) => {
const handleError = (errorType: string, err: Error) => {
logger.error(err, errorType);
client.destroy();
reject(new Error(`${errorType.replace("LDAP ", "")} - ${err.message}`));
};
client.on("error", (err: Error) => handleError("LDAP Error", err));
client.on("connectError", (err: Error) => handleError("LDAP Connection Error", err));
client.on("connectRefused", (err: Error) => handleError("LDAP Connection Refused", err));
client.on("connectTimeout", (err: Error) => handleError("LDAP Connection Timeout", err));
client.on("connect", () => {
client.bind(dn, password, (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
client.destroy();
reject(new Error(`Bind Error: ${err.message}`));
return;
}
try {
const result = onSuccess(client);
if (result instanceof Promise) {
result.then((value) => resolve(value)).catch(reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
});
});
});
};
export const getLdapConnectionListItem = () => { export const getLdapConnectionListItem = () => {
return { return {
name: "LDAP" as const, name: "LDAP" as const,
@@ -36,8 +81,6 @@ export const getLdapConnectionListItem = () => {
}; };
}; };
const LDAP_TIMEOUT = 15_000;
export const getLdapConnectionClient = async ({ export const getLdapConnectionClient = async ({
url, url,
dn, dn,
@@ -45,59 +88,23 @@ export const getLdapConnectionClient = async ({
sslCertificate, sslCertificate,
sslRejectUnauthorized = true sslRejectUnauthorized = true
}: TLdapConnectionConfig["credentials"]) => { }: TLdapConnectionConfig["credentials"]) => {
await blockLocalAndPrivateIpAddresses(url); await blockLocalAndPrivateIpAddresses(url, false);
const isSSL = url.startsWith("ldaps"); const isSSL = url.startsWith("ldaps");
return new Promise<ldap.Client>((resolve, reject) => { const client = ldap.createClient({
const client = ldap.createClient({ url,
url, timeout: LDAP_TIMEOUT,
timeout: LDAP_TIMEOUT, connectTimeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT, tlsOptions: isSSL
tlsOptions: isSSL ? {
? { rejectUnauthorized: sslRejectUnauthorized,
rejectUnauthorized: sslRejectUnauthorized, ca: sslCertificate ? [sslCertificate] : undefined
ca: sslCertificate ? [sslCertificate] : undefined
}
: undefined
});
client.on("error", (err: Error) => {
logger.error(err, "LDAP Error");
client.destroy();
reject(new Error(`Provider Error - ${err.message}`));
});
client.on("connectError", (err: Error) => {
logger.error(err, "LDAP Connection Error");
client.destroy();
reject(new Error(`Provider Connect Error - ${err.message}`));
});
client.on("connectRefused", (err: Error) => {
logger.error(err, "LDAP Connection Refused");
client.destroy();
reject(new Error(`Provider Connection Refused - ${err.message}`));
});
client.on("connectTimeout", (err: Error) => {
logger.error(err, "LDAP Connection Timeout");
client.destroy();
reject(new Error(`Provider Connection Timeout - ${err.message}`));
});
client.on("connect", () => {
client.bind(dn, password, (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
reject(new Error(`Bind Error: ${err.message}`));
client.destroy();
} }
: undefined
resolve(client);
});
});
}); });
return setupLdapClientHandlers<ldap.Client>(client, dn, password, (ldapClient) => ldapClient);
}; };
export const executeWithPotentialGateway = async <T>( export const executeWithPotentialGateway = async <T>(
@@ -108,8 +115,10 @@ export const executeWithPotentialGateway = async <T>(
): Promise<T> => { ): Promise<T> => {
const { gatewayId, credentials } = config; const { gatewayId, credentials } = config;
const { protocol, host, port } = parseLdapUrl(credentials.url); const { protocol, host, port } = parseLdapUrl(credentials.url);
const appCfg = getConfig();
if (gatewayId && gatewayService && gatewayV2Service) { if (gatewayId && gatewayService && gatewayV2Service) {
await blockLocalAndPrivateIpAddresses(credentials.url, true);
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
gatewayId, gatewayId,
targetHost: host, targetHost: host,
@@ -121,62 +130,28 @@ export const executeWithPotentialGateway = async <T>(
async (proxyPort) => { async (proxyPort) => {
const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort); const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort);
const isSSL = protocol === "ldaps"; const isSSL = protocol === "ldaps";
const client = ldap.createClient({ const client = ldap.createClient({
url: proxyUrl, url: proxyUrl,
timeout: LDAP_TIMEOUT, timeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT, connectTimeout: LDAP_TIMEOUT,
tlsOptions: isSSL tlsOptions: isSSL
? { ? {
rejectUnauthorized: sslRejectUnauthorized, rejectUnauthorized: config.credentials.sslRejectUnauthorized,
ca: sslCertificate ? [sslCertificate] : undefined ca: config.credentials.sslCertificate ? [config.credentials.sslCertificate] : undefined,
servername: host,
// bypass hostname verification for development
...(appCfg.isDevelopmentMode ? { checkServerIdentity: () => undefined } : {})
} }
: undefined : undefined
}); });
return new Promise<T>((resolve, reject) => { return setupLdapClientHandlers<T>(client, credentials.dn, credentials.password, async (ldapClient) => {
client.on("error", (err: Error) => { try {
logger.error(err, "LDAP Error"); return await operation(ldapClient);
client.destroy(); } finally {
reject(new Error(`Provider Error - ${err.message}`)); ldapClient.destroy();
}); }
client.on("connectError", (err: Error) => {
logger.error(err, "LDAP Connection Error");
client.destroy();
reject(new Error(`Provider Connect Error - ${err.message}`));
});
client.on("connectRefused", (err: Error) => {
logger.error(err, "LDAP Connection Refused");
client.destroy();
reject(new Error(`Provider Connection Refused - ${err.message}`));
});
client.on("connectTimeout", (err: Error) => {
logger.error(err, "LDAP Connection Timeout");
client.destroy();
reject(new Error(`Provider Connection Timeout - ${err.message}`));
});
client.on("connect", () => {
client.bind(credentials.dn, credentials.password, async (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
client.destroy();
reject(new Error(`Bind Error: ${err.message}`));
return;
}
try {
const result = await operation(client);
resolve(result);
} catch (opError) {
reject(opError);
} finally {
client.destroy();
}
});
});
}); });
}, },
{ {
@@ -194,61 +169,28 @@ export const executeWithPotentialGateway = async <T>(
async (proxyPort) => { async (proxyPort) => {
const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort); const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort);
const isSSL = protocol === "ldaps"; const isSSL = protocol === "ldaps";
const client = ldap.createClient({ const client = ldap.createClient({
url: proxyUrl, url: proxyUrl,
timeout: LDAP_TIMEOUT, timeout: LDAP_TIMEOUT,
connectTimeout: LDAP_TIMEOUT, connectTimeout: LDAP_TIMEOUT,
tlsOptions: isSSL tlsOptions: isSSL
? { ? {
rejectUnauthorized: sslRejectUnauthorized, rejectUnauthorized: config.credentials.sslRejectUnauthorized,
ca: sslCertificate ? [sslCertificate] : undefined ca: config.credentials.sslCertificate ? [config.credentials.sslCertificate] : undefined,
servername: host,
// bypass hostname verification for development
...(appCfg.isDevelopmentMode ? { checkServerIdentity: () => undefined } : {})
} }
: undefined : undefined
}); });
return new Promise<T>((resolve, reject) => {
client.on("error", (err: Error) => {
logger.error(err, "LDAP Error");
client.destroy();
reject(new Error(`Provider Error - ${err.message}`));
});
client.on("connectError", (err: Error) => { return setupLdapClientHandlers<T>(client, credentials.dn, credentials.password, async (ldapClient) => {
logger.error(err, "LDAP Connection Error"); try {
client.destroy(); return await operation(ldapClient);
reject(new Error(`Provider Connect Error - ${err.message}`)); } finally {
}); ldapClient.destroy();
}
client.on("connectRefused", (err: Error) => {
logger.error(err, "LDAP Connection Refused");
client.destroy();
reject(new Error(`Provider Connection Refused - ${err.message}`));
});
client.on("connectTimeout", (err: Error) => {
logger.error(err, "LDAP Connection Timeout");
client.destroy();
reject(new Error(`Provider Connection Timeout - ${err.message}`));
});
client.on("connect", () => {
client.bind(credentials.dn, credentials.password, async (err) => {
if (err) {
logger.error(err, "LDAP Bind Error");
client.destroy();
reject(new Error(`Bind Error: ${err.message}`));
return;
}
try {
const result = await operation(client);
resolve(result);
} catch (opError) {
reject(opError);
} finally {
client.destroy();
}
});
});
}); });
}, },
{ {
@@ -277,23 +219,26 @@ export const executeWithPotentialGateway = async <T>(
} }
}; };
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => { export const validateLdapConnectionCredentials = async (
let client: ldap.Client | undefined; config: TLdapConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
) => {
try { try {
client = await getLdapConnectionClient(credentials); await executeWithPotentialGateway(config, gatewayService, gatewayV2Service, async (client) => {
// this shouldn't occur as handle connection error events in client but here as fallback
if (!client.connected) {
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
}
});
// this shouldn't occur as handle connection error events in client but here as fallback return config.credentials;
if (!client.connected) { } catch (error) {
throw new BadRequestError({ message: "Unable to connect to LDAP server" });
}
return credentials;
} catch (e: unknown) {
throw new BadRequestError({ throw new BadRequestError({
message: `Unable to validate connection: ${(e as Error).message || "verify credentials"}` message: `Unable to validate connection: ${
(error as Error)?.message?.replaceAll(config.credentials.password, "********************") ??
"verify credentials"
}`
}); });
} finally {
client?.destroy();
} }
}; };