initial configuration for tests

This commit is contained in:
Victor Santos
2025-11-05 21:03:22 -03:00
parent f77bdc5347
commit e2e0c374b0
4 changed files with 263 additions and 2 deletions
@@ -1,6 +1,10 @@
import ldap from "ldapjs"; 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 { 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 { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums"; 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 { LdapConnectionMethod } from "./ldap-connection-enums";
import { TLdapConnectionConfig } from "./ldap-connection-types"; 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 = () => { export const getLdapConnectionListItem = () => {
return { return {
name: "LDAP" as const, name: "LDAP" as const,
@@ -80,6 +100,183 @@ export const getLdapConnectionClient = async ({
}); });
}; };
export const executeWithPotentialGateway = async <T>(
config: TLdapConnectionConfig,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
operation: (client: ldap.Client) => Promise<T>
): Promise<T> => {
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<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) => {
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<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) => {
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) => { export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
let client: ldap.Client | undefined; let client: ldap.Client | undefined;
@@ -74,7 +74,9 @@ export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("met
]); ]);
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and( export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP) GenericCreateAppConnectionFieldsSchema(AppConnection.LDAP, {
supportsGateways: true
})
); );
export const UpdateLdapConnectionSchema = z export const UpdateLdapConnectionSchema = z
@@ -17,6 +17,9 @@ export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> &
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema; export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & { export type TLdapConnectionConfig = DiscriminativePick<
TLdapConnectionInput,
"method" | "app" | "credentials" | "gatewayId"
> & {
orgId: string; orgId: string;
}; };
@@ -4,8 +4,10 @@ import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod"; import { z } from "zod";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
FormControl, FormControl,
@@ -18,8 +20,11 @@ import {
TextArea, TextArea,
Tooltip Tooltip
} from "@app/components/v2"; } 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 { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/helpers/string"; import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/helpers/string";
import { gatewaysQueryKeys } from "@app/hooks/api";
import { import {
LdapConnectionMethod, LdapConnectionMethod,
LdapConnectionProvider, LdapConnectionProvider,
@@ -84,6 +89,7 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
defaultValues: appConnection ?? { defaultValues: appConnection ?? {
app: AppConnection.LDAP, app: AppConnection.LDAP,
method: LdapConnectionMethod.SimpleBind, method: LdapConnectionMethod.SimpleBind,
gatewayId: null,
credentials: { credentials: {
provider: LdapConnectionProvider.ActiveDirectory, provider: LdapConnectionProvider.ActiveDirectory,
url: "", url: "",
@@ -104,6 +110,8 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
const selectedProvider = watch("credentials.provider"); const selectedProvider = watch("credentials.provider");
const sslEnabled = watch("credentials.url")?.startsWith("ldaps://") ?? false; const sslEnabled = watch("credentials.url")?.startsWith("ldaps://") ?? false;
const { subscription } = useSubscription();
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
@@ -114,6 +122,57 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
}} }}
> >
{!isUpdate && <GenericAppConnectionsFields />} {!isUpdate && <GenericAppConnectionsFields />}
{subscription.gateway && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
)}
<div className="grid grid-cols-2 items-center gap-2"> <div className="grid grid-cols-2 items-center gap-2">
<Controller <Controller
name="method" name="method"