From e2e0c374b0a0eeb027de16115e20c3525c4ea35e Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Wed, 5 Nov 2025 21:03:22 -0300 Subject: [PATCH 1/5] initial configuration for tests --- .../ldap/ldap-connection-fns.ts | 197 ++++++++++++++++++ .../ldap/ldap-connection-schemas.ts | 4 +- .../ldap/ldap-connection-types.ts | 5 +- .../AppConnectionForm/LdapConnectionForm.tsx | 59 ++++++ 4 files changed, 263 insertions(+), 2 deletions(-) diff --git a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts index 03005c7d70..f7eb2189d3 100644 --- a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts +++ b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts @@ -1,6 +1,10 @@ import ldap from "ldapjs"; +import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; +import { TGatewayV2ServiceFactory } from "@app/ee/services/gateway-v2/gateway-v2-service"; import { BadRequestError } from "@app/lib/errors"; +import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { logger } from "@app/lib/logger"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { AppConnection } from "@app/services/app-connection/app-connection-enums"; @@ -8,6 +12,22 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums import { LdapConnectionMethod } from "./ldap-connection-enums"; import { TLdapConnectionConfig } from "./ldap-connection-types"; +const parseLdapUrl = (url: string): { protocol: string; host: string; port: number } => { + const urlObj = new URL(url); + const isSSL = urlObj.protocol === "ldaps:"; + const defaultPort = isSSL ? 636 : 389; + + return { + protocol: urlObj.protocol.replace(":", ""), + host: urlObj.hostname, + port: urlObj.port ? parseInt(urlObj.port, 10) : defaultPort + }; +}; + +const constructLdapUrl = (protocol: string, host: string, port: number): string => { + return `${protocol}://${host}:${port}`; +}; + export const getLdapConnectionListItem = () => { return { name: "LDAP" as const, @@ -80,6 +100,183 @@ export const getLdapConnectionClient = async ({ }); }; +export const executeWithPotentialGateway = async ( + config: TLdapConnectionConfig, + gatewayService: Pick, + gatewayV2Service: Pick, + operation: (client: ldap.Client) => Promise +): Promise => { + const { gatewayId, credentials } = config; + const { protocol, host, port } = parseLdapUrl(credentials.url); + + if (gatewayId && gatewayService && gatewayV2Service) { + const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ + gatewayId, + targetHost: host, + targetPort: port + }); + + if (platformConnectionDetails) { + return withGatewayV2Proxy( + async (proxyPort) => { + const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort); + const isSSL = protocol === "ldaps"; + const client = ldap.createClient({ + url: proxyUrl, + timeout: LDAP_TIMEOUT, + connectTimeout: LDAP_TIMEOUT, + tlsOptions: isSSL + ? { + rejectUnauthorized: sslRejectUnauthorized, + ca: sslCertificate ? [sslCertificate] : undefined + } + : undefined + }); + + return new Promise((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) => { + 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(); + } + }); + }); + }); + }, + { + protocol: GatewayProxyProtocol.Tcp, + relayHost: platformConnectionDetails.relayHost, + gateway: platformConnectionDetails.gateway, + relay: platformConnectionDetails.relay + } + ); + } + + const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); + const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); + return withGatewayProxy( + async (proxyPort) => { + const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort); + const isSSL = protocol === "ldaps"; + const client = ldap.createClient({ + url: proxyUrl, + timeout: LDAP_TIMEOUT, + connectTimeout: LDAP_TIMEOUT, + tlsOptions: isSSL + ? { + rejectUnauthorized: sslRejectUnauthorized, + ca: sslCertificate ? [sslCertificate] : undefined + } + : undefined + }); + return new Promise((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) => { + 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(); + } + }); + }); + }); + }, + { + protocol: GatewayProxyProtocol.Tcp, + targetHost: host, + targetPort: port, + relayHost, + relayPort: Number(relayPort), + identityId: relayDetails.identityId, + orgId: relayDetails.orgId, + tlsOptions: { + ca: relayDetails.certChain, + cert: relayDetails.certificate, + key: relayDetails.privateKey.toString() + } + } + ); + } + + // Non-gateway path - calls getLdapConnectionClient which has validation + const client = await getLdapConnectionClient(credentials); + try { + return await operation(client); + } finally { + client.destroy(); + } +}; + export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => { let client: ldap.Client | undefined; diff --git a/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts index 134b9667bd..7fc56d9d18 100644 --- a/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts +++ b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts @@ -74,7 +74,9 @@ export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("met ]); export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and( - GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP) + GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP, { + supportsGateways: true + }) ); export const UpdateLdapConnectionSchema = z diff --git a/backend/src/services/app-connection/ldap/ldap-connection-types.ts b/backend/src/services/app-connection/ldap/ldap-connection-types.ts index ee69b2542e..581fca5604 100644 --- a/backend/src/services/app-connection/ldap/ldap-connection-types.ts +++ b/backend/src/services/app-connection/ldap/ldap-connection-types.ts @@ -17,6 +17,9 @@ export type TLdapConnectionInput = z.infer & export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema; -export type TLdapConnectionConfig = DiscriminativePick & { +export type TLdapConnectionConfig = DiscriminativePick< + TLdapConnectionInput, + "method" | "app" | "credentials" | "gatewayId" +> & { orgId: string; }; diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx index 21e376c2e3..d786a7d758 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/LdapConnectionForm.tsx @@ -4,8 +4,10 @@ import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tab } from "@headlessui/react"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { z } from "zod"; +import { OrgPermissionCan } from "@app/components/permissions"; import { Button, FormControl, @@ -18,8 +20,11 @@ import { TextArea, Tooltip } from "@app/components/v2"; +import { OrgPermissionSubjects, useSubscription } from "@app/context"; +import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types"; import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/helpers/string"; +import { gatewaysQueryKeys } from "@app/hooks/api"; import { LdapConnectionMethod, LdapConnectionProvider, @@ -84,6 +89,7 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => { defaultValues: appConnection ?? { app: AppConnection.LDAP, method: LdapConnectionMethod.SimpleBind, + gatewayId: null, credentials: { provider: LdapConnectionProvider.ActiveDirectory, url: "", @@ -104,6 +110,8 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => { const selectedProvider = watch("credentials.provider"); const sslEnabled = watch("credentials.url")?.startsWith("ldaps://") ?? false; + const { subscription } = useSubscription(); + const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list()); return ( @@ -114,6 +122,57 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => { }} > {!isUpdate && } + {subscription.gateway && ( + + {(isAllowed) => ( + ( + + +
+ +
+
+
+ )} + /> + )} +
+ )}
Date: Thu, 6 Nov 2025 20:17:18 -0300 Subject: [PATCH 2/5] refactor: update LDAP password rotation functions to use executeWithPotentialGateway for connection handling and improve error management --- .../ldap-password-rotation-fns.ts | 65 ++--- backend/src/lib/validator/validate-url.ts | 4 +- .../ldap/ldap-connection-fns.ts | 259 +++++++----------- 3 files changed, 138 insertions(+), 190 deletions(-) diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts index 9b1fd14a02..7bcdb2818b 100644 --- a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts @@ -10,7 +10,7 @@ import { import { logger } from "@app/lib/logger"; import { DistinguishedNameRegex } from "@app/lib/regex"; 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 { @@ -71,17 +71,19 @@ export const ldapPasswordRotationFactory: TRotationFactory< TLdapPasswordRotationWithConnection, TLdapPasswordRotationGeneratedCredentials, TLdapPasswordRotationInput["temporaryParameters"] -> = (secretRotation, appConnectionDAL, kmsService) => { +> = (secretRotation, appConnectionDAL, kmsService, gatewayService, gatewayV2Service) => { const { connection, parameters, secretsMapping, activeIndex } = secretRotation; const { dn, passwordRequirements } = parameters; const $verifyCredentials = async (credentials: Pick) => { try { - const client = await getLdapConnectionClient({ ...connection.credentials, ...credentials }); - - client.unbind(); - client.destroy(); + await executeWithPotentialGateway( + { ...connection, credentials: { ...connection.credentials, ...credentials } }, + gatewayService, + gatewayV2Service, + async () => {} + ); } catch (error) { 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"); - const client = await getLdapConnectionClient( - currentPassword - ? { - ...credentials, - password: currentPassword, - dn - } - : credentials - ); const isConnectionRotation = credentials.dn === dn; - const password = generatePassword(passwordRequirements); let changes: ldap.Change[] | ldap.Change; @@ -147,22 +139,33 @@ export const ldapPasswordRotationFactory: TRotationFactory< throw new Error(`Unhandled provider: ${credentials.provider as LdapProvider}`); } - try { - const userDn = await getDN(dn, client); - await new Promise((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(true); - } + await executeWithPotentialGateway( + { + ...connection, + credentials: currentPassword + ? { + ...credentials, + password: currentPassword, + dn + } + : credentials + }, + gatewayService, + gatewayV2Service, + async (client) => { + const userDn = await getDN(dn, client); + await new Promise((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 }); diff --git a/backend/src/lib/validator/validate-url.ts b/backend/src/lib/validator/validate-url.ts index a4c07b37dc..7b2b09209a 100644 --- a/backend/src/lib/validator/validate-url.ts +++ b/backend/src/lib/validator/validate-url.ts @@ -8,10 +8,10 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError } from "../errors"; import { isPrivateIp } from "../ip/ipRange"; -export const blockLocalAndPrivateIpAddresses = async (url: string) => { +export const blockLocalAndPrivateIpAddresses = async (url: string, isGateway = false) => { const appCfg = getConfig(); - if (appCfg.isDevelopmentMode) return; + if (appCfg.isDevelopmentMode || isGateway) return; const validUrl = new URL(url); diff --git a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts index f7eb2189d3..36036f4600 100644 --- a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts +++ b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts @@ -2,6 +2,7 @@ import ldap from "ldapjs"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-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 { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; 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 { TLdapConnectionConfig } from "./ldap-connection-types"; +const LDAP_TIMEOUT = 15_000; + const parseLdapUrl = (url: string): { protocol: string; host: string; port: number } => { const urlObj = new URL(url); const isSSL = urlObj.protocol === "ldaps:"; @@ -28,6 +31,48 @@ const constructLdapUrl = (protocol: string, host: string, port: number): string return `${protocol}://${host}:${port}`; }; +const setupLdapClientHandlers = ( + client: ldap.Client, + dn: string, + password: string, + onSuccess: (client: ldap.Client) => T | Promise +): Promise => { + return new Promise((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 = () => { return { name: "LDAP" as const, @@ -36,8 +81,6 @@ export const getLdapConnectionListItem = () => { }; }; -const LDAP_TIMEOUT = 15_000; - export const getLdapConnectionClient = async ({ url, dn, @@ -45,59 +88,23 @@ export const getLdapConnectionClient = async ({ sslCertificate, sslRejectUnauthorized = true }: TLdapConnectionConfig["credentials"]) => { - await blockLocalAndPrivateIpAddresses(url); + await blockLocalAndPrivateIpAddresses(url, false); const isSSL = url.startsWith("ldaps"); - return new Promise((resolve, reject) => { - const client = ldap.createClient({ - url, - timeout: LDAP_TIMEOUT, - connectTimeout: LDAP_TIMEOUT, - tlsOptions: isSSL - ? { - rejectUnauthorized: sslRejectUnauthorized, - 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(); + const client = ldap.createClient({ + url, + timeout: LDAP_TIMEOUT, + connectTimeout: LDAP_TIMEOUT, + tlsOptions: isSSL + ? { + rejectUnauthorized: sslRejectUnauthorized, + ca: sslCertificate ? [sslCertificate] : undefined } - - resolve(client); - }); - }); + : undefined }); + + return setupLdapClientHandlers(client, dn, password, (ldapClient) => ldapClient); }; export const executeWithPotentialGateway = async ( @@ -108,8 +115,10 @@ export const executeWithPotentialGateway = async ( ): Promise => { const { gatewayId, credentials } = config; const { protocol, host, port } = parseLdapUrl(credentials.url); + const appCfg = getConfig(); if (gatewayId && gatewayService && gatewayV2Service) { + await blockLocalAndPrivateIpAddresses(credentials.url, true); const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ gatewayId, targetHost: host, @@ -121,62 +130,28 @@ export const executeWithPotentialGateway = async ( async (proxyPort) => { const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort); const isSSL = protocol === "ldaps"; + const client = ldap.createClient({ url: proxyUrl, timeout: LDAP_TIMEOUT, connectTimeout: LDAP_TIMEOUT, tlsOptions: isSSL ? { - rejectUnauthorized: sslRejectUnauthorized, - ca: sslCertificate ? [sslCertificate] : undefined + rejectUnauthorized: config.credentials.sslRejectUnauthorized, + ca: config.credentials.sslCertificate ? [config.credentials.sslCertificate] : undefined, + servername: host, + // bypass hostname verification for development + ...(appCfg.isDevelopmentMode ? { checkServerIdentity: () => undefined } : {}) } : undefined }); - return new Promise((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) => { - 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(); - } - }); - }); + return setupLdapClientHandlers(client, credentials.dn, credentials.password, async (ldapClient) => { + try { + return await operation(ldapClient); + } finally { + ldapClient.destroy(); + } }); }, { @@ -194,61 +169,28 @@ export const executeWithPotentialGateway = async ( async (proxyPort) => { const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort); const isSSL = protocol === "ldaps"; + const client = ldap.createClient({ url: proxyUrl, timeout: LDAP_TIMEOUT, connectTimeout: LDAP_TIMEOUT, tlsOptions: isSSL ? { - rejectUnauthorized: sslRejectUnauthorized, - ca: sslCertificate ? [sslCertificate] : undefined + rejectUnauthorized: config.credentials.sslRejectUnauthorized, + ca: config.credentials.sslCertificate ? [config.credentials.sslCertificate] : undefined, + servername: host, + // bypass hostname verification for development + ...(appCfg.isDevelopmentMode ? { checkServerIdentity: () => undefined } : {}) } : undefined }); - return new Promise((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) => { - 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(); - } - }); - }); + return setupLdapClientHandlers(client, credentials.dn, credentials.password, async (ldapClient) => { + try { + return await operation(ldapClient); + } finally { + ldapClient.destroy(); + } }); }, { @@ -277,23 +219,26 @@ export const executeWithPotentialGateway = async ( } }; -export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => { - let client: ldap.Client | undefined; - +export const validateLdapConnectionCredentials = async ( + config: TLdapConnectionConfig, + gatewayService: Pick, + gatewayV2Service: Pick +) => { try { - client = await getLdapConnectionClient(credentials); - - // 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" }); - } - - return credentials; - } catch (e: unknown) { - throw new BadRequestError({ - message: `Unable to validate connection: ${(e as Error).message || "verify 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" }); + } + }); + + return config.credentials; + } catch (error) { + throw new BadRequestError({ + message: `Unable to validate connection: ${ + (error as Error)?.message?.replaceAll(config.credentials.password, "********************") ?? + "verify credentials" + }` }); - } finally { - client?.destroy(); } }; From c2e8e7ec37f674d012c90df73bf3e03dc985098a Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Thu, 6 Nov 2025 20:32:28 -0300 Subject: [PATCH 3/5] feat: enhance LDAP connection schema to support gateways in update operations --- .../services/app-connection/ldap/ldap-connection-schemas.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts index 7fc56d9d18..109173ec0c 100644 --- a/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts +++ b/backend/src/services/app-connection/ldap/ldap-connection-schemas.ts @@ -85,7 +85,11 @@ export const UpdateLdapConnectionSchema = z AppConnections.UPDATE(AppConnection.LDAP).credentials ) }) - .and(GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP)); + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.LDAP, { + supportsGateways: true + }) + ); export const LdapConnectionListItemSchema = z.object({ name: z.literal("LDAP"), From ac4231a4083350eb6b0d679c4cabde054bf07a1f Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Wed, 12 Nov 2025 10:13:34 -0300 Subject: [PATCH 4/5] refactor: remove gatewayService dependency from LDAP connection functions --- .../ldap-password-rotation-fns.ts | 2 - .../ldap/ldap-connection-fns.ts | 53 ++----------------- 2 files changed, 3 insertions(+), 52 deletions(-) diff --git a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts index 7bcdb2818b..cc96cf327d 100644 --- a/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/ldap-password/ldap-password-rotation-fns.ts @@ -80,7 +80,6 @@ export const ldapPasswordRotationFactory: TRotationFactory< try { await executeWithPotentialGateway( { ...connection, credentials: { ...connection.credentials, ...credentials } }, - gatewayService, gatewayV2Service, async () => {} ); @@ -150,7 +149,6 @@ export const ldapPasswordRotationFactory: TRotationFactory< } : credentials }, - gatewayService, gatewayV2Service, async (client) => { const userDn = await getDN(dn, client); diff --git a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts index 36036f4600..eb11e8459d 100644 --- a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts +++ b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts @@ -4,7 +4,7 @@ import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-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 { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway"; +import { GatewayProxyProtocol } from "@app/lib/gateway"; import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { logger } from "@app/lib/logger"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; @@ -109,7 +109,6 @@ export const getLdapConnectionClient = async ({ export const executeWithPotentialGateway = async ( config: TLdapConnectionConfig, - gatewayService: Pick, gatewayV2Service: Pick, operation: (client: ldap.Client) => Promise ): Promise => { @@ -117,7 +116,7 @@ export const executeWithPotentialGateway = async ( const { protocol, host, port } = parseLdapUrl(credentials.url); const appCfg = getConfig(); - if (gatewayId && gatewayService && gatewayV2Service) { + if (gatewayId && gatewayV2Service) { await blockLocalAndPrivateIpAddresses(credentials.url, true); const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({ gatewayId, @@ -162,52 +161,6 @@ export const executeWithPotentialGateway = async ( } ); } - - const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(gatewayId); - const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); - return withGatewayProxy( - async (proxyPort) => { - const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort); - const isSSL = protocol === "ldaps"; - - const client = ldap.createClient({ - url: proxyUrl, - timeout: LDAP_TIMEOUT, - connectTimeout: LDAP_TIMEOUT, - tlsOptions: isSSL - ? { - rejectUnauthorized: config.credentials.sslRejectUnauthorized, - ca: config.credentials.sslCertificate ? [config.credentials.sslCertificate] : undefined, - servername: host, - // bypass hostname verification for development - ...(appCfg.isDevelopmentMode ? { checkServerIdentity: () => undefined } : {}) - } - : undefined - }); - - return setupLdapClientHandlers(client, credentials.dn, credentials.password, async (ldapClient) => { - try { - return await operation(ldapClient); - } finally { - ldapClient.destroy(); - } - }); - }, - { - protocol: GatewayProxyProtocol.Tcp, - targetHost: host, - targetPort: port, - relayHost, - relayPort: Number(relayPort), - identityId: relayDetails.identityId, - orgId: relayDetails.orgId, - tlsOptions: { - ca: relayDetails.certChain, - cert: relayDetails.certificate, - key: relayDetails.privateKey.toString() - } - } - ); } // Non-gateway path - calls getLdapConnectionClient which has validation @@ -225,7 +178,7 @@ export const validateLdapConnectionCredentials = async ( gatewayV2Service: Pick ) => { try { - await executeWithPotentialGateway(config, gatewayService, gatewayV2Service, async (client) => { + await executeWithPotentialGateway(config, 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" }); From 84d505bbb0f5e4bd5a8ce1250236d704e9385584 Mon Sep 17 00:00:00 2001 From: Victor Santos Date: Mon, 17 Nov 2025 09:36:57 -0300 Subject: [PATCH 5/5] refactor: improve error handling for gateway connection in LDAP functions --- .../ldap/ldap-connection-fns.ts | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts index eb11e8459d..961dcff597 100644 --- a/backend/src/services/app-connection/ldap/ldap-connection-fns.ts +++ b/backend/src/services/app-connection/ldap/ldap-connection-fns.ts @@ -124,43 +124,45 @@ export const executeWithPotentialGateway = async ( targetPort: port }); - if (platformConnectionDetails) { - return withGatewayV2Proxy( - async (proxyPort) => { - const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort); - const isSSL = protocol === "ldaps"; - - const client = ldap.createClient({ - url: proxyUrl, - timeout: LDAP_TIMEOUT, - connectTimeout: LDAP_TIMEOUT, - tlsOptions: isSSL - ? { - rejectUnauthorized: config.credentials.sslRejectUnauthorized, - ca: config.credentials.sslCertificate ? [config.credentials.sslCertificate] : undefined, - servername: host, - // bypass hostname verification for development - ...(appCfg.isDevelopmentMode ? { checkServerIdentity: () => undefined } : {}) - } - : undefined - }); - - return setupLdapClientHandlers(client, credentials.dn, credentials.password, async (ldapClient) => { - try { - return await operation(ldapClient); - } finally { - ldapClient.destroy(); - } - }); - }, - { - protocol: GatewayProxyProtocol.Tcp, - relayHost: platformConnectionDetails.relayHost, - gateway: platformConnectionDetails.gateway, - relay: platformConnectionDetails.relay - } - ); + if (!platformConnectionDetails) { + throw new BadRequestError({ message: "Unable to connect to gateway, no platform connection details found" }); } + + return withGatewayV2Proxy( + async (proxyPort) => { + const proxyUrl = constructLdapUrl(protocol, "localhost", proxyPort); + const isSSL = protocol === "ldaps"; + + const client = ldap.createClient({ + url: proxyUrl, + timeout: LDAP_TIMEOUT, + connectTimeout: LDAP_TIMEOUT, + tlsOptions: isSSL + ? { + rejectUnauthorized: config.credentials.sslRejectUnauthorized, + ca: config.credentials.sslCertificate ? [config.credentials.sslCertificate] : undefined, + servername: host, + // bypass hostname verification for development + ...(appCfg.isDevelopmentMode ? { checkServerIdentity: () => undefined } : {}) + } + : undefined + }); + + return setupLdapClientHandlers(client, credentials.dn, credentials.password, async (ldapClient) => { + try { + return await operation(ldapClient); + } finally { + ldapClient.destroy(); + } + }); + }, + { + protocol: GatewayProxyProtocol.Tcp, + relayHost: platformConnectionDetails.relayHost, + gateway: platformConnectionDetails.gateway, + relay: platformConnectionDetails.relay + } + ); } // Non-gateway path - calls getLdapConnectionClient which has validation