feature: ldap connection and password rotation

This commit is contained in:
Scott Wilson
2025-04-18 17:55:03 -07:00
parent f1b1d6f480
commit 9032bbe514
92 changed files with 1980 additions and 90 deletions
@@ -0,0 +1,4 @@
export * from "./ldap-connection-enums";
export * from "./ldap-connection-fns";
export * from "./ldap-connection-schemas";
export * from "./ldap-connection-types";
@@ -0,0 +1,7 @@
export enum LdapConnectionMethod {
SimpleBind = "simple-bind"
}
export enum LdapProvider {
ActiveDirectory = "active-directory"
}
@@ -0,0 +1,101 @@
import ldap from "ldapjs";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { LdapConnectionMethod } from "./ldap-connection-enums";
import { TLdapConnectionConfig } from "./ldap-connection-types";
export const getLdapConnectionListItem = () => {
return {
name: "LDAP" as const,
app: AppConnection.Ldap as const,
methods: Object.values(LdapConnectionMethod) as [LdapConnectionMethod.SimpleBind]
};
};
const LDAP_TIMEOUT = 15_000;
export const getLdapConnectionClient = async ({
url,
dn,
password,
sslCertificate,
sslRejectUnauthorized = true
}: TLdapConnectionConfig["credentials"]) => {
await blockLocalAndPrivateIpAddresses(url);
const isSSL = url.startsWith("ldaps");
return new Promise<ldap.Client>((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");
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();
}
resolve(client);
});
});
});
};
export const validateLdapConnectionCredentials = async ({ credentials }: TLdapConnectionConfig) => {
let client: ldap.Client | undefined;
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"}`
});
} finally {
client?.destroy();
}
};
@@ -0,0 +1,82 @@
import { z } from "zod";
import { AppConnections } from "@app/lib/api-docs";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
BaseAppConnectionSchema,
GenericCreateAppConnectionFieldsSchema,
GenericUpdateAppConnectionFieldsSchema
} from "@app/services/app-connection/app-connection-schemas";
import { LdapConnectionMethod, LdapProvider } from "./ldap-connection-enums";
export const LdapConnectionSimpleBindCredentialsSchema = z.object({
provider: z.nativeEnum(LdapProvider).describe(AppConnections.CREDENTIALS.LDAP.provider),
url: z.string().trim().min(1, "URL required").describe(AppConnections.CREDENTIALS.LDAP.url),
dn: z.string().trim().min(1, "Distinguished Name (DN) required").describe(AppConnections.CREDENTIALS.LDAP.dn),
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.LDAP.password),
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.LDAP.sslRejectUnauthorized),
sslCertificate: z
.string()
.trim()
.transform((value) => value || undefined)
.optional()
.describe(AppConnections.CREDENTIALS.LDAP.sslCertificate)
});
const BaseLdapConnectionSchema = BaseAppConnectionSchema.extend({
app: z.literal(AppConnection.Ldap)
});
export const LdapConnectionSchema = z.intersection(
BaseLdapConnectionSchema,
z.discriminatedUnion("method", [
z.object({
method: z.literal(LdapConnectionMethod.SimpleBind),
credentials: LdapConnectionSimpleBindCredentialsSchema
})
])
);
export const SanitizedLdapConnectionSchema = z.discriminatedUnion("method", [
BaseLdapConnectionSchema.extend({
method: z.literal(LdapConnectionMethod.SimpleBind),
credentials: LdapConnectionSimpleBindCredentialsSchema.pick({
provider: true,
url: true,
dn: true,
sslEnabled: true,
sslRejectUnauthorized: true,
sslCertificate: true
})
})
]);
export const ValidateLdapConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({
method: z.literal(LdapConnectionMethod.SimpleBind).describe(AppConnections.CREATE(AppConnection.Ldap).method),
credentials: LdapConnectionSimpleBindCredentialsSchema.describe(
AppConnections.CREATE(AppConnection.Ldap).credentials
)
})
]);
export const CreateLdapConnectionSchema = ValidateLdapConnectionCredentialsSchema.and(
GenericCreateAppConnectionFieldsSchema(AppConnection.Ldap)
);
export const UpdateLdapConnectionSchema = z
.object({
credentials: LdapConnectionSimpleBindCredentialsSchema.optional().describe(
AppConnections.UPDATE(AppConnection.Ldap).credentials
)
})
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Ldap));
export const LdapConnectionListItemSchema = z.object({
name: z.literal("LDAP"),
app: z.literal(AppConnection.Ldap),
// the below is preferable but currently breaks with our zod to json schema parser
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
methods: z.nativeEnum(LdapConnectionMethod).array()
});
@@ -0,0 +1,22 @@
import { z } from "zod";
import { DiscriminativePick } from "@app/lib/types";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateLdapConnectionSchema,
LdapConnectionSchema,
ValidateLdapConnectionCredentialsSchema
} from "./ldap-connection-schemas";
export type TLdapConnection = z.infer<typeof LdapConnectionSchema>;
export type TLdapConnectionInput = z.infer<typeof CreateLdapConnectionSchema> & {
app: AppConnection.Ldap;
};
export type TValidateLdapConnectionCredentialsSchema = typeof ValidateLdapConnectionCredentialsSchema;
export type TLdapConnectionConfig = DiscriminativePick<TLdapConnection, "method" | "app" | "credentials"> & {
orgId: string;
};